Commit 450a6a626b for woocommerce
commit 450a6a626b5c8af1d7ba978ce38e1b79b90e3e6e
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date: Sat Jan 10 17:17:30 2026 -0300
[Fraud Protection] Added Blocks checkout event tracker (#62655)
* 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
* Added hooks for payment methods to add their customer data when a payment method is selected.
* Added tests
* Added hooks documentation
* trigger tests
* Fixed lint errors
* phpstan fix
* Added class to hook into the shortcode checkout
* Added tracking to the shortcode payment method select event
* Added tests for PaymentMethodHelper class
* Added tests for the CheckoutEventTracker class
* Created a centralized tracker for all classes
* Fixed tests after refactoring the track_event methods
* Fixed shipping methods event tracking key
* Removed unecessary test
* Removed unecessary batching code
* Removed batching and fixed the scheduled action to contain valid customer data
* Added a custom query to remove the correct scheduled actions for a session
* Created method with a custom query to remove previous unexecuted scheduled actions
* Linting and phpstan fixes
* Linting and phpstan fixes
* phpstan fixes
* more phpstan fixes
* Fixed test missing session setup
* Now the event listener that tracks payment method changes is only going to be added when fraud protection is enabled
* Test fixes
* Test fix
* Added hooks to track events that update the customer on the blocks checkout page
* Created the CheckoutEventScheduler class to re-use the scheduler for block checkout integration.
* Created BlockCheckoutEventTracker to track block checkout events
* Renamed CheckoutEventTrackerTest to ShortcodeCheckoutEventTrackerTest
* Created tests for CheckoutEventScheduler
* Created tests for BlocksCheckoutEventTracker
* Fixed ShortCodeCheckoutEventTracker class name and added BlocksCheckoutEventTracker
* Added code to track payment method selection on blocks checout
* Added tests for the BlocksCheckoutEventTracker
* Added test for the FraudProtectionPaymentMethodSelected class
* Added js test for the payment method switch tracker
* Lint fixes
* More lint fixes
* Fixed MSW errors
* More lint fixes
* More lint fixes
* More lint fixes
* 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
* Fixed hook comment
* Added explanation of when the woocommerce_fraud_protection_payment_data hooks are called
* Fixed tests to use the woocommerce hook system instead of manually calling the hooks
* Removed unecessary space
* Updated test to use assertLogged instead of manually checking each log key
* Fix array alignment in PaymentMethodEventTrackerTest
* Removed unecessary hook and related tests and documentation
* Removed whitespace
* Fixed PaymentMethodEventTracker expected data format
* Lint fixes
* Reverted msw related chagnes
* Fixed test that was loading msw when it was not needed
* Removed unecessary scheduled action to track the checkout form data
* Removed whitespace
* Removed whitespace
* Removed payment method change tracking
* Fixed broken tests and removed unused code
* Removed reference to inexisting class
* Renamed event_tracker to event_dispatcher
* Removed payment method selection tracker
* Removed whitespace
* Merged default payment data with data that has been previously collected
* Removed left over data
* Fixed function call
* Reverted function call
* Reverted headers_sent check
* Moved method that gets payment gateway name by it's id to the WC_Payment_Gateways class
* Fixed linting and phpstan errors
* Fixed linting and phpstan errors
* Merged both blocks and shortcode checkout event tracker classes
* Now the shipping method and payment methods are tracked when the customer updates the blocks checkout form
* Fixed failing tests because of headers already sent
* Added more billing and shipping data to the SessionDataCollector
* Moved from using hooks to calling the checkout update tracking function directly
* Added comment
* Update test to check track_blocks_checkout_update
* Added shipping selected event tracker
* Removed unecessary methods
* php stan and lint errors fix
* Fixed call function
* Fixed linting errors
* Removed unecessary tests and methods.
* Fixed tests after changes on address key changes
* Removed get_shipping_method_names
* Fixed error when there is no payment method selected in the shortcode checkout
* Fixed address fields name
* Fixed comment
* Removed billing email
* Fixed linting errors
* Removed unecessary function
* Prevented fatal error when shipping_method is not available
* Update plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
Co-authored-by: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
* Renamed checkout event
* Removed blocks checkout shipping method tracker
* Reverted calculate totals removal
* Removed shipping data
---------
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/src/Internal/FraudProtection/CartEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
index a854843ffb..dab6e489bf 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
@@ -25,11 +25,11 @@ defined( 'ABSPATH' ) || exit;
class CartEventTracker implements RegisterHooksInterface {
/**
- * Fraud protection tracker instance.
+ * Fraud protection dispatcher instance.
*
- * @var FraudProtectionTracker
+ * @var FraudProtectionDispatcher
*/
- private FraudProtectionTracker $tracker;
+ private FraudProtectionDispatcher $dispatcher;
/**
* Session data collector instance.
@@ -50,16 +50,16 @@ class CartEventTracker implements RegisterHooksInterface {
*
* @internal
*
- * @param FraudProtectionTracker $tracker The fraud protection tracker instance.
+ * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
* @param SessionDataCollector $data_collector The session data collector instance.
* @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
*/
final public function init(
- FraudProtectionTracker $tracker,
+ FraudProtectionDispatcher $dispatcher,
SessionDataCollector $data_collector,
FraudProtectionController $fraud_protection_controller
): void {
- $this->tracker = $tracker;
+ $this->dispatcher = $dispatcher;
$this->data_collector = $data_collector;
$this->fraud_protection_controller = $fraud_protection_controller;
}
@@ -110,7 +110,7 @@ class CartEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'cart_item_added', $event_data );
- $this->tracker->track_event( 'cart_item_added', $collected_data );
+ $this->dispatcher->dispatch_event( 'cart_item_added', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -164,7 +164,7 @@ class CartEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'cart_item_updated', $event_data );
- $this->tracker->track_event( 'cart_item_updated', $collected_data );
+ $this->dispatcher->dispatch_event( 'cart_item_updated', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -214,7 +214,7 @@ class CartEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'cart_item_removed', $event_data );
- $this->tracker->track_event( 'cart_item_removed', $collected_data );
+ $this->dispatcher->dispatch_event( 'cart_item_removed', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -264,7 +264,7 @@ class CartEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'cart_item_restored', $event_data );
- $this->tracker->track_event( 'cart_item_restored', $collected_data );
+ $this->dispatcher->dispatch_event( 'cart_item_restored', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
index a6842837f3..d4247a3f02 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
@@ -14,9 +14,9 @@ defined( 'ABSPATH' ) || exit;
/**
* Tracks checkout events for fraud protection analysis.
*
- * This class hooks into WooCommerce checkout events (billing/email changes,
- * payment selection) and triggers comprehensive event tracking with full session
- * context.
+ * This class hooks into both WooCommerce Blocks (Store API) and traditional
+ * shortcode checkout events, triggering comprehensive event tracking with
+ * full session context for fraud protection analysis.
*
* @since 10.5.0
* @internal This class is part of the internal API and is subject to change without notice.
@@ -24,50 +24,51 @@ defined( 'ABSPATH' ) || exit;
class CheckoutEventTracker implements RegisterHooksInterface {
/**
- * Fraud protection tracker instance.
+ * Fraud protection dispatcher instance.
*
- * @var FraudProtectionTracker
+ * @var FraudProtectionDispatcher
*/
- private FraudProtectionTracker $tracker;
+ private FraudProtectionDispatcher $dispatcher;
/**
- * Session data collector instance.
+ * Fraud protection controller instance.
*
- * @var SessionDataCollector
+ * @var FraudProtectionController
*/
- private SessionDataCollector $data_collector;
+ private FraudProtectionController $fraud_protection_controller;
/**
- * Fraud protection controller instance.
+ * Session data collector instance.
*
- * @var FraudProtectionController
+ * @var SessionDataCollector
*/
- private FraudProtectionController $fraud_protection_controller;
+ private SessionDataCollector $data_collector;
/**
* Initialize with dependencies.
*
* @internal
*
- * @param FraudProtectionTracker $tracker The fraud protection tracker instance.
- * @param SessionDataCollector $data_collector The session data collector instance.
+ * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
* @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
+ * @param SessionDataCollector $data_collector The session data collector instance.
*/
final public function init(
- FraudProtectionTracker $tracker,
- SessionDataCollector $data_collector,
- FraudProtectionController $fraud_protection_controller
+ FraudProtectionDispatcher $dispatcher,
+ FraudProtectionController $fraud_protection_controller,
+ SessionDataCollector $data_collector
): void {
- $this->tracker = $tracker;
- $this->data_collector = $data_collector;
+ $this->dispatcher = $dispatcher;
$this->fraud_protection_controller = $fraud_protection_controller;
+ $this->data_collector = $data_collector;
}
/**
* Register checkout event hooks.
*
- * Hooks into WooCommerce checkout actions to track fraud protection events.
- * Only registers hooks if the fraud protection feature is enabled.
+ * Hooks into both WooCommerce Blocks (Store API) and traditional checkout
+ * actions to track fraud protection events. Only registers hooks if the
+ * fraud protection feature is enabled.
*
* @return void
*/
@@ -77,12 +78,28 @@ class CheckoutEventTracker implements RegisterHooksInterface {
return;
}
- // Traditional checkout: Track when checkout fields are updated.
- add_action( 'woocommerce_checkout_update_order_review', array( $this, 'handle_checkout_field_update' ), 10, 1 );
+ // Shortcode checkout: Track when checkout fields are updated.
+ add_action( 'woocommerce_checkout_update_order_review', array( $this, 'handle_shortcode_checkout_field_update' ), 10, 1 );
}
/**
- * Handle traditional checkout field update event.
+ * Handle Store API customer update event (WooCommerce Blocks checkout).
+ *
+ * Triggered when customer information is updated via the Store API endpoint
+ * /wc/store/v1/cart/update-customer during Blocks checkout flow.
+ *
+ * @internal
+ * @return void
+ */
+ public function track_blocks_checkout_update(): void {
+ // The data collector already has up to date customer data, so we don't need to pass it as a parameter.
+ // At this point we don't have any payment or shipping data, so we don't need to pass it as a parameter either.
+ $collected_data = $this->data_collector->collect( 'checkout_update', array() );
+ $this->dispatcher->dispatch_event( 'checkout_update', $collected_data );
+ }
+
+ /**
+ * Handle shortcode checkout field update event.
*
* Triggered when checkout fields are updated via AJAX (woocommerce_update_order_review).
*
@@ -91,15 +108,16 @@ class CheckoutEventTracker implements RegisterHooksInterface {
* @param string $posted_data Serialized checkout form data.
* @return void
*/
- public function handle_checkout_field_update( $posted_data ): void {
+ public function handle_shortcode_checkout_field_update( $posted_data ): void {
// Parse the posted data to extract relevant fields.
$data = array();
if ( $posted_data ) {
parse_str( $posted_data, $data );
}
- $event_data = $this->build_checkout_event_data( 'field_update', $data );
- $this->track_checkout_event( 'checkout_field_update', $event_data );
+ // Build and dispatch the event (traditional checkout includes payment/shipping methods).
+ $event_data = $this->format_checkout_event_data( 'field_update', $data );
+ $this->dispatcher->dispatch_event( 'checkout_field_update', $event_data );
}
/**
@@ -108,49 +126,24 @@ class CheckoutEventTracker implements RegisterHooksInterface {
* Prepares the checkout event data including action type and any changed fields.
* This data will be merged with comprehensive session data during event tracking.
*
- * @param string $action Action type (field_update, payment_method_selected, store_api_update).
- * @param array $posted_data Posted form data or event context.
+ * @param string $action Action type (field_update, store_api_update).
+ * @param array $collected_event_data Posted form data or event context (may include session data).
* @return array Checkout event data.
*/
- private function build_checkout_event_data( string $action, array $posted_data ): array {
+ private function format_checkout_event_data( string $action, array $collected_event_data ): array {
$event_data = array( 'action' => $action );
// Extract and merge all checkout field groups.
$event_data = array_merge(
$event_data,
- $this->extract_billing_fields( $posted_data ),
- $this->extract_shipping_fields( $posted_data ),
- $this->extract_payment_method( $posted_data ),
- $this->extract_shipping_methods( $posted_data )
+ $this->extract_billing_fields( $collected_event_data ),
+ $this->extract_shipping_fields( $collected_event_data ),
+ $this->extract_payment_method( $collected_event_data ),
);
return $event_data;
}
- /**
- * Extract payment method data from posted data.
- *
- * Extracts payment method ID and retrieves the readable gateway name.
- *
- * @param array $posted_data Posted form data.
- * @return array Payment method data with ID and name, or empty array if not found.
- */
- private static function extract_payment_method( array $posted_data ): array {
- $payment_data = array();
-
- if ( ! empty( $posted_data['payment']['payment_method_type'] ) ) {
- $payment_gateway_id = sanitize_text_field( wp_unslash( $posted_data['payment']['payment_method_type'] ) );
- $payment_gateway_name = WC()->payment_gateways()->get_payment_gateway_name_by_id( $payment_gateway_id );
-
- $payment_data['payment'] = array(
- 'payment_gateway_type' => $payment_gateway_id,
- 'payment_gateway_name' => $payment_gateway_name,
- );
- }
-
- return $payment_data;
- }
-
/**
* Extract billing fields from posted data.
*
@@ -230,145 +223,25 @@ class CheckoutEventTracker implements RegisterHooksInterface {
}
/**
- * Extract and convert shipping method IDs to readable names.
- *
- * @param array $posted_data Posted form data.
- * @return array Shipping method data wrapped in 'shipping_methods' key.
- */
- private function extract_shipping_methods( array $posted_data ): array {
- $shipping_method_data = array();
-
- if ( ! empty( $posted_data['shipping_method'] ) ) {
- $shipping_method_ids = $posted_data['shipping_method'];
-
- $shipping_methods = $this->get_shipping_method_names( $shipping_method_ids );
- if ( ! empty( $shipping_methods ) ) {
- $shipping_method_data['shipping_methods'] = $shipping_methods;
- }
- }
-
- return $shipping_method_data;
- }
-
- /**
- * Track a checkout event immediately.
- *
- * Collects comprehensive session data and tracks the event immediately.
- *
- * @param string $event_type Event type identifier.
- * @param array $event_specific_data Event-specific data to merge with session context.
- * @return void
- */
- private function track_checkout_event( string $event_type, array $event_specific_data ): void {
- // Collect comprehensive session data.
- try {
- $collected_data = $this->data_collector->collect( $event_type, $event_specific_data );
- } catch ( \Exception $e ) {
- // If collection fails, log and abort tracking.
- FraudProtectionController::log(
- 'error',
- sprintf(
- 'Failed to collect session data for checkout event: %s | Error: %s',
- $event_type,
- $e->getMessage()
- ),
- array(
- 'event_type' => $event_type,
- 'exception' => $e,
- )
- );
- return;
- }
-
- // Track the event immediately.
- $this->tracker->track_event( $event_type, $collected_data );
- }
-
- /**
- * Get readable shipping method names from shipping method IDs.
+ * Extract payment method data from posted data.
*
- * Converts shipping method IDs (e.g., "flat_rate:1", "free_shipping:2")
- * to their human-readable labels by loading the shipping method instances.
+ * Extracts payment method ID and retrieves the readable gateway name.
*
- * @param array $shipping_method_ids Array of shipping method IDs.
- * @return array Associative array mapping shipping method IDs to their names.
+ * @param array $posted_data Posted form data.
+ * @return array Payment method data with ID and name, or empty array if not found.
*/
- private function get_shipping_method_names( array $shipping_method_ids ): array {
- $shipping_method_map = array();
-
- try {
- // Get WooCommerce shipping instance.
- $shipping = WC()->shipping();
- if ( ! $shipping ) {
- return $shipping_method_map;
- }
-
- // Get all available shipping methods.
- $shipping_methods = $shipping->get_shipping_methods();
-
- foreach ( $shipping_method_ids as $method_id ) {
- if ( ! is_string( $method_id ) ) {
- continue;
- }
-
- // Sanitize the method ID.
- $method_id = sanitize_text_field( $method_id );
-
- // Shipping method IDs can be in format "method_id:instance_id".
- // Extract the base method ID.
- $method_parts = explode( ':', $method_id );
- $base_method_id = $method_parts[0];
- $instance_id = isset( $method_parts[1] ) ? $method_parts[1] : null;
-
- // Try to get the method label.
- $method_label = null;
-
- // If we have an instance ID, try to get the specific instance label.
- if ( $instance_id && WC()->session instanceof \WC_Session ) {
- // Get chosen shipping methods from session or packages.
- $packages = WC()->shipping()->get_packages();
-
- foreach ( $packages as $package ) {
- if ( isset( $package['rates'][ $method_id ] ) ) {
- $rate = $package['rates'][ $method_id ];
- $method_label = $rate->get_label();
- break;
- }
- }
- }
-
- // Fallback to base method title if no instance label found.
- if ( ! $method_label && isset( $shipping_methods[ $base_method_id ] ) ) {
- $method = $shipping_methods[ $base_method_id ];
- if ( method_exists( $method, 'get_method_title' ) ) {
- $method_label = $method->get_method_title();
- } elseif ( property_exists( $method, 'method_title' ) ) {
- $method_label = $method->method_title;
- }
- }
+ private function extract_payment_method( array $posted_data ): array {
+ $payment_data = array();
- // Use the method ID as fallback if no label found.
- if ( ! $method_label ) {
- $method_label = $method_id;
- }
+ if ( ! empty( $posted_data['payment_method'] ) ) {
+ $payment_gateway_name = WC()->payment_gateways()->get_payment_gateway_name_by_id( $posted_data['payment_method'] );
- $shipping_method_map[ $method_id ] = $method_label;
- }
- } catch ( \Exception $e ) {
- // Gracefully handle errors - return what we have so far.
- FraudProtectionController::log(
- 'warning',
- sprintf(
- 'Failed to get shipping method names: %s',
- $e->getMessage()
- ),
- array(
- 'shipping_method_ids' => $shipping_method_ids,
- 'exception' => $e,
- )
+ $payment_data['payment'] = array(
+ 'payment_gateway_type' => $posted_data['payment_method'],
+ 'payment_gateway_name' => $payment_gateway_name,
);
}
- return $shipping_method_map;
+ return $payment_data;
}
}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php
new file mode 100644
index 0000000000..50ad869794
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionDispatcher.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * FraudProtectionDispatcher class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Centralized fraud protection event dispatcher.
+ *
+ * This class provides a unified interface for dispatching fraud protection events.
+ * It logs events for the fraud protection service using already-collected data.
+ *
+ * @since 10.5.0
+ * @internal This class is part of the internal API and is subject to change without notice.
+ */
+class FraudProtectionDispatcher {
+
+ /**
+ * Track fraud protection event with already-collected data.
+ *
+ * This method accepts fully-collected event data (including session context)
+ * and logs it for the fraud protection service.
+ *
+ * The method implements graceful degradation - any errors during tracking
+ * will be logged but will not break the functionality.
+ *
+ * @param string $event_type Event type identifier (e.g., 'cart_item_added').
+ * @param array $collected_data Fully-collected event data including session context.
+ * @return void
+ */
+ public function dispatch_event( string $event_type, array $collected_data ): void {
+ try {
+ // 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, $collected_data );
+ //
+ // For now, log the event for debugging and verification.
+ FraudProtectionController::log(
+ 'info',
+ sprintf(
+ 'Fraud protection event tracked: %s | Session ID: %s',
+ $event_type,
+ $collected_data['session']['session_id'] ?? 'N/A'
+ ),
+ array(
+ 'event_type' => $event_type,
+ 'collected_data' => $collected_data,
+ )
+ );
+ } catch ( \Exception $e ) {
+ // Gracefully handle errors - fraud protection should never break functionality.
+ 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/src/Internal/FraudProtection/PaymentMethodEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
index d52af3f2a6..61b8229ef2 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
@@ -27,11 +27,11 @@ defined( 'ABSPATH' ) || exit;
class PaymentMethodEventTracker implements RegisterHooksInterface {
/**
- * Fraud protection tracker instance.
+ * Fraud protection dispatcher instance.
*
- * @var FraudProtectionTracker
+ * @var FraudProtectionDispatcher
*/
- private FraudProtectionTracker $tracker;
+ private FraudProtectionDispatcher $dispatcher;
/**
* Session data collector instance.
@@ -52,16 +52,16 @@ class PaymentMethodEventTracker implements RegisterHooksInterface {
*
* @internal
*
- * @param FraudProtectionTracker $tracker The fraud protection tracker instance.
+ * @param FraudProtectionDispatcher $dispatcher The fraud protection dispatcher instance.
* @param SessionDataCollector $data_collector The session data collector instance.
* @param FraudProtectionController $fraud_protection_controller The fraud protection controller instance.
*/
final public function init(
- FraudProtectionTracker $tracker,
+ FraudProtectionDispatcher $dispatcher,
SessionDataCollector $data_collector,
FraudProtectionController $fraud_protection_controller
): void {
- $this->tracker = $tracker;
+ $this->dispatcher = $dispatcher;
$this->data_collector = $data_collector;
$this->fraud_protection_controller = $fraud_protection_controller;
}
@@ -100,7 +100,7 @@ class PaymentMethodEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'payment_method_added', $event_data );
- $this->tracker->track_event( 'payment_method_added', $collected_data );
+ $this->dispatcher->dispatch_event( 'payment_method_added', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -140,7 +140,7 @@ class PaymentMethodEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'payment_method_updated', $event_data );
- $this->tracker->track_event( 'payment_method_updated', $collected_data );
+ $this->dispatcher->dispatch_event( 'payment_method_updated', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -174,7 +174,7 @@ class PaymentMethodEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'payment_method_set_default', $event_data );
- $this->tracker->track_event( 'payment_method_set_default', $collected_data );
+ $this->dispatcher->dispatch_event( 'payment_method_set_default', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
@@ -208,7 +208,7 @@ class PaymentMethodEventTracker implements RegisterHooksInterface {
// Collect comprehensive session data.
try {
$collected_data = $this->data_collector->collect( 'payment_method_deleted', $event_data );
- $this->tracker->track_event( 'payment_method_deleted', $collected_data );
+ $this->dispatcher->dispatch_event( 'payment_method_deleted', $collected_data );
} catch ( \Exception $e ) {
// Log error but don't break functionality.
FraudProtectionController::log(
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php b/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
index 99a19fb35b..57cfde9088 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
@@ -70,7 +70,6 @@ class SessionDataCollector {
'order' => $this->get_order_data( $order_id_from_event ),
'shipping_address' => $this->get_shipping_address(),
'billing_address' => $this->get_billing_address(),
- 'payment' => $this->get_payment_data( $event_data ),
'event_data' => $event_data,
);
}
@@ -132,59 +131,52 @@ class SessionDataCollector {
* @return array Customer data array with 4 keys.
*/
private function get_customer_data(): array {
+ $customer_data = array(
+ 'first_name' => null,
+ 'last_name' => null,
+ 'billing_email' => null,
+ 'lifetime_order_count' => 0,
+ );
try {
- $first_name = null;
- $last_name = null;
- $billing_email = null;
$lifetime_order_count = 0;
// Try WC_Customer object first.
if ( WC()->customer instanceof \WC_Customer ) {
- $first_name = WC()->customer->get_billing_first_name();
- $last_name = WC()->customer->get_billing_last_name();
- $billing_email = WC()->customer->get_billing_email();
-
if ( WC()->customer->get_id() > 0 ) {
// We need to reload the customer so it uses the correct data store to count the orders.
$customer = new \WC_Customer( WC()->customer->get_id() );
$lifetime_order_count = $customer->get_order_count();
}
- // Sanitize email.
- if ( $billing_email ) {
- $billing_email = \sanitize_email( $billing_email );
- }
+ $customer_data = array_merge(
+ $customer_data,
+ array(
+ 'first_name' => \sanitize_text_field( WC()->customer->get_billing_first_name() ),
+ 'last_name' => \sanitize_text_field( WC()->customer->get_billing_last_name() ),
+ 'billing_email' => \sanitize_email( \WC()->customer->get_billing_email() ),
+ 'lifetime_order_count' => $lifetime_order_count,
+ )
+ );
+
} elseif ( WC()->session instanceof \WC_Session ) {
// Fallback to session customer data if WC_Customer not available.
- $customer_data = WC()->session->get( 'customer' );
- if ( is_array( $customer_data ) ) {
- if ( ! empty( $customer_data['first_name'] ) ) {
- $first_name = \sanitize_text_field( $customer_data['first_name'] );
- }
- if ( ! empty( $customer_data['last_name'] ) ) {
- $last_name = \sanitize_text_field( $customer_data['last_name'] );
- }
- if ( ! empty( $customer_data['email'] ) ) {
- $billing_email = \sanitize_email( $customer_data['email'] );
- }
+ $customer_session_data = WC()->session->get( 'customer' );
+ if ( is_array( $customer_session_data ) ) {
+ $customer_data = array_merge(
+ $customer_data,
+ array(
+ 'first_name' => \sanitize_text_field( $customer_session_data['first_name'] ?? null ),
+ 'last_name' => \sanitize_text_field( $customer_session_data['last_name'] ?? null ),
+ 'billing_email' => \sanitize_email( $customer_session_data['email'] ?? null ),
+ )
+ );
}
}
-
- return array(
- 'first_name' => $first_name ? $first_name : null,
- 'last_name' => $last_name ? $last_name : null,
- 'billing_email' => $billing_email ? $billing_email : null,
- 'lifetime_order_count' => $lifetime_order_count,
- );
- } catch ( \Exception $e ) {
- // Graceful degradation - return structure with null values.
- return array(
- 'first_name' => null,
- 'last_name' => null,
- 'billing_email' => null,
- 'lifetime_order_count' => 0,
- );
+ } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // Graceful degradation - return as much data as possible.
}
+
+ return $customer_data;
}
/**
@@ -345,62 +337,62 @@ class SessionDataCollector {
* @return array Billing address array with 6 keys.
*/
private function get_billing_address(): array {
- try {
- $street = null;
- $street2 = null;
- $city = null;
- $state_province = null;
- $country = null;
- $zip_code = null;
+ $billing_data = array(
+ 'first_name' => null,
+ 'last_name' => null,
+ 'address' => null,
+ 'address_1' => null,
+ 'address_2' => null,
+ 'city' => null,
+ 'state' => null,
+ 'country' => null,
+ 'phone' => null,
+ 'postcode' => null,
+ );
+ try {
+ // Try WC_Customer object first.
if ( WC()->customer instanceof \WC_Customer ) {
- $street = WC()->customer->get_billing_address_1();
- $street2 = WC()->customer->get_billing_address_2();
- $city = WC()->customer->get_billing_city();
- $state_province = WC()->customer->get_billing_state();
- $country = WC()->customer->get_billing_country();
- $zip_code = WC()->customer->get_billing_postcode();
-
- // Sanitize all fields.
- if ( $street ) {
- $street = \sanitize_text_field( $street );
- }
- if ( $street2 ) {
- $street2 = \sanitize_text_field( $street2 );
- }
- if ( $city ) {
- $city = \sanitize_text_field( $city );
- }
- if ( $state_province ) {
- $state_province = \sanitize_text_field( $state_province );
- }
- if ( $country ) {
- $country = \sanitize_text_field( $country );
- }
- if ( $zip_code ) {
- $zip_code = \sanitize_text_field( $zip_code );
+ $billing_data = array_merge(
+ $billing_data,
+ array(
+ 'first_name' => \sanitize_text_field( WC()->customer->get_billing_first_name() ),
+ 'last_name' => \sanitize_text_field( WC()->customer->get_billing_last_name() ),
+ 'address_1' => \sanitize_text_field( WC()->customer->get_billing_address_1() ),
+ 'address_2' => \sanitize_text_field( WC()->customer->get_billing_address_2() ),
+ 'city' => \sanitize_text_field( WC()->customer->get_billing_city() ),
+ 'state' => \sanitize_text_field( WC()->customer->get_billing_state() ),
+ 'country' => \sanitize_text_field( WC()->customer->get_billing_country() ),
+ 'phone' => \sanitize_text_field( WC()->customer->get_billing_phone() ),
+ 'postcode' => \sanitize_text_field( WC()->customer->get_billing_postcode() ),
+ )
+ );
+ } elseif ( WC()->session instanceof \WC_Session ) {
+ // Fallback to session customer data if WC_Customer not available.
+ $customer_data = WC()->session->get( 'customer' );
+ if ( is_array( $customer_data ) ) {
+ $billing_data = array_merge(
+ $billing_data,
+ array(
+ 'first_name' => \sanitize_text_field( $customer_data['first_name'] ?? null ),
+ 'last_name' => \sanitize_text_field( $customer_data['last_name'] ?? null ),
+ 'address' => \sanitize_text_field( $customer_data['address'] ?? null ),
+ 'address_1' => \sanitize_text_field( $customer_data['address_1'] ?? null ),
+ 'address_2' => \sanitize_text_field( $customer_data['address_2'] ?? null ),
+ 'city' => \sanitize_text_field( $customer_data['city'] ?? null ),
+ 'state' => \sanitize_text_field( $customer_data['state'] ?? null ),
+ 'country' => \sanitize_text_field( $customer_data['country'] ?? null ),
+ 'phone' => \sanitize_text_field( $customer_data['phone'] ?? null ),
+ 'postcode' => \sanitize_text_field( $customer_data['postcode'] ?? null ),
+ )
+ );
}
}
-
- return array(
- 'street' => $street ? $street : null,
- 'street2' => $street2 ? $street2 : null,
- 'city' => $city ? $city : null,
- 'state_province' => $state_province ? $state_province : null,
- 'country' => $country ? $country : null,
- 'zip_code' => $zip_code ? $zip_code : null,
- );
- } catch ( \Exception $e ) {
- // Graceful degradation - return structure with null values.
- return array(
- 'street' => null,
- 'street2' => null,
- 'city' => null,
- 'state_province' => null,
- 'country' => null,
- 'zip_code' => null,
- );
+ } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // Graceful degradation - prevents any errors from being thrown.
}
+
+ return $billing_data;
}
/**
@@ -414,135 +406,56 @@ class SessionDataCollector {
* @return array Shipping address array with 6 keys.
*/
private function get_shipping_address(): array {
+ $shipping_data = array(
+ 'first_name' => null,
+ 'last_name' => null,
+ 'address' => null,
+ 'address_1' => null,
+ 'address_2' => null,
+ 'city' => null,
+ 'state' => null,
+ 'postcode' => null,
+ 'country' => null,
+ );
try {
- $street = null;
- $street2 = null;
- $city = null;
- $state_province = null;
- $country = null;
- $zip_code = null;
-
if ( WC()->customer instanceof \WC_Customer ) {
- $street = WC()->customer->get_shipping_address_1();
- $street2 = WC()->customer->get_shipping_address_2();
- $city = WC()->customer->get_shipping_city();
- $state_province = WC()->customer->get_shipping_state();
- $country = WC()->customer->get_shipping_country();
- $zip_code = WC()->customer->get_shipping_postcode();
-
- // Sanitize all fields.
- if ( $street ) {
- $street = \sanitize_text_field( $street );
- }
- if ( $street2 ) {
- $street2 = \sanitize_text_field( $street2 );
- }
- if ( $city ) {
- $city = \sanitize_text_field( $city );
- }
- if ( $state_province ) {
- $state_province = \sanitize_text_field( $state_province );
- }
- if ( $country ) {
- $country = \sanitize_text_field( $country );
- }
- if ( $zip_code ) {
- $zip_code = \sanitize_text_field( $zip_code );
+ $shipping_data = array_merge(
+ $shipping_data,
+ array(
+ 'first_name' => \sanitize_text_field( WC()->customer->get_shipping_first_name() ),
+ 'last_name' => \sanitize_text_field( WC()->customer->get_shipping_last_name() ),
+ 'address_1' => \sanitize_text_field( WC()->customer->get_shipping_address_1() ),
+ 'address_2' => \sanitize_text_field( WC()->customer->get_shipping_address_2() ),
+ 'city' => \sanitize_text_field( WC()->customer->get_shipping_city() ),
+ 'state' => \sanitize_text_field( WC()->customer->get_shipping_state() ),
+ 'postcode' => \sanitize_text_field( WC()->customer->get_shipping_postcode() ),
+ 'country' => \sanitize_text_field( WC()->customer->get_shipping_country() ),
+ )
+ );
+ } elseif ( WC()->session instanceof \WC_Session ) {
+ // Fallback to session customer data if WC_Customer not available.
+ $customer_data = WC()->session->get( 'customer' );
+ if ( is_array( $customer_data ) ) {
+ $shipping_data = array_merge(
+ $shipping_data,
+ array(
+ 'first_name' => \sanitize_text_field( $customer_data['shipping_first_name'] ?? null ),
+ 'last_name' => \sanitize_text_field( $customer_data['shipping_last_name'] ?? null ),
+ 'address_1' => \sanitize_text_field( $customer_data['shipping_address_1'] ?? null ),
+ 'address_2' => \sanitize_text_field( $customer_data['shipping_address_2'] ?? null ),
+ 'city' => \sanitize_text_field( $customer_data['shipping_city'] ?? null ),
+ 'state' => \sanitize_text_field( $customer_data['shipping_state'] ?? null ),
+ 'postcode' => \sanitize_text_field( $customer_data['shipping_postcode'] ?? null ),
+ 'country' => \sanitize_text_field( $customer_data['shipping_country'] ?? null ),
+ )
+ );
}
}
-
- return array(
- 'street' => $street ? $street : null,
- 'street2' => $street2 ? $street2 : null,
- 'city' => $city ? $city : null,
- 'state_province' => $state_province ? $state_province : null,
- 'country' => $country ? $country : null,
- 'zip_code' => $zip_code ? $zip_code : null,
- );
- } catch ( \Exception $e ) {
- // Graceful degradation - return structure with null values.
- return array(
- 'street' => null,
- 'street2' => null,
- 'city' => null,
- 'state_province' => null,
- 'country' => null,
- 'zip_code' => null,
- );
+ } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // Graceful degradation - returns as much data as possible.
}
- }
-
- /**
- * Get payment data structure for fraud protection analysis.
- *
- * Returns payment data structure with all 11 supported fields. Currently populates
- * payment_gateway_name and payment_method_type when available from the chosen payment
- * method. Other fields are initialized with null values.
- *
- * @since 10.5.0
- *
- * @param array $event_data Event-specific data that may contain payment information.
- * @return array Payment data array with 11 keys.
- */
- private function get_payment_data( array $event_data = array() ): array {
- $payment_data = array(
- 'payment_gateway_name' => null,
- 'payment_method_type' => null,
- 'card_bin' => null,
- 'card_last4' => null,
- 'card_brand' => null,
- 'payer_id' => null,
- 'outcome' => null,
- 'decline_reason' => null,
- 'avs_result' => null,
- 'cvc_result' => null,
- 'tokenized_card_identifier' => null,
- );
- try {
- if ( ! empty( $event_data['payment'] ) ) {
- return array_merge( $payment_data, $event_data['payment'] );
- }
-
- // Try to get chosen payment method from session.
- $chosen_payment_method = $this->get_chosen_payment_method();
- if ( $chosen_payment_method ) {
- $payment_data['payment_gateway_name'] = \sanitize_text_field( $chosen_payment_method );
- $payment_data['payment_method_type'] = \sanitize_text_field( $chosen_payment_method );
- }
-
- return $payment_data;
- } catch ( \Exception $e ) {
- // Graceful degradation.
- return $payment_data;
- }
- }
-
- /**
- * Get the chosen payment method from session or POST data.
- *
- * Tries to get payment method from session first, then falls back to
- * POST data during checkout submission.
- *
- * @since 10.5.0
- *
- * @return string|null Payment method ID or null if not available.
- */
- private function get_chosen_payment_method(): ?string {
- // Try getting from session first.
- if ( WC()->session instanceof \WC_Session ) {
- $chosen_payment_method = WC()->session->get( 'chosen_payment_method' );
- if ( $chosen_payment_method ) {
- return $chosen_payment_method;
- }
- }
-
- // Try getting from POST data (during checkout).
- if ( isset( $_POST['payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
- return \sanitize_text_field( \wp_unslash( $_POST['payment_method'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
- }
-
- return null;
+ return $shipping_data;
}
/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
index 7fd63372c6..db0dd9348c 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartSelectShippingRate.php
@@ -38,27 +38,27 @@ class CartSelectShippingRate extends AbstractCartRoute {
* @return array An array of endpoints.
*/
public function get_args() {
- return [
- [
+ return array(
+ array(
'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => [ $this, 'get_response' ],
+ 'callback' => array( $this, 'get_response' ),
'permission_callback' => '__return_true',
- 'args' => [
+ 'args' => array(
'package_id' => array(
'description' => __( 'The ID of the package being shipped. Leave blank to apply to all packages.', 'woocommerce' ),
- 'type' => [ 'integer', 'string', 'null' ],
+ 'type' => array( 'integer', 'string', 'null' ),
'required' => false,
),
- 'rate_id' => [
+ 'rate_id' => array(
'description' => __( 'The chosen rate ID for the package.', 'woocommerce' ),
'type' => 'string',
'required' => true,
- ],
- ],
- ],
- 'schema' => [ $this->schema, 'get_public_item_schema' ],
- 'allow_batch' => [ 'v1' => true ],
- ];
+ ),
+ ),
+ ),
+ 'schema' => array( $this->schema, 'get_public_item_schema' ),
+ 'allow_batch' => array( 'v1' => true ),
+ );
}
/**
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
index 9705042918..7728b48b31 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
@@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
+use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
+use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
/**
@@ -42,31 +44,31 @@ class CartUpdateCustomer extends AbstractCartRoute {
* @return array An array of endpoints.
*/
public function get_args() {
- return [
- [
+ return array(
+ array(
'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => [ $this, 'get_response' ],
+ 'callback' => array( $this, 'get_response' ),
'permission_callback' => '__return_true',
- 'args' => [
- 'billing_address' => [
+ 'args' => array(
+ 'billing_address' => array(
'description' => __( 'Billing address.', 'woocommerce' ),
'type' => 'object',
- 'context' => [ 'view', 'edit' ],
+ 'context' => array( 'view', 'edit' ),
'properties' => $this->schema->billing_address_schema->get_properties(),
'sanitize_callback' => null,
- ],
- 'shipping_address' => [
+ ),
+ 'shipping_address' => array(
'description' => __( 'Shipping address.', 'woocommerce' ),
'type' => 'object',
- 'context' => [ 'view', 'edit' ],
+ 'context' => array( 'view', 'edit' ),
'properties' => $this->schema->shipping_address_schema->get_properties(),
'sanitize_callback' => null,
- ],
- ],
- ],
- 'schema' => [ $this->schema, 'get_public_item_schema' ],
- 'allow_batch' => [ 'v1' => true ],
- ];
+ ),
+ ),
+ ),
+ 'schema' => array( $this->schema, 'get_public_item_schema' ),
+ 'allow_batch' => array( 'v1' => true ),
+ );
}
/**
@@ -110,11 +112,11 @@ class CartUpdateCustomer extends AbstractCartRoute {
'rest_invalid_param',
/* translators: %s: List of invalid parameters. */
sprintf( __( 'Invalid parameter(s): %s', 'woocommerce' ), implode( ', ', array_keys( $invalid_params ) ) ),
- [
+ array(
'status' => 400,
'params' => $invalid_params,
'details' => $invalid_details,
- ]
+ )
);
}
@@ -132,8 +134,8 @@ class CartUpdateCustomer extends AbstractCartRoute {
$customer = wc()->customer;
// Get data from request object and merge with customer object.
- $billing = wp_parse_args( $request['billing_address'] ?? [], $this->get_customer_billing_address( $customer ) );
- $shipping = wp_parse_args( $request['shipping_address'] ?? [], $this->get_customer_shipping_address( $customer ) );
+ $billing = wp_parse_args( $request['billing_address'] ?? array(), $this->get_customer_billing_address( $customer ) );
+ $shipping = wp_parse_args( $request['shipping_address'] ?? array(), $this->get_customer_shipping_address( $customer ) );
// If the cart does not need shipping, shipping address is forced to match billing address unless defined.
if ( ! $cart->needs_shipping() && ! isset( $request['shipping_address'] ) ) {
@@ -213,6 +215,11 @@ class CartUpdateCustomer extends AbstractCartRoute {
$customer->save();
+ $container = wc_get_container();
+ if ( $container->get( FraudProtectionController::class )->feature_is_enabled() ) {
+ $container->get( CheckoutEventTracker::class )->track_blocks_checkout_update();
+ }
+
$this->cart_controller->calculate_totals();
return rest_ensure_response( $this->schema->get_item_response( $cart ) );
@@ -228,7 +235,7 @@ class CartUpdateCustomer extends AbstractCartRoute {
$additional_fields = $this->additional_fields_controller->get_all_fields_from_object( $customer, 'billing' );
return array_merge(
- [
+ array(
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
@@ -240,7 +247,7 @@ class CartUpdateCustomer extends AbstractCartRoute {
'country' => $customer->get_billing_country(),
'phone' => $customer->get_billing_phone(),
'email' => $customer->get_billing_email(),
- ],
+ ),
$additional_fields
);
}
@@ -255,7 +262,7 @@ class CartUpdateCustomer extends AbstractCartRoute {
$additional_fields = $this->additional_fields_controller->get_all_fields_from_object( $customer, 'shipping' );
return array_merge(
- [
+ array(
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
@@ -266,7 +273,7 @@ class CartUpdateCustomer extends AbstractCartRoute {
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
'phone' => $customer->get_shipping_phone(),
- ],
+ ),
$additional_fields
);
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
index d1f5fd87d5..7bc33f73d7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
@@ -8,7 +8,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionTracker;
+use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
@@ -29,9 +29,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
/**
* Mock fraud protection tracker.
*
- * @var FraudProtectionTracker|\PHPUnit\Framework\MockObject\MockObject
+ * @var FraudProtectionDispatcher|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_tracker;
+ private $mock_dispatcher;
/**
* Mock session data collector.
@@ -66,14 +66,14 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
}
// Create mocks.
- $this->mock_tracker = $this->createMock( FraudProtectionTracker::class );
+ $this->mock_dispatcher = $this->createMock( FraudProtectionDispatcher::class );
$this->mock_data_collector = $this->createMock( SessionDataCollector::class );
$this->mock_controller = $this->createMock( FraudProtectionController::class );
// Create system under test.
$this->sut = new CartEventTracker();
$this->sut->init(
- $this->mock_tracker,
+ $this->mock_dispatcher,
$this->mock_data_collector,
$this->mock_controller
);
@@ -138,9 +138,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
->willReturn( $collected_data );
// Mock the tracker to verify track_event is called with collected data.
- $this->mock_tracker
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'cart_item_added' ),
$this->equalTo( $collected_data )
@@ -178,9 +178,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
->willReturn( $collected_data );
// Mock the tracker.
- $this->mock_tracker
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'cart_item_updated' ),
$this->equalTo( $collected_data )
@@ -214,9 +214,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
->willReturn( $collected_data );
// Mock the tracker.
- $this->mock_tracker
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'cart_item_removed' ),
$this->equalTo( $collected_data )
@@ -248,9 +248,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
->willReturn( $collected_data );
// Mock the tracker.
- $this->mock_tracker
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'cart_item_restored' ),
$this->equalTo( $collected_data )
@@ -285,9 +285,9 @@ class CartEventTrackerTest extends \WC_Unit_Test_Case {
->willReturn( $collected_data );
// Mock the tracker to capture event data.
- $this->mock_tracker
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'cart_item_added' ),
$this->equalTo( $collected_data )
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
index 9e66c02c33..4d8a827188 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
@@ -8,9 +8,9 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
+use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
+use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
/**
* Tests for CheckoutEventTracker.
@@ -27,25 +27,25 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
private $sut;
/**
- * Mock fraud protection tracker.
+ * Mock fraud protection dispatcher.
*
- * @var FraudProtectionTracker|\PHPUnit\Framework\MockObject\MockObject
+ * @var FraudProtectionDispatcher|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_tracker;
+ private $mock_dispatcher;
/**
- * Mock session data collector.
+ * Mock fraud protection controller.
*
- * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
+ * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_data_collector;
+ private $mock_controller;
/**
- * Mock fraud protection controller.
+ * Mock session data collector.
*
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
+ * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
*/
- private $mock_controller;
+ private $mock_data_collector;
/**
* Runs before each test.
@@ -59,16 +59,16 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
}
// Create mocks.
- $this->mock_tracker = $this->createMock( FraudProtectionTracker::class );
- $this->mock_data_collector = $this->createMock( SessionDataCollector::class );
+ $this->mock_dispatcher = $this->createMock( FraudProtectionDispatcher::class );
$this->mock_controller = $this->createMock( FraudProtectionController::class );
+ $this->mock_data_collector = $this->createMock( SessionDataCollector::class );
// Create system under test.
$this->sut = new CheckoutEventTracker();
$this->sut->init(
- $this->mock_tracker,
- $this->mock_data_collector,
- $this->mock_controller
+ $this->mock_dispatcher,
+ $this->mock_controller,
+ $this->mock_data_collector
);
}
@@ -83,11 +83,11 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
$this->sut->register();
// Verify hooks were not registered.
- $this->assertFalse( has_action( 'woocommerce_checkout_update_order_review', array( $this->sut, 'handle_checkout_field_update' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_checkout_update_order_review', array( $this->sut, 'handle_shortcode_checkout_field_update' ) ) );
}
/**
- * Test that register registers hooks when feature is enabled.
+ * Test that register registers shortcode checkout hooks when feature is enabled.
*/
public function test_register_registers_hooks_when_feature_enabled(): void {
// Mock feature as enabled.
@@ -96,36 +96,98 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// Call register.
$this->sut->register();
- // Verify hooks were registered.
- $this->assertNotFalse( has_action( 'woocommerce_checkout_update_order_review', array( $this->sut, 'handle_checkout_field_update' ) ) );
+ // Verify hook was registered.
+ $this->assertNotFalse( has_action( 'woocommerce_checkout_update_order_review', array( $this->sut, 'handle_shortcode_checkout_field_update' ) ) );
}
+ // ========================================
+ // Blocks Checkout Tests
+ // ========================================
+
/**
- * Test handle_checkout_field_update tracks event with billing email immediately.
+ * Test track_blocks_checkout_update dispatches event with session data.
*/
- public function test_handle_checkout_field_update_tracks_event_with_billing_email(): void {
- // Mock feature as enabled.
- $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- $collected_data = array(
- 'action' => 'field_update',
- 'billing_email' => 'test@example.com',
- 'session' => array( 'session_id' => 'test-session' ),
+ public function test_track_blocks_checkout_update_dispatches_event_with_session_data(): void {
+ // Mock data collector to return session data.
+ $session_data = array(
+ 'session_id' => 'test_session_123',
+ 'billing_email' => 'test@example.com',
+ 'billing_address' => array(
+ 'first_name' => 'John',
+ 'last_name' => 'Doe',
+ ),
+ 'shipping_address' => array(
+ 'city' => 'New York',
+ ),
);
-
- // Mock data collector to return collected data.
$this->mock_data_collector
->expects( $this->once() )
->method( 'collect' )
- ->willReturn( $collected_data );
+ ->with(
+ $this->equalTo( 'checkout_update' ),
+ $this->equalTo( array() )
+ )
+ ->willReturn( $session_data );
- // Mock tracker to verify track_event is called immediately with collected data.
- $this->mock_tracker
+ // Mock dispatcher to verify event is dispatched.
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
+ ->method( 'dispatch_event' )
+ ->with(
+ $this->equalTo( 'checkout_update' ),
+ $this->equalTo( $session_data )
+ );
+
+ // Call the method.
+ $this->sut->track_blocks_checkout_update();
+ }
+
+ /**
+ * Test track_blocks_checkout_update can be called directly without hooks.
+ */
+ public function test_track_blocks_checkout_update_works_without_hooks(): void {
+ // Mock data collector to return minimal session data.
+ $session_data = array(
+ 'session_id' => 'test_session_456',
+ );
+ $this->mock_data_collector
+ ->method( 'collect' )
+ ->willReturn( $session_data );
+
+ // Mock dispatcher to verify event is dispatched.
+ $this->mock_dispatcher
+ ->expects( $this->once() )
+ ->method( 'dispatch_event' );
+
+ // Call the method directly (as done from CartUpdateCustomer endpoint).
+ $this->sut->track_blocks_checkout_update();
+ }
+
+ // ========================================
+ // Shortcode Checkout Tests
+ // ========================================
+
+ /**
+ * Test handle_shortcode_checkout_field_update schedules event with billing email.
+ */
+ public function test_handle_shortcode_checkout_field_update_schedules_event_with_billing_email(): void {
+ // Mock feature as enabled.
+ $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
+
+ // Mock scheduler to verify dispatch_event is called.
+ $this->mock_dispatcher
+ ->expects( $this->once() )
+ ->method( 'dispatch_event' )
->with(
$this->equalTo( 'checkout_field_update' ),
- $this->equalTo( $collected_data )
+ $this->callback(
+ function ( $event_data ) {
+ return isset( $event_data['action'] )
+ && 'field_update' === $event_data['action']
+ && isset( $event_data['billing_email'] )
+ && 'test@example.com' === $event_data['billing_email'];
+ }
+ )
);
// Register hooks.
@@ -133,44 +195,104 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
// Simulate checkout field update with billing email.
$posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe';
- $this->sut->handle_checkout_field_update( $posted_data );
+ $this->sut->handle_shortcode_checkout_field_update( $posted_data );
}
/**
- * Test handle_checkout_field_update tracks shipping method immediately.
+ * Test handle_shortcode_checkout_field_update extracts billing fields correctly.
*/
- public function test_handle_checkout_field_update_tracks_shipping_method(): void {
+ public function test_handle_shortcode_checkout_field_update_extracts_billing_fields(): void {
// Mock feature as enabled.
$this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
- $collected_data = array(
- 'action' => 'field_update',
- 'billing_email' => 'test@example.com',
- 'shipping_methods' => array( 'flat_rate:1' => 'Flat rate' ),
- 'session' => array( 'session_id' => 'test-session' ),
- );
+ // Mock scheduler to capture event data.
+ $captured_event_data = null;
+ $this->mock_dispatcher
+ ->expects( $this->once() )
+ ->method( 'dispatch_event' )
+ ->willReturnCallback(
+ function ( $event_type, $event_data ) use ( &$captured_event_data ) {
+ $captured_event_data = $event_data;
+ }
+ );
- // Mock data collector to return collected data.
- $this->mock_data_collector
+ // Register hooks.
+ $this->sut->register();
+
+ // Simulate checkout field update with multiple billing fields.
+ $posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe&billing_country=US&billing_city=New+York';
+ $this->sut->handle_shortcode_checkout_field_update( $posted_data );
+
+ // Verify extracted fields.
+ $this->assertNotNull( $captured_event_data );
+ $this->assertEquals( 'field_update', $captured_event_data['action'] );
+ $this->assertEquals( 'test@example.com', $captured_event_data['billing_email'] );
+ $this->assertEquals( 'John', $captured_event_data['billing_first_name'] );
+ $this->assertEquals( 'Doe', $captured_event_data['billing_last_name'] );
+ $this->assertEquals( 'US', $captured_event_data['billing_country'] );
+ $this->assertEquals( 'New York', $captured_event_data['billing_city'] );
+ }
+
+ /**
+ * Test handle_shortcode_checkout_field_update extracts shipping fields when ship_to_different_address is set.
+ */
+ public function test_handle_shortcode_checkout_field_update_extracts_shipping_fields(): void {
+ // Mock feature as enabled.
+ $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
+
+ // Mock scheduler to capture event data.
+ $captured_event_data = null;
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'collect' )
- ->willReturn( $collected_data );
+ ->method( 'dispatch_event' )
+ ->willReturnCallback(
+ function ( $event_type, $event_data ) use ( &$captured_event_data ) {
+ $captured_event_data = $event_data;
+ }
+ );
- // Mock tracker to verify track_event is called with collected data.
- $this->mock_tracker
+ // Register hooks.
+ $this->sut->register();
+
+ // Simulate checkout field update with shipping fields.
+ $posted_data = 'billing_email=test@example.com&ship_to_different_address=1&shipping_first_name=Jane&shipping_last_name=Smith&shipping_city=Los+Angeles';
+ $this->sut->handle_shortcode_checkout_field_update( $posted_data );
+
+ // Verify extracted fields.
+ $this->assertNotNull( $captured_event_data );
+ $this->assertEquals( 'Jane', $captured_event_data['shipping_first_name'] );
+ $this->assertEquals( 'Smith', $captured_event_data['shipping_last_name'] );
+ $this->assertEquals( 'Los Angeles', $captured_event_data['shipping_city'] );
+ }
+
+ /**
+ * Test handle_shortcode_checkout_field_update does not extract shipping fields when ship_to_different_address is not set.
+ */
+ public function test_handle_shortcode_checkout_field_update_skips_shipping_fields_when_not_different_address(): void {
+ // Mock feature as enabled.
+ $this->mock_controller->method( 'feature_is_enabled' )->willReturn( true );
+
+ // Mock scheduler to capture event data.
+ $captured_event_data = null;
+ $this->mock_dispatcher
->expects( $this->once() )
- ->method( 'track_event' )
- ->with(
- $this->equalTo( 'checkout_field_update' ),
- $this->equalTo( $collected_data )
+ ->method( 'dispatch_event' )
+ ->willReturnCallback(
+ function ( $event_type, $event_data ) use ( &$captured_event_data ) {
+ $captured_event_data = $event_data;
+ }
);
// Register hooks.
$this->sut->register();
- // Simulate checkout field update with shipping method.
- // shipping_method is passed as an array in the posted data.
- $posted_data = 'billing_email=test@example.com&shipping_method[0]=flat_rate:1';
- $this->sut->handle_checkout_field_update( $posted_data );
+ // Simulate checkout field update without ship_to_different_address.
+ $posted_data = 'billing_email=test@example.com&shipping_first_name=Jane&shipping_last_name=Smith';
+ $this->sut->handle_shortcode_checkout_field_update( $posted_data );
+
+ // Verify shipping fields are not extracted.
+ $this->assertNotNull( $captured_event_data );
+ $this->assertArrayNotHasKey( 'shipping_first_name', $captured_event_data );
+ $this->assertArrayNotHasKey( 'shipping_last_name', $captured_event_data );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php
new file mode 100644
index 0000000000..e8d62afbcd
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionDispatcherTest.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * FraudProtectionDispatcherTest class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher;
+use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
+
+/**
+ * Tests for FraudProtectionDispatcher.
+ *
+ * @covers \Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionDispatcher
+ */
+class FraudProtectionDispatcherTest extends \WC_Unit_Test_Case {
+
+ use LoggerSpyTrait;
+
+ /**
+ * The system under test.
+ *
+ * @var FraudProtectionDispatcher
+ */
+ private $sut;
+
+ /**
+ * Runs before each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ // Create system under test.
+ $this->sut = new FraudProtectionDispatcher();
+ }
+
+ /**
+ * Test track_event logs collected data successfully.
+ */
+ public function test_track_event_logs_collected_data_successfully(): void {
+ $event_type = 'test_event';
+ $collected_data = array(
+ 'session' => array( 'session_id' => 'test-session-123' ),
+ 'action' => 'test_action',
+ 'product_id' => 123,
+ );
+
+ // Call track_event.
+ $this->sut->dispatch_event( $event_type, $collected_data );
+
+ // Verify the log was captured.
+ $this->assertCount( 1, $this->captured_logs );
+ $this->assertEquals( 'info', $this->captured_logs[0]['level'] );
+ $this->assertStringContainsString( 'test_event', $this->captured_logs[0]['message'] );
+ $this->assertStringContainsString( 'test-session-123', $this->captured_logs[0]['message'] );
+ $this->assertEquals( 'test_event', $this->captured_logs[0]['context']['event_type'] );
+ $this->assertEquals( $collected_data, $this->captured_logs[0]['context']['collected_data'] );
+ }
+
+ /**
+ * Test track_event handles data without session gracefully.
+ */
+ public function test_track_event_handles_data_without_session_gracefully(): void {
+ $event_type = 'test_event';
+ $collected_data = array( 'invalid' => 'data_without_session' );
+
+ // Call track_event with data without session - should handle gracefully and log with N/A.
+ $this->sut->dispatch_event( $event_type, $collected_data );
+
+ // Verify the log was captured with N/A for session ID.
+ $this->assertCount( 1, $this->captured_logs );
+ $this->assertEquals( 'info', $this->captured_logs[0]['level'] );
+ $this->assertStringContainsString( 'test_event', $this->captured_logs[0]['message'] );
+ $this->assertStringContainsString( 'N/A', $this->captured_logs[0]['message'] );
+ $this->assertEquals( 'test_event', $this->captured_logs[0]['context']['event_type'] );
+ $this->assertEquals( $collected_data, $this->captured_logs[0]['context']['collected_data'] );
+ }
+
+ /**
+ * Test track_event accepts various data structures.
+ */
+ public function test_track_event_accepts_various_data_structures(): void {
+ $event_type = 'cart_item_added';
+ $collected_data = array(
+ 'session' => array( 'session_id' => 'test' ),
+ 'action' => 'item_added',
+ 'product_id' => 456,
+ 'quantity' => 2,
+ );
+
+ // Call track_event.
+ $this->sut->dispatch_event( $event_type, $collected_data );
+
+ // Verify the log was captured with all data.
+ $this->assertCount( 1, $this->captured_logs );
+ $this->assertEquals( 'info', $this->captured_logs[0]['level'] );
+ $this->assertStringContainsString( 'cart_item_added', $this->captured_logs[0]['message'] );
+ $this->assertEquals( 'cart_item_added', $this->captured_logs[0]['context']['event_type'] );
+ $this->assertEquals( $collected_data, $this->captured_logs[0]['context']['collected_data'] );
+ $this->assertEquals( 'item_added', $this->captured_logs[0]['context']['collected_data']['action'] );
+ $this->assertEquals( 456, $this->captured_logs[0]['context']['collected_data']['product_id'] );
+ $this->assertEquals( 2, $this->captured_logs[0]['context']['collected_data']['quantity'] );
+ }
+
+ /**
+ * Test track_event works with empty collected data.
+ */
+ public function test_track_event_works_with_empty_collected_data(): void {
+ $event_type = 'test_event';
+ $collected_data = array();
+
+ // Call track_event with empty data.
+ $this->sut->dispatch_event( $event_type, $collected_data );
+
+ // Verify the log was captured even with empty data.
+ $this->assertCount( 1, $this->captured_logs );
+ $this->assertEquals( 'info', $this->captured_logs[0]['level'] );
+ $this->assertStringContainsString( 'test_event', $this->captured_logs[0]['message'] );
+ $this->assertStringContainsString( 'N/A', $this->captured_logs[0]['message'] );
+ $this->assertEquals( 'test_event', $this->captured_logs[0]['context']['event_type'] );
+ $this->assertEquals( $collected_data, $this->captured_logs[0]['context']['collected_data'] );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
index 6fb0fb61f3..e05694e583 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
@@ -51,7 +51,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test that collect() method returns properly structured nested array with 9 top-level keys.
+ * Test that collect() method returns properly structured nested array with 8 top-level keys.
*/
public function test_collect_returns_properly_structured_nested_array() {
$result = $this->sut->collect();
@@ -64,9 +64,8 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertArrayHasKey( 'order', $result );
$this->assertArrayHasKey( 'shipping_address', $result );
$this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'payment', $result );
$this->assertArrayHasKey( 'event_data', $result );
- $this->assertCount( 9, $result );
+ $this->assertCount( 8, $result );
}
/**
@@ -95,7 +94,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$result = $this->sut->collect();
$this->assertIsArray( $result );
- $this->assertCount( 9, $result );
+ $this->assertCount( 8, $result );
// All sections should be initialized even if session unavailable.
$this->assertIsArray( $result['session'] );
$this->assertIsArray( $result['customer'] );
@@ -142,7 +141,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['payment'] );
}
/**
@@ -447,7 +445,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test billing address includes all 6 required fields.
+ * Test billing address includes all required fields.
*/
public function test_billing_address_includes_all_required_fields() {
// Set billing address data.
@@ -463,26 +461,26 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$result = $this->sut->collect();
$this->assertIsArray( $result['billing_address'] );
- $this->assertArrayHasKey( 'street', $result['billing_address'] );
- $this->assertArrayHasKey( 'street2', $result['billing_address'] );
+ $this->assertArrayHasKey( 'address_1', $result['billing_address'] );
+ $this->assertArrayHasKey( 'address_2', $result['billing_address'] );
$this->assertArrayHasKey( 'city', $result['billing_address'] );
- $this->assertArrayHasKey( 'state_province', $result['billing_address'] );
+ $this->assertArrayHasKey( 'state', $result['billing_address'] );
$this->assertArrayHasKey( 'country', $result['billing_address'] );
- $this->assertArrayHasKey( 'zip_code', $result['billing_address'] );
+ $this->assertArrayHasKey( 'postcode', $result['billing_address'] );
// Verify values.
if ( isset( WC()->customer ) ) {
- $this->assertEquals( '123 Main St', $result['billing_address']['street'] );
- $this->assertEquals( 'Apt 4B', $result['billing_address']['street2'] );
+ $this->assertEquals( '123 Main St', $result['billing_address']['address_1'] );
+ $this->assertEquals( 'Apt 4B', $result['billing_address']['address_2'] );
$this->assertEquals( 'New York', $result['billing_address']['city'] );
- $this->assertEquals( 'NY', $result['billing_address']['state_province'] );
+ $this->assertEquals( 'NY', $result['billing_address']['state'] );
$this->assertEquals( 'US', $result['billing_address']['country'] );
- $this->assertEquals( '10001', $result['billing_address']['zip_code'] );
+ $this->assertEquals( '10001', $result['billing_address']['postcode'] );
}
}
/**
- * Test shipping address includes all 6 required fields.
+ * Test shipping address includes all required fields.
*/
public function test_shipping_address_includes_all_required_fields() {
// Set shipping address data.
@@ -498,21 +496,21 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$result = $this->sut->collect();
$this->assertIsArray( $result['shipping_address'] );
- $this->assertArrayHasKey( 'street', $result['shipping_address'] );
- $this->assertArrayHasKey( 'street2', $result['shipping_address'] );
+ $this->assertArrayHasKey( 'address_1', $result['shipping_address'] );
+ $this->assertArrayHasKey( 'address_2', $result['shipping_address'] );
$this->assertArrayHasKey( 'city', $result['shipping_address'] );
- $this->assertArrayHasKey( 'state_province', $result['shipping_address'] );
+ $this->assertArrayHasKey( 'state', $result['shipping_address'] );
$this->assertArrayHasKey( 'country', $result['shipping_address'] );
- $this->assertArrayHasKey( 'zip_code', $result['shipping_address'] );
+ $this->assertArrayHasKey( 'postcode', $result['shipping_address'] );
// Verify values.
if ( isset( WC()->customer ) ) {
- $this->assertEquals( '456 Oak Ave', $result['shipping_address']['street'] );
- $this->assertEquals( 'Suite 100', $result['shipping_address']['street2'] );
+ $this->assertEquals( '456 Oak Ave', $result['shipping_address']['address_1'] );
+ $this->assertEquals( 'Suite 100', $result['shipping_address']['address_2'] );
$this->assertEquals( 'Los Angeles', $result['shipping_address']['city'] );
- $this->assertEquals( 'CA', $result['shipping_address']['state_province'] );
+ $this->assertEquals( 'CA', $result['shipping_address']['state'] );
$this->assertEquals( 'US', $result['shipping_address']['country'] );
- $this->assertEquals( '90001', $result['shipping_address']['zip_code'] );
+ $this->assertEquals( '90001', $result['shipping_address']['postcode'] );
}
}
@@ -582,27 +580,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
}
/**
- * Test payment data structure includes all 11 required fields.
- */
- public function test_payment_data_includes_all_required_fields() {
- $result = $this->sut->collect();
-
- $this->assertIsArray( $result['payment'] );
- $this->assertArrayHasKey( 'payment_gateway_name', $result['payment'] );
- $this->assertArrayHasKey( 'payment_method_type', $result['payment'] );
- $this->assertArrayHasKey( 'card_bin', $result['payment'] );
- $this->assertArrayHasKey( 'card_last4', $result['payment'] );
- $this->assertArrayHasKey( 'card_brand', $result['payment'] );
- $this->assertArrayHasKey( 'payer_id', $result['payment'] );
- $this->assertArrayHasKey( 'outcome', $result['payment'] );
- $this->assertArrayHasKey( 'decline_reason', $result['payment'] );
- $this->assertArrayHasKey( 'avs_result', $result['payment'] );
- $this->assertArrayHasKey( 'cvc_result', $result['payment'] );
- $this->assertArrayHasKey( 'tokenized_card_identifier', $result['payment'] );
- }
-
- /**
- * Test complete collect() output includes all 9 top-level sections with data.
+ * Test complete collect() output includes all 8 top-level sections with data.
*/
public function test_complete_collect_output_includes_all_sections() {
// Create a logged-in user.
@@ -629,7 +607,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$result = $this->sut->collect( 'checkout_started', array( 'test' => 'data' ) );
- // Verify all 9 sections exist.
+ // Verify all 8 sections exist.
$this->assertArrayHasKey( 'event_type', $result );
$this->assertArrayHasKey( 'timestamp', $result );
$this->assertArrayHasKey( 'session', $result );
@@ -637,7 +615,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertArrayHasKey( 'order', $result );
$this->assertArrayHasKey( 'shipping_address', $result );
$this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'payment', $result );
$this->assertArrayHasKey( 'event_data', $result );
// Verify sections contain expected data types.
@@ -648,7 +625,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['payment'] );
$this->assertEquals( array( 'test' => 'data' ), $result['event_data'] );
}
@@ -730,17 +706,13 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertCount( 2, $result['order']['items'] );
// Billing address.
- $this->assertEquals( '123 Test St', $result['billing_address']['street'] );
+ $this->assertEquals( '123 Test St', $result['billing_address']['address_1'] );
$this->assertEquals( 'Test City', $result['billing_address']['city'] );
// Shipping address.
- $this->assertEquals( '456 Ship St', $result['shipping_address']['street'] );
+ $this->assertEquals( '456 Ship St', $result['shipping_address']['address_1'] );
$this->assertEquals( 'Ship City', $result['shipping_address']['city'] );
- // Payment data structure exists.
- $this->assertIsArray( $result['payment'] );
- $this->assertArrayHasKey( 'payment_gateway_name', $result['payment'] );
-
// Event data.
$this->assertEquals( array( 'gateway' => 'stripe' ), $result['event_data'] );
}
@@ -770,7 +742,7 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
// Verify structure is intact even with minimal data.
$this->assertIsArray( $result );
- $this->assertCount( 9, $result );
+ $this->assertCount( 8, $result );
// All sections should be arrays.
$this->assertIsArray( $result['session'] );
@@ -778,7 +750,6 @@ class SessionDataCollectorTest extends \WC_Unit_Test_Case {
$this->assertIsArray( $result['order'] );
$this->assertIsArray( $result['shipping_address'] );
$this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['payment'] );
// Key fields should have appropriate defaults.
$this->assertEquals( 'guest', $result['order']['customer_id'] );