@@ -296,6 +296,7 @@ public function process_payment( $order_id ) {
296296 }
297297 } catch ( Exception $ exception ) {
298298 $ this ->logger ->error ( '[AXO] Payment processing failed: ' . $ exception ->getMessage () );
299+
299300 return array (
300301 'result ' => 'failure ' ,
301302 'message ' => $ this ->get_user_friendly_error_message ( $ exception ),
@@ -330,6 +331,47 @@ protected function process_3ds_return( WC_Order $wc_order, string $token ): arra
330331 );
331332 }
332333
334+ $ purchase_units = $ paypal_order ->purchase_units ();
335+ $ purchase_unit = $ purchase_units [0 ] ?? null ;
336+
337+ if ( null === $ purchase_unit ) {
338+ $ this ->logger ->error (
339+ sprintf (
340+ '[AXO] 3DS return validation failed: PayPal order has no purchase units. WC order ID: %s ' ,
341+ $ wc_order ->get_id (),
342+ )
343+ );
344+
345+ return array (
346+ 'result ' => 'failure ' ,
347+ 'message ' => __ ( 'Payment validation failed. Please try again. ' , 'woocommerce-paypal-payments ' ),
348+ );
349+ }
350+
351+ /*
352+ * If the custom_id === order_id condition is true, we know that the PayPal order
353+ * and our WC order match, and there's no need for other checks.
354+ *
355+ * Specifically, we do not compare the WC_Order total amount against the
356+ * purchase unit's value, as the PU value can deviate from the WC order total, which
357+ * might reject legitimate payments.
358+ */
359+ if ( $ purchase_unit ->custom_id () !== (string ) $ wc_order ->get_id () ) {
360+ $ this ->logger ->error (
361+ sprintf (
362+ '[AXO] 3DS return validation failed: custom_id mismatch. Token: %s, WC order ID: %s, PayPal custom_id: %s ' ,
363+ $ token ,
364+ $ wc_order ->get_id (),
365+ $ purchase_unit ->custom_id ()
366+ )
367+ );
368+
369+ return array (
370+ 'result ' => 'failure ' ,
371+ 'message ' => __ ( 'Payment validation failed. Please try again. ' , 'woocommerce-paypal-payments ' ),
372+ );
373+ }
374+
333375 /**
334376 * This filter controls if the method 'process()' from OrderProcessor will be called.
335377 * So you can implement your own for example on subscriptions
@@ -344,6 +386,7 @@ protected function process_3ds_return( WC_Order $wc_order, string $token ): arra
344386 }
345387 } catch ( Exception $ exception ) {
346388 $ this ->logger ->error ( '[AXO] 3DS return processing failed: ' . $ exception ->getMessage () );
389+
347390 return array (
348391 'result ' => 'failure ' ,
349392 'message ' => $ this ->get_user_friendly_error_message ( $ exception ),
@@ -453,7 +496,7 @@ protected function build_payment_source_properties( string $payment_token ): obj
453496 'single_use_token ' => $ payment_token ,
454497 );
455498
456- $ three_d_secure = $ this ->settings_model ->get_three_d_secure_enum ();
499+ $ three_d_secure = $ this ->settings_model ->get_three_d_secure_enum ();
457500
458501 if ( 'SCA_ALWAYS ' === $ three_d_secure || 'SCA_WHEN_REQUIRED ' === $ three_d_secure ) {
459502 $ properties ['attributes ' ] = array (
@@ -482,7 +525,7 @@ protected function build_order_data(): array {
482525
483526 $ data ['experience_context ' ] = $ experience_context ->to_array ();
484527
485- $ three_d_secure = $ this ->settings_model ->get_three_d_secure_enum ();
528+ $ three_d_secure = $ this ->settings_model ->get_three_d_secure_enum ();
486529
487530 if ( $ three_d_secure === 'SCA_ALWAYS ' || $ three_d_secure === 'SCA_WHEN_REQUIRED ' ) {
488531 $ data ['transaction_context ' ] = array (
0 commit comments