Commit 1e72592b53 for woocommerce

commit 1e72592b539dff0f33c40fb018ca9e503d460787
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date:   Tue Jan 6 14:48:09 2026 -0300

    [Fraud Protection] Hook into payment Method events (#62594)

    * Add SessionClearanceManager for fraud protection

    Implements session status management for the WooCommerce fraud protection
    feature. This class tracks three session states (pending, allowed, blocked)
    and provides the foundation for fraud detection decision enforcement.

    Key features:
    - Session status management (pending, allowed, blocked)
    - Cart emptying on blocked sessions
    - Logging via FraudProtectionController helper
    - DEFAULT_STATUS constant for consistent default behavior
    - Comprehensive unit tests (9 tests, 20 assertions)

    Closes WOOSUBS-1246

    🤖 Generated with [Claude Code](https://claude.com/claude-code)

    Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>

    * Address coderabbit comments

    * fix lint error

    * Address phpstan error

    * Add SessionDataCollector foundation class and tests

    Create SessionDataCollector class with basic structure to collect session data for fraud protection. This initial implementation establishes the class foundation with proper nested data structure, dependency injection, and comprehensive unit tests.

    - Add SessionDataCollector class with collect() method
    - Implement nested data structure with 9 top-level sections
    - Add dependency injection for SessionClearanceManager
    - Add UTC timestamp generation
    - Create 6 focused unit tests covering foundation requirements
    - Follow WooCommerce coding standards and patterns

    Part of Task Group 1 for WOOSUBS-1248 (Session Data Collection Framework).

    * Improve SessionDataCollector dependency injection and data collection

    - Update to use init() pattern instead of constructor for dependency injection (follows WooCommerce standards)
    - Add session data fallback for customer data collection (first_name, last_name, billing_email)
    - Reorder email fallback chain to check WC_Customer before session data
    - Add test for customer data session fallback
    - Fix PHPCS issues: replace short ternaries, fix array/variable alignment, remove trailing whitespace

    All tests passing (15 tests, 64 assertions).

    * Implement order, cart, and address data collection (Task Group 3)

    Add comprehensive order, cart item, and address data collection methods to SessionDataCollector with multiple improvements based on WooCommerce best practices.

    Implementation:
    - Add get_order_data() method collecting 11 order fields including totals, currency, and cart hash
    - Add get_cart_items() method extracting 12 detailed fields per cart item
    - Add get_billing_address() and get_shipping_address() methods with 6 fields each
    - Add get_product_category_names() helper for cleaner category extraction
    - Update collect() method to populate order and address sections

    Key improvements:
    - Use WC()->customer->get_id() for customer_id instead of user login check (proper WooCommerce abstraction)
    - Use get_subtotal() for items_total (before discounts) instead of get_cart_contents_total() (after discounts)
    - Use get_total('edit') for total instead of manual calculation (more reliable)
    - Use wc_get_product_terms() for categories with caching (WooCommerce helper vs WordPress get_term)
    - Return all product categories as comma-separated list (better fraud detection data)
    - Extract category logic to reusable helper method (cleaner code)

    Testing:
    - Add 9 new focused tests for order, cart, and address data (24 total tests, 137 assertions)
    - Fix cart state isolation between tests (add empty_cart() calls)
    - Add customer reinitialization in logged-in user test
    - All tests passing

    Part of Task Group 3 for WOOSUBS-1248 (Session Data Collection Framework).

    * Add payment data structure and complete SessionDataCollector (Task Group 4)

    Complete SessionDataCollector implementation with payment data structure and comprehensive integration testing. All data collection methods now implemented with graceful degradation throughout.

    Implementation:
    - Add get_payment_data() method with 11 payment fields (gateway name/type populated, card details null for future gateway extensions)
    - Add get_product_category_names() helper for cleaner category extraction
    - Update collect() to populate payment section and extract order_id from event_data
    - Update get_order_data() to accept order_id parameter from event_data

    Integration testing:
    - Add 6 comprehensive integration tests (35 total tests, 202 assertions)
    - Test payment data structure with all 11 required fields
    - Test complete collect() output with all 9 sections
    - Test end-to-end data collection with full cart scenario
    - Test graceful degradation across all sections (empty cart, no user, minimal data)
    - Test manual triggering verification
    - Fix graceful degradation test to properly initialize guest customer

    Data collection improvements:
    - Order ID now extracted from event_data when provided by calling code
    - Category extraction uses wc_get_product_terms() with caching (WooCommerce helper)
    - Returns all product categories as comma-separated list (better fraud detection data)
    - Comprehensive PHPDoc comments throughout

    All acceptance criteria met:
    - Payment structure complete with gateway extension placeholders
    - All 9 sections implemented and tested
    - Graceful degradation verified end-to-end
    - Manual triggering only (no automatic hooks in SessionDataCollector)
    - All tests passing (35 tests, 202 assertions)

    Part of Task Group 4 for WOOSUBS-1248 (Session Data Collection Framework).

    * Added Cart Event Tracker class to capture cart events

    * Added test for the CartEventTracker class

    * Add SessionDataCollector foundation class and tests

    Create SessionDataCollector class with basic structure to collect session data for fraud protection. This initial implementation establishes the class foundation with proper nested data structure, dependency injection, and comprehensive unit tests.

    - Add SessionDataCollector class with collect() method
    - Implement nested data structure with 9 top-level sections
    - Add dependency injection for SessionClearanceManager
    - Add UTC timestamp generation
    - Create 6 focused unit tests covering foundation requirements
    - Follow WooCommerce coding standards and patterns

    Part of Task Group 1 for WOOSUBS-1248 (Session Data Collection Framework).

    * Improve SessionDataCollector dependency injection and data collection

    - Update to use init() pattern instead of constructor for dependency injection (follows WooCommerce standards)
    - Add session data fallback for customer data collection (first_name, last_name, billing_email)
    - Reorder email fallback chain to check WC_Customer before session data
    - Add test for customer data session fallback
    - Fix PHPCS issues: replace short ternaries, fix array/variable alignment, remove trailing whitespace

    All tests passing (15 tests, 64 assertions).

    * Implement order, cart, and address data collection (Task Group 3)

    Add comprehensive order, cart item, and address data collection methods to SessionDataCollector with multiple improvements based on WooCommerce best practices.

    Implementation:
    - Add get_order_data() method collecting 11 order fields including totals, currency, and cart hash
    - Add get_cart_items() method extracting 12 detailed fields per cart item
    - Add get_billing_address() and get_shipping_address() methods with 6 fields each
    - Add get_product_category_names() helper for cleaner category extraction
    - Update collect() method to populate order and address sections

    Key improvements:
    - Use WC()->customer->get_id() for customer_id instead of user login check (proper WooCommerce abstraction)
    - Use get_subtotal() for items_total (before discounts) instead of get_cart_contents_total() (after discounts)
    - Use get_total('edit') for total instead of manual calculation (more reliable)
    - Use wc_get_product_terms() for categories with caching (WooCommerce helper vs WordPress get_term)
    - Return all product categories as comma-separated list (better fraud detection data)
    - Extract category logic to reusable helper method (cleaner code)

    Testing:
    - Add 9 new focused tests for order, cart, and address data (24 total tests, 137 assertions)
    - Fix cart state isolation between tests (add empty_cart() calls)
    - Add customer reinitialization in logged-in user test
    - All tests passing

    Part of Task Group 3 for WOOSUBS-1248 (Session Data Collection Framework).

    * Add payment data structure and complete SessionDataCollector (Task Group 4)

    Complete SessionDataCollector implementation with payment data structure and comprehensive integration testing. All data collection methods now implemented with graceful degradation throughout.

    Implementation:
    - Add get_payment_data() method with 11 payment fields (gateway name/type populated, card details null for future gateway extensions)
    - Add get_product_category_names() helper for cleaner category extraction
    - Update collect() to populate payment section and extract order_id from event_data
    - Update get_order_data() to accept order_id parameter from event_data

    Integration testing:
    - Add 6 comprehensive integration tests (35 total tests, 202 assertions)
    - Test payment data structure with all 11 required fields
    - Test complete collect() output with all 9 sections
    - Test end-to-end data collection with full cart scenario
    - Test graceful degradation across all sections (empty cart, no user, minimal data)
    - Test manual triggering verification
    - Fix graceful degradation test to properly initialize guest customer

    Data collection improvements:
    - Order ID now extracted from event_data when provided by calling code
    - Category extraction uses wc_get_product_terms() with caching (WooCommerce helper)
    - Returns all product categories as comma-separated list (better fraud detection data)
    - Comprehensive PHPDoc comments throughout

    All acceptance criteria met:
    - Payment structure complete with gateway extension placeholders
    - All 9 sections implemented and tested
    - Graceful degradation verified end-to-end
    - Manual triggering only (no automatic hooks in SessionDataCollector)
    - All tests passing (35 tests, 202 assertions)

    Part of Task Group 4 for WOOSUBS-1248 (Session Data Collection Framework).

    * Remove todo comment in order to calm down the linter

    * Use get_order_count on customer for optimized order count retrieval with caching support

    * Add ApiClient for Fraud Protection API communication

    Implements WOOSUBS-1247: Creates an API client that sends session data
    to the WPCOM fraud protection endpoint via Jetpack Connection.

    Key features:
    - Fail-open pattern: returns "allow" on all error conditions
    - Comprehensive error logging for debugging
    - 30s timeout for API requests
    - Validates verdict responses (allow/block)

    Also adds LoggerSpyTrait for cleaner log assertions in tests.

    * Fix lint and PHPStan errors

    * Fixed empty customer $lifetime_order_count

    * Added comment about why we're reloading the customer

    * Now we always create a unique session id

    * Fixed session hash name

    * Removed unused imports

    * Removed unecessary dependency

    * Updated isset check to the null coallese operator

    * Casting $old_quantity to int

    * Added check to prevent sending cart update events when the number of items didn't change

    * Disabled taxes to prevent fraud tests from failing when checking the cart totals

    * Lint fixes

    * Added extra checks for when the decoded json is not an array or is null

    * Added microtime to the sku so each product under test is unique

    * Fixed lint errors

    * Added class to hook into payment method actions to track fraud data

    * Changed the log method so it's possible to override it during test running

    * Added test to check if the PaymentMethodEventTracker works as expected

    * Revert "Changed the log method so it's possible to override it during test running"

    This reverts commit b9fd1f501777cbe0f5aa1ad93c36029830a0f9c0.

    * Updated test to use the LoggerSpyTrait

    * Removed unecessary dependency

    * phpcs fixes

    * Fixed lint error about todo task

    * Removed unecessary hook

    * Removed woocommerce_new_payment_token manual trigger to rely on the core calling it

    * Fixed sut and testdox

    * Removed unecessary tests for woocommerce_payment_token_add_failed

    ---------

    Co-authored-by: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
    Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
    Co-authored-by: Luiz Reis <luiz.reis@automattic.com>

diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index dc8595243f..6492bde084 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -377,6 +377,7 @@ final class WooCommerce {
 		$container->get( Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\FraudProtection\AdminSettingsHandler::class )->register();
+		$container->get( Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub::class )->register();
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
new file mode 100644
index 0000000000..1061e08035
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
@@ -0,0 +1,232 @@
+<?php
+/**
+ * PaymentMethodEventTracker class file.
+ *
+ * @package WooCommerce\Classes
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Tracks payment method events for fraud protection analysis.
+ *
+ * This class hooks into WooCommerce payment method events in My Account
+ * (add, update, set default, delete, add failed) and triggers comprehensive event
+ * tracking with full session context. It orchestrates the event tracking by collecting
+ * session data and preparing it for the fraud protection service.
+ *
+ * @since 10.5.0
+ * @internal This class is part of the internal API and is subject to change without notice.
+ */
+class PaymentMethodEventTracker implements RegisterHooksInterface {
+
+	/**
+	 * Session data collector instance.
+	 *
+	 * @var SessionDataCollector
+	 */
+	private SessionDataCollector $data_collector;
+
+	/**
+	 * Fraud protection controller instance.
+	 *
+	 * @var FraudProtectionController
+	 */
+	private FraudProtectionController $fraud_protection_controller;
+
+	/**
+	 * Initialize with dependencies.
+	 *
+	 * @internal
+	 *
+	 * @param SessionDataCollector      $data_collector              The session data collector instance.
+	 * @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
+	 */
+	final public function init(
+		SessionDataCollector $data_collector,
+		FraudProtectionController $fraud_protection_controller
+	): void {
+		$this->data_collector              = $data_collector;
+		$this->fraud_protection_controller = $fraud_protection_controller;
+	}
+
+	/**
+	 * Register payment method event hooks.
+	 *
+	 * Hooks into WooCommerce payment token actions to track fraud protection events.
+	 * Only registers hooks if the fraud protection feature is enabled.
+	 */
+	public function register(): void {
+		// Only register hooks if fraud protection is enabled.
+		if ( ! $this->fraud_protection_controller->feature_is_enabled() ) {
+			return;
+		}
+
+		add_action( 'woocommerce_new_payment_token', array( $this, 'handle_payment_method_added' ), 10, 2 );
+		add_action( 'woocommerce_payment_token_updated', array( $this, 'handle_payment_method_updated' ), 10, 1 );
+		add_action( 'woocommerce_payment_token_set_default', array( $this, 'handle_payment_method_set_default' ), 10, 2 );
+		add_action( 'woocommerce_payment_token_deleted', array( $this, 'handle_payment_method_deleted' ), 10, 2 );
+	}
+
+	/**
+	 * Handle payment method added event.
+	 *
+	 * Triggers fraud protection event tracking when a payment method is added.
+	 *
+	 * @internal
+	 *
+	 * @param int               $token_id The newly created token ID.
+	 * @param \WC_Payment_Token $token    The payment token object.
+	 */
+	public function handle_payment_method_added( $token_id, $token ): void {
+		$event_data = $this->build_payment_method_event_data( 'added', $token );
+		$this->track_event( 'payment_method_added', $event_data );
+	}
+
+	/**
+	 * Handle payment method updated event.
+	 *
+	 * Triggers fraud protection event tracking when a payment method is updated.
+	 *
+	 * @internal
+	 *
+	 * @param int $token_id The ID of the updated token.
+	 */
+	public function handle_payment_method_updated( $token_id ): void {
+		// Get the token object to extract details.
+		$token = \WC_Payment_Tokens::get( $token_id );
+
+		if ( ! $token instanceof \WC_Payment_Token ) {
+			return;
+		}
+
+		$event_data = $this->build_payment_method_event_data( 'updated', $token );
+		$this->track_event( 'payment_method_updated', $event_data );
+	}
+
+	/**
+	 * Handle payment method set as default event.
+	 *
+	 * Triggers fraud protection event tracking when a payment method is set as default.
+	 *
+	 * @internal
+	 *
+	 * @param int               $token_id The ID of the token being set as default.
+	 * @param \WC_Payment_Token $token    The payment token object.
+	 */
+	public function handle_payment_method_set_default( $token_id, $token ): void {
+		$event_data = $this->build_payment_method_event_data( 'set_default', $token );
+		$this->track_event( 'payment_method_set_default', $event_data );
+	}
+
+	/**
+	 * Handle payment method deleted event.
+	 *
+	 * Triggers fraud protection event tracking when a payment method is deleted.
+	 *
+	 * @internal
+	 *
+	 * @param int               $token_id The ID of the deleted token.
+	 * @param \WC_Payment_Token $token    The payment token object.
+	 */
+	public function handle_payment_method_deleted( $token_id, $token ): void {
+		$event_data = $this->build_payment_method_event_data( 'deleted', $token );
+		$this->track_event( 'payment_method_deleted', $event_data );
+	}
+
+	/**
+	 * Build payment method event-specific data.
+	 *
+	 * Extracts relevant information from the payment token object including
+	 * token type, gateway ID, user ID, and card details for card tokens.
+	 * This data will be merged with comprehensive session data during event tracking.
+	 *
+	 * @param string            $action Action type (added, updated, set_default, deleted, add_failed).
+	 * @param \WC_Payment_Token $token  The payment token object.
+	 * @return array Payment method event data.
+	 */
+	private function build_payment_method_event_data( string $action, \WC_Payment_Token $token ): array {
+		$event_data = array(
+			'action'     => $action,
+			'token_id'   => $token->get_id(),
+			'token_type' => $token->get_type(),
+			'gateway_id' => $token->get_gateway_id(),
+			'user_id'    => $token->get_user_id(),
+			'is_default' => $token->is_default(),
+		);
+
+		// Add card-specific details if this is a credit card token.
+		if ( $token instanceof \WC_Payment_Token_CC ) {
+			$event_data['card_type']    = $token->get_card_type();
+			$event_data['card_last4']   = $token->get_last4();
+			$event_data['expiry_month'] = $token->get_expiry_month();
+			$event_data['expiry_year']  = $token->get_expiry_year();
+		}
+
+		return $event_data;
+	}
+
+	/**
+	 * Track fraud protection event with comprehensive session context.
+	 *
+	 * This method orchestrates the event tracking by:
+	 * 1. Collecting comprehensive session data via SessionDataCollector
+	 * 2. Merging with event-specific data
+	 * 3. Logging the event (will call EventTracker/API client once available)
+	 *
+	 * The method implements graceful degradation - any errors during tracking
+	 * will be logged but will not break the payment method functionality.
+	 *
+	 * @param string $event_type          Event type identifier (e.g., 'payment_method_added').
+	 * @param array  $event_specific_data Event-specific data to merge with session context.
+	 */
+	private function track_event( string $event_type, array $event_specific_data ): void {
+		try {
+			// Collect comprehensive session data.
+			$session_data = $this->data_collector->collect( $event_type, $event_specific_data );
+
+			// phpcs:ignore Generic.Commenting.Todo.TaskFound
+			// TODO: Once EventTracker/API client is implemented (WOOSUBS-1249), call it here:
+			// $event_tracker = wc_get_container()->get( EventTracker::class );
+			// $event_tracker->track( $event_type, $session_data );
+			//
+			// For now, log the event for debugging and verification.
+			FraudProtectionController::log(
+				'info',
+				sprintf(
+					'Fraud protection event tracked: %s | Token ID: %s | Gateway: %s | User ID: %s | Session ID: %s',
+					$event_type,
+					$event_specific_data['token_id'] ?? 'N/A',
+					$event_specific_data['gateway_id'] ?? 'N/A',
+					$event_specific_data['user_id'] ?? 'N/A',
+					$session_data['session']['session_id'] ?? 'N/A'
+				),
+				array(
+					'event_type'   => $event_type,
+					'event_data'   => $event_specific_data,
+					'session_data' => $session_data,
+				)
+			);
+		} catch ( \Exception $e ) {
+			// Gracefully handle errors - fraud protection should never break payment method management.
+			FraudProtectionController::log(
+				'error',
+				sprintf(
+					'Failed to track fraud protection event: %s | Error: %s',
+					$event_type,
+					$e->getMessage()
+				),
+				array(
+					'event_type' => $event_type,
+					'exception'  => $e,
+				)
+			);
+		}
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
new file mode 100644
index 0000000000..038294eda8
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
@@ -0,0 +1,250 @@
+<?php
+/**
+ * PaymentMethodEventTrackerTest class file.
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker;
+use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
+
+/**
+ * Tests for the PaymentMethodEventTracker class.
+ */
+class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
+
+	use LoggerSpyTrait;
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var PaymentMethodEventTracker
+	 */
+	private $sut;
+
+	/**
+	 * Setup test.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		// Enable the fraud protection feature.
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
+
+		$container = wc_get_container();
+		$container->reset_all_resolved();
+
+		$this->sut = $container->get( PaymentMethodEventTracker::class );
+	}
+
+	/**
+	 * @testdox Should register hooks when feature is enabled.
+	 */
+	public function test_hooks_registered_when_feature_enabled(): void {
+		$this->sut->register();
+
+		$this->assertNotFalse( has_action( 'woocommerce_new_payment_token', array( $this->sut, 'handle_payment_method_added' ) ) );
+		$this->assertNotFalse( has_action( 'woocommerce_payment_token_updated', array( $this->sut, 'handle_payment_method_updated' ) ) );
+		$this->assertNotFalse( has_action( 'woocommerce_payment_token_set_default', array( $this->sut, 'handle_payment_method_set_default' ) ) );
+		$this->assertNotFalse( has_action( 'woocommerce_payment_token_deleted', array( $this->sut, 'handle_payment_method_deleted' ) ) );
+	}
+
+	/**
+	 * @testdox Should not register hooks when feature is disabled.
+	 */
+	public function test_hooks_not_registered_when_feature_disabled(): void {
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'no' );
+
+		$container = wc_get_container();
+		$container->reset_all_resolved();
+		$this->sut = $container->get( PaymentMethodEventTracker::class );
+
+		$this->sut->register();
+
+		$this->assertFalse( has_action( 'woocommerce_new_payment_token', array( $this->sut, 'handle_payment_method_added' ) ) );
+		$this->assertFalse( has_action( 'woocommerce_payment_token_updated', array( $this->sut, 'handle_payment_method_updated' ) ) );
+		$this->assertFalse( has_action( 'woocommerce_payment_token_set_default', array( $this->sut, 'handle_payment_method_set_default' ) ) );
+		$this->assertFalse( has_action( 'woocommerce_payment_token_deleted', array( $this->sut, 'handle_payment_method_deleted' ) ) );
+	}
+
+	/**
+	 * @testdox Should track payment method added event.
+	 */
+	public function test_handle_payment_method_added(): void {
+		$this->sut->register();
+
+		$user_id = $this->factory->user->create();
+
+		$token = new \WC_Payment_Token_CC();
+		$token->set_token( 'test_token_123' );
+		$token->set_gateway_id( 'stripe' );
+		$token->set_card_type( 'visa' );
+		$token->set_last4( '4242' );
+		$token->set_expiry_month( '12' );
+		$token->set_expiry_year( '2025' );
+		$token->set_user_id( $user_id );
+		$token->save();
+
+		$this->assertLogged(
+			'info',
+			'payment_method_added',
+			array(
+				'source'     => 'woo-fraud-protection',
+				'event_type' => 'payment_method_added',
+				'event_data' => array(
+					'action'     => 'added',
+					'token_id'   => $token->get_id(),
+					'gateway_id' => 'stripe',
+				),
+			)
+		);
+	}
+
+	/**
+	 * @testdox Should track payment method updated event.
+	 */
+	public function test_handle_payment_method_updated(): void {
+		$this->sut->register();
+
+		$user_id = $this->factory->user->create();
+
+		$token = new \WC_Payment_Token_CC();
+		$token->set_token( 'test_token_456' );
+		$token->set_gateway_id( 'stripe' );
+		$token->set_card_type( 'mastercard' );
+		$token->set_last4( '5555' );
+		$token->set_expiry_month( '06' );
+		$token->set_expiry_year( '2026' );
+		$token->set_user_id( $user_id );
+		$token->save();
+
+		// Update the token to trigger the 'updated' event.
+		$token->set_expiry_year( '2027' );
+		$token->save();
+
+		$this->assertLogged(
+			'info',
+			'payment_method_updated',
+			array(
+				'source'     => 'woo-fraud-protection',
+				'event_type' => 'payment_method_updated',
+				'event_data' => array(
+					'action'     => 'updated',
+					'token_id'   => $token->get_id(),
+					'gateway_id' => 'stripe',
+				),
+			)
+		);
+	}
+
+	/**
+	 * @testdox Should track payment method set as default event.
+	 */
+	public function test_handle_payment_method_set_default(): void {
+		$this->sut->register();
+
+		$user_id = $this->factory->user->create();
+
+		// Create first token (will be automatically set as default since it's the user's first token).
+		$token1 = new \WC_Payment_Token_CC();
+		$token1->set_token( 'test_token_first' );
+		$token1->set_gateway_id( 'stripe' );
+		$token1->set_card_type( 'visa' );
+		$token1->set_last4( '1111' );
+		$token1->set_expiry_month( '01' );
+		$token1->set_expiry_year( '2026' );
+		$token1->set_user_id( $user_id );
+		$token1->save();
+
+		// Create second token (won't be default).
+		$token2 = new \WC_Payment_Token_CC();
+		$token2->set_token( 'test_token_789' );
+		$token2->set_gateway_id( 'stripe' );
+		$token2->set_card_type( 'amex' );
+		$token2->set_last4( '0005' );
+		$token2->set_expiry_month( '03' );
+		$token2->set_expiry_year( '2027' );
+		$token2->set_user_id( $user_id );
+		$token2->save();
+
+		// Note: We use do_action() here because WC_Payment_Tokens::set_users_default()
+		// relies on get_customer_tokens() which doesn't retrieve tokens properly in the test environment.
+		// In production, the hook is triggered by WC_Payment_Tokens::set_users_default().
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment, WooCommerce.Commenting.CommentHooks.MissingSinceComment
+		do_action( 'woocommerce_payment_token_set_default', $token2->get_id(), $token2 );
+
+		$this->assertLogged(
+			'info',
+			'payment_method_set_default',
+			array(
+				'source'     => 'woo-fraud-protection',
+				'event_type' => 'payment_method_set_default',
+				'event_data' => array(
+					'action'     => 'set_default',
+					'token_id'   => $token2->get_id(),
+					'gateway_id' => 'stripe',
+					'is_default' => true,
+				),
+			)
+		);
+	}
+
+	/**
+	 * @testdox Should track payment method deleted event.
+	 */
+	public function test_handle_payment_method_deleted(): void {
+		$this->sut->register();
+
+		$user_id = $this->factory->user->create();
+
+		$token = new \WC_Payment_Token_CC();
+		$token->set_token( 'test_token_delete' );
+		$token->set_gateway_id( 'stripe' );
+		$token->set_card_type( 'visa' );
+		$token->set_last4( '1111' );
+		$token->set_expiry_month( '09' );
+		$token->set_expiry_year( '2028' );
+		$token->set_user_id( $user_id );
+		$token->save();
+
+		// Delete the token to trigger the 'deleted' event.
+		\WC_Payment_Tokens::delete( $token->get_id() );
+
+		$this->assertLogged(
+			'info',
+			'payment_method_deleted',
+			array(
+				'source'     => 'woo-fraud-protection',
+				'event_type' => 'payment_method_deleted',
+				'event_data' => array(
+					'action'     => 'deleted',
+					'token_id'   => $token->get_id(),
+					'gateway_id' => 'stripe',
+				),
+			)
+		);
+	}
+
+	/**
+	 * Cleanup after test.
+	 */
+	public function tearDown(): void {
+		parent::tearDown();
+
+		// Remove all hooks.
+		remove_all_actions( 'woocommerce_new_payment_token' );
+		remove_all_actions( 'woocommerce_payment_token_updated' );
+		remove_all_actions( 'woocommerce_payment_token_set_default' );
+		remove_all_actions( 'woocommerce_payment_token_deleted' );
+
+		// Clean up options.
+		delete_option( 'woocommerce_feature_fraud_protection_enabled' );
+
+		// Reset container.
+		wc_get_container()->reset_all_resolved();
+	}
+}