Commit 08fb5bd490 for woocommerce
commit 08fb5bd490a3e860536a6ea2ce67bb6608102d0e
Author: Luiz Reis <luiz.reis@automattic.com>
Date: Mon Feb 16 16:34:42 2026 -0300
Fraud Protection: Remove code from WooCommerce Core (#63123)
* Remove FraudProtection direct code from WooCommerce Core
Remove all FraudProtection integration points from Core in preparation
for moving to a separate hooks-based repository. This removes tracking
calls, session blocking checks, use statements, DI container registration,
the feature flag, and deletes the entire FraudProtection source and test
directories (21 files) plus the Blackbox JS init script.
diff --git a/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js b/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js
deleted file mode 100644
index 8c51eed96e..0000000000
--- a/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Woo Fraud Protection - Blackbox Initialization
- *
- * Configures the Blackbox JS SDK with the site's API key and blog ID.
- * Loaded on checkout, pay-for-order, and add-payment-method pages.
- */
-( function () {
- 'use strict';
-
- var config = window.wcBlackboxConfig;
- if ( ! config ) {
- return;
- }
-
- if ( ! window.Blackbox || ! window.Blackbox.configure ) {
- return;
- }
-
- window.Blackbox.configure( {
- apiKey: config.apiKey,
- blogId: config.blogId,
- } );
-} )();
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 20123fb36b..626b9e6771 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -15,8 +15,6 @@ use Automattic\WooCommerce\Internal\Orders\CouponsController;
use Automattic\WooCommerce\Internal\Orders\TaxesController;
use Automattic\WooCommerce\Internal\Orders\OrderNoteGroup;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
@@ -404,12 +402,6 @@ class WC_AJAX {
do_action( 'woocommerce_checkout_update_order_review', isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
- // Track checkout field update for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CheckoutEventTracker::class )
- ->track_shortcode_checkout_field_update( isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
- }
-
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index 2ef103ba8b..5610d39129 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -12,10 +12,6 @@
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductType;
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
use Automattic\WooCommerce\Utilities\DiscountsUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Utilities\ShippingUtil;
@@ -1096,14 +1092,6 @@ class WC_Cart extends WC_Legacy_Cart {
*/
public function add_to_cart( $product_id = 0, $quantity = 1, $variation_id = 0, $variation = array(), $cart_item_data = array() ) {
try {
- // Block add-to-cart if session is blocked by fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- throw new Exception(
- wc_get_container()->get( BlockedSessionNotice::class )->get_message_html( 'purchase' )
- );
- }
-
$product_id = absint( $product_id );
$variation_id = absint( $variation_id );
@@ -1387,12 +1375,6 @@ class WC_Cart extends WC_Legacy_Cart {
do_action( 'woocommerce_add_to_cart', $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data );
- // Track cart event for fraud protection (only for newly added items).
- if ( ! $item_was_already_in_cart && wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_item_added( $cart_item_key, (int) $product_id, (int) $quantity, $variation_id );
- }
-
return $cart_item_key;
} catch ( Exception $e ) {
@@ -1411,16 +1393,6 @@ class WC_Cart extends WC_Legacy_Cart {
* @return bool
*/
public function remove_cart_item( $cart_item_key ) {
- // Block remove if session is blocked by fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- wc_add_notice(
- wc_get_container()->get( BlockedSessionNotice::class )->get_message_html( 'purchase' ),
- 'error'
- );
- return false;
- }
-
if ( isset( $this->cart_contents[ $cart_item_key ] ) ) {
$this->removed_cart_contents[ $cart_item_key ] = $this->cart_contents[ $cart_item_key ];
@@ -1428,12 +1400,6 @@ class WC_Cart extends WC_Legacy_Cart {
do_action( 'woocommerce_remove_cart_item', $cart_item_key, $this );
- // Track cart event for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_item_removed( $cart_item_key, $this );
- }
-
unset( $this->cart_contents[ $cart_item_key ] );
do_action( 'woocommerce_cart_item_removed', $cart_item_key, $this );
@@ -1457,12 +1423,6 @@ class WC_Cart extends WC_Legacy_Cart {
do_action( 'woocommerce_restore_cart_item', $cart_item_key, $this );
- // Track cart event for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_item_restored( $cart_item_key, $this );
- }
-
unset( $this->removed_cart_contents[ $cart_item_key ] );
do_action( 'woocommerce_cart_item_restored', $cart_item_key, $this );
@@ -1487,28 +1447,12 @@ class WC_Cart extends WC_Legacy_Cart {
return $this->remove_cart_item( $cart_item_key );
}
- // Block quantity update if session is blocked by fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- wc_add_notice(
- wc_get_container()->get( BlockedSessionNotice::class )->get_message_html( 'purchase' ),
- 'error'
- );
- return false;
- }
-
// Update qty.
$old_quantity = $this->cart_contents[ $cart_item_key ]['quantity'];
$this->cart_contents[ $cart_item_key ]['quantity'] = $quantity;
do_action( 'woocommerce_after_cart_item_quantity_update', $cart_item_key, $quantity, $old_quantity, $this );
- // Track cart event for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_item_updated( $cart_item_key, $quantity, $old_quantity, $this );
- }
-
if ( $refresh_totals ) {
$this->calculate_totals();
}
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index 841316e9fa..a29db8c1ea 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -11,8 +11,6 @@
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
defined( 'ABSPATH' ) || exit;
@@ -1102,15 +1100,6 @@ class WC_Checkout {
true
);
- // Track successful order placement.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- $fp_order = wc_get_order( $order_id );
- if ( $fp_order instanceof \WC_Order ) {
- wc_get_container()->get( CheckoutEventTracker::class )
- ->track_order_placed( $order_id, $fp_order );
- }
- }
-
if ( ! wp_doing_ajax() ) {
// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
wp_redirect( $result['redirect'] );
@@ -1142,12 +1131,6 @@ class WC_Checkout {
true
);
- // Track successful order placement.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() && $order instanceof \WC_Order ) {
- wc_get_container()->get( CheckoutEventTracker::class )
- ->track_order_placed( $order_id, $order );
- }
-
if ( ! wp_doing_ajax() ) {
wp_safe_redirect(
apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order )
diff --git a/plugins/woocommerce/includes/class-wc-payment-gateways.php b/plugins/woocommerce/includes/class-wc-payment-gateways.php
index 3eda6b0d83..7322df7d6a 100644
--- a/plugins/woocommerce/includes/class-wc-payment-gateways.php
+++ b/plugins/woocommerce/includes/class-wc-payment-gateways.php
@@ -11,8 +11,6 @@
use Automattic\WooCommerce\Enums\PaymentGatewayFeature;
use Automattic\WooCommerce\Internal\Admin\Settings\Payments as SettingsPaymentsService;
use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
use Automattic\WooCommerce\Internal\Logging\SafeGlobalFunctionProxy;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
@@ -394,12 +392,6 @@ All at %6$s
* @return array The available payment gateways.
*/
public function get_available_payment_gateways() {
- // Early return if fraud protection blocks session.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- return array();
- }
-
$_available_gateways = array();
foreach ( $this->payment_gateways as $gateway ) {
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index b2106ab2ed..537a65349c 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -388,7 +388,6 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\Orders\OrderAttributionController::class )->register();
$container->get( Automattic\WooCommerce\Internal\Orders\OrderAttributionBlocksController::class )->register();
$container->get( Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController::class )->register();
- $container->get( Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController::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/includes/data-stores/class-wc-payment-token-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
index 2f57210473..c6d6e5012b 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
@@ -5,9 +5,6 @@
* @package WooCommerce\DataStores
*/
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker;
-
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -75,12 +72,6 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_
}
do_action( 'woocommerce_new_payment_token', $token_id, $token );
-
- // Track payment method event for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( PaymentMethodEventTracker::class )
- ->track_payment_method_added( $token_id, $token );
- }
}
/**
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php
index 14430ef0c6..f273e52669 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-cart.php
@@ -8,9 +8,6 @@
* @version 2.3.0
*/
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-
defined( 'ABSPATH' ) || exit;
/**
@@ -81,12 +78,6 @@ class WC_Shortcode_Cart {
// Constants.
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
- // Track cart page loaded for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_page_loaded();
- }
-
$atts = shortcode_atts( array(), $atts, 'woocommerce_cart' );
$nonce_value = wc_get_var( $_REQUEST['woocommerce-shipping-calculator-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
index 33176cf2f1..46faae2384 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-checkout.php
@@ -11,8 +11,6 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Enums\OrderStatus;
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\Internal\Utilities\Users;
/**
@@ -347,12 +345,6 @@ class WC_Shortcode_Checkout {
return;
}
- // Track checkout page loaded for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CheckoutEventTracker::class )
- ->track_checkout_page_loaded();
- }
-
// Check cart contents for errors.
do_action( 'woocommerce_check_cart_items' );
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
index f24a2c6ade..caff53be5a 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
@@ -8,9 +8,6 @@
* @version 2.0.0
*/
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker;
-
defined( 'ABSPATH' ) || exit;
/**
@@ -414,12 +411,6 @@ class WC_Shortcode_My_Account {
wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) );
exit();
} else {
- // Track add payment method page loaded for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( PaymentMethodEventTracker::class )
- ->track_add_payment_method_page_loaded();
- }
-
do_action( 'before_woocommerce_add_payment_method' );
wc_get_template( 'myaccount/form-add-payment-method.php' );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
index c1cc692282..deba60cd3c 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
@@ -2,8 +2,6 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
/**
* Cart class.
@@ -166,12 +164,6 @@ class Cart extends AbstractBlock {
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
- // Track cart page loaded for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_page_loaded();
- }
-
/**
* We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration.
* We test the iteration version by searching for new blocks brought in by it.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index 8baf7f4933..510a74ed05 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -10,8 +10,6 @@ use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\Utilities\PaymentUtils;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\Validation;
use Automattic\WooCommerce\Internal\AddressProvider\AddressProviderController;
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
/**
* Checkout class.
@@ -232,12 +230,6 @@ class Checkout extends AbstractBlock {
return wp_is_block_theme() ? do_shortcode( '[woocommerce_checkout]' ) : '[woocommerce_checkout]';
}
- // Track checkout page loaded for fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CheckoutEventTracker::class )
- ->track_checkout_page_loaded();
- }
-
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index f8e96c04d1..7669687f4d 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -591,18 +591,6 @@ class FeaturesController {
'is_experimental' => true,
'disable_ui' => false,
),
- 'fraud_protection' => array(
- 'name' => __( 'Fraud protection', 'woocommerce' ),
- 'description' => __(
- 'Enable fraud protection features for your store.',
- 'woocommerce'
- ),
- 'enabled_by_default' => false,
- 'disable_ui' => true,
- 'is_experimental' => true,
- 'skip_compatibility_checks' => true,
- 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
- ),
);
if ( ! $tracking_enabled ) {
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php b/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php
deleted file mode 100644
index e71574255f..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/ApiClient.php
+++ /dev/null
@@ -1,316 +0,0 @@
-<?php
-/**
- * ApiClient class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Handles communication with the Blackbox fraud protection API.
- *
- * Uses Jetpack Connection for authenticated requests to the Blackbox API
- * to verify sessions and report fraud events. The API returns fraud protection
- * decisions (allow, block, or challenge).
- *
- * This class implements a fail-open pattern: if the endpoint is unreachable,
- * times out, or returns an error, it returns an "allow" decision to ensure
- * legitimate transactions are never blocked due to service issues.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class ApiClient {
-
- /**
- * Default timeout for API requests in seconds.
- *
- * Using 10 seconds as a reasonable timeout for fraud verification during checkout.
- * This balances giving the API enough time to respond while not blocking
- * checkout for too long if the service is slow.
- */
- private const DEFAULT_TIMEOUT = 10;
-
- /**
- * Blackbox API base URL.
- */
- private const BLACKBOX_API_BASE_URL = 'https://blackbox-api.wp.com/v1';
-
- /**
- * Blackbox API verify endpoint path.
- */
- private const VERIFY_ENDPOINT = '/verify';
-
- /**
- * Blackbox API report endpoint path.
- */
- private const REPORT_ENDPOINT = '/report';
-
- /**
- * Decision type: allow session.
- */
- public const DECISION_ALLOW = 'allow';
-
- /**
- * Decision type: block session.
- */
- public const DECISION_BLOCK = 'block';
-
- /**
- * Decision type: challenge session.
- */
- public const DECISION_CHALLENGE = 'challenge';
-
- /**
- * Valid decision values that can be returned by the API.
- *
- * @var array<string>
- */
- public const VALID_DECISIONS = array(
- self::DECISION_ALLOW,
- self::DECISION_BLOCK,
- );
-
- /**
- * Verify a session with the Blackbox API and get a fraud decision.
- *
- * Implements fail-open pattern: if the endpoint is unreachable or times out,
- * returns "allow" decision and logs the error.
- *
- * @since 10.5.0
- *
- * @param string $session_id Session ID to verify.
- * @param array<string, mixed> $payload Event data to send to the endpoint.
- * @return string Decision: "allow" or "block".
- */
- public function verify( string $session_id, array $payload ): string {
- FraudProtectionController::log(
- 'info',
- 'Verifying session with Blackbox API',
- array(
- 'session_id' => $session_id,
- 'payload' => $payload,
- )
- );
-
- $response = $this->make_request( 'POST', self::VERIFY_ENDPOINT, $session_id, $payload );
-
- return $this->process_decision_response( $response, $payload );
- }
-
- /**
- * Report a fraud event to the Blackbox API.
- *
- * Used for reporting outcomes and feedback to improve fraud detection.
- * This is a fire-and-forget operation - errors are logged but do not
- * affect the checkout flow.
- *
- * @since 10.5.0
- *
- * @param string $session_id Session ID to report.
- * @param array<string, mixed> $payload Event data to send to the endpoint.
- * @return bool True if report was sent successfully, false otherwise.
- */
- public function report( string $session_id, array $payload ): bool {
- FraudProtectionController::log(
- 'info',
- 'Reporting event to Blackbox API',
- array( 'payload' => $payload )
- );
-
- $response = $this->make_request( 'POST', self::REPORT_ENDPOINT, $session_id, $payload );
-
- if ( is_wp_error( $response ) ) {
- FraudProtectionController::log(
- 'error',
- sprintf(
- 'Failed to report event to Blackbox API: %s',
- $response->get_error_message()
- ),
- array( 'error' => $response->get_error_data() )
- );
- return false;
- }
-
- FraudProtectionController::log(
- 'info',
- 'Event reported successfully',
- array( 'response' => $response )
- );
-
- return true;
- }
-
- /**
- * Process the API response and extract the decision.
- *
- * @param array<string, mixed>|\WP_Error $response API response or WP_Error.
- * @param array<string, mixed> $event_data Event data for logging.
- * @return string Decision: "allow" or "block".
- */
- private function process_decision_response( $response, array $event_data ): string {
- if ( is_wp_error( $response ) ) {
- $error_data = $response->get_error_data() ?? array();
- $error_data = is_array( $error_data ) ? $error_data : array( 'error' => $error_data );
- FraudProtectionController::log(
- 'error',
- sprintf(
- 'Blackbox API request failed: %s. Failing open with "allow" decision.',
- $response->get_error_message()
- ),
- $error_data
- );
- return self::DECISION_ALLOW;
- }
-
- if ( ! isset( $response['decision'] ) ) {
- FraudProtectionController::log(
- 'error',
- 'Response missing "decision" field. Failing open with "allow" decision.',
- array( 'response' => $response )
- );
- return self::DECISION_ALLOW;
- }
-
- $decision = $response['decision'];
-
- if ( ! in_array( $decision, self::VALID_DECISIONS, true ) ) {
- FraudProtectionController::log(
- 'error',
- sprintf(
- 'Invalid decision value "%s". Failing open with "allow" decision.',
- $decision
- ),
- array( 'response' => $response )
- );
- return self::DECISION_ALLOW;
- }
-
- $session = is_array( $event_data['session'] ?? null ) ? $event_data['session'] : array();
- $session_id = $session['session_id'] ?? 'unknown';
- $event_type = $event_data['event_type'] ?? 'unknown';
- FraudProtectionController::log(
- 'info',
- sprintf(
- 'Fraud decision received: %s | Event: %s | Session: %s',
- $decision,
- $event_type,
- $session_id
- ),
- array( 'response' => $response )
- );
-
- return $decision;
- }
-
- /**
- * Make an HTTP request to the Blackbox API via Jetpack Connection.
- *
- * Uses Jetpack's signed request mechanism which authenticates with the
- * blog token scoped to the blog_id.
- *
- * @param string $method HTTP method (GET, POST, etc.).
- * @param string $path Endpoint path (relative to Blackbox API base URL).
- * @param string $session_id Session ID for the request.
- * @param array<string, mixed> $payload Request payload.
- * @return array<string, mixed>|\WP_Error Parsed JSON response or WP_Error on failure.
- */
- private function make_request( string $method, string $path, string $session_id, array $payload ) {
- if ( ! class_exists( Jetpack_Connection_Client::class ) ) {
- return new \WP_Error(
- 'jetpack_not_available',
- 'Jetpack Connection is not available'
- );
- }
-
- $blog_id = $this->get_blog_id();
- if ( ! $blog_id ) {
- return new \WP_Error(
- 'no_blog_id',
- 'Jetpack blog ID not found. Is the site connected to WordPress.com?'
- );
- }
-
- $payload['blog_id'] = $blog_id;
-
- $body = \wp_json_encode(
- array(
- 'session_id' => $session_id,
- 'private_key' => '', // Woo will not use private keys for now.
- 'extra' => $payload,
- )
- );
-
- if ( false === $body ) {
- return new \WP_Error(
- 'json_encode_error',
- 'Failed to encode payload',
- array( 'payload' => $payload )
- );
- }
-
- $url = self::BLACKBOX_API_BASE_URL . $path;
-
- // Use Jetpack Connection Client to make a signed request.
- // This authenticates with the blog token automatically.
- $response = Jetpack_Connection_Client::remote_request(
- array(
- 'url' => $url,
- 'method' => $method,
- 'timeout' => self::DEFAULT_TIMEOUT,
- 'headers' => array( 'Content-Type' => 'application/json' ),
- 'auth_location' => 'header',
- ),
- $body
- );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- /**
- * Type assertion for PHPStan - Jetpack returns array on success.
- *
- * @var array $response
- */
- $response_code = wp_remote_retrieve_response_code( $response );
- $response_body = wp_remote_retrieve_body( $response );
-
- $data = json_decode( $response_body, true );
-
- if ( $response_code >= 300 ) {
- return new \WP_Error(
- 'api_error',
- sprintf( 'Blackbox API %s %s returned status code %d', $method, $path, $response_code ),
- array( 'response' => JSON_ERROR_NONE === json_last_error() ? $data : $response_body )
- );
- }
-
- if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
- return new \WP_Error(
- 'json_decode_error',
- sprintf( 'Failed to decode JSON response: %s', json_last_error_msg() ),
- array( 'response' => $response_body )
- );
- }
-
- return $data;
- }
-
- /**
- * Get the Jetpack blog ID.
- *
- * @return int|false Blog ID or false if not available.
- */
- private function get_blog_id() {
- if ( ! class_exists( \Jetpack_Options::class ) ) {
- return false;
- }
- return \Jetpack_Options::get_option( 'id' );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php b/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php
deleted file mode 100644
index 5fa39a0ab6..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-/**
- * BlackboxScriptHandler class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-use Automattic\Jetpack\Constants;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Handles loading Blackbox JS telemetry script on payment method pages.
- *
- * Enqueues the external Blackbox JS SDK and a small initialization script
- * on checkout, pay-for-order, and add-payment-method pages. The init script
- * calls Blackbox.configure() with the site's API key and Jetpack blog ID.
- *
- * @since 10.6.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class BlackboxScriptHandler {
-
- /**
- * Blackbox JS SDK URL.
- */
- private const BLACKBOX_JS_URL = 'https://blackbox-api.wp.com/v1/dist/v.js';
-
- /**
- * API key identifying WooCommerce as a Blackbox client.
- */
- private const API_KEY = 'woocommerce';
-
- /**
- * Register hooks for Blackbox script loading.
- *
- * Called from FraudProtectionController::on_init() which already checks
- * if the feature is enabled.
- *
- * @return void
- */
- public function register(): void {
- add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_scripts' ) );
- }
-
- /**
- * Conditionally enqueue Blackbox scripts on payment method pages.
- *
- * Loads scripts on checkout (including custom pages with the checkout block),
- * pay-for-order, and add-payment-method pages.
- * Extensions can use the `woocommerce_fraud_protection_enqueue_blackbox_scripts`
- * filter to load scripts on additional pages (e.g., product pages for express payments).
- *
- * @return void
- */
- public function maybe_enqueue_scripts(): void {
- global $wp;
-
- $should_enqueue = is_checkout() ||
- has_block( 'woocommerce/checkout' ) ||
- is_checkout_pay_page() ||
- // Check add-payment-method query_var to avoid loading on regular payment methods page.
- ( is_add_payment_method_page() && isset( $wp->query_vars['add-payment-method'] ) );
-
- /**
- * Filter whether to enqueue Blackbox fraud protection scripts on the current page.
- *
- * By default, scripts are loaded on checkout, pay-for-order, and add-payment-method pages.
- * Extensions can return true to load scripts on additional pages where payment methods
- * are rendered (e.g., product pages for express checkout buttons).
- *
- * @since 10.6.0
- *
- * @param bool $should_enqueue Whether to enqueue Blackbox scripts on the current page.
- */
- $should_enqueue = (bool) apply_filters( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', $should_enqueue );
-
- if ( ! $should_enqueue ) {
- return;
- }
-
- $blog_id = $this->get_blog_id();
-
- if ( ! $blog_id ) {
- FraudProtectionController::log(
- 'error',
- 'Blackbox scripts not loaded: Jetpack blog ID not available. Is the site connected to Jetpack?'
- );
- return;
- }
-
- $this->enqueue_scripts( $blog_id );
- }
-
- /**
- * Enqueue the Blackbox SDK and initialization scripts.
- *
- * @param int $blog_id The Jetpack blog ID.
- * @return void
- */
- private function enqueue_scripts( int $blog_id ): void {
- wp_enqueue_script(
- 'wc-fraud-protection-blackbox',
- self::BLACKBOX_JS_URL,
- array(),
- null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External SDK, version managed by Blackbox CDN.
- array( 'in_footer' => true )
- );
-
- $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
-
- // Enqueue the Woo Fraud Protection init script.
- wp_enqueue_script(
- 'wc-fraud-protection-blackbox-init',
- plugins_url( 'assets/js/frontend/fraud-protection/blackbox-init' . $suffix . '.js', WC_PLUGIN_FILE ),
- array( 'wc-fraud-protection-blackbox' ),
- WC_VERSION,
- array( 'in_footer' => true )
- );
-
- wp_localize_script(
- 'wc-fraud-protection-blackbox-init',
- 'wcBlackboxConfig',
- array(
- 'apiKey' => self::API_KEY,
- 'blogId' => $blog_id,
- )
- );
- }
-
- /**
- * Get the Jetpack blog ID.
- *
- * @return int|false Blog ID or false if not available.
- */
- private function get_blog_id() {
- if ( ! class_exists( \Jetpack_Options::class ) ) {
- return false;
- }
-
- $blog_id = \Jetpack_Options::get_option( 'id' );
-
- if ( ! is_numeric( $blog_id ) || (int) $blog_id <= 0 ) {
- return false;
- }
-
- return (int) $blog_id;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php b/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php
deleted file mode 100644
index b8ae656836..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-/**
- * BlockedSessionNotice class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Handles blocked session messaging for fraud protection.
- *
- * This class provides:
- * - Hook into shortcode checkout to display blocked notice
- * - Message generation for both HTML (shortcode) and plaintext (Store API) contexts
- *
- * Note: Store API (block checkout) and payment gateway filtering are handled
- * directly in WC Core classes (Checkout.php and WC_Payment_Gateways).
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class BlockedSessionNotice implements RegisterHooksInterface {
-
- /**
- * Session clearance manager instance.
- *
- * @var SessionClearanceManager
- */
- private SessionClearanceManager $session_manager;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionClearanceManager $session_manager The session clearance manager instance.
- */
- final public function init( SessionClearanceManager $session_manager ): void {
- $this->session_manager = $session_manager;
- }
-
- /**
- * Register hooks for displaying blocked notice.
- *
- * This method should only be called when fraud protection is enabled.
- *
- * @return void
- */
- public function register(): void {
- // Shop, cart, and checkout pages (both blocks and shortcode) - add notice via wc_add_notice on wp hook.
- add_action( 'wp', array( $this, 'maybe_add_blocked_purchase_notice' ), 10, 0 );
-
- add_action( 'before_woocommerce_add_payment_method', array( $this, 'maybe_display_generic_blocked_notice' ), 1, 0 );
- }
-
- /**
- * Add blocked purchase notice on shop, cart, and checkout pages (both blocks and shortcode),
- * if the session is blocked. Skips duplicate notices.
- *
- * Uses wc_add_notice() to add an error notice that will be rendered by:
- * - StoreNoticesContainer component for blocks
- * - wc_print_notices() for shortcodes
- *
- * @internal
- *
- * @return void
- */
- public function maybe_add_blocked_purchase_notice(): void {
- if ( ! $this->session_manager->is_session_blocked() ) {
- return;
- }
-
- if ( ! is_checkout() && ! is_cart() && ! is_shop() && ! is_product_taxonomy() ) {
- return;
- }
-
- $message = $this->get_message_html( 'purchase' );
-
- if ( wc_has_notice( $message, 'error' ) ) {
- return;
- }
-
- wc_add_notice( $message, 'error' );
- }
-
- /**
- * Display blocked notice for non-cart/checkout pages, if the session is blocked.
- *
- * Shows a generic message explaining that the request cannot be
- * processed online and provides contact information for support.
- *
- * @internal
- *
- * @return void
- */
- public function maybe_display_generic_blocked_notice(): void {
- if ( ! $this->session_manager->is_session_blocked() ) {
- return;
- }
-
- wc_print_notice( $this->get_message_html(), 'error' );
- }
-
- /**
- * Get the blocked session message as HTML.
- *
- * Includes a mailto link for the support email.
- *
- * @param string $context Message context: 'purchase' for purchase-specific message, 'generic' for general use.
- * @return string HTML message with mailto link.
- */
- public function get_message_html( string $context = 'generic' ): string {
- $email = WC()->mailer()->get_from_address();
-
- if ( 'purchase' === $context ) {
- return sprintf(
- /* translators: %1$s: mailto link, %2$s: email address */
- __( 'We are unable to process this request online. Please <a href="%1$s">contact support (%2$s)</a> to complete your purchase.', 'woocommerce' ),
- esc_url( 'mailto:' . $email ),
- esc_html( $email )
- );
- }
-
- return sprintf(
- /* translators: %1$s: mailto link, %2$s: email address */
- __( 'We are unable to process this request online. Please <a href="%1$s">contact support (%2$s)</a> for assistance.', 'woocommerce' ),
- esc_url( 'mailto:' . $email ),
- esc_html( $email )
- );
- }
-
- /**
- * Get the blocked session message as plaintext.
- *
- * Used by Store API responses where HTML is not supported.
- *
- * @param string $context Message context: 'purchase' for purchase-specific message, 'generic' for general use.
- * @return string Plaintext message with email address.
- */
- public function get_message_plaintext( string $context = 'generic' ): string {
- $email = WC()->mailer()->get_from_address();
-
- if ( 'purchase' === $context ) {
- return sprintf(
- /* translators: %s: support email address */
- __( 'We are unable to process this request online. Please contact support (%s) to complete your purchase.', 'woocommerce' ),
- $email
- );
- }
-
- return sprintf(
- /* translators: %s: support email address */
- __( 'We are unable to process this request online. Please contact support (%s) for assistance.', 'woocommerce' ),
- $email
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
deleted file mode 100644
index 467e824dd6..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/CartEventTracker.php
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-/**
- * CartEventTracker class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Tracks cart events for fraud protection analysis.
- *
- * This class provides methods to track cart events (add, update, remove, restore)
- * for fraud protection. Event-specific data is passed
- * to the SessionDataCollector which handles session data storage internally.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class CartEventTracker {
-
- /**
- * Session data collector instance.
- *
- * @var SessionDataCollector
- */
- private SessionDataCollector $session_data_collector;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionDataCollector $session_data_collector The session data collector instance.
- */
- final public function init( SessionDataCollector $session_data_collector ): void {
- $this->session_data_collector = $session_data_collector;
- }
-
- /**
- * Track cart page loaded event.
- *
- * Collects session data when the cart page is initially loaded.
- * This captures the initial session state before any user interactions.
- *
- * @internal
- * @return void
- */
- public function track_cart_page_loaded(): void {
- $this->session_data_collector->collect( 'cart_page_loaded', array() );
- }
-
- /**
- * Track cart item added event.
- *
- * Collects session data when an item is added to the cart.
- *
- * @internal
- *
- * @param string $cart_item_key Cart item key.
- * @param int $product_id Product ID.
- * @param int $quantity Quantity added.
- * @param int $variation_id Variation ID.
- * @return void
- */
- public function track_cart_item_added( $cart_item_key, $product_id, $quantity, $variation_id ): void {
- $event_data = $this->build_cart_event_data(
- 'item_added',
- $product_id,
- $quantity,
- $variation_id
- );
-
- $this->session_data_collector->collect( 'cart_item_added', $event_data );
- }
-
- /**
- * Track cart item quantity updated event.
- *
- * Collects session data when cart item quantity is updated.
- *
- * @internal
- *
- * @param string $cart_item_key Cart item key.
- * @param int $quantity New quantity.
- * @param int $old_quantity Old quantity.
- * @param object $cart Cart object.
- * @return void
- */
- public function track_cart_item_updated( $cart_item_key, $quantity, $old_quantity, $cart ): void {
- $cart_item = $cart->cart_contents[ $cart_item_key ] ?? null;
-
- if ( (int) $quantity === (int) $old_quantity || ! $cart_item ) {
- return;
- }
-
- $product_id = $cart_item['product_id'] ?? 0;
- $variation_id = $cart_item['variation_id'] ?? 0;
-
- $event_data = $this->build_cart_event_data(
- 'item_updated',
- $product_id,
- (int) $quantity,
- $variation_id
- );
-
- $event_data['old_quantity'] = (int) $old_quantity;
-
- $this->session_data_collector->collect( 'cart_item_updated', $event_data );
- }
-
- /**
- * Track cart item removed event.
- *
- * Collects session data when an item is removed from the cart.
- *
- * @internal
- *
- * @param string $cart_item_key Cart item key.
- * @param object $cart Cart object.
- * @return void
- */
- public function track_cart_item_removed( $cart_item_key, $cart ): void {
- $cart_item = $cart->removed_cart_contents[ $cart_item_key ] ?? null;
-
- if ( ! $cart_item ) {
- return;
- }
-
- $product_id = $cart_item['product_id'] ?? 0;
- $variation_id = $cart_item['variation_id'] ?? 0;
- $quantity = $cart_item['quantity'] ?? 0;
-
- $event_data = $this->build_cart_event_data(
- 'item_removed',
- $product_id,
- $quantity,
- $variation_id
- );
-
- $this->session_data_collector->collect( 'cart_item_removed', $event_data );
- }
-
- /**
- * Track cart item restored event.
- *
- * Collects session data when a removed item is restored to the cart.
- *
- * @internal
- *
- * @param string $cart_item_key Cart item key.
- * @param object $cart Cart object.
- * @return void
- */
- public function track_cart_item_restored( $cart_item_key, $cart ): void {
- $cart_item = $cart->cart_contents[ $cart_item_key ] ?? null;
-
- if ( ! $cart_item ) {
- return;
- }
-
- $product_id = $cart_item['product_id'] ?? 0;
- $variation_id = $cart_item['variation_id'] ?? 0;
- $quantity = $cart_item['quantity'] ?? 0;
-
- $event_data = $this->build_cart_event_data(
- 'item_restored',
- $product_id,
- $quantity,
- $variation_id
- );
-
- $this->session_data_collector->collect( 'cart_item_restored', $event_data );
- }
-
- /**
- * Build cart event-specific data.
- *
- * Prepares the cart event data including action type, product details,
- * and current cart state. This data will be merged with comprehensive
- * session data during event dispatching.
- *
- * @param string $action Action type (item_added, item_updated, item_removed, item_restored).
- * @param int $product_id Product ID.
- * @param int $quantity Quantity.
- * @param int $variation_id Variation ID.
- * @return array Cart event data.
- */
- private function build_cart_event_data( string $action, int $product_id, int $quantity, int $variation_id ): array {
- $cart_item_count = 0;
-
- // Get current cart item count if cart is available.
- if ( WC()->cart instanceof \WC_Cart ) {
- $cart_item_count = WC()->cart->get_cart_contents_count();
- }
-
- return array(
- 'action' => $action,
- 'product_id' => $product_id,
- 'quantity' => $quantity,
- 'variation_id' => $variation_id,
- 'cart_item_count' => $cart_item_count,
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
deleted file mode 100644
index b2ff257508..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
+++ /dev/null
@@ -1,265 +0,0 @@
-<?php
-/**
- * CheckoutEventTracker class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Tracks checkout events for fraud protection analysis.
- *
- * This class provides methods to track both WooCommerce Blocks (Store API) and traditional
- * shortcode checkout events for fraud protection. Event-specific data is passed to the
- * SessionDataCollector which handles session data storage internally.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class CheckoutEventTracker {
- /**
- * Session data collector instance.
- *
- * @var SessionDataCollector
- */
- private SessionDataCollector $session_data_collector;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionDataCollector $session_data_collector The session data collector instance.
- */
- final public function init( SessionDataCollector $session_data_collector ): void {
- $this->session_data_collector = $session_data_collector;
- }
-
- /**
- * Track checkout page loaded event.
- *
- * Collects session data when the checkout page is initially loaded.
- * This captures the initial session state before any user interactions.
- *
- * @internal
- * @return void
- */
- public function track_checkout_page_loaded(): void {
- $this->session_data_collector->collect( 'checkout_page_loaded', array() );
- }
-
- /**
- * Track 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 {
- // At this point we don't have any payment or shipping data, so we pass an empty array.
- $this->session_data_collector->collect( 'checkout_update', array() );
- }
-
- /**
- * Track shortcode checkout field update event.
- *
- * Triggered when checkout fields are updated via AJAX (woocommerce_update_order_review).
- * Only dispatches event when billing or shipping country changes to reduce unnecessary API calls.
- *
- * @internal
- *
- * @param string $posted_data Serialized checkout form data.
- * @return void
- */
- public function track_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 );
- }
-
- // Get current customer countries using SessionDataCollector.
- $current_billing_country = $this->session_data_collector->get_current_billing_country();
- $current_shipping_country = $this->session_data_collector->get_current_shipping_country();
-
- // Get posted countries.
- $posted_billing_country = $data['billing_country'] ?? '';
- $posted_shipping_country = $data['shipping_country'] ?? '';
-
- // Check if billing country changed.
- $billing_changed = ! empty( $posted_billing_country ) && $posted_billing_country !== $current_billing_country;
-
- // Check if shipping country changed.
- $ship_to_different = ! empty( $data['ship_to_different_address'] );
- if ( $ship_to_different ) {
- // User wants different shipping address - check if shipping country changed.
- $shipping_changed = ! empty( $posted_shipping_country ) && $posted_shipping_country !== $current_shipping_country;
- } else {
- // User wants same address for billing and shipping.
- // If current shipping country exists and differs from billing country, it's a change.
- $effective_billing_country = ! empty( $posted_billing_country ) ? $posted_billing_country : $current_billing_country;
- $shipping_changed = ! empty( $current_shipping_country ) && $current_shipping_country !== $effective_billing_country;
- }
-
- // Only dispatch if either country changed.
- if ( $billing_changed || $shipping_changed ) {
- $event_data = $this->format_checkout_event_data( 'field_update', $data );
- $this->session_data_collector->collect( 'checkout_update', $event_data );
- }
- }
-
- /**
- * Build checkout event-specific data.
- *
- * Prepares the checkout event data including action type and any changed fields.
- *
- * @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 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( $collected_event_data ),
- $this->extract_shipping_fields( $collected_event_data ),
- $this->extract_payment_method( $collected_event_data ),
- );
-
- return $event_data;
- }
-
- /**
- * Extract billing fields from posted data.
- *
- * @param array $posted_data Posted form data.
- * @return array Billing fields.
- */
- private function extract_billing_fields( array $posted_data ): array {
- $field_map = array(
- 'billing_email' => 'sanitize_email',
- 'billing_first_name' => 'sanitize_text_field',
- 'billing_last_name' => 'sanitize_text_field',
- 'billing_country' => 'sanitize_text_field',
- 'billing_address_1' => 'sanitize_text_field',
- 'billing_address_2' => 'sanitize_text_field',
- 'billing_city' => 'sanitize_text_field',
- 'billing_state' => 'sanitize_text_field',
- 'billing_postcode' => 'sanitize_text_field',
- 'billing_phone' => 'sanitize_text_field',
- );
-
- $extracted_fields = $this->extract_fields_by_map( $field_map, $posted_data );
-
- // Store API uses 'email' instead of 'billing_email'.
- if ( empty( $extracted_fields['billing_email'] ) && ! empty( $posted_data['email'] ) ) {
- $extracted_fields['email'] = sanitize_email( $posted_data['email'] );
- }
-
- return $extracted_fields;
- }
-
- /**
- * Extract shipping fields from posted data.
- *
- * @param array $posted_data Posted form data.
- * @return array Shipping fields.
- */
- private function extract_shipping_fields( array $posted_data ): array {
- if ( ! isset( $posted_data['ship_to_different_address'] ) || ! $posted_data['ship_to_different_address'] ) {
- return array();
- }
-
- $field_map = array(
- 'shipping_first_name' => 'sanitize_text_field',
- 'shipping_last_name' => 'sanitize_text_field',
- 'shipping_country' => 'sanitize_text_field',
- 'shipping_address_1' => 'sanitize_text_field',
- 'shipping_address_2' => 'sanitize_text_field',
- 'shipping_city' => 'sanitize_text_field',
- 'shipping_state' => 'sanitize_text_field',
- 'shipping_postcode' => 'sanitize_text_field',
- );
-
- return $this->extract_fields_by_map( $field_map, $posted_data );
- }
-
- /**
- * Extract and sanitize fields from posted data using a field map.
- *
- * Generic extraction method that iterates through a field map and extracts
- * non-empty fields from posted data, applying the appropriate sanitization
- * function to each field.
- *
- * @param array $field_map Map of field names to sanitization functions.
- * @param array $posted_data Posted form data.
- * @return array Extracted and sanitized fields.
- */
- private function extract_fields_by_map( array $field_map, array $posted_data ): array {
- $extracted_fields = array();
-
- foreach ( $field_map as $field_name => $sanitize_function ) {
- if ( ! empty( $posted_data[ $field_name ] ) ) {
- $extracted_fields[ $field_name ] = $sanitize_function( wp_unslash( $posted_data[ $field_name ] ) );
- }
- }
-
- return $extracted_fields;
- }
-
- /**
- * 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 function extract_payment_method( array $posted_data ): array {
- $payment_data = array();
-
- if ( ! empty( $posted_data['payment_method'] ) ) {
- $payment_gateway_name = WC()->payment_gateways()->get_payment_gateway_name_by_id( $posted_data['payment_method'] );
-
- $payment_data['payment'] = array(
- 'payment_gateway_type' => $posted_data['payment_method'],
- 'payment_gateway_name' => $payment_gateway_name,
- );
- }
-
- return $payment_data;
- }
-
- /**
- * Track successful order placement.
- *
- * Called when an order is successfully placed, with or without payment.
- * Works for both shortcode and Store API checkout flows.
- *
- * @internal
- *
- * @param int $order_id The order ID.
- * @param \WC_Order $order The order object.
- * @return void
- */
- public function track_order_placed( int $order_id, \WC_Order $order ): void {
- $customer_id = $order->get_customer_id();
- $event_data = array(
- 'order_id' => $order_id,
- 'payment_method' => $order->get_payment_method(),
- 'total' => (float) $order->get_total(),
- 'currency' => $order->get_currency(),
- 'customer_id' => $customer_id ? $customer_id : 'guest',
- 'status' => $order->get_status(),
- );
-
- $this->session_data_collector->collect( 'order_placed', $event_data );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/DecisionHandler.php b/plugins/woocommerce/src/Internal/FraudProtection/DecisionHandler.php
deleted file mode 100644
index 856a528ec2..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/DecisionHandler.php
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-/**
- * DecisionHandler class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Handles fraud protection decision application.
- *
- * This class is responsible for:
- * - Applying extension override filters for whitelisting
- * - Coordinating with SessionClearanceManager to apply decisions
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class DecisionHandler {
-
- /**
- * Session clearance manager instance.
- *
- * @var SessionClearanceManager
- */
- private SessionClearanceManager $session_manager;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionClearanceManager $session_manager The session clearance manager instance.
- */
- final public function init( SessionClearanceManager $session_manager ): void {
- $this->session_manager = $session_manager;
- }
-
- /**
- * Apply a fraud protection decision.
- *
- * This method processes a decision from the API, applies any override filters,
- * validates the result, and updates the session status accordingly.
- *
- * The input decision is expected to be pre-validated by ApiClient.
- *
- * The decision flow:
- * 1. Apply the `woocommerce_fraud_protection_decision` filter for overrides
- * 2. Validate the filtered decision (third-party filters may return invalid values)
- * 3. Update session status via SessionClearanceManager
- *
- * @since 10.5.0
- *
- * @param string $decision The decision from the API (allow, block).
- * @param array<string, mixed> $session_data The session data that was sent to the API.
- * @return string The final applied decision after any filter overrides.
- */
- public function apply_decision( string $decision, array $session_data ): string {
- // Validate input decision and fail open if invalid.
- if ( ! $this->is_valid_decision( $decision ) ) {
- FraudProtectionController::log(
- 'warning',
- sprintf( 'Invalid decision "%s" received. Defaulting to "allow".', $decision ),
- array( 'session_data' => $session_data )
- );
- $decision = ApiClient::DECISION_ALLOW;
- }
-
- $original_decision = $decision;
-
- /**
- * Filters the fraud protection decision before it is applied.
- *
- * This filter allows extensions to override fraud protection decisions
- * to implement custom whitelisting logic. Common use cases:
- * - Whitelist specific users (e.g., admins, trusted customers)
- * - Whitelist specific conditions (e.g., certain IP ranges, logged-in users)
- * - Integrate with external fraud detection services
- *
- * Note: This filter can only change the decision to ApiClient::VALID_DECISIONS.
- * Any other value will be rejected and the original decision will be used.
- *
- * @since 10.5.0
- *
- * @param string $decision The decision from the API (allow, block).
- * @param array<string, mixed> $session_data The session data that was analyzed.
- */
- $decision = apply_filters( 'woocommerce_fraud_protection_decision', $decision, $session_data );
-
- // Validate filtered decision (third-party filters may return invalid values).
- if ( ! $this->is_valid_decision( $decision ) ) {
- FraudProtectionController::log(
- 'warning',
- sprintf( 'Filter `woocommerce_fraud_protection_decision` returned invalid decision "%s". Using original decision "%s".', $decision, $original_decision ),
- array(
- 'original_decision' => $original_decision,
- 'filtered_decision' => $decision,
- 'session_data' => $session_data,
- )
- );
- $decision = $original_decision;
- }
-
- // Log if decision was overridden.
- if ( $decision !== $original_decision ) {
- FraudProtectionController::log(
- 'info',
- sprintf( 'Decision overridden by filter `woocommerce_fraud_protection_decision`: "%s" -> "%s"', $original_decision, $decision ),
- array(
- 'original_decision' => $original_decision,
- 'final_decision' => $decision,
- 'session_data' => $session_data,
- )
- );
- }
-
- // Apply the decision to the session.
- $this->update_session_status( $decision );
-
- return $decision;
- }
-
- /**
- * Check if a decision value is valid.
- *
- * @param mixed $decision The decision to validate.
- * @return bool True if valid, false otherwise.
- */
- private function is_valid_decision( $decision ): bool {
- if ( ! is_string( $decision ) ) {
- return false;
- }
- return in_array( $decision, ApiClient::VALID_DECISIONS, true );
- }
-
- /**
- * Update the session status based on the decision.
- *
- * Important: Once a session is blocked, it stays blocked until explicitly reset.
- * This prevents race conditions where emptying the cart (done during block_session)
- * causes subsequent fraud checks to return "allow" (due to lower cart value),
- * which would incorrectly unblock the session.
- *
- * @param string $decision The validated decision to apply.
- * @return void
- */
- private function update_session_status( string $decision ): void {
- // Don't overwrite a blocked session with an allow decision.
- // Once blocked, a session should stay blocked until explicitly reset.
- if ( ApiClient::DECISION_ALLOW === $decision && $this->session_manager->is_session_blocked() ) {
- FraudProtectionController::log(
- 'info',
- 'Preserving blocked session status. Allow decision not applied to already-blocked session.'
- );
- return;
- }
-
- switch ( $decision ) {
- case ApiClient::DECISION_ALLOW:
- $this->session_manager->allow_session();
- break;
-
- case ApiClient::DECISION_BLOCK:
- $this->session_manager->block_session();
- break;
- }
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
deleted file mode 100644
index 2a13c43c85..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
+++ /dev/null
@@ -1,193 +0,0 @@
-<?php
-/**
- * FraudProtectionController class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection;
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Main controller for fraud protection features.
- *
- * This class orchestrates all fraud protection components and ensures
- * zero-impact when the feature flag is disabled.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class FraudProtectionController implements RegisterHooksInterface {
-
- /**
- * Features controller instance.
- *
- * @var FeaturesController
- */
- private FeaturesController $features_controller;
-
- /**
- * Blocked session notice instance.
- *
- * @var BlockedSessionNotice
- */
- private BlockedSessionNotice $blocked_session_notice;
-
- /**
- * Blackbox script handler instance.
- *
- * @var BlackboxScriptHandler
- */
- private BlackboxScriptHandler $blackbox_script_handler;
-
- /**
- * Register hooks.
- */
- public function register(): void {
- add_action( 'init', array( $this, 'on_init' ) );
- add_action( 'admin_notices', array( $this, 'on_admin_notices' ) );
- add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'maybe_register_jetpack_connection' ), 10, 2 );
- }
-
- /**
- * Initialize the instance, runs when the instance is created by the dependency injection container.
- *
- * @internal
- *
- * @param FeaturesController $features_controller The instance of FeaturesController to use.
- * @param BlockedSessionNotice $blocked_session_notice The instance of BlockedSessionNotice to use.
- * @param BlackboxScriptHandler $blackbox_script_handler The instance of BlackboxScriptHandler to use.
- */
- final public function init(
- FeaturesController $features_controller,
- BlockedSessionNotice $blocked_session_notice,
- BlackboxScriptHandler $blackbox_script_handler
- ): void {
- $this->features_controller = $features_controller;
- $this->blocked_session_notice = $blocked_session_notice;
- $this->blackbox_script_handler = $blackbox_script_handler;
- }
-
- /**
- * Hook into WordPress on init.
- *
- * @internal
- */
- public function on_init(): void {
- // Bail if the feature is not enabled.
- if ( ! $this->feature_is_enabled() ) {
- return;
- }
-
- $this->blocked_session_notice->register();
- $this->blackbox_script_handler->register();
- }
-
- /**
- * Display admin notice when Jetpack connection is not available.
- *
- * @internal
- */
- public function on_admin_notices(): void {
- // Only show if feature is enabled.
- if ( ! $this->feature_is_enabled() || JetpackConnection::get_manager()->is_connected() ) {
- return;
- }
-
- // Only show on WooCommerce settings page.
- $screen = get_current_screen();
-
- if ( ! $screen || 'woocommerce_page_wc-settings' !== $screen->id ) {
- return;
- }
-
- ?>
- <div class="notice notice-warning is-dismissible">
- <p>
- <?php
- printf(
- /* translators: %s: Getting Started with Jetpack documentation URL */
- wp_kses_post( __( 'Your site failed to connect to Jetpack automatically. Fraud protection will fail open and allow all sessions until your site is connected to Jetpack. <a href="%s">How to connect to Jetpack</a>', 'woocommerce' ) ),
- esc_url( 'https://jetpack.com/support/getting-started-with-jetpack/' )
- );
- ?>
- </p>
- </div>
- <?php
- }
-
- /**
- * Maybe register Jetpack connection when fraud protection is enabled.
- *
- * Attempts to automatically register the site with Jetpack when the fraud protection
- * feature is enabled and the site is not already connected.
- *
- * @since 10.5.0
- *
- * @internal
- *
- * @param string $feature_id The feature ID being toggled.
- * @param bool $is_enabled Whether the feature is being enabled or disabled.
- */
- public function maybe_register_jetpack_connection( string $feature_id, bool $is_enabled ): void {
- if ( 'fraud_protection' !== $feature_id || ! $is_enabled ) {
- return;
- }
-
- $manager = JetpackConnection::get_manager();
-
- if ( $manager->is_connected() ) {
- return;
- }
-
- $result = $manager->try_registration();
-
- if ( is_wp_error( $result ) ) {
- $this->log( 'error', 'Failed to register Jetpack connection: ' . $result->get_error_message() );
- return;
- }
-
- $this->log( 'info', 'Jetpack connection registered successfully' );
- }
-
- /**
- * Check if fraud protection feature is enabled.
- *
- * This method can be used by other fraud protection classes to check
- * the feature flag status. Returns false (fail-open) if init hasn't run yet.
- *
- * @return bool True if enabled, false if not enabled or init hasn't run yet.
- */
- public function feature_is_enabled(): bool {
- // Fail-open: don't block if init hasn't run yet to avoid FeaturesController translation notices.
- if ( ! did_action( 'init' ) ) {
- return false;
- }
- return $this->features_controller->feature_is_enabled( 'fraud_protection' );
- }
-
- /**
- * Log helper method for consistent logging across all fraud protection components.
- *
- * This static method ensures all fraud protection logs are written with
- * the same 'woo-fraud-protection' source for easy filtering in WooCommerce logs.
- *
- * @param string $level Log level (emergency, alert, critical, error, warning, notice, info, debug).
- * @param string $message Log message.
- * @param array $context Optional context data.
- *
- * @return void
- */
- public static function log( string $level, string $message, array $context = array() ): void {
- wc_get_logger()->log(
- $level,
- $message,
- array_merge( $context, array( 'source' => 'woo-fraud-protection' ) )
- );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
deleted file mode 100644
index f3aa41548a..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/PaymentMethodEventTracker.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-/**
- * PaymentMethodEventTracker class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Tracks payment method events for fraud protection analysis.
- *
- * This class provides methods to track events for adding payment methods in My Account page
- * for fraud protection. Event-specific data is passed to the SessionDataCollector which
- * handles session data storage internally.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class PaymentMethodEventTracker {
-
- /**
- * Session data collector instance.
- *
- * @var SessionDataCollector
- */
- private SessionDataCollector $session_data_collector;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionDataCollector $session_data_collector The session data collector instance.
- */
- final public function init( SessionDataCollector $session_data_collector ): void {
- $this->session_data_collector = $session_data_collector;
- }
-
- /**
- * Track add payment method page loaded event.
- *
- * Collects session data when the add payment method page is initially loaded.
- * This captures the initial session state before any user interactions.
- *
- * @internal
- * @return void
- */
- public function track_add_payment_method_page_loaded(): void {
- $this->session_data_collector->collect( 'add_payment_method_page_loaded', array() );
- }
-
- /**
- * Track payment method added event.
- *
- * Collects session data 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 track_payment_method_added( $token_id, $token ): void {
- $event_data = $this->build_payment_method_event_data( 'added', $token );
-
- $this->session_data_collector->collect( 'payment_method_added', $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 session data during collection.
- *
- * @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;
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/SessionClearanceManager.php b/plugins/woocommerce/src/Internal/FraudProtection/SessionClearanceManager.php
deleted file mode 100644
index d583c40d9a..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/SessionClearanceManager.php
+++ /dev/null
@@ -1,232 +0,0 @@
-<?php
-/**
- * SessionClearanceManager class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Manages session clearance state for fraud protection.
- *
- * This class handles the session status tracking for fraud protection decisions,
- * managing three possible states: pending, allowed, and blocked. It integrates
- * with WooCommerce sessions and uses the FraudProtectionController logging helper
- * to maintain consistent audit logs.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class SessionClearanceManager {
-
- /**
- * Session key for storing clearance status.
- */
- private const SESSION_KEY = '_fraud_protection_clearance_status';
-
- /**
- * Session status: pending clearance.
- */
- public const STATUS_PENDING = 'pending';
-
- /**
- * Session status: allowed.
- */
- public const STATUS_ALLOWED = 'allowed';
-
- /**
- * Session status: blocked.
- */
- public const STATUS_BLOCKED = 'blocked';
-
- /**
- * Default session status.
- */
- public const DEFAULT_STATUS = self::STATUS_ALLOWED;
-
- /**
- * Check if the current session is allowed.
- *
- * @return bool True if session is allowed, false otherwise.
- */
- public function is_session_allowed(): bool {
- $status = $this->get_session_status();
- return self::STATUS_ALLOWED === $status;
- }
-
- /**
- * Check if the current session is blocked.
- *
- * @return bool True if session is blocked, false otherwise.
- */
- public function is_session_blocked(): bool {
- $status = $this->get_session_status();
- return self::STATUS_BLOCKED === $status;
- }
-
- /**
- * Mark the current session as allowed.
- *
- * @return void
- */
- public function allow_session(): void {
- $this->set_session_status( self::STATUS_ALLOWED );
- $this->log_session_update_event( 'allowed' );
- }
-
- /**
- * Mark the current session as pending (challenge required).
- *
- * @return void
- */
- public function challenge_session(): void {
- $this->set_session_status( self::STATUS_PENDING );
- $this->log_session_update_event( 'challenged' );
- }
-
- /**
- * Mark the current session as blocked and empty the cart.
- *
- * Emptying the cart prevents express payment methods (e.g., PayPal) from
- * rendering on cart pages, as they are loaded via third-party SDKs that
- * don't respect WooCommerce's payment method filtering.
- *
- * @return void
- */
- public function block_session(): void {
- $this->set_session_status( self::STATUS_BLOCKED );
- $this->log_session_update_event( 'blocked' );
- $this->empty_cart();
- }
-
- /**
- * Get the current session clearance status.
- *
- * @return string One of: pending, allowed, blocked.
- */
- public function get_session_status(): string {
- if ( ! $this->is_session_available() ) {
- return self::DEFAULT_STATUS;
- }
-
- $status = WC()->session->get( self::SESSION_KEY, self::DEFAULT_STATUS );
-
- // Validate status value - return default for invalid values.
- if ( ! in_array( $status, array( self::STATUS_PENDING, self::STATUS_ALLOWED, self::STATUS_BLOCKED ), true ) ) {
- return self::DEFAULT_STATUS;
- }
-
- return $status;
- }
-
- /**
- * Set the session clearance status.
- *
- * @param string $status One of: pending, allowed, blocked.
- * @return void
- */
- private function set_session_status( string $status ): void {
- if ( ! $this->is_session_available() ) {
- return;
- }
-
- WC()->session->set( self::SESSION_KEY, $status );
-
- // Ensure session cookie is set so the session persists across page loads.
- // This is important because fraud protection may set session status before
- // any cart action triggers the cookie to be set.
- // Skip cookie setting if headers have already been sent (e.g., in test environment).
- if ( WC()->session instanceof \WC_Session_Handler ) {
- WC()->session->set_customer_session_cookie( true );
- }
- }
-
- /**
- * Reset the session clearance status to default (allowed).
- *
- * @return void
- */
- public function reset_session(): void {
- $this->set_session_status( self::DEFAULT_STATUS );
- }
-
- /**
- * Ensure cart and session are available.
- *
- * Loads cart if not already loaded, which initializes session for both
- * traditional (cookie) and Store API (token) flows.
- *
- * @return void
- */
- public function ensure_cart_loaded(): void {
- if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
- WC()->call_function( 'wc_load_cart' );
- }
- }
-
- /**
- * Check if WooCommerce session is available.
- *
- * @return bool True if session is available.
- */
- private function is_session_available(): bool {
- $this->ensure_cart_loaded();
- return WC()->session instanceof \WC_Session;
- }
-
- /**
- * Get a unique identifier for the current session.
- *
- * @return string Session identifier.
- */
- public function get_session_id(): string {
- if ( ! $this->is_session_available() ) {
- return 'no-session';
- }
-
- // Use or generate a stable session ID for tracking consistency.
- $fraud_customer_session_id = WC()->session->get( '_fraud_protection_customer_session_id' );
- if ( ! $fraud_customer_session_id ) {
- $fraud_customer_session_id = WC()->call_function( 'wc_rand_hash', 'customer_', 30 );
- WC()->session->set( '_fraud_protection_customer_session_id', $fraud_customer_session_id );
- }
- return $fraud_customer_session_id;
- }
-
- /**
- * Empty the cart.
- *
- * @return void
- */
- private function empty_cart(): void {
- if ( function_exists( 'WC' ) && WC()->cart ) {
- WC()->cart->empty_cart();
- }
- }
-
- /**
- * Log a session update event using FraudProtectionController's logging helper.
- *
- * @param string $action The action taken (allowed, challenged, or blocked).
- * @return void
- */
- private function log_session_update_event( string $action ): void {
- $session_id = $this->get_session_id();
- $user_id = get_current_user_id();
- $user_info = $user_id ? "User: {$user_id}" : 'User: guest';
- $timestamp = current_time( 'mysql' );
-
- $message = sprintf(
- 'Session updated: %s | %s | Action: %s | Timestamp: %s',
- $session_id,
- $user_info,
- $action,
- $timestamp
- );
-
- FraudProtectionController::log( 'info', $message );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php b/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
deleted file mode 100644
index a1f1975ef3..0000000000
--- a/plugins/woocommerce/src/Internal/FraudProtection/SessionDataCollector.php
+++ /dev/null
@@ -1,686 +0,0 @@
-<?php
-/**
- * SessionDataCollector class file.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\FraudProtection;
-
-defined( 'ABSPATH' ) || exit;
-
-/**
- * Collects comprehensive session and order data for fraud protection analysis.
- *
- * This class provides manual data collection for fraud protection events, gathering
- * session, customer, order, address, and payment information in the exact nested format
- * required by the WPCOM fraud protection service. All data collection is designed to
- * degrade gracefully when fields are unavailable, ensuring checkout never fails due to
- * missing fraud protection data.
- *
- * @since 10.5.0
- * @internal This class is part of the internal API and is subject to change without notice.
- */
-class SessionDataCollector {
-
- /**
- * SessionClearanceManager instance.
- *
- * @var SessionClearanceManager
- */
- private SessionClearanceManager $session_clearance_manager;
-
- /**
- * Initialize with dependencies.
- *
- * @internal
- *
- * @param SessionClearanceManager $session_clearance_manager The session clearance manager instance.
- */
- final public function init( SessionClearanceManager $session_clearance_manager ): void {
- $this->session_clearance_manager = $session_clearance_manager;
- }
-
- /**
- * Collect comprehensive session and order data for fraud protection.
- *
- * This method is called manually at specific points in the checkout/payment flow
- * to gather all relevant data for fraud analysis. It returns data in the nested
- * format expected by the WPCOM fraud protection service.
- *
- * @since 10.5.0
- *
- * @param string|null $event_type Optional event type identifier (e.g., 'checkout_started', 'payment_attempt').
- * @param array $event_data Optional event-specific additional context data.
- */
- public function collect( ?string $event_type = null, array $event_data = array() ): void {
- // Ensure cart and session are loaded.
- $this->session_clearance_manager->ensure_cart_loaded();
-
- $data = array(
- 'event_type' => $event_type,
- 'timestamp' => gmdate( 'Y-m-d H:i:s' ),
- 'event_data' => $event_data,
- );
-
- // Save the collected data in the session for fraud analysis tracking, preserving multiple calls.
- if ( WC()->session instanceof \WC_Session ) {
- // Retrieve existing data array or initialize if not present.
- $collected_data = WC()->session->get( 'fraud_protection_collected_data' );
- if ( ! is_array( $collected_data ) ) {
- $collected_data = array();
- }
- $collected_data[] = $data;
- $collected_data = $this->trim_to_max_size( $collected_data );
- WC()->session->set( 'fraud_protection_collected_data', $collected_data );
- } else {
- FraudProtectionController::log(
- 'error',
- 'Attempted to save fraud protection data, but no valid WooCommerce session exists.',
- array(
- 'context' => 'SessionDataCollector::collect',
- 'event_type' => $event_type,
- 'event_data' => $event_data,
- )
- );
- }
- }
-
- /**
- * Get all collected fraud protection data from the session.
- *
- * Retrieves the array of collected event data stored during this session.
- * Returns an empty array if no data has been collected or session is unavailable.
- *
- * @since 10.5.0
- *
- * @param int|null $order_id Optional order ID to include order data in the response.
- * @return array Array of collected fraud protection event data.
- */
- public function get_collected_data( ?int $order_id = null ): array {
- $data = array(
- 'wc_version' => WC()->version,
- 'session' => $this->get_session_data(),
- 'customer' => $this->get_customer_data(),
- 'order' => array(),
- 'shipping_address' => $this->get_shipping_address(),
- 'billing_address' => $this->get_billing_address(),
- 'collected_events' => array(),
- );
-
- if ( $order_id ) {
- $data['order'] = $this->get_order_data( $order_id );
- }
-
- // Calculate base data size to ensure total response stays under limit.
- // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
- $base_size = strlen( serialize( $data ) );
-
- if ( WC()->session instanceof \WC_Session ) {
- $collected_data = WC()->session->get( 'fraud_protection_collected_data' );
- if ( is_array( $collected_data ) ) {
- $data['collected_events'] = $this->trim_to_max_size( $collected_data, $base_size );
- }
- }
-
- return $data;
- }
-
- /**
- * Get current billing country from customer data.
- *
- * Reuses the same logic as get_billing_address() but returns only the country.
- * Tries WC_Customer first, falls back to session data, with graceful error handling.
- *
- * @since 10.5.0
- *
- * @return string|null Current billing country code or null if unavailable.
- */
- public function get_current_billing_country(): ?string {
- try {
- if ( WC()->customer instanceof \WC_Customer ) {
- $country = WC()->customer->get_billing_country();
- return ! empty( $country ) ? \sanitize_text_field( $country ) : null;
- } elseif ( WC()->session instanceof \WC_Session ) {
- $customer_data = WC()->session->get( 'customer' );
- if ( is_array( $customer_data ) && ! empty( $customer_data['country'] ) ) {
- return \sanitize_text_field( $customer_data['country'] );
- }
- }
- } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // Graceful degradation.
- }
- return null;
- }
-
- /**
- * Get current shipping country from customer data.
- *
- * Reuses the same logic as get_shipping_address() but returns only the country.
- * Tries WC_Customer first, falls back to session data, with graceful error handling.
- *
- * @since 10.5.0
- *
- * @return string|null Current shipping country code or null if unavailable.
- */
- public function get_current_shipping_country(): ?string {
- try {
- if ( WC()->customer instanceof \WC_Customer ) {
- $country = WC()->customer->get_shipping_country();
- return ! empty( $country ) ? \sanitize_text_field( $country ) : null;
- } elseif ( WC()->session instanceof \WC_Session ) {
- $customer_data = WC()->session->get( 'customer' );
- if ( is_array( $customer_data ) && ! empty( $customer_data['shipping_country'] ) ) {
- return \sanitize_text_field( $customer_data['shipping_country'] );
- }
- }
- } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // Graceful degradation.
- }
- return null;
- }
-
- /**
- * Get session data including session ID, IP address, email, and user agent.
- *
- * Collects session identification and tracking data with graceful degradation
- * for unavailable fields. Email collection follows the fallback chain:
- * logged-in user email → session customer data → WC_Customer billing email.
- *
- * @since 10.5.0
- *
- * @return array Session data array with 6 keys.
- */
- private function get_session_data(): array {
- try {
- $session_id = $this->session_clearance_manager->get_session_id();
- $ip_address = $this->get_ip_address();
- $email = $this->get_email();
- $user_agent = $this->get_user_agent();
-
- /**
- * $is_user_session is flag that we have a real browser session vs API-based interaction.
- * We start with a very basic check, but we might need a more sophisticated way to detect it in the future.
- */
- $is_user_session = 'no-session' !== $session_id;
-
- return array(
- 'session_id' => $session_id,
- 'ip_address' => $ip_address,
- 'email' => $email,
- 'ja3_hash' => null,
- 'user_agent' => $user_agent,
- 'is_user_session' => $is_user_session,
- );
- } catch ( \Exception $e ) {
- // Graceful degradation - return structure with null values.
- return array(
- 'session_id' => null,
- 'ip_address' => null,
- 'email' => null,
- 'ja3_hash' => null,
- 'user_agent' => null,
- 'is_user_session' => false,
- );
- }
- }
-
- /**
- * Get customer data including name, billing email, and order history.
- *
- * Collects customer identification and history data with graceful degradation.
- * Tries WC_Customer object first, then falls back to session data if values are empty.
- * Includes lifetime_order_count which counts all orders regardless of status.
- *
- * @since 10.5.0
- *
- * @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 {
- $lifetime_order_count = 0;
-
- // Try WC_Customer object first.
- if ( WC()->customer instanceof \WC_Customer ) {
- 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();
- }
-
- $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_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 ),
- )
- );
- }
- }
- } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // Graceful degradation - return as much data as possible.
- }
-
- return $customer_data;
- }
-
- /**
- * Get order data including totals, currency, cart hash, and cart items.
- *
- * Collects comprehensive order information from the cart with graceful degradation.
- * Calculates shipping_tax_rate from shipping tax and shipping total. Sets customer_id
- * to 'guest' for non-logged-in users.
- *
- * @since 10.5.0
- *
- * @param int|null $order_id_from_event Optional order ID from event data.
- * @return array Order data array with 11 keys including items array.
- */
- private function get_order_data( ?int $order_id_from_event = null ): array {
- try {
- // Initialize default values.
- $order_id = $order_id_from_event;
- $customer_id = 'guest';
- $total = 0;
- $items_total = 0;
- $shipping_total = 0;
- $tax_total = 0;
- $shipping_tax_rate = null;
- $discount_total = 0;
- $currency = WC()->call_function( 'get_woocommerce_currency' );
- $cart_hash = null;
- $items = array();
-
- // Get customer ID from WooCommerce customer object if available.
- // We don't need to fallback to session data here, because customer id won't be stored there.
- if ( WC()->customer instanceof \WC_Customer ) {
- $id = WC()->customer->get_id();
- if ( $id ) {
- $customer_id = $id;
- }
- }
- // Get cart data if available.
- if ( WC()->cart instanceof \WC_Cart ) {
- $items_total = (float) WC()->cart->get_subtotal();
- $shipping_total = (float) WC()->cart->get_shipping_total();
- $tax_total = (float) WC()->cart->get_cart_contents_tax();
- $discount_total = (float) WC()->cart->get_discount_total();
- $cart_hash = WC()->cart->get_cart_hash();
- $items = $this->get_cart_items();
- $total = (float) WC()->cart->get_total( 'edit' );
-
- // Calculate shipping_tax_rate.
- $shipping_tax = (float) WC()->cart->get_shipping_tax();
- if ( $shipping_total > 0 && $shipping_tax > 0 ) {
- $shipping_tax_rate = $shipping_tax / $shipping_total;
- }
- }
-
- return array(
- 'order_id' => $order_id,
- 'customer_id' => $customer_id,
- 'total' => $total,
- 'items_total' => $items_total,
- 'shipping_total' => $shipping_total,
- 'tax_total' => $tax_total,
- 'shipping_tax_rate' => $shipping_tax_rate,
- 'discount_total' => $discount_total,
- 'currency' => $currency,
- 'cart_hash' => $cart_hash,
- 'items' => $items,
- );
- } catch ( \Exception $e ) {
- // Graceful degradation - return structure with default values.
- return array(
- 'order_id' => null,
- 'customer_id' => 'guest',
- 'total' => 0,
- 'items_total' => 0,
- 'shipping_total' => 0,
- 'tax_total' => 0,
- 'shipping_tax_rate' => null,
- 'discount_total' => 0,
- 'currency' => WC()->call_function( 'get_woocommerce_currency' ),
- 'cart_hash' => null,
- 'items' => array(),
- );
- }
- }
-
- /**
- * Get cart items with detailed product information.
- *
- * Iterates through cart items and extracts comprehensive product data including
- * name, description, category, SKU, pricing, quantities, and WooCommerce-specific
- * attributes. Returns array of item objects with 12 fields each.
- *
- * @since 10.5.0
- *
- * @return array Array of cart item objects with detailed product information.
- */
- private function get_cart_items(): array {
- $items = array();
-
- try {
- if ( ! WC()->cart instanceof \WC_Cart ) {
- return $items;
- }
-
- foreach ( WC()->cart->get_cart() as $cart_item ) {
- try {
- $product = $cart_item['data'] ?? null;
-
- if ( ! $product instanceof \WC_Product ) {
- continue;
- }
-
- $quantity = $cart_item['quantity'] ?? 1;
-
- // Calculate per-unit amounts.
- $unit_price = (float) $product->get_price();
- $line_tax = $cart_item['line_tax'] ?? 0;
- $unit_tax_amount = $quantity > 0 ? ( (float) $line_tax / $quantity ) : 0;
- $line_discount = $cart_item['line_subtotal'] - $cart_item['line_total'];
- $unit_discount_amount = $quantity > 0 ? ( (float) $line_discount / $quantity ) : 0;
- $category = $this->get_product_category_names( $product );
-
- $items[] = array(
- 'name' => $product->get_name() ? $product->get_name() : null,
- 'description' => $product->get_description() ? $product->get_description() : null,
- 'category' => $category,
- 'sku' => $product->get_sku() ? $product->get_sku() : null,
- 'quantity' => $quantity,
- 'unit_price' => $unit_price,
- 'unit_tax_amount' => $unit_tax_amount,
- 'unit_discount_amount' => $unit_discount_amount,
- 'product_type' => $product->get_type() ? $product->get_type() : null,
- 'is_virtual' => $product->is_virtual(),
- 'is_downloadable' => $product->is_downloadable(),
- 'attributes' => $product->get_attributes() ? $product->get_attributes() : array(),
- );
- } catch ( \Exception $e ) {
- // Skip this item if there's an error, continue with next item.
- continue;
- }
- }
- } catch ( \Exception $e ) {
- // Return empty array on error.
- return array();
- }
-
- return $items;
- }
-
- /**
- * Get billing address from customer data.
- *
- * Collects billing address fields from WC_Customer object with graceful degradation.
- * Returns array with 6 address fields, sanitized with sanitize_text_field().
- *
- * @since 10.5.0
- *
- * @return array Billing address array with 6 keys.
- */
- private function get_billing_address(): array {
- $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 ) {
- $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 ),
- )
- );
- }
- }
- } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // Graceful degradation - prevents any errors from being thrown.
- }
-
- return $billing_data;
- }
-
- /**
- * Get shipping address from customer data.
- *
- * Collects shipping address fields from WC_Customer object with graceful degradation.
- * Returns array with 6 address fields, sanitized with sanitize_text_field().
- *
- * @since 10.5.0
- *
- * @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 {
- if ( WC()->customer instanceof \WC_Customer ) {
- $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 ),
- )
- );
- }
- }
- } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // Graceful degradation - returns as much data as possible.
- }
-
- return $shipping_data;
- }
-
- /**
- * Get client IP address using WooCommerce geolocation utility.
- *
- * @since 10.5.0
- *
- * @return string|null IP address or null if not available.
- */
- private function get_ip_address(): ?string {
- if ( class_exists( 'WC_Geolocation' ) ) {
- $ip = \WC_Geolocation::get_ip_address();
- return $ip ? $ip : null;
- }
- return null;
- }
-
- /**
- * Get customer email with fallback chain.
- *
- * Tries logged-in user email first, then WC_Customer billing email,
- * then session customer data as fallback.
- *
- * @since 10.5.0
- *
- * @return string|null Email address or null if not available.
- */
- private function get_email(): ?string {
- // Try logged-in user first.
- if ( \is_user_logged_in() ) {
- $user = \wp_get_current_user();
- if ( $user && $user->user_email ) {
- return \sanitize_email( $user->user_email );
- }
- }
-
- // Try WC_Customer object.
- if ( WC()->customer instanceof \WC_Customer ) {
- $email = WC()->customer->get_billing_email();
- if ( $email ) {
- return \sanitize_email( $email );
- }
- }
-
- // Fallback to session customer data if WC_Customer not available.
- if ( WC()->session instanceof \WC_Session ) {
- $customer_data = WC()->session->get( 'customer' );
- if ( is_array( $customer_data ) && ! empty( $customer_data['email'] ) ) {
- return \sanitize_email( $customer_data['email'] );
- }
- }
-
- return null;
- }
-
- /**
- * Get user agent string from HTTP headers.
- *
- * @since 10.5.0
- *
- * @return string|null User agent or null if not available.
- */
- private function get_user_agent(): ?string {
- if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
- return sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) );
- }
- return null;
- }
-
- /**
- * Get product category names as comma-separated list.
- *
- * Uses WooCommerce helper with caching for better performance.
- * Returns all categories for the product, not just the primary one.
- *
- * @since 10.5.0
- *
- * @param \WC_Product $product The product object.
- * @return string|null Comma-separated category names or null if none.
- */
- private function get_product_category_names( \WC_Product $product ): ?string {
- $terms = WC()->call_function( 'wc_get_product_terms', $product->get_id(), 'product_cat' );
- if ( empty( $terms ) || ! is_array( $terms ) ) {
- return null;
- }
- $category_names = array_map(
- function ( $term ) {
- return $term->name;
- },
- $terms
- );
- return implode( ', ', $category_names );
- }
-
- /**
- * Trim collected data array to ensure it stays within 1 MB size limit.
- *
- * Removes oldest entries from the array until the serialized size is under the limit.
- * Always keeps at least one entry (the most recent).
- *
- * @since 10.5.0
- *
- * @param array $data Array of collected event data.
- * @param int $base_size Size in bytes of additional data that will be combined with this array.
- * @return array Trimmed array that fits within the size limit.
- */
- private function trim_to_max_size( array $data, int $base_size = 0 ): array {
- $max_size_bytes = 1 * 1024 * 1024 - $base_size; // 1 MB minus base data size.
- $data_count = count( $data );
- // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
- $data_size = strlen( serialize( $data ) );
-
- while ( $data_count > 1 && $data_size > $max_size_bytes ) {
- array_shift( $data );
- $data_count = count( $data );
- // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for size calculation only.
- $data_size = strlen( serialize( $data ) );
- }
-
- return $data;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
index 99f1e3e91a..84d680e352 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
@@ -4,9 +4,6 @@ namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
@@ -123,18 +120,6 @@ abstract class AbstractCartRoute extends AbstractRoute {
$response = $nonce_check;
}
- // Block cart modifications if session is blocked by fraud protection.
- if ( ! $response && $this->is_update_request( $request ) ) {
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- $response = $this->get_route_error_response(
- 'woocommerce_rest_cart_error',
- wc_get_container()->get( BlockedSessionNotice::class )->get_message_plaintext( 'purchase' ),
- 403
- );
- }
- }
-
if ( ! $response ) {
try {
$response = $this->get_response_by_request_method( $request );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
index 7728b48b31..d25754a0d3 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/CartUpdateCustomer.php
@@ -1,8 +1,6 @@
<?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;
/**
@@ -215,11 +213,6 @@ 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 ) );
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index 52273b5d70..00ac3ad489 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -8,10 +8,6 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
/**
* Checkout class.
@@ -157,16 +153,6 @@ class Checkout extends AbstractCartRoute {
$response = $nonce_check;
}
- // Block early if session is blocked by fraud protection.
- if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled()
- && wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
- $response = $this->get_route_error_response(
- 'woocommerce_rest_checkout_error',
- wc_get_container()->get( BlockedSessionNotice::class )->get_message_plaintext( 'purchase' ),
- 403
- );
- }
-
if ( ! $response ) {
try {
$response = $this->get_response_by_request_method( $request );
@@ -583,15 +569,6 @@ class Checkout extends AbstractCartRoute {
true
);
- // Track successful order placement (success or pending payment).
- if ( in_array( $payment_result->get_status(), array( 'success', 'pending' ), true ) ) {
- $container = wc_get_container();
- if ( $container->get( FraudProtectionController::class )->feature_is_enabled() ) {
- $container->get( CheckoutEventTracker::class )
- ->track_order_placed( $this->order->get_id(), $this->order );
- }
- }
-
return $this->prepare_item_for_response(
(object) [
'order' => wc_get_order( $this->order ),
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
index a6e1aafe85..a2120758e8 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
@@ -10,8 +10,6 @@ use Automattic\WooCommerce\StoreApi\Exceptions\OutOfStockException;
use Automattic\WooCommerce\StoreApi\Exceptions\PartialOutOfStockException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Exceptions\TooManyInCartException;
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\StoreApi\Utilities\ArrayUtils;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\StoreApi\Utilities\NoticeHandler;
@@ -225,12 +223,6 @@ class CartController {
$request['cart_item_data']
);
- // Track cart event for fraud protection.
- if ( $product instanceof \WC_Product && wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
- wc_get_container()->get( CartEventTracker::class )
- ->track_cart_item_added( $cart_id, $this->get_product_id( $product ), (int) $request_quantity, $this->get_variation_id( $product ) );
- }
-
return $cart_id;
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
index 16200b1cc3..e55ae95a3a 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
@@ -3,8 +3,6 @@
* @package WooCommerce\Tests\PaymentGateways
*/
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-
/**
* Class WC_Payment_Gateways_Test.
*/
@@ -37,9 +35,7 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
*/
public function tearDown(): void {
parent::tearDown();
- delete_option( 'woocommerce_feature_fraud_protection_enabled' );
delete_option( 'jetpack_activation_source' );
- wc_get_container()->get( SessionClearanceManager::class )->reset_session();
}
/**
@@ -102,36 +98,6 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
remove_filter( 'wp_mail', $watcher );
}
- /**
- * Test that payment gateways are hidden when fraud protection blocks the session.
- */
- public function test_get_available_payment_gateways_returns_empty_when_session_blocked() {
- // Enable fraud protection and block the session.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
- wc_get_container()->get( SessionClearanceManager::class )->block_session();
-
- $this->enable_all_gateways();
-
- $gateways = $this->sut->get_available_payment_gateways();
-
- $this->assertEmpty( $gateways, 'Should return empty array when session is blocked' );
- }
-
- /**
- * Test that payment gateways are returned when fraud protection is disabled, even if session is blocked.
- */
- public function test_get_available_payment_gateways_returns_gateways_when_feature_disabled() {
- // Disable fraud protection but block the session.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'no' );
- wc_get_container()->get( SessionClearanceManager::class )->block_session();
-
- $this->enable_all_gateways();
-
- $gateways = $this->sut->get_available_payment_gateways();
-
- $this->assertNotEmpty( $gateways, 'Should return gateways when feature is disabled' );
- }
-
/**
* Test get_payment_gateway_name_by_id returns gateway title for known gateway.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartFraudProtection.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartFraudProtection.php
deleted file mode 100644
index 872fc34d69..0000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartFraudProtection.php
+++ /dev/null
@@ -1,163 +0,0 @@
-<?php
-/**
- * Cart Fraud Protection Tests.
- */
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
-
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Enums\ProductStockStatus;
-
-/**
- * Cart Fraud Protection Tests.
- *
- * Tests that cart modification routes are blocked when a session is blocked by fraud protection.
- * All cart routes extend AbstractCartRoute which checks session status before processing mutations.
- */
-class CartFraudProtection extends ControllerTestCase {
-
- /**
- * Test product.
- *
- * @var \WC_Product
- */
- private $product;
-
- /**
- * Mock FraudProtectionController.
- *
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
- */
- private $fraud_controller_mock;
-
- /**
- * Mock SessionClearanceManager.
- *
- * @var SessionClearanceManager|\PHPUnit\Framework\MockObject\MockObject
- */
- private $session_manager_mock;
-
- /**
- * Mock BlockedSessionNotice.
- *
- * @var BlockedSessionNotice|\PHPUnit\Framework\MockObject\MockObject
- */
- private $blocked_notice_mock;
-
- /**
- * Setup test product data.
- */
- protected function setUp(): void {
- parent::setUp();
-
- $fixtures = new FixtureData();
- $this->product = $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- )
- );
-
- wc_empty_cart();
-
- $this->fraud_controller_mock = $this->createMock( FraudProtectionController::class );
- $this->session_manager_mock = $this->createMock( SessionClearanceManager::class );
- $this->blocked_notice_mock = $this->createMock( BlockedSessionNotice::class );
-
- wc_get_container()->replace( FraudProtectionController::class, $this->fraud_controller_mock );
- wc_get_container()->replace( SessionClearanceManager::class, $this->session_manager_mock );
- wc_get_container()->replace( BlockedSessionNotice::class, $this->blocked_notice_mock );
- }
-
- /**
- * Tear down.
- */
- protected function tearDown(): void {
- wc_get_container()->reset_replacement( FraudProtectionController::class );
- wc_get_container()->reset_replacement( SessionClearanceManager::class );
- wc_get_container()->reset_replacement( BlockedSessionNotice::class );
-
- parent::tearDown();
- }
-
- /**
- * @testdox Cart mutations return 403 with blocked message when session is blocked.
- */
- public function test_cart_mutations_blocked_when_session_blocked(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( true );
- $this->blocked_notice_mock->method( 'get_message_plaintext' )->willReturn( 'Session blocked message' );
-
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/add-item' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params(
- array(
- 'id' => $this->product->get_id(),
- 'quantity' => 1,
- )
- );
-
- $this->assertApiResponse(
- $request,
- 403,
- array(
- 'code' => 'woocommerce_rest_cart_error',
- 'message' => 'Session blocked message',
- )
- );
- }
-
- /**
- * @testdox Cart mutations succeed when session is allowed.
- */
- public function test_cart_mutations_allowed_when_session_allowed(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( false );
-
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/add-item' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params(
- array(
- 'id' => $this->product->get_id(),
- 'quantity' => 1,
- )
- );
-
- $this->assertApiResponse( $request, 201 );
- }
-
- /**
- * @testdox Cart mutations succeed when fraud protection is disabled.
- */
- public function test_cart_mutations_allowed_when_feature_disabled(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( false );
- $this->session_manager_mock->expects( $this->never() )->method( 'is_session_blocked' );
-
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/cart/add-item' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params(
- array(
- 'id' => $this->product->get_id(),
- 'quantity' => 1,
- )
- );
-
- $this->assertApiResponse( $request, 201 );
- }
-
- /**
- * @testdox GET requests are not blocked (read-only).
- */
- public function test_get_requests_allowed_when_session_blocked(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( true );
-
- $this->assertApiResponse( '/wc/store/v1/cart', 200 );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
index 1c9e9757a0..3aee672561 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
@@ -12,7 +12,6 @@ use Automattic\WooCommerce\StoreApi\Formatters\HtmlFormatter;
use Automattic\WooCommerce\StoreApi\Formatters\CurrencyFormatter;
use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema;
use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
use Automattic\WooCommerce\StoreApi\Routes\V1\Checkout as CheckoutRoute;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\Blocks\Package;
@@ -35,10 +34,6 @@ class Checkout extends MockeryTestCase {
protected function setUp(): void {
parent::setUp();
- // Set jetpack_activation_source option to prevent "Cannot use bool as array" error
- // in Jetpack Connection Manager's apply_activation_source_to_args method.
- update_option( 'jetpack_activation_source', array( '', '' ) );
-
add_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ), 10, 4 );
update_option( 'woocommerce_enable_guest_checkout', 'yes' );
@@ -131,10 +126,6 @@ class Checkout extends MockeryTestCase {
remove_all_actions( 'woocommerce_store_api_checkout_order_processed' );
remove_all_actions( 'woocommerce_valid_order_statuses_for_payment' );
- delete_option( 'woocommerce_feature_fraud_protection_enabled' );
- delete_option( 'jetpack_activation_source' );
- wc_get_container()->get( SessionClearanceManager::class )->reset_session();
-
update_option( 'woocommerce_ship_to_countries', 'all' );
update_option( 'woocommerce_allowed_countries', 'all' );
update_option( 'woocommerce_enable_guest_checkout', 'yes' );
@@ -1797,53 +1788,4 @@ class Checkout extends MockeryTestCase {
// Order shouldn't stay in custom status, instead we let payment gateway set the correct status.
$this->assertEquals( 'on-hold', $order->get_status(), 'Order status should be controlled by the payment gateway, not remain custom.' );
}
-
- /**
- * Test that checkout is blocked when fraud protection blocks the session.
- */
- public function test_checkout_blocked_when_session_blocked() {
- // Enable fraud protection and block the session.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
- wc_get_container()->get( SessionClearanceManager::class )->block_session();
-
- $request = new \WP_REST_Request( 'POST', '/wc/store/v1/checkout' );
- $request->set_header( 'Nonce', wp_create_nonce( 'wc_store_api' ) );
- $request->set_body_params(
- array(
- 'billing_address' => (object) array(
- 'first_name' => 'test',
- 'last_name' => 'test',
- 'company' => '',
- 'address_1' => 'test',
- 'address_2' => '',
- 'city' => 'test',
- 'state' => '',
- 'postcode' => 'cb241ab',
- 'country' => 'GB',
- 'phone' => '',
- 'email' => 'testaccount@test.com',
- ),
- 'shipping_address' => (object) array(
- 'first_name' => 'test',
- 'last_name' => 'test',
- 'company' => '',
- 'address_1' => 'test',
- 'address_2' => '',
- 'city' => 'test',
- 'state' => '',
- 'postcode' => 'cb241ab',
- 'country' => 'GB',
- 'phone' => '',
- ),
- 'payment_method' => WC_Gateway_BACS::ID,
- )
- );
-
- $response = rest_get_server()->dispatch( $request );
-
- $this->assertEquals( 403, $response->get_status(), 'Should return 403 when session is blocked' );
- $this->assertEquals( 'woocommerce_rest_checkout_error', $response->get_data()['code'] );
- $this->assertStringContainsString( 'unable to process this request online', $response->get_data()['message'] );
- $this->assertStringContainsString( 'to complete your purchase', $response->get_data()['message'], 'Should use checkout-specific message' );
- }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
index c7cc674876..6d90ea2bd9 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
@@ -3,8 +3,6 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Utilities;
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
use Automattic\WooCommerce\StoreApi\Utilities\CartController;
use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
@@ -290,150 +288,4 @@ class CartControllerTests extends TestCase {
remove_all_filters( 'woocommerce_cart_shipping_packages' );
}
-
- /**
- * Test that fraud protection tracking is called when adding items via Store API.
- */
- public function test_add_to_cart_triggers_fraud_protection_tracking(): void {
- $fixtures = new FixtureData();
- $product = $fixtures->get_simple_product(
- array(
- 'name' => 'Fraud Protection Test Product',
- 'regular_price' => 10,
- )
- );
-
- // Create mock for CartEventTracker.
- $mock_cart_tracker = $this->createMock( CartEventTracker::class );
-
- // Create mock for FraudProtectionController that returns enabled.
- $mock_controller = $this->createMock( FraudProtectionController::class );
- $mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Replace container instances with mocks.
- $container = wc_get_container();
- $container->replace( FraudProtectionController::class, $mock_controller );
- $container->replace( CartEventTracker::class, $mock_cart_tracker );
-
- // Expect track_cart_item_added to be called once with correct parameters.
- $mock_cart_tracker
- ->expects( $this->once() )
- ->method( 'track_cart_item_added' )
- ->with(
- $this->isType( 'string' ), // cart_id.
- $this->equalTo( $product->get_id() ), // product_id.
- $this->equalTo( 2 ), // quantity.
- $this->equalTo( 0 ) // variation_id.
- );
-
- // Use Store API CartController to add item.
- $cart_controller = new CartController();
- $cart_controller->add_to_cart(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 2,
- )
- );
- }
-
- /**
- * Test that fraud protection tracking is NOT called when feature is disabled.
- */
- public function test_add_to_cart_skips_fraud_protection_tracking_when_disabled(): void {
- $fixtures = new FixtureData();
- $product = $fixtures->get_simple_product(
- array(
- 'name' => 'Fraud Protection Disabled Test Product',
- 'regular_price' => 10,
- )
- );
-
- // Create mock for CartEventTracker.
- $mock_cart_tracker = $this->createMock( CartEventTracker::class );
-
- // Create mock for FraudProtectionController that returns disabled.
- $mock_controller = $this->createMock( FraudProtectionController::class );
- $mock_controller->method( 'feature_is_enabled' )->willReturn( false );
-
- // Replace container instances with mocks.
- $container = wc_get_container();
- $container->replace( FraudProtectionController::class, $mock_controller );
- $container->replace( CartEventTracker::class, $mock_cart_tracker );
-
- // Expect track_cart_item_added to NOT be called.
- $mock_cart_tracker
- ->expects( $this->never() )
- ->method( 'track_cart_item_added' );
-
- // Use Store API CartController to add item.
- $cart_controller = new CartController();
- $cart_controller->add_to_cart(
- array(
- 'id' => $product->get_id(),
- 'quantity' => 1,
- )
- );
- }
-
- /**
- * Test that fraud protection tracking includes variation_id for variable products.
- */
- public function test_add_to_cart_tracks_variation_id_for_variable_products(): void {
- // Create a variable product with variations using WC_Helper_Product.
- $variable_product = \WC_Helper_Product::create_variation_product();
-
- // Get the third variation which has all attributes defined (size, colour, number).
- $children = $variable_product->get_children();
- $variation_id = $children[2]; // "DUMMY SKU VARIABLE HUGE RED 0" - has all attributes.
-
- // Create mock for CartEventTracker.
- $mock_cart_tracker = $this->createMock( CartEventTracker::class );
-
- // Create mock for FraudProtectionController that returns enabled.
- $mock_controller = $this->createMock( FraudProtectionController::class );
- $mock_controller->method( 'feature_is_enabled' )->willReturn( true );
-
- // Replace container instances with mocks.
- $container = wc_get_container();
- $container->replace( FraudProtectionController::class, $mock_controller );
- $container->replace( CartEventTracker::class, $mock_cart_tracker );
-
- // Expect track_cart_item_added to be called with the correct variation_id.
- $mock_cart_tracker
- ->expects( $this->once() )
- ->method( 'track_cart_item_added' )
- ->with(
- $this->isType( 'string' ), // cart_id.
- $this->equalTo( $variable_product->get_id() ), // product_id.
- $this->equalTo( 1 ), // quantity.
- $this->equalTo( $variation_id ) // variation_id.
- );
-
- // Use Store API CartController to add variation with all required attributes.
- // CartController::parse_variation_data() expects an array of {attribute, value} pairs.
- $cart_controller = new CartController();
- $cart_controller->add_to_cart(
- array(
- 'id' => $variation_id,
- 'quantity' => 1,
- 'variation' => array(
- array(
- 'attribute' => 'attribute_pa_size',
- 'value' => 'huge',
- ),
- array(
- 'attribute' => 'attribute_pa_colour',
- 'value' => 'red',
- ),
- array(
- 'attribute' => 'attribute_pa_number',
- 'value' => '0',
- ),
- ),
- )
- );
-
- // Clean up.
- $variable_product->delete( true );
- }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php
deleted file mode 100644
index f926f03021..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/ApiClientTest.php
+++ /dev/null
@@ -1,352 +0,0 @@
-<?php
-/**
- * ApiClientTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\ApiClient;
-use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
-use WC_Unit_Test_Case;
-use WP_Error;
-
-/**
- * Tests for the ApiClient class.
- *
- * Tests the Blackbox API client which provides:
- * - verify(): Verify a session and get a fraud decision (allow/block)
- * - report(): Report fraud events for feedback
- */
-class ApiClientTest extends WC_Unit_Test_Case {
-
- use LoggerSpyTrait;
-
- /**
- * The System Under Test.
- *
- * @var ApiClient
- */
- private ApiClient $sut;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->sut = new ApiClient();
-
- update_option( 'jetpack_options', array( 'id' => 12345 ) );
- update_option( 'jetpack_private_options', array( 'blog_token' => 'IAM.AJETPACKBLOGTOKEN' ) );
- }
-
- /**
- * Tear down test fixtures.
- */
- public function tearDown(): void {
- remove_all_filters( 'pre_http_request' );
- delete_option( 'jetpack_options' );
- delete_option( 'jetpack_private_options' );
- parent::tearDown();
- }
-
- /*
- |--------------------------------------------------------------------------
- | verify() Tests
- |--------------------------------------------------------------------------
- */
-
- /**
- * Test verify calls correct endpoint with payload.
- *
- * @testdox verify() calls Blackbox API /verify endpoint with the correct payload
- */
- public function test_verify_calls_verify_endpoint(): void {
- $captured_url = null;
- $captured_body = null;
-
- add_filter(
- 'pre_http_request',
- function ( $preempt, $args, $url ) use ( &$captured_url, &$captured_body ) {
- unset( $preempt );
- $captured_body = json_decode( $args['body'], true );
- $captured_url = $url;
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'allow' ) ),
- );
- },
- 10,
- 3
- );
-
- $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertStringContainsString( 'blackbox-api.wp.com/v1/verify', $captured_url );
- $this->assertSame( 'test-session-id', $captured_body['session_id'] );
- $this->assertArrayHasKey( 'extra', $captured_body );
- $this->assertArrayHasKey( 'blog_id', $captured_body['extra'] );
- $this->assertSame( 12345, $captured_body['extra']['blog_id'] );
- }
-
- /**
- * Test verify returns allow decision.
- *
- * @testdox verify() returns allow decision from API
- */
- public function test_verify_returns_allow_decision(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'allow' ) ),
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- }
-
- /**
- * Test verify returns block decision.
- *
- * @testdox verify() returns block decision from API
- */
- public function test_verify_returns_block_decision(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'block' ) ),
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_BLOCK, $result );
- }
-
- /**
- * Test verify fails open when blog_id not found.
- *
- * @testdox verify() fails open with allow when blog_id not found
- */
- public function test_verify_fails_open_when_blog_id_not_found(): void {
- update_option( 'jetpack_options', array( 'id' => null ) );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'Jetpack blog ID not found' );
- }
-
- /**
- * Test verify fails open on HTTP error.
- *
- * @testdox verify() fails open with allow when HTTP request fails
- */
- public function test_verify_fails_open_on_http_error(): void {
- add_filter(
- 'pre_http_request',
- fn() => new WP_Error( 'http_error', 'Connection timeout' )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'Connection timeout' );
- }
-
- /**
- * Test verify fails open on server error.
- *
- * @testdox verify() fails open with allow when API returns 5xx error
- */
- public function test_verify_fails_open_on_server_error(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 500 ),
- 'body' => 'Internal Server Error',
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'status code 500' );
- }
-
- /**
- * Test verify fails open on invalid JSON.
- *
- * @testdox verify() fails open with allow when API returns invalid JSON
- */
- public function test_verify_fails_open_on_invalid_json(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => 'not valid json',
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'Failed to decode JSON' );
- }
-
- /**
- * Test verify fails open when decision field missing.
- *
- * @testdox verify() fails open with allow when response missing decision field
- */
- public function test_verify_fails_open_when_missing_decision(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'risk_score' => 50 ) ),
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'missing "decision" field' );
- }
-
- /**
- * Test verify fails open on invalid decision value.
- *
- * @testdox verify() fails open with allow when decision value is invalid
- */
- public function test_verify_fails_open_on_invalid_decision(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'decision' => 'unknown_value' ) ),
- )
- );
-
- $result = $this->sut->verify( 'test-session-id', array( 'event_type' => 'checkout_started' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'error', 'Invalid decision value' );
- }
-
- /*
- |--------------------------------------------------------------------------
- | report() Tests
- |--------------------------------------------------------------------------
- */
-
- /**
- * Test report calls correct endpoint.
- *
- * @testdox report() calls Blackbox API /report endpoint
- */
- public function test_report_calls_report_endpoint(): void {
- $captured_url = null;
- $captured_body = null;
-
- add_filter(
- 'pre_http_request',
- function ( $preempt, $args, $url ) use ( &$captured_url, &$captured_body ) {
- unset( $preempt );
- $captured_url = $url;
- $captured_body = json_decode( $args['body'], true );
- return array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'status' => 'ok' ) ),
- );
- },
- 10,
- 3
- );
-
- $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
-
- $this->assertStringContainsString( 'blackbox-api.wp.com/v1/report', $captured_url );
- $this->assertSame( 'test-session-id', $captured_body['session_id'] );
- $this->assertArrayHasKey( 'extra', $captured_body );
- $this->assertArrayHasKey( 'blog_id', $captured_body['extra'] );
- $this->assertSame( 12345, $captured_body['extra']['blog_id'] );
- }
-
- /**
- * Test report returns true on success.
- *
- * @testdox report() returns true on success
- */
- public function test_report_returns_true_on_success(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 200 ),
- 'body' => wp_json_encode( array( 'status' => 'ok' ) ),
- )
- );
-
- $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
-
- $this->assertTrue( $result );
- $this->assertLogged( 'info', 'Event reported successfully' );
- }
-
- /**
- * Test report returns false when blog_id not found.
- *
- * @testdox report() returns false when blog_id not found
- */
- public function test_report_returns_false_when_blog_id_not_found(): void {
- update_option( 'jetpack_options', array( 'id' => null ) );
-
- $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
-
- $this->assertFalse( $result );
- $this->assertLogged( 'error', 'Jetpack blog ID not found' );
- }
-
- /**
- * Test report returns false on HTTP error.
- *
- * @testdox report() returns false when HTTP request fails
- */
- public function test_report_returns_false_on_http_error(): void {
- add_filter(
- 'pre_http_request',
- fn() => new WP_Error( 'http_error', 'Connection timeout' )
- );
-
- $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
-
- $this->assertFalse( $result );
- $this->assertLogged( 'error', 'Failed to report event' );
- }
-
- /**
- * Test report returns false on server error.
- *
- * @testdox report() returns false when API returns error status
- */
- public function test_report_returns_false_on_server_error(): void {
- add_filter(
- 'pre_http_request',
- fn() => array(
- 'response' => array( 'code' => 500 ),
- 'body' => 'Internal Server Error',
- )
- );
-
- $result = $this->sut->report( 'test-session-id', array( 'event_type' => 'payment_success' ) );
-
- $this->assertFalse( $result );
- $this->assertLogged( 'error', 'status code 500' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php
deleted file mode 100644
index b3ac8f3331..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php
+++ /dev/null
@@ -1,231 +0,0 @@
-<?php
-/**
- * BlackboxScriptHandlerTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\BlackboxScriptHandler;
-use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for BlackboxScriptHandler.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\BlackboxScriptHandler
- */
-class BlackboxScriptHandlerTest extends WC_Unit_Test_Case {
-
- use LoggerSpyTrait;
-
- /**
- * The System Under Test.
- *
- * @var BlackboxScriptHandler
- */
- private $sut;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->sut = new BlackboxScriptHandler();
- $this->sut->register();
- }
-
- /**
- * Tear down test fixtures.
- */
- public function tearDown(): void {
- parent::tearDown();
- remove_all_filters( 'woocommerce_fraud_protection_enqueue_blackbox_scripts' );
- remove_all_filters( 'woocommerce_is_checkout' );
- remove_all_filters( 'pre_option_jetpack_options' );
- remove_all_filters( 'pre_option_woocommerce_myaccount_page_id' );
- wp_dequeue_script( 'wc-fraud-protection-blackbox' );
- wp_dequeue_script( 'wc-fraud-protection-blackbox-init' );
- wp_deregister_script( 'wc-fraud-protection-blackbox' );
- wp_deregister_script( 'wc-fraud-protection-blackbox-init' );
-
- // Clean up global query vars and post.
- global $wp, $post;
- unset( $wp->query_vars['order-pay'] );
- unset( $wp->query_vars['add-payment-method'] );
- $post = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Test teardown cleanup.
- }
-
- /**
- * @testdox Should enqueue Blackbox scripts on checkout page.
- */
- public function test_enqueues_scripts_on_checkout(): void {
- $this->mock_jetpack_blog_id( 12345 );
- $this->mock_wc_page( 'checkout' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on checkout' );
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on checkout' );
- }
-
- /**
- * @testdox Should enqueue Blackbox scripts on pay-for-order page.
- */
- public function test_enqueues_scripts_on_pay_for_order(): void {
- $this->mock_jetpack_blog_id( 12345 );
- $this->mock_wc_page( 'order-pay' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on pay-for-order' );
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on pay-for-order' );
- }
-
- /**
- * @testdox Should enqueue Blackbox scripts on add-payment-method page.
- */
- public function test_enqueues_scripts_on_add_payment_method(): void {
- $this->mock_jetpack_blog_id( 12345 );
- $this->mock_wc_page( 'add-payment-method' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on add-payment-method' );
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on add-payment-method' );
- }
-
- /**
- * @testdox Should enqueue Blackbox scripts on a custom page with the checkout block.
- */
- public function test_enqueues_scripts_on_custom_checkout_block_page(): void {
- $this->mock_jetpack_blog_id( 12345 );
- $this->mock_wc_page( 'custom-blocks-checkout' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on custom checkout block page' );
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on custom checkout block page' );
- }
-
- /**
- * @testdox Should not enqueue Blackbox scripts on non-payment pages.
- */
- public function test_does_not_enqueue_scripts_on_other_pages(): void {
- $this->markTestSkipped( 'Flaky in full suite due to is_checkout returning true (despite the resets).' );
-
- $this->mock_jetpack_blog_id( 12345 );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued on non-payment pages' );
- $this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should not be enqueued on non-payment pages' );
- }
-
- /**
- * @testdox Should not enqueue scripts and log error when Jetpack blog ID is unavailable.
- */
- public function test_does_not_enqueue_scripts_without_blog_id(): void {
- $this->mock_wc_page( 'checkout' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued without blog ID' );
- $this->assertLogged( 'error', 'Jetpack blog ID not available' );
- }
-
- /**
- * @testdox Should pass correct config data via wp_localize_script.
- */
- public function test_passes_correct_config_data(): void {
- $this->mock_jetpack_blog_id( 42 );
- $this->mock_wc_page( 'checkout' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $data = wp_scripts()->get_data( 'wc-fraud-protection-blackbox-init', 'data' );
- $this->assertStringContainsString( '"woocommerce"', $data, 'Should contain API key' );
- $this->assertStringContainsString( '"42"', $data, 'Should contain blog ID' );
- }
-
- /**
- * @testdox Should allow extensions to enable scripts on additional pages via filter.
- */
- public function test_filter_enables_scripts_on_custom_pages(): void {
- $this->mock_jetpack_blog_id( 12345 );
- add_filter( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', '__return_true' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued when filter returns true' );
- }
-
- /**
- * @testdox Should allow extensions to disable scripts on checkout via filter.
- */
- public function test_filter_disables_scripts_on_checkout(): void {
- $this->mock_jetpack_blog_id( 12345 );
- $this->mock_wc_page( 'checkout' );
- add_filter( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', '__return_false' );
-
- do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- $this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued when filter returns false' );
- }
-
- /**
- * Mock the Jetpack blog ID via the pre_option_jetpack_options filter.
- *
- * @param int $blog_id The blog ID to return.
- */
- private function mock_jetpack_blog_id( int $blog_id ): void {
- add_filter(
- 'pre_option_jetpack_options',
- function () use ( $blog_id ) {
- return array( 'id' => $blog_id );
- }
- );
- }
-
- /**
- * Mock a WooCommerce page URL.
- *
- * @param string $page The page to mock (e.g., 'checkout', 'custom-blocks-checkout', 'order-pay', 'add-payment-method').
- */
- private function mock_wc_page( string $page ): void {
- global $wp, $post, $wp_query;
-
- switch ( $page ) {
- case 'checkout':
- add_filter( 'woocommerce_is_checkout', '__return_true' );
- break;
- case 'custom-blocks-checkout':
- $post = $this->factory()->post->create_and_get( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Test needs to simulate a page with checkout block.
- array( 'post_content' => '<!-- wp:woocommerce/checkout --><div class="wp-block-woocommerce-checkout"></div><!-- /wp:woocommerce/checkout -->' )
- );
- break;
- case 'order-pay':
- $wp->query_vars['order-pay'] = true;
- add_filter( 'woocommerce_is_checkout', '__return_true' );
- break;
- case 'add-payment-method':
- $page_id = $this->factory()->post->create(
- array(
- 'post_type' => 'page',
- 'post_title' => 'My account',
- )
- );
- add_filter(
- 'pre_option_woocommerce_myaccount_page_id',
- function () use ( $page_id ) {
- return $page_id;
- }
- );
- $this->go_to( '?page_id=' . $page_id );
- $wp->query_vars['add-payment-method'] = true;
- break;
- }
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
deleted file mode 100644
index e4a5e8c548..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?php
-/**
- * BlockedSessionNoticeTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-
-/**
- * Tests for BlockedSessionNotice.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice
- */
-class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
-
- /**
- * The System Under Test.
- *
- * @var BlockedSessionNotice
- */
- private $sut;
-
- /**
- * Mock session clearance manager.
- *
- * @var SessionClearanceManager|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_session_manager;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->mock_session_manager = $this->createMock( SessionClearanceManager::class );
-
- $this->sut = new BlockedSessionNotice();
- $this->sut->init( $this->mock_session_manager );
- $this->sut->register();
-
- // Set a custom support email.
- update_option( 'woocommerce_email_from_address', 'support@example.com' );
- }
-
- /**
- * Tear down test fixtures.
- */
- public function tearDown(): void {
- parent::tearDown();
- remove_all_actions( 'before_woocommerce_add_payment_method' );
- remove_all_actions( 'wp' );
- delete_option( 'woocommerce_email_from_address' );
- wc_clear_notices();
- }
-
- /**
- * Test blocked purchase notice added on checkout.
- *
- * @testdox maybe_add_blocked_purchase_notice should add notice when session is blocked and on checkout page.
- */
- public function test_blocked_purchase_notice_added_on_checkout(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
-
- // Mock being on checkout page.
- add_filter( 'woocommerce_is_checkout', '__return_true' );
-
- do_action( 'wp' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- remove_filter( 'woocommerce_is_checkout', '__return_true' );
-
- $this->assertTrue( wc_has_notice( $this->sut->get_message_html( 'purchase' ), 'error' ), 'Should add purchase notice on checkout' );
- }
-
- /**
- * Test blocked purchase notice added on cart.
- *
- * @testdox maybe_add_blocked_purchase_notice should add notice when session is blocked and on cart page.
- */
- public function test_blocked_purchase_notice_added_on_cart(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
-
- // Mock being on cart page.
- add_filter( 'woocommerce_is_cart', '__return_true' );
-
- do_action( 'wp' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- remove_filter( 'woocommerce_is_cart', '__return_true' );
-
- $this->assertTrue( wc_has_notice( $this->sut->get_message_html( 'purchase' ), 'error' ), 'Should add purchase notice on cart' );
- }
-
- /**
- * Test blocked purchase notice not added when session allowed.
- *
- * @testdox maybe_add_blocked_purchase_notice should not add notice when session is not blocked.
- */
- public function test_blocked_purchase_notice_not_added_when_session_allowed(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( false );
-
- // Mock being on checkout page.
- add_filter( 'woocommerce_is_checkout', '__return_true' );
-
- do_action( 'wp' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- remove_filter( 'woocommerce_is_checkout', '__return_true' );
-
- $this->assertFalse( wc_has_notice( $this->sut->get_message_html( 'purchase' ), 'error' ), 'Should not add notice when session is allowed' );
- }
-
- /**
- * Test blocked purchase notice prevents duplicates.
- *
- * @testdox maybe_add_blocked_purchase_notice should not add duplicate notices.
- */
- public function test_blocked_purchase_notice_prevents_duplicates(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
-
- // Mock being on checkout page.
- add_filter( 'woocommerce_is_checkout', '__return_true' );
-
- do_action( 'wp' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- do_action( 'wp' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
-
- remove_filter( 'woocommerce_is_checkout', '__return_true' );
-
- // Count error notices.
- $notices = wc_get_notices( 'error' );
- $message = $this->sut->get_message_html( 'purchase' );
- $notice_count = 0;
- foreach ( $notices as $notice ) {
- if ( $notice['notice'] === $message ) {
- ++$notice_count;
- }
- }
-
- $this->assertEquals( 1, $notice_count, 'Should only have one notice even after calling twice' );
- }
-
- /**
- * Test add payment method action displays blocked message.
- *
- * @testdox Should display generic error notice when before_woocommerce_add_payment_method action fires for blocked sessions.
- */
- public function test_add_payment_method_action_displays_blocked_message(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
-
- ob_start();
- do_action( 'before_woocommerce_add_payment_method' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- $output = ob_get_clean();
-
- $this->assertStringContainsString( 'unable to process this request online', $output, 'Should display blocked message on add payment method page' );
- $this->assertStringContainsString( 'for assistance', $output, 'Should display generic message' );
- $this->assertStringContainsString( 'support@example.com', $output, 'Should include support email in message' );
- $this->assertStringContainsString( 'mailto:support@example.com', $output, 'Should include mailto link' );
- }
-
- /**
- * Test add payment method action no message for non blocked session.
- *
- * @testdox Should not display message when add payment method action fires for non-blocked sessions.
- */
- public function test_add_payment_method_action_no_message_for_non_blocked_session(): void {
- $this->mock_session_manager->method( 'is_session_blocked' )->willReturn( false );
-
- ob_start();
- do_action( 'before_woocommerce_add_payment_method' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- $output = ob_get_clean();
-
- $this->assertEmpty( $output, 'Non-blocked sessions should not display any message' );
- }
-
- /**
- * Test get message html purchase context.
- *
- * @testdox get_message_html should return purchase-specific message when context is 'purchase'.
- */
- public function test_get_message_html_purchase_context(): void {
- $message = $this->sut->get_message_html( 'purchase' );
-
- $this->assertEquals(
- 'We are unable to process this request online. Please <a href="mailto:support@example.com">contact support (support@example.com)</a> to complete your purchase.',
- $message
- );
- }
-
- /**
- * Test get message html generic context.
- *
- * @testdox get_message_html should return generic message when context is 'generic' or not specified.
- */
- public function test_get_message_html_generic_context(): void {
- $message_default = $this->sut->get_message_html();
- $message_explicit = $this->sut->get_message_html( 'generic' );
-
- $expected = 'We are unable to process this request online. Please <a href="mailto:support@example.com">contact support (support@example.com)</a> for assistance.';
-
- $this->assertEquals( $expected, $message_default, 'Default context should return generic message' );
- $this->assertEquals( $expected, $message_explicit, 'Explicit generic context should return generic message' );
- }
-
- /**
- * Test get message plaintext purchase context.
- *
- * @testdox get_message_plaintext should return purchase-specific message when context is 'purchase'.
- */
- public function test_get_message_plaintext_purchase_context(): void {
- $message = $this->sut->get_message_plaintext( 'purchase' );
-
- $this->assertEquals(
- 'We are unable to process this request online. Please contact support (support@example.com) to complete your purchase.',
- $message
- );
- }
-
- /**
- * Test get message plaintext generic context.
- *
- * @testdox get_message_plaintext should return generic message when context is 'generic' or not specified.
- */
- public function test_get_message_plaintext_generic_context(): void {
- $message_default = $this->sut->get_message_plaintext();
- $message_explicit = $this->sut->get_message_plaintext( 'generic' );
-
- $expected = 'We are unable to process this request online. Please contact support (support@example.com) for assistance.';
-
- $this->assertEquals( $expected, $message_default, 'Default context should return generic message' );
- $this->assertEquals( $expected, $message_explicit, 'Explicit generic context should return generic message' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php
deleted file mode 100644
index 539047e849..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartBlockingTest.php
+++ /dev/null
@@ -1,232 +0,0 @@
-<?php
-/**
- * CartBlockingTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\BlockedSessionNotice;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-
-/**
- * Tests for cart blocking when session is blocked by fraud protection.
- *
- * Tests WC_Cart method integration (add_to_cart, remove_cart_item, set_quantity).
- *
- * @covers \WC_Cart
- */
-class CartBlockingTest extends \WC_Unit_Test_Case {
-
- /**
- * Mock FraudProtectionController.
- *
- * @var FraudProtectionController|\PHPUnit\Framework\MockObject\MockObject
- */
- private $fraud_controller_mock;
-
- /**
- * Mock SessionClearanceManager.
- *
- * @var SessionClearanceManager|\PHPUnit\Framework\MockObject\MockObject
- */
- private $session_manager_mock;
-
- /**
- * Mock BlockedSessionNotice.
- *
- * @var BlockedSessionNotice|\PHPUnit\Framework\MockObject\MockObject
- */
- private $blocked_notice_mock;
-
- /**
- * Test product.
- *
- * @var \WC_Product
- */
- private $product;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->product = \WC_Helper_Product::create_simple_product();
-
- $this->fraud_controller_mock = $this->createMock( FraudProtectionController::class );
- $this->session_manager_mock = $this->createMock( SessionClearanceManager::class );
- $this->blocked_notice_mock = $this->createMock( BlockedSessionNotice::class );
-
- wc_get_container()->replace( FraudProtectionController::class, $this->fraud_controller_mock );
- wc_get_container()->replace( SessionClearanceManager::class, $this->session_manager_mock );
- wc_get_container()->replace( BlockedSessionNotice::class, $this->blocked_notice_mock );
-
- wc_empty_cart();
- wc_clear_notices();
- }
-
- /**
- * Tear down.
- */
- public function tearDown(): void {
- wc_get_container()->reset_replacement( FraudProtectionController::class );
- wc_get_container()->reset_replacement( SessionClearanceManager::class );
- wc_get_container()->reset_replacement( BlockedSessionNotice::class );
-
- $this->product->delete( true );
- wc_empty_cart();
- wc_clear_notices();
-
- parent::tearDown();
- }
-
- /**
- * Test add to cart blocked when session blocked.
- *
- * @testdox add_to_cart returns false and adds notice when session is blocked.
- */
- public function test_add_to_cart_blocked_when_session_blocked(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( true );
- $this->blocked_notice_mock->method( 'get_message_html' )->willReturn( 'Blocked message' );
-
- $result = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
-
- $this->assertFalse( $result );
- $this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
- $this->assertTrue( wc_has_notice( 'Blocked message', 'error' ) );
- }
-
- /**
- * Test add to cart allowed when session allowed.
- *
- * @testdox add_to_cart succeeds when session is allowed.
- */
- public function test_add_to_cart_allowed_when_session_allowed(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( false );
-
- $result = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
-
- $this->assertNotFalse( $result );
- $this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test add to cart allowed when feature disabled.
- *
- * @testdox add_to_cart succeeds when fraud protection is disabled.
- */
- public function test_add_to_cart_allowed_when_feature_disabled(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( false );
- $this->session_manager_mock->expects( $this->never() )->method( 'is_session_blocked' );
-
- $result = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
-
- $this->assertNotFalse( $result );
- $this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test remove cart item blocked when session blocked.
- *
- * @testdox remove_cart_item returns false when session is blocked.
- */
- public function test_remove_cart_item_blocked_when_session_blocked(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock
- ->method( 'is_session_blocked' )
- ->willReturnOnConsecutiveCalls( false, true ); // Allow add, block remove.
- $this->blocked_notice_mock->method( 'get_message_html' )->willReturn( 'Blocked message' );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->remove_cart_item( $cart_item_key );
-
- $this->assertFalse( $result );
- $this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test remove cart item allowed when session allowed.
- *
- * @testdox remove_cart_item succeeds when session is allowed.
- */
- public function test_remove_cart_item_allowed_when_session_allowed(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( false );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->remove_cart_item( $cart_item_key );
-
- $this->assertTrue( $result );
- $this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test remove cart item allowed when feature disabled.
- *
- * @testdox remove_cart_item succeeds when fraud protection is disabled.
- */
- public function test_remove_cart_item_allowed_when_feature_disabled(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( false );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->remove_cart_item( $cart_item_key );
-
- $this->assertTrue( $result );
- $this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test set quantity blocked when session blocked.
- *
- * @testdox set_quantity returns false when session is blocked.
- */
- public function test_set_quantity_blocked_when_session_blocked(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock
- ->method( 'is_session_blocked' )
- ->willReturnOnConsecutiveCalls( false, true ); // Allow add, block update.
- $this->blocked_notice_mock->method( 'get_message_html' )->willReturn( 'Blocked message' );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->set_quantity( $cart_item_key, 5 );
-
- $this->assertFalse( $result );
- $this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test set quantity allowed when session allowed.
- *
- * @testdox set_quantity succeeds when session is allowed.
- */
- public function test_set_quantity_allowed_when_session_allowed(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( true );
- $this->session_manager_mock->method( 'is_session_blocked' )->willReturn( false );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->set_quantity( $cart_item_key, 5 );
-
- $this->assertTrue( $result );
- $this->assertEquals( 5, WC()->cart->get_cart_contents_count() );
- }
-
- /**
- * Test set quantity allowed when feature disabled.
- *
- * @testdox set_quantity succeeds when fraud protection is disabled.
- */
- public function test_set_quantity_allowed_when_feature_disabled(): void {
- $this->fraud_controller_mock->method( 'feature_is_enabled' )->willReturn( false );
-
- $cart_item_key = WC()->cart->add_to_cart( $this->product->get_id(), 1 );
- $result = WC()->cart->set_quantity( $cart_item_key, 5 );
-
- $this->assertTrue( $result );
- $this->assertEquals( 5, WC()->cart->get_cart_contents_count() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
deleted file mode 100644
index 725285f718..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CartEventTrackerTest.php
+++ /dev/null
@@ -1,252 +0,0 @@
-<?php
-/**
- * CartEventTrackerTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
-
-/**
- * Tests for CartEventTracker.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker
- */
-class CartEventTrackerTest extends \WC_Unit_Test_Case {
-
- /**
- * The system under test.
- *
- * @var CartEventTracker
- */
- private $sut;
-
- /**
- * Mock session data collector.
- *
- * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_collector;
-
- /**
- * Test product.
- *
- * @var \WC_Product
- */
- private $test_product;
-
- /**
- * Runs before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Ensure WooCommerce cart and session are available.
- if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
- wc_load_cart();
- }
-
- // Create mock.
- $this->mock_collector = $this->createMock( SessionDataCollector::class );
-
- // Create system under test.
- $this->sut = new CartEventTracker();
- $this->sut->init( $this->mock_collector );
-
- // Create a test product.
- $this->test_product = \WC_Helper_Product::create_simple_product();
-
- // Empty cart before each test.
- WC()->cart->empty_cart();
- }
-
- /**
- * Test cart page loaded collects data.
- *
- * @testdox track_cart_page_loaded() collects session data with empty event data.
- */
- public function test_track_cart_page_loaded_collects_data(): void {
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_page_loaded' ),
- $this->equalTo( array() )
- );
-
- $this->sut->track_cart_page_loaded();
- }
-
- /**
- * Test cart item added collects data.
- *
- * @testdox track_cart_item_added() collects session data with event details.
- */
- public function test_track_cart_item_added_collects_data(): void {
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_item_added' ),
- $this->callback(
- function ( $event_data ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'item_added', $event_data['action'] );
- $this->assertArrayHasKey( 'product_id', $event_data );
- $this->assertEquals( $this->test_product->get_id(), $event_data['product_id'] );
- $this->assertArrayHasKey( 'quantity', $event_data );
- $this->assertEquals( 2, $event_data['quantity'] );
- return true;
- }
- )
- );
-
- $this->sut->track_cart_item_added(
- 'test_cart_key',
- $this->test_product->get_id(),
- 2,
- 0
- );
- }
-
- /**
- * Test cart item updated collects data.
- *
- * @testdox track_cart_item_updated() collects session data with quantity change.
- */
- public function test_track_cart_item_updated_collects_data(): void {
- $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_item_updated' ),
- $this->callback(
- function ( $event_data ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'item_updated', $event_data['action'] );
- $this->assertArrayHasKey( 'quantity', $event_data );
- $this->assertEquals( 5, $event_data['quantity'] );
- $this->assertArrayHasKey( 'old_quantity', $event_data );
- $this->assertEquals( 1, $event_data['old_quantity'] );
- return true;
- }
- )
- );
-
- $this->sut->track_cart_item_updated(
- $cart_item_key,
- 5,
- 1,
- WC()->cart
- );
- }
-
- /**
- * Test cart item removed collects data.
- *
- * @testdox track_cart_item_removed() collects session data.
- */
- public function test_track_cart_item_removed_collects_data(): void {
- $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_item_removed' ),
- $this->callback(
- function ( $event_data ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'item_removed', $event_data['action'] );
- return true;
- }
- )
- );
-
- WC()->cart->remove_cart_item( $cart_item_key );
-
- $this->sut->track_cart_item_removed( $cart_item_key, WC()->cart );
- }
-
- /**
- * Test cart item restored collects data.
- *
- * @testdox track_cart_item_restored() collects session data.
- */
- public function test_track_cart_item_restored_collects_data(): void {
- $cart_item_key = WC()->cart->add_to_cart( $this->test_product->get_id(), 1 );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_item_restored' ),
- $this->callback(
- function ( $event_data ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'item_restored', $event_data['action'] );
- return true;
- }
- )
- );
-
- $this->sut->track_cart_item_restored(
- $cart_item_key,
- WC()->cart
- );
- }
-
- /**
- * Test cart events include variation_id.
- *
- * @testdox Cart events include variation_id when present.
- */
- public function test_cart_events_include_variation_id(): void {
- $variable_product = \WC_Helper_Product::create_variation_product();
- $variations = $variable_product->get_available_variations();
- $variation_id = $variations[0]['variation_id'];
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'cart_item_added' ),
- $this->callback(
- function ( $event_data ) use ( $variation_id ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'item_added', $event_data['action'] );
- $this->assertArrayHasKey( 'variation_id', $event_data );
- $this->assertEquals( $variation_id, $event_data['variation_id'] );
- return true;
- }
- )
- );
-
- $this->sut->track_cart_item_added(
- 'test_cart_key',
- $variable_product->get_id(),
- 1,
- $variation_id
- );
-
- $variable_product->delete( true );
- }
-
- /**
- * Cleanup after test.
- */
- public function tearDown(): void {
- parent::tearDown();
-
- if ( $this->test_product ) {
- $this->test_product->delete( true );
- }
-
- WC()->cart->empty_cart();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
deleted file mode 100644
index 9e19678f9c..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
+++ /dev/null
@@ -1,367 +0,0 @@
-<?php
-/**
- * CheckoutEventTrackerTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
-
-/**
- * Tests for CheckoutEventTracker.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker
- */
-class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
-
- /**
- * The system under test.
- *
- * @var CheckoutEventTracker
- */
- private $sut;
-
- /**
- * Mock session data collector.
- *
- * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_collector;
-
- /**
- * Runs before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Ensure WooCommerce cart and session are available.
- if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
- wc_load_cart();
- }
-
- // Create mock.
- $this->mock_collector = $this->createMock( SessionDataCollector::class );
-
- // Create system under test.
- $this->sut = new CheckoutEventTracker();
- $this->sut->init( $this->mock_collector );
- }
-
- // ========================================
- // Checkout Page Load Tests
- // ========================================
-
- /**
- * Test checkout page loaded collects data.
- *
- * @testdox track_checkout_page_loaded() collects session data with empty event data.
- */
- public function test_track_checkout_page_loaded_collects_data(): void {
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'checkout_page_loaded' ),
- $this->equalTo( array() )
- );
-
- $this->sut->track_checkout_page_loaded();
- }
-
- // ========================================
- // Blocks Checkout Tests
- // ========================================
-
- /**
- * Test blocks checkout update collects data.
- *
- * @testdox track_blocks_checkout_update() collects session data with empty event data.
- */
- public function test_track_blocks_checkout_update_collects_data(): void {
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'checkout_update' ),
- $this->equalTo( array() )
- );
-
- $this->sut->track_blocks_checkout_update();
- }
-
- // ========================================
- // Shortcode Checkout Tests
- // ========================================
-
- /**
- * Test shortcode checkout field update collects data on billing country change.
- *
- * @testdox track_shortcode_checkout_field_update() collects data when billing country changes.
- */
- public function test_track_shortcode_checkout_field_update_collects_data_on_billing_country_change(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'CA' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'checkout_update' ),
- $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'];
- }
- )
- );
-
- $posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe&billing_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test shortcode checkout field update extracts billing fields.
- *
- * @testdox track_shortcode_checkout_field_update() extracts billing fields correctly.
- */
- public function test_track_shortcode_checkout_field_update_extracts_billing_fields(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'CA' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- $captured_event_data = null;
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->willReturnCallback(
- function ( $event_type, $event_data ) use ( &$captured_event_data ) {
- $captured_event_data = $event_data;
- return array();
- }
- );
-
- $posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_last_name=Doe&billing_country=US&billing_city=New+York';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
-
- $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 shortcode checkout field update extracts shipping fields.
- *
- * @testdox track_shortcode_checkout_field_update() extracts shipping fields when ship_to_different_address is set.
- */
- public function test_track_shortcode_checkout_field_update_extracts_shipping_fields(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( null );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'CA' );
-
- $captured_event_data = null;
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->willReturnCallback(
- function ( $event_type, $event_data ) use ( &$captured_event_data ) {
- $captured_event_data = $event_data;
- return array();
- }
- );
-
- $posted_data = 'billing_email=test@example.com&ship_to_different_address=1&shipping_first_name=Jane&shipping_last_name=Smith&shipping_city=Los+Angeles&shipping_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
-
- $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 shortcode checkout field update skips shipping fields when not different address.
- *
- * @testdox track_shortcode_checkout_field_update() skips shipping fields when not shipping to different address.
- */
- public function test_track_shortcode_checkout_field_update_skips_shipping_fields_when_not_different_address(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'CA' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- $captured_event_data = null;
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->willReturnCallback(
- function ( $event_type, $event_data ) use ( &$captured_event_data ) {
- $captured_event_data = $event_data;
- return array();
- }
- );
-
- $posted_data = 'billing_email=test@example.com&billing_country=US&shipping_first_name=Jane&shipping_last_name=Smith';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
-
- $this->assertNotNull( $captured_event_data );
- $this->assertArrayNotHasKey( 'shipping_first_name', $captured_event_data );
- $this->assertArrayNotHasKey( 'shipping_last_name', $captured_event_data );
- }
-
- // ========================================
- // Country Change Detection Tests
- // ========================================
-
- /**
- * Test no collection when no country changes.
- *
- * @testdox Event is NOT collected when neither country changes.
- */
- public function test_no_collection_when_no_country_changes(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'US' );
-
- $this->mock_collector
- ->expects( $this->never() )
- ->method( 'collect' );
-
- $posted_data = 'billing_email=test@example.com&billing_country=US&shipping_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test no collection when only non-country fields change.
- *
- * @testdox Event is NOT collected when only non-country fields change.
- */
- public function test_no_collection_when_only_non_country_fields_change(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- $this->mock_collector
- ->expects( $this->never() )
- ->method( 'collect' );
-
- $posted_data = 'billing_email=test@example.com&billing_first_name=John&billing_phone=1234567890';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test collection when billing country changes from null.
- *
- * @testdox Event is collected when billing country changes from null.
- */
- public function test_collection_when_billing_country_changes_from_null(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( null );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( null );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' );
-
- $posted_data = 'billing_email=test@example.com&billing_country=US';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- /**
- * Test collection when ship_to_different_address unchecked with different countries.
- *
- * @testdox Event is collected when ship_to_different_address unchecked with different countries.
- */
- public function test_collection_when_ship_to_different_address_unchecked_with_different_countries(): void {
- $this->mock_collector
- ->method( 'get_current_billing_country' )
- ->willReturn( 'US' );
-
- $this->mock_collector
- ->method( 'get_current_shipping_country' )
- ->willReturn( 'CA' );
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'checkout_update' ),
- $this->anything()
- );
-
- $posted_data = 'billing_country=US&billing_email=test@example.com';
- $this->sut->track_shortcode_checkout_field_update( $posted_data );
- }
-
- // ========================================
- // Order Placed Tests
- // ========================================
-
- /**
- * Test track order placed collects data.
- *
- * @testdox track_order_placed() collects session data with order details.
- */
- public function test_track_order_placed_collects_data(): void {
- $order = \WC_Helper_Order::create_order();
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'order_placed' ),
- $this->callback(
- function ( $event_data ) use ( $order ) {
- $this->assertArrayHasKey( 'order_id', $event_data );
- $this->assertEquals( $order->get_id(), $event_data['order_id'] );
- $this->assertArrayHasKey( 'payment_method', $event_data );
- $this->assertArrayHasKey( 'total', $event_data );
- $this->assertArrayHasKey( 'currency', $event_data );
- $this->assertArrayHasKey( 'customer_id', $event_data );
- $this->assertArrayHasKey( 'status', $event_data );
- return true;
- }
- )
- );
-
- $this->sut->track_order_placed( $order->get_id(), $order );
-
- $order->delete( true );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php
deleted file mode 100644
index 1039a21728..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/DecisionHandlerTest.php
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-/**
- * DecisionHandlerTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\ApiClient;
-use Automattic\WooCommerce\Internal\FraudProtection\DecisionHandler;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
-use WC_Unit_Test_Case;
-
-/**
- * Tests for the DecisionHandler class.
- */
-class DecisionHandlerTest extends WC_Unit_Test_Case {
-
- use LoggerSpyTrait;
-
- /**
- * The System Under Test.
- *
- * @var DecisionHandler
- */
- private $sut;
-
- /**
- * Mock session clearance manager.
- *
- * @var SessionClearanceManager
- */
- private $session_manager;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->session_manager = $this->createMock( SessionClearanceManager::class );
- $this->sut = new DecisionHandler();
- $this->sut->init( $this->session_manager );
- }
-
- /**
- * Tear down test fixtures.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_fraud_protection_decision' );
- parent::tearDown();
- }
-
- /**
- * Test apply allow decision.
- *
- * @testdox Should apply allow decision and update session to allowed when session is not blocked.
- */
- public function test_apply_allow_decision(): void {
- $this->session_manager
- ->method( 'is_session_blocked' )
- ->willReturn( false );
-
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'allow_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_ALLOW, array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- }
-
- /**
- * Test allow decision does not overwrite blocked session.
- *
- * @testdox Should preserve blocked session status when allow decision is received.
- *
- * This prevents race conditions where emptying the cart during block_session
- * causes subsequent fraud checks to return "allow" (due to lower cart value).
- */
- public function test_allow_decision_does_not_overwrite_blocked_session(): void {
- $this->session_manager
- ->method( 'is_session_blocked' )
- ->willReturn( true );
-
- $this->session_manager
- ->expects( $this->never() )
- ->method( 'allow_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_ALLOW, array( 'session_id' => 'test' ) );
-
- $this->assertLogged( 'info', 'Preserving blocked session status' );
- }
-
- /**
- * Test apply block decision.
- *
- * @testdox Should apply block decision and update session to blocked.
- */
- public function test_apply_block_decision(): void {
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'block_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_BLOCK, array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_BLOCK, $result );
- }
-
- /**
- * Test invalid decision defaults to allow.
- *
- * @testdox Should default to allow for invalid decision and log warning.
- */
- public function test_invalid_decision_defaults_to_allow(): void {
- $this->session_manager
- ->method( 'is_session_blocked' )
- ->willReturn( false );
-
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'allow_session' );
-
- $result = $this->sut->apply_decision( 'invalid_decision', array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'warning', 'Invalid decision "invalid_decision" received' );
- }
-
- /**
- * Test filter can override block to allow.
- *
- * @testdox Should allow filter to override decision from block to allow.
- */
- public function test_filter_can_override_block_to_allow(): void {
- add_filter(
- 'woocommerce_fraud_protection_decision',
- function () {
- return ApiClient::DECISION_ALLOW;
- }
- );
-
- $this->session_manager
- ->method( 'is_session_blocked' )
- ->willReturn( false );
-
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'allow_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_BLOCK, array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_ALLOW, $result );
- $this->assertLogged( 'info', 'Decision overridden by filter `woocommerce_fraud_protection_decision`' );
- }
-
- /**
- * Test filter can override allow to block.
- *
- * @testdox Should allow filter to override decision from allow to block.
- */
- public function test_filter_can_override_allow_to_block(): void {
- add_filter(
- 'woocommerce_fraud_protection_decision',
- function () {
- return ApiClient::DECISION_BLOCK;
- }
- );
-
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'block_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_ALLOW, array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_BLOCK, $result );
- $this->assertLogged( 'info', 'Decision overridden by filter `woocommerce_fraud_protection_decision`' );
- }
-
- /**
- * Test filter invalid return uses original decision.
- *
- * @testdox Should reject invalid filter return value and use original decision.
- */
- public function test_filter_invalid_return_uses_original_decision(): void {
- add_filter(
- 'woocommerce_fraud_protection_decision',
- function () {
- return 'totally_invalid';
- }
- );
-
- $this->session_manager
- ->expects( $this->once() )
- ->method( 'block_session' );
-
- $result = $this->sut->apply_decision( ApiClient::DECISION_BLOCK, array( 'session_id' => 'test' ) );
-
- $this->assertSame( ApiClient::DECISION_BLOCK, $result );
- $this->assertLogged( 'warning', 'Filter `woocommerce_fraud_protection_decision` returned invalid decision "totally_invalid"' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionControllerTest.php
deleted file mode 100644
index 0a7cefda03..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/FraudProtectionControllerTest.php
+++ /dev/null
@@ -1,187 +0,0 @@
-<?php
-
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
-
-/**
- * Tests for the FraudProtectionController class.
- */
-class FraudProtectionControllerTest extends \WC_Unit_Test_Case {
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Set jetpack_activation_source option to prevent "Cannot use bool as array" error
- // in Jetpack Connection Manager's apply_activation_source_to_args method.
- update_option( 'jetpack_activation_source', array( '', '' ) );
- }
-
- /**
- * Get a fresh controller instance with reset container.
- *
- * @return FraudProtectionController
- */
- private function get_fresh_controller(): FraudProtectionController {
- $container = wc_get_container();
- $container->reset_all_resolved();
- return $container->get( FraudProtectionController::class );
- }
-
- /**
- * Test logging functionality.
- */
- public function test_log_writes_to_woo_fraud_protection_source(): void {
- // Mock the logger.
- $logger = $this->getMockBuilder( \WC_Logger_Interface::class )
- ->getMock();
-
- // Expect the log method to be called with correct parameters.
- $logger->expects( $this->once() )
- ->method( 'log' )
- ->with(
- $this->equalTo( 'info' ),
- $this->equalTo( 'Test message' ),
- $this->equalTo( array( 'source' => 'woo-fraud-protection' ) )
- );
-
- // Replace the logger with our mock.
- add_filter(
- 'woocommerce_logging_class',
- function () use ( $logger ) {
- return $logger;
- }
- );
-
- // Call the log method.
- FraudProtectionController::log( 'info', 'Test message' );
- }
-
- /**
- * Test logging with context data.
- */
- public function test_log_merges_context_with_source(): void {
- // Mock the logger.
- $logger = $this->getMockBuilder( \WC_Logger_Interface::class )
- ->getMock();
-
- $expected_context = array(
- 'foo' => 'bar',
- 'source' => 'woo-fraud-protection',
- );
-
- // Expect the log method to be called with merged context.
- $logger->expects( $this->once() )
- ->method( 'log' )
- ->with(
- $this->equalTo( 'debug' ),
- $this->equalTo( 'Test with context' ),
- $this->equalTo( $expected_context )
- );
-
- // Replace the logger with our mock.
- add_filter(
- 'woocommerce_logging_class',
- function () use ( $logger ) {
- return $logger;
- }
- );
-
- // Call the log method with context.
- FraudProtectionController::log( 'debug', 'Test with context', array( 'foo' => 'bar' ) );
- }
-
- /**
- * Test that on_init does nothing when feature is disabled.
- */
- public function test_no_hooks_when_feature_disabled(): void {
- // Ensure feature is disabled.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'no' );
-
- // Get a fresh controller instance.
- $controller = $this->get_fresh_controller();
-
- // Count hooks before calling on_init.
- global $wp_filter;
- $hook_count_before = count( $wp_filter );
-
- // Call on_init.
- $controller->on_init();
-
- // Count hooks after - should be the same (no new hooks registered).
- $hook_count_after = count( $wp_filter );
-
- // Note: This is a basic test. In a full implementation, we would check
- // for specific hooks that should be registered when enabled.
- $this->assertEquals( $hook_count_before, $hook_count_after );
- }
-
- /**
- * Test that register method registers init action.
- */
- public function test_register_registers_init_action(): void {
- // Get a fresh controller instance.
- $controller = $this->get_fresh_controller();
-
- // Call register.
- $controller->register();
-
- // Check if the init action is registered for our callback.
- $priority = has_action( 'init', array( $controller, 'on_init' ) );
-
- // The priority should be 10 (default).
- $this->assertSame( 10, $priority, 'Init action should be registered with default priority 10' );
- }
-
- /**
- * Test that feature_is_enabled returns true when feature is enabled.
- */
- public function test_feature_is_enabled_returns_true_when_enabled(): void {
- // Enable the feature.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
-
- // Get a fresh controller instance to pick up the option change.
- $controller = $this->get_fresh_controller();
-
- // Check if the method returns true.
- $this->assertTrue( $controller->feature_is_enabled() );
- }
-
- /**
- * Test that feature_is_enabled returns false when feature is disabled.
- */
- public function test_feature_is_enabled_returns_false_when_disabled(): void {
- // Disable the feature.
- update_option( 'woocommerce_feature_fraud_protection_enabled', 'no' );
-
- // Get a fresh controller instance to pick up the option change.
- $controller = $this->get_fresh_controller();
-
- // Check if the method returns false.
- $this->assertFalse( $controller->feature_is_enabled() );
- }
-
- /**
- * Cleanup after test.
- */
- public function tearDown(): void {
- parent::tearDown();
-
- // Clean up any filters or options.
- remove_all_filters( 'woocommerce_logging_class' );
- delete_option( 'woocommerce_feature_fraud_protection_enabled' );
- delete_option( 'jetpack_activation_source' );
-
- // Remove any init hooks registered by the controller.
- remove_all_actions( 'init' );
-
- // Reset container.
- wc_get_container()->reset_all_resolved();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
deleted file mode 100644
index e508862c7a..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/PaymentMethodEventTrackerTest.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?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\Internal\FraudProtection\SessionDataCollector;
-
-/**
- * Tests for the PaymentMethodEventTracker class.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\PaymentMethodEventTracker
- */
-class PaymentMethodEventTrackerTest extends \WC_Unit_Test_Case {
-
- /**
- * The System Under Test.
- *
- * @var PaymentMethodEventTracker
- */
- private $sut;
-
- /**
- * Mock session data collector.
- *
- * @var SessionDataCollector|\PHPUnit\Framework\MockObject\MockObject
- */
- private $mock_collector;
-
- /**
- * Setup test.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Create mock.
- $this->mock_collector = $this->createMock( SessionDataCollector::class );
-
- // Create system under test with mock.
- $this->sut = new PaymentMethodEventTracker();
- $this->sut->init( $this->mock_collector );
- }
-
- /**
- * Test add payment method page loaded collects data.
- *
- * @testdox track_add_payment_method_page_loaded() collects session data with empty event data.
- */
- public function test_track_add_payment_method_page_loaded_collects_data(): void {
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'add_payment_method_page_loaded' ),
- $this->equalTo( array() )
- );
-
- $this->sut->track_add_payment_method_page_loaded();
- }
-
- /**
- * Test payment method added collects data.
- *
- * @testdox track_payment_method_added() collects session data with token details.
- */
- public function test_track_payment_method_added_collects_data(): void {
- $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->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'payment_method_added' ),
- $this->callback(
- function ( $event_data ) use ( $token ) {
- $this->assertArrayHasKey( 'action', $event_data );
- $this->assertEquals( 'added', $event_data['action'] );
- $this->assertArrayHasKey( 'token_id', $event_data );
- $this->assertEquals( $token->get_id(), $event_data['token_id'] );
- $this->assertArrayHasKey( 'token_type', $event_data );
- $this->assertArrayHasKey( 'gateway_id', $event_data );
- $this->assertEquals( 'stripe', $event_data['gateway_id'] );
- $this->assertArrayHasKey( 'card_type', $event_data );
- $this->assertEquals( 'visa', $event_data['card_type'] );
- $this->assertArrayHasKey( 'card_last4', $event_data );
- $this->assertEquals( '4242', $event_data['card_last4'] );
- return true;
- }
- )
- );
-
- $this->sut->track_payment_method_added( $token->get_id(), $token );
-
- $token->delete();
- }
-
- /**
- * Test payment method added includes expiry for CC tokens.
- *
- * @testdox track_payment_method_added() includes expiry info for CC tokens.
- */
- public function test_track_payment_method_added_includes_expiry_for_cc_tokens(): void {
- $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( '2028' );
- $token->set_user_id( $user_id );
- $token->save();
-
- $this->mock_collector
- ->expects( $this->once() )
- ->method( 'collect' )
- ->with(
- $this->equalTo( 'payment_method_added' ),
- $this->callback(
- function ( $event_data ) {
- $this->assertArrayHasKey( 'expiry_month', $event_data );
- $this->assertEquals( '06', $event_data['expiry_month'] );
- $this->assertArrayHasKey( 'expiry_year', $event_data );
- $this->assertEquals( '2028', $event_data['expiry_year'] );
- return true;
- }
- )
- );
-
- $this->sut->track_payment_method_added( $token->get_id(), $token );
-
- $token->delete();
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionClearanceManagerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionClearanceManagerTest.php
deleted file mode 100644
index a3646ab1d7..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionClearanceManagerTest.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-/**
- * SessionClearanceManagerTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-
-/**
- * Tests for SessionClearanceManager.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager
- */
-class SessionClearanceManagerTest extends \WC_Unit_Test_Case {
-
- /**
- * The system under test.
- *
- * @var SessionClearanceManager
- */
- private $sut;
-
- /**
- * Runs before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- WC()->session = new \WC_Session_Handler();
- WC()->session->init();
-
- $this->sut = new SessionClearanceManager();
- }
-
- /**
- * Test that session status constants are defined correctly.
- */
- public function test_session_status_constants() {
- $this->assertEquals( 'pending', SessionClearanceManager::STATUS_PENDING );
- $this->assertEquals( 'allowed', SessionClearanceManager::STATUS_ALLOWED );
- $this->assertEquals( 'blocked', SessionClearanceManager::STATUS_BLOCKED );
- $this->assertEquals( SessionClearanceManager::STATUS_ALLOWED, SessionClearanceManager::DEFAULT_STATUS );
- }
-
- /**
- * Test default session status when session is not available.
- */
- public function test_default_session_status_without_session() {
- // If session is not available, should return DEFAULT_STATUS.
- $status = $this->sut->get_session_status();
- $this->assertEquals( SessionClearanceManager::DEFAULT_STATUS, $status );
- }
-
- /**
- * Test that is_session_allowed returns true for allowed status.
- */
- public function test_is_session_allowed_returns_true_for_allowed() {
- $this->sut->allow_session();
- $this->assertTrue( $this->sut->is_session_allowed() );
- $this->assertFalse( $this->sut->is_session_blocked() );
- }
-
- /**
- * Test that pending session is neither allowed nor blocked.
- */
- public function test_is_session_allowed_returns_false_for_pending() {
- $this->sut->challenge_session();
- $this->assertFalse( $this->sut->is_session_allowed() );
- $this->assertFalse( $this->sut->is_session_blocked() );
- }
-
- /**
- * Test blocked status.
- */
- public function test_is_session_allowed_returns_false_for_blocked() {
- $this->sut->block_session();
- $this->assertFalse( $this->sut->is_session_allowed() );
- $this->assertTrue( $this->sut->is_session_blocked() );
- }
-
- /**
- * Test block_session empties the cart.
- */
- public function test_block_session_empties_cart() {
- // Add item to cart.
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
- $this->assertGreaterThan( 0, WC()->cart->get_cart_contents_count() );
-
- // Block session should empty cart.
- $this->sut->block_session();
- $this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
-
- // Clean up.
- $product->delete( true );
- }
-
- /**
- * Test reset_session sets status to DEFAULT_STATUS.
- */
- public function test_reset_session_sets_status_to_default() {
- // Set to blocked first.
- $this->sut->block_session();
- $this->assertEquals( SessionClearanceManager::STATUS_BLOCKED, $this->sut->get_session_status() );
-
- // Reset should go back to DEFAULT_STATUS.
- $this->sut->reset_session();
- $this->assertEquals( SessionClearanceManager::DEFAULT_STATUS, $this->sut->get_session_status() );
- }
-
- /**
- * Test session status transitions.
- */
- public function test_session_status_transitions() {
- // Start with allowed.
- $this->sut->allow_session();
- $this->assertEquals( SessionClearanceManager::STATUS_ALLOWED, $this->sut->get_session_status() );
-
- // Transition to pending.
- $this->sut->challenge_session();
- $this->assertEquals( SessionClearanceManager::STATUS_PENDING, $this->sut->get_session_status() );
-
- // Transition to blocked.
- $this->sut->block_session();
- $this->assertEquals( SessionClearanceManager::STATUS_BLOCKED, $this->sut->get_session_status() );
-
- // Transition back to allowed.
- $this->sut->allow_session();
- $this->assertEquals( SessionClearanceManager::STATUS_ALLOWED, $this->sut->get_session_status() );
- }
-
- /**
- * Test get_session_status returns default status for invalid stored values.
- */
- public function test_get_session_status_returns_default_status_for_invalid_values() {
- // Set an invalid value directly in session.
- WC()->session->set( '_fraud_protection_clearance_status', 'invalid_status' );
-
- // Should return default status for invalid values.
- $this->assertEquals( SessionClearanceManager::DEFAULT_STATUS, $this->sut->get_session_status() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
deleted file mode 100644
index f3e3d64f10..0000000000
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/SessionDataCollectorTest.php
+++ /dev/null
@@ -1,970 +0,0 @@
-<?php
-/**
- * SessionDataCollectorTest class file.
- */
-
-declare( strict_types = 1 );
-
-namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
-
-use Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector;
-use Automattic\WooCommerce\Internal\FraudProtection\SessionClearanceManager;
-
-/**
- * Tests for SessionDataCollector.
- *
- * @covers \Automattic\WooCommerce\Internal\FraudProtection\SessionDataCollector
- */
-class SessionDataCollectorTest extends \WC_Unit_Test_Case {
-
- /**
- * The system under test.
- *
- * @var SessionDataCollector
- */
- private $sut;
-
- /**
- * SessionClearanceManager instance.
- *
- * @var SessionClearanceManager
- */
- private $session_clearance_manager;
-
- /**
- * Runs before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Ensure WooCommerce cart and session are available.
- if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
- wc_load_cart();
- }
-
- $this->session_clearance_manager = new SessionClearanceManager();
- $this->sut = new SessionDataCollector();
- $this->sut->init( $this->session_clearance_manager );
-
- // Disable taxes before adding products to cart.
- update_option( 'woocommerce_calc_taxes', 'no' );
-
- // Clear any existing session data before each test.
- WC()->session->set( 'fraud_protection_collected_data', null );
- }
-
- /**
- * Helper method to collect data and retrieve event from session.
- *
- * Events only contain: event_type, timestamp, event_data.
- * For full data (session, customer, order, addresses), use get_collected_data().
- *
- * @param string|null $event_type Optional event type.
- * @param array $event_data Optional event data.
- * @return array The collected event data from session.
- */
- private function collect_and_get_event( ?string $event_type = null, array $event_data = array() ): array {
- $this->sut->collect( $event_type, $event_data );
- $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
- return $stored_data[0] ?? array();
- }
-
- /**
- * Helper method to collect data and retrieve full response via get_collected_data().
- *
- * Returns: wc_version, session, customer, shipping_address, billing_address, collected_events.
- *
- * @param string|null $event_type Optional event type.
- * @param array $event_data Optional event data.
- * @return array The full collected data response.
- */
- private function collect_and_get_data( ?string $event_type = null, array $event_data = array() ): array {
- $this->sut->collect( $event_type, $event_data );
- return $this->sut->get_collected_data();
- }
-
- /**
- * @testdox collect() stores properly structured event with 3 top-level keys.
- */
- public function test_collect_stores_properly_structured_event(): void {
- $event = $this->collect_and_get_event();
-
- $this->assertIsArray( $event );
- $this->assertArrayHasKey( 'event_type', $event );
- $this->assertArrayHasKey( 'timestamp', $event );
- $this->assertArrayHasKey( 'event_data', $event );
- $this->assertCount( 3, $event );
- }
-
- /**
- * @testdox get_collected_data() returns properly structured response with 7 top-level keys.
- */
- public function test_get_collected_data_returns_properly_structured_response(): void {
- $result = $this->collect_and_get_data();
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'wc_version', $result );
- $this->assertArrayHasKey( 'session', $result );
- $this->assertArrayHasKey( 'customer', $result );
- $this->assertArrayHasKey( 'order', $result );
- $this->assertArrayHasKey( 'shipping_address', $result );
- $this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'collected_events', $result );
- $this->assertCount( 7, $result );
- }
-
- /**
- * Test that collect() accepts event_type and event_data parameters.
- */
- public function test_collect_accepts_event_type_and_event_data_parameters(): void {
- $event_type = 'checkout_started';
- $event_data = array(
- 'page' => 'checkout',
- 'source' => 'test',
- );
-
- $event = $this->collect_and_get_event( $event_type, $event_data );
-
- $this->assertEquals( $event_type, $event['event_type'] );
- $this->assertEquals( $event_data, $event['event_data'] );
- }
-
- /**
- * @testdox collect() degrades gracefully when session is unavailable.
- */
- public function test_graceful_degradation_when_session_unavailable(): void {
- // This test verifies that collect() doesn't throw exceptions even if session is unavailable.
- // We can't easily simulate session being unavailable in unit tests without mocking,
- // but we can verify that calling collect() stores valid event structure.
- $event = $this->collect_and_get_event();
-
- $this->assertIsArray( $event );
- $this->assertCount( 3, $event );
- }
-
- /**
- * Test wc_version field is included in get_collected_data response.
- */
- public function test_wc_version_is_included(): void {
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertEquals( WC()->version, $result['wc_version'] );
- }
-
- /**
- * Test timestamp format is UTC (gmdate format).
- */
- public function test_timestamp_format_is_utc(): void {
- $event = $this->collect_and_get_event();
-
- $this->assertArrayHasKey( 'timestamp', $event );
- $this->assertNotEmpty( $event['timestamp'] );
-
- // Verify timestamp is in Y-m-d H:i:s format.
- $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $event['timestamp'] );
-
- // Verify timestamp is recent (within last 10 seconds).
- $timestamp = strtotime( $event['timestamp'] );
- $current_time = time();
- $time_difference = abs( $current_time - $timestamp );
- $this->assertLessThanOrEqual( 10, $time_difference, 'Timestamp should be recent (within 10 seconds)' );
- }
-
- /**
- * Test that collect() uses default values when parameters not provided.
- */
- public function test_collect_uses_default_values_when_parameters_not_provided(): void {
- $event = $this->collect_and_get_event();
-
- $this->assertNull( $event['event_type'] );
- $this->assertEquals( array(), $event['event_data'] );
- }
-
- /**
- * @testdox Nested sections are initialized as arrays.
- */
- public function test_nested_sections_initialized_as_arrays(): void {
- $result = $this->collect_and_get_data();
-
- $this->assertIsArray( $result['session'] );
- $this->assertIsArray( $result['customer'] );
- $this->assertIsArray( $result['order'] );
- $this->assertIsArray( $result['shipping_address'] );
- $this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['collected_events'] );
-
- $this->assertCount( 1, $result['collected_events'] );
- }
-
- /**
- * Test session data includes all 6 required fields.
- */
- public function test_session_data_includes_all_required_fields(): void {
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $result['session'] );
- $this->assertArrayHasKey( 'session_id', $result['session'] );
- $this->assertArrayHasKey( 'ip_address', $result['session'] );
- $this->assertArrayHasKey( 'email', $result['session'] );
- $this->assertArrayHasKey( 'ja3_hash', $result['session'] );
- $this->assertArrayHasKey( 'user_agent', $result['session'] );
- $this->assertArrayHasKey( 'is_user_session', $result['session'] );
- }
-
- /**
- * Test session_id is retrieved from SessionClearanceManager.
- */
- public function test_session_id_retrieved_from_session_clearance_manager(): void {
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertArrayHasKey( 'session_id', $result['session'] );
- // Session ID should be a string when session is available.
- if ( isset( WC()->session ) ) {
- $this->assertIsString( $result['session']['session_id'] );
- $this->assertNotEmpty( $result['session']['session_id'] );
- }
- }
-
- /**
- * Test email collection fallback chain for logged-in user.
- */
- public function test_email_collection_for_logged_in_user(): void {
- // Create a test user and log them in.
- $user_id = $this->factory->user->create(
- array(
- 'user_email' => 'testuser@example.com',
- )
- );
- wp_set_current_user( $user_id );
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertArrayHasKey( 'email', $result['session'] );
- $this->assertEquals( 'testuser@example.com', $result['session']['email'] );
- }
-
- /**
- * Test email collection from WC_Customer when user not logged in.
- */
- public function test_email_collection_from_wc_customer(): void {
- // Ensure no user is logged in.
- wp_set_current_user( 0 );
-
- // Set customer billing email.
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_email( 'customer@example.com' );
- }
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertArrayHasKey( 'email', $result['session'] );
- // Email should be from customer object if available.
- if ( isset( WC()->customer ) ) {
- $this->assertEquals( 'customer@example.com', $result['session']['email'] );
- }
- }
-
- /**
- * Test customer data includes all 4 required fields.
- */
- public function test_customer_data_includes_all_required_fields(): void {
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $result['customer'] );
- $this->assertArrayHasKey( 'first_name', $result['customer'] );
- $this->assertArrayHasKey( 'last_name', $result['customer'] );
- $this->assertArrayHasKey( 'billing_email', $result['customer'] );
- $this->assertArrayHasKey( 'lifetime_order_count', $result['customer'] );
- }
-
- /**
- * Test customer name collection from WC_Customer.
- */
- public function test_customer_name_collection_from_wc_customer(): void {
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_first_name( 'John' );
- WC()->customer->set_billing_last_name( 'Doe' );
- }
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertArrayHasKey( 'first_name', $result['customer'] );
- $this->assertArrayHasKey( 'last_name', $result['customer'] );
-
- if ( isset( WC()->customer ) ) {
- $this->assertEquals( 'John', $result['customer']['first_name'] );
- $this->assertEquals( 'Doe', $result['customer']['last_name'] );
- }
- }
-
- /**
- * Test customer data fallback to session when WC_Customer not available.
- */
- public function test_customer_data_fallback_to_session(): void {
- // Ensure no user is logged in.
- wp_set_current_user( 0 );
-
- // Set customer data in session.
- if ( isset( WC()->session ) ) {
- WC()->session->set(
- 'customer',
- array(
- 'first_name' => 'Jane',
- 'last_name' => 'Smith',
- 'email' => 'jane.smith@example.com',
- )
- );
- }
-
- // Nullify WC_Customer to force fallback to session.
- $original_customer = WC()->customer;
- WC()->customer = null;
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- // Restore original customer.
- WC()->customer = $original_customer;
-
- // Verify session data was used.
- if ( isset( WC()->session ) ) {
- $this->assertEquals( 'Jane', $result['customer']['first_name'] );
- $this->assertEquals( 'Smith', $result['customer']['last_name'] );
- $this->assertEquals( 'jane.smith@example.com', $result['customer']['billing_email'] );
- }
- }
-
- /**
- * Test lifetime_order_count field exists and uses WC_Customer::get_order_count().
- */
- public function test_lifetime_order_count_for_registered_customer(): void {
- // Create a test user.
- $user_id = $this->factory->user->create(
- array(
- 'user_email' => 'customer@example.com',
- )
- );
- wp_set_current_user( $user_id );
-
- // Initialize customer with logged-in user.
- WC()->customer = new \WC_Customer( $user_id, true );
-
- // Set customer billing data.
- WC()->customer->set_billing_first_name( 'John' );
- WC()->customer->set_billing_last_name( 'Doe' );
- WC()->customer->set_billing_email( 'customer@example.com' );
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- // Verify lifetime_order_count field exists and returns a valid integer.
- // In test environment, the method returns 0 because the cache is not automatically
- // populated by order lifecycle hooks. In production, WooCommerce maintains this cache.
- $this->assertArrayHasKey( 'lifetime_order_count', $result['customer'] );
- $this->assertIsInt( $result['customer']['lifetime_order_count'] );
- $this->assertGreaterThanOrEqual( 0, $result['customer']['lifetime_order_count'] );
- }
-
- /**
- * Test graceful degradation when customer data unavailable.
- */
- public function test_graceful_degradation_when_customer_data_unavailable(): void {
- // Ensure no user is logged in.
- wp_set_current_user( 0 );
-
- // Clear customer data.
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_first_name( '' );
- WC()->customer->set_billing_last_name( '' );
- WC()->customer->set_billing_email( '' );
- }
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- // Should return customer section with fields, even if empty/null.
- $this->assertIsArray( $result['customer'] );
- $this->assertArrayHasKey( 'first_name', $result['customer'] );
- $this->assertArrayHasKey( 'last_name', $result['customer'] );
- $this->assertArrayHasKey( 'billing_email', $result['customer'] );
- $this->assertArrayHasKey( 'lifetime_order_count', $result['customer'] );
- }
-
- /**
- * @testdox Order data includes all required fields with proper structure when order_id is provided.
- */
- public function test_order_data_includes_all_required_fields(): void {
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertIsArray( $result['order'] );
- $this->assertArrayHasKey( 'order_id', $result['order'] );
- $this->assertArrayHasKey( 'customer_id', $result['order'] );
- $this->assertArrayHasKey( 'total', $result['order'] );
- $this->assertArrayHasKey( 'items_total', $result['order'] );
- $this->assertArrayHasKey( 'shipping_total', $result['order'] );
- $this->assertArrayHasKey( 'tax_total', $result['order'] );
- $this->assertArrayHasKey( 'shipping_tax_rate', $result['order'] );
- $this->assertArrayHasKey( 'discount_total', $result['order'] );
- $this->assertArrayHasKey( 'currency', $result['order'] );
- $this->assertArrayHasKey( 'cart_hash', $result['order'] );
- $this->assertArrayHasKey( 'items', $result['order'] );
- $this->assertIsArray( $result['order']['items'] );
- }
-
- /**
- * @testdox Order totals are collected from cart when order_id is provided.
- */
- public function test_order_totals_collected_from_cart(): void {
- WC()->cart->empty_cart();
-
- $product = \WC_Helper_Product::create_simple_product();
- $product->set_regular_price( 50.00 );
- $product->save();
-
- WC()->cart->add_to_cart( $product->get_id(), 2 );
- WC()->cart->calculate_totals();
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'items_total', $result['order'] );
- $this->assertArrayHasKey( 'total', $result['order'] );
- $this->assertEquals( 100.00, $result['order']['items_total'] );
- }
-
- /**
- * @testdox Shipping tax rate is calculated correctly when order_id is provided.
- */
- public function test_shipping_tax_rate_calculation(): void {
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'shipping_tax_rate', $result['order'] );
- if ( 0 === (float) $result['order']['shipping_total'] ) {
- $this->assertNull( $result['order']['shipping_tax_rate'] );
- }
- }
-
- /**
- * @testdox Cart item data includes all 12 required fields when order_id is provided.
- */
- public function test_cart_item_includes_all_required_fields(): void {
- WC()->cart->empty_cart();
-
- $product = \WC_Helper_Product::create_simple_product();
- $product->set_name( 'Test Product' );
- $product->set_description( 'Test product description' );
- $product->set_sku( 'TEST-SKU-123' );
- $product->set_regular_price( 25.00 );
- $product->save();
-
- WC()->cart->add_to_cart( $product->get_id(), 2 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'items', $result['order'] );
- $this->assertIsArray( $result['order']['items'] );
- $this->assertCount( 1, $result['order']['items'] );
-
- $item = $result['order']['items'][0];
- $this->assertArrayHasKey( 'name', $item );
- $this->assertArrayHasKey( 'description', $item );
- $this->assertArrayHasKey( 'category', $item );
- $this->assertArrayHasKey( 'sku', $item );
- $this->assertArrayHasKey( 'quantity', $item );
- $this->assertArrayHasKey( 'unit_price', $item );
- $this->assertArrayHasKey( 'unit_tax_amount', $item );
- $this->assertArrayHasKey( 'unit_discount_amount', $item );
- $this->assertArrayHasKey( 'product_type', $item );
- $this->assertArrayHasKey( 'is_virtual', $item );
- $this->assertArrayHasKey( 'is_downloadable', $item );
- $this->assertArrayHasKey( 'attributes', $item );
-
- $this->assertEquals( 'Test Product', $item['name'] );
- $this->assertEquals( 'Test product description', $item['description'] );
- $this->assertEquals( 'TEST-SKU-123', $item['sku'] );
- $this->assertEquals( 2, $item['quantity'] );
- $this->assertEquals( 'simple', $item['product_type'] );
- }
-
- /**
- * Test billing address includes all required fields.
- */
- public function test_billing_address_includes_all_required_fields(): void {
- // Set billing address data.
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_address_1( '123 Main St' );
- WC()->customer->set_billing_address_2( 'Apt 4B' );
- WC()->customer->set_billing_city( 'New York' );
- WC()->customer->set_billing_state( 'NY' );
- WC()->customer->set_billing_country( 'US' );
- WC()->customer->set_billing_postcode( '10001' );
- }
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $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', $result['billing_address'] );
- $this->assertArrayHasKey( 'country', $result['billing_address'] );
- $this->assertArrayHasKey( 'postcode', $result['billing_address'] );
-
- // Verify values.
- if ( isset( WC()->customer ) ) {
- $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'] );
- $this->assertEquals( 'US', $result['billing_address']['country'] );
- $this->assertEquals( '10001', $result['billing_address']['postcode'] );
- }
- }
-
- /**
- * Test shipping address includes all required fields.
- */
- public function test_shipping_address_includes_all_required_fields(): void {
- // Set shipping address data.
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_shipping_address_1( '456 Oak Ave' );
- WC()->customer->set_shipping_address_2( 'Suite 100' );
- WC()->customer->set_shipping_city( 'Los Angeles' );
- WC()->customer->set_shipping_state( 'CA' );
- WC()->customer->set_shipping_country( 'US' );
- WC()->customer->set_shipping_postcode( '90001' );
- }
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $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', $result['shipping_address'] );
- $this->assertArrayHasKey( 'country', $result['shipping_address'] );
- $this->assertArrayHasKey( 'postcode', $result['shipping_address'] );
-
- // Verify values.
- if ( isset( WC()->customer ) ) {
- $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'] );
- $this->assertEquals( 'US', $result['shipping_address']['country'] );
- $this->assertEquals( '90001', $result['shipping_address']['postcode'] );
- }
- }
-
- /**
- * @testdox Order data degrades gracefully when cart is empty and order_id is provided.
- */
- public function test_graceful_degradation_when_cart_is_empty(): void {
- WC()->cart->empty_cart();
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertIsArray( $result['order'] );
- $this->assertArrayHasKey( 'items', $result['order'] );
- $this->assertIsArray( $result['order']['items'] );
- $this->assertEmpty( $result['order']['items'] );
-
- $this->assertEquals( 0, $result['order']['items_total'] );
- $this->assertEquals( 0, $result['order']['total'] );
- }
-
- /**
- * @testdox customer_id is set to 'guest' for guest users when order_id is provided.
- */
- public function test_customer_id_for_guest_users(): void {
- wp_set_current_user( 0 );
-
- WC()->customer = new \WC_Customer( 0, true );
-
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'customer_id', $result['order'] );
- $this->assertEquals( 'guest', $result['order']['customer_id'] );
- }
-
- /**
- * @testdox customer_id is set to user ID for logged-in users when order_id is provided.
- */
- public function test_customer_id_for_logged_in_users(): void {
- $user_id = $this->factory->user->create(
- array(
- 'user_email' => 'logged-in-user@example.com',
- )
- );
- wp_set_current_user( $user_id );
-
- WC()->customer = new \WC_Customer( $user_id, true );
-
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'customer_id', $result['order'] );
- $this->assertEquals( $user_id, $result['order']['customer_id'] );
- }
-
- /**
- * @testdox get_collected_data() output includes all 7 top-level sections with data.
- */
- public function test_complete_collect_output_includes_all_sections(): void {
- $user_id = $this->factory->user->create(
- array(
- 'user_email' => 'complete-test@example.com',
- )
- );
- wp_set_current_user( $user_id );
-
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_first_name( 'Test' );
- WC()->customer->set_billing_last_name( 'User' );
- WC()->customer->set_billing_email( 'complete-test@example.com' );
- WC()->customer->set_billing_address_1( '123 Test St' );
- WC()->customer->set_billing_city( 'Test City' );
- WC()->customer->set_billing_country( 'US' );
- }
-
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect( 'checkout_started', array( 'test' => 'data' ) );
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertArrayHasKey( 'wc_version', $result );
- $this->assertArrayHasKey( 'session', $result );
- $this->assertArrayHasKey( 'customer', $result );
- $this->assertArrayHasKey( 'order', $result );
- $this->assertArrayHasKey( 'shipping_address', $result );
- $this->assertArrayHasKey( 'billing_address', $result );
- $this->assertArrayHasKey( 'collected_events', $result );
-
- $this->assertIsString( $result['wc_version'] );
- $this->assertIsArray( $result['session'] );
- $this->assertIsArray( $result['customer'] );
- $this->assertIsArray( $result['order'] );
- $this->assertIsArray( $result['shipping_address'] );
- $this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['collected_events'] );
-
- $this->assertCount( 1, $result['collected_events'] );
- $event = $result['collected_events'][0];
- $this->assertEquals( 'checkout_started', $event['event_type'] );
- $this->assertIsString( $event['timestamp'] );
- $this->assertEquals( array( 'test' => 'data' ), $event['event_data'] );
- }
-
- /**
- * @testdox End-to-end data collection with full cart scenario works correctly.
- */
- public function test_end_to_end_data_collection_with_full_cart(): void {
- WC()->cart->empty_cart();
-
- $user_id = $this->factory->user->create(
- array(
- 'user_email' => 'e2e-test@example.com',
- )
- );
- wp_set_current_user( $user_id );
-
- $existing_order = wc_create_order();
- $existing_order->set_customer_id( $user_id );
- $existing_order->set_status( 'completed' );
- $existing_order->save();
-
- if ( isset( WC()->customer ) ) {
- WC()->customer = new \WC_Customer( $user_id, true );
- WC()->customer->set_billing_first_name( 'John' );
- WC()->customer->set_billing_last_name( 'Doe' );
- WC()->customer->set_billing_email( 'e2e-test@example.com' );
- WC()->customer->set_billing_address_1( '123 Test St' );
- WC()->customer->set_billing_address_2( 'Apt 1' );
- WC()->customer->set_billing_city( 'Test City' );
- WC()->customer->set_billing_state( 'CA' );
- WC()->customer->set_billing_country( 'US' );
- WC()->customer->set_billing_postcode( '90210' );
- WC()->customer->set_shipping_address_1( '456 Ship St' );
- WC()->customer->set_shipping_city( 'Ship City' );
- WC()->customer->set_shipping_state( 'NY' );
- WC()->customer->set_shipping_country( 'US' );
- WC()->customer->set_shipping_postcode( '10001' );
- }
-
- $product1 = \WC_Helper_Product::create_simple_product();
- $product1->set_name( 'Product 1' );
- $product1->set_regular_price( 100.00 );
- $product1->save();
-
- $product2 = \WC_Helper_Product::create_simple_product();
- $product2->set_name( 'Product 2' );
- $product2->set_regular_price( 50.00 );
- $product2->save();
-
- WC()->cart->add_to_cart( $product1->get_id(), 2 );
- WC()->cart->add_to_cart( $product2->get_id(), 1 );
- WC()->cart->calculate_totals();
-
- $new_order = wc_create_order();
- $new_order->save();
-
- $this->sut->collect( 'payment_attempt', array( 'gateway' => 'stripe' ) );
- $result = $this->sut->get_collected_data( $new_order->get_id() );
-
- $this->assertArrayHasKey( 'wc_version', $result );
- $this->assertArrayHasKey( 'collected_events', $result );
- $this->assertCount( 1, $result['collected_events'] );
-
- $event = $result['collected_events'][0];
-
- $this->assertEquals( 'payment_attempt', $event['event_type'] );
- $this->assertNotEmpty( $event['timestamp'] );
-
- $this->assertNotEmpty( $result['session']['session_id'] );
- $this->assertEquals( 'e2e-test@example.com', $result['session']['email'] );
-
- $this->assertEquals( 'John', $result['customer']['first_name'] );
- $this->assertEquals( 'Doe', $result['customer']['last_name'] );
- $this->assertIsInt( $result['customer']['lifetime_order_count'] );
- $this->assertGreaterThanOrEqual( 0, $result['customer']['lifetime_order_count'] );
-
- $this->assertGreaterThan( 0, $result['order']['total'] );
- $this->assertCount( 2, $result['order']['items'] );
-
- $this->assertEquals( '123 Test St', $result['billing_address']['address_1'] );
- $this->assertEquals( 'Test City', $result['billing_address']['city'] );
-
- $this->assertEquals( '456 Ship St', $result['shipping_address']['address_1'] );
- $this->assertEquals( 'Ship City', $result['shipping_address']['city'] );
-
- $this->assertEquals( array( 'gateway' => 'stripe' ), $event['event_data'] );
- }
-
- /**
- * @testdox Graceful degradation across all sections when data is minimal.
- */
- public function test_graceful_degradation_across_all_sections(): void {
- wp_set_current_user( 0 );
-
- WC()->customer = new \WC_Customer( 0, true );
-
- WC()->cart->empty_cart();
-
- if ( isset( WC()->customer ) ) {
- WC()->customer->set_billing_first_name( '' );
- WC()->customer->set_billing_last_name( '' );
- WC()->customer->set_billing_email( '' );
- }
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertIsArray( $result );
- $this->assertCount( 7, $result );
-
- $this->assertIsArray( $result['session'] );
- $this->assertIsArray( $result['customer'] );
- $this->assertIsArray( $result['order'] );
- $this->assertIsArray( $result['shipping_address'] );
- $this->assertIsArray( $result['billing_address'] );
- $this->assertIsArray( $result['collected_events'] );
-
- $this->assertCount( 1, $result['collected_events'] );
-
- $this->assertEquals( 'guest', $result['order']['customer_id'] );
- $this->assertEquals( 0, $result['customer']['lifetime_order_count'] );
- $this->assertEmpty( $result['order']['items'] );
- }
-
- /**
- * @testdox Data collection requires manual triggering (no automatic hooks).
- */
- public function test_manual_triggering_only(): void {
- // This test verifies that SessionDataCollector doesn't automatically
- // hook into WooCommerce events. It should only collect data when
- // collect() is explicitly called.
-
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $order = wc_create_order();
- $order->save();
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data( $order->get_id() );
-
- $this->assertIsArray( $result );
- $this->assertCount( 1, $result['order']['items'] );
-
- // No automatic data collection should have occurred.
- // This is a design verification test - the class should not register hooks.
- }
-
- /**
- * Test collect stores event data in session.
- *
- * @testdox collect() stores event data in WooCommerce session under 'fraud_protection_collected_data' key.
- */
- public function test_collect_stores_event_data_in_session(): void {
- // Collect data with a specific event type.
- $this->sut->collect( 'cart_page_loaded', array( 'source' => 'test' ) );
-
- // Verify data was stored in session.
- $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
-
- $this->assertIsArray( $stored_data );
- $this->assertCount( 1, $stored_data );
- $this->assertEquals( 'cart_page_loaded', $stored_data[0]['event_type'] );
- $this->assertEquals( array( 'source' => 'test' ), $stored_data[0]['event_data'] );
- }
-
- /**
- * Test multiple collect calls append data to session.
- *
- * @testdox Multiple collect() calls append data to session array, preserving event history.
- */
- public function test_multiple_collect_calls_append_data_to_session(): void {
- // First collect call.
- $this->sut->collect( 'cart_page_loaded', array() );
-
- // Second collect call.
- $this->sut->collect( 'checkout_page_loaded', array() );
-
- // Third collect call.
- $this->sut->collect( 'order_placed', array( 'order_id' => 123 ) );
-
- // Verify all three events are stored.
- $stored_data = WC()->session->get( 'fraud_protection_collected_data' );
-
- $this->assertIsArray( $stored_data );
- $this->assertCount( 3, $stored_data );
- $this->assertEquals( 'cart_page_loaded', $stored_data[0]['event_type'] );
- $this->assertEquals( 'checkout_page_loaded', $stored_data[1]['event_type'] );
- $this->assertEquals( 'order_placed', $stored_data[2]['event_type'] );
- $this->assertEquals( 123, $stored_data[2]['event_data']['order_id'] );
- }
-
- /**
- * Test get_collected_data returns structure with empty collected_events when no data collected.
- *
- * @testdox get_collected_data() returns structure with empty collected_events when no data has been collected.
- */
- public function test_get_collected_data_returns_empty_collected_events_when_no_data_collected(): void {
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'collected_events', $result );
- $this->assertEmpty( $result['collected_events'] );
- }
-
- /**
- * Test get_collected_data returns structure with empty collected_events when session unavailable.
- *
- * @testdox get_collected_data() returns structure with empty collected_events when session is unavailable.
- */
- public function test_get_collected_data_returns_empty_collected_events_when_session_unavailable(): void {
- // Store original session.
- $original_session = WC()->session;
-
- // Set session to null to simulate unavailability.
- WC()->session = null;
-
- $result = $this->sut->get_collected_data();
-
- // Restore original session.
- WC()->session = $original_session;
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'collected_events', $result );
- $this->assertEmpty( $result['collected_events'] );
- }
-
- /**
- * @testdox get_collected_data() returns empty order array when no order_id is provided.
- */
- public function test_get_collected_data_returns_empty_order_when_no_order_id(): void {
- $product = \WC_Helper_Product::create_simple_product();
- WC()->cart->add_to_cart( $product->get_id(), 1 );
-
- $this->sut->collect();
- $result = $this->sut->get_collected_data();
-
- $this->assertArrayHasKey( 'order', $result );
- $this->assertIsArray( $result['order'] );
- $this->assertEmpty( $result['order'] );
- }
-
- /**
- * @testdox get_collected_data() returns collected_events array after collect() is called.
- */
- public function test_get_collected_data_returns_data_after_collect(): void {
- // Collect some data.
- $this->sut->collect( 'cart_page_loaded', array( 'source' => 'test' ) );
- $this->sut->collect( 'checkout_started', array( 'gateway' => 'stripe' ) );
-
- // Get collected data using the new method.
- $result = $this->sut->get_collected_data();
-
- $this->assertIsArray( $result );
- $this->assertArrayHasKey( 'collected_events', $result );
- $this->assertCount( 2, $result['collected_events'] );
- $this->assertEquals( 'cart_page_loaded', $result['collected_events'][0]['event_type'] );
- $this->assertEquals( array( 'source' => 'test' ), $result['collected_events'][0]['event_data'] );
- $this->assertEquals( 'checkout_started', $result['collected_events'][1]['event_type'] );
- $this->assertEquals( array( 'gateway' => 'stripe' ), $result['collected_events'][1]['event_data'] );
- }
-}