Commit bc14677e7c for woocommerce
commit bc14677e7c360909235e7ff0a53b7c70c04f1174
Author: Vasily Belolapotkov <vasily.belolapotkov@automattic.com>
Date: Tue Jan 13 15:13:10 2026 +0100
[Fraud Protection] Add fraud protection tracking for successful order placements (#62765)
* Add fraud protection tracking for successful order placements
This commit adds tracking for successful order placements in the fraud protection system to capture critical fraud signals when orders are completed.
Changes:
1. Extended CheckoutEventTracker with track_order_placed() method
- Captures order_id, payment_method, total, currency, customer_id, status
- Works for both shortcode and Store API checkout flows
2. Added tracking calls at 3 strategic points:
- Shortcode: After successful payment in process_order_payment() (~line 1103)
- Shortcode: After no-payment orders in process_order_without_payment() (~line 1140)
- Store API: After successful checkout in Checkout.php (~line 585)
3. Added unit test for track_order_placed() method
- Verifies event dispatching with correct data structure
- Total: 14 tests passing, 35 assertions
Technical notes:
- Payment failure tracking was intentionally NOT implemented because payment
extensions handle failures on the frontend (Stripe.js, PayPal flows, etc.),
so they never reach backend catch blocks
- Direct tracking calls follow established pattern from class-wc-ajax.php
- All calls wrapped in feature_is_enabled() check for zero-impact when disabled
- SessionDataCollector automatically gathers comprehensive order context
* Fix short ternary linting error in CheckoutEventTracker
---------
Co-authored-by: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index a29db8c1ea..841316e9fa 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -11,6 +11,8 @@
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;
@@ -1100,6 +1102,15 @@ 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'] );
@@ -1131,6 +1142,12 @@ 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/src/Internal/FraudProtection/CheckoutEventTracker.php b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
index 48c3c29928..317a76f528 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/CheckoutEventTracker.php
@@ -247,4 +247,30 @@ class CheckoutEventTracker {
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->dispatcher->dispatch_event( 'order_placed', $event_data );
+ }
}
diff --git a/plugins/woocommerce/src/StoreApi/Payments/PaymentResult.php b/plugins/woocommerce/src/StoreApi/Payments/PaymentResult.php
index b700fc77d1..02ff5f0467 100644
--- a/plugins/woocommerce/src/StoreApi/Payments/PaymentResult.php
+++ b/plugins/woocommerce/src/StoreApi/Payments/PaymentResult.php
@@ -56,6 +56,16 @@ class PaymentResult {
return null;
}
+ /**
+ * Get payment status.
+ *
+ * @since 10.5.0
+ * @return string Current payment status.
+ */
+ public function get_status(): string {
+ return $this->status;
+ }
+
/**
* Set payment status.
*
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index cc82c98bd7..9d5d11dde4 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -9,6 +9,7 @@ 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;
@@ -582,6 +583,15 @@ 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/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
index 2fc5e62791..68c63204f6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/CheckoutEventTrackerTest.php
@@ -485,4 +485,35 @@ class CheckoutEventTrackerTest extends \WC_Unit_Test_Case {
$posted_data = 'billing_country=US&billing_email=test@example.com';
$this->sut->track_shortcode_checkout_field_update( $posted_data );
}
+
+ /**
+ * Test track_order_placed dispatches event with correct data structure.
+ */
+ public function test_track_order_placed_dispatches_event(): void {
+ $order = \WC_Helper_Order::create_order();
+
+ $this->mock_dispatcher
+ ->expects( $this->once() )
+ ->method( 'dispatch_event' )
+ ->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 );
+
+ // Clean up.
+ $order->delete( true );
+ }
}