diff --git a/changelog/add-support-agentic_commerce-in-core b/changelog/add-support-agentic_commerce-in-core
new file mode 100644
index 00000000000..6b0f131be9d
--- /dev/null
+++ b/changelog/add-support-agentic_commerce-in-core
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Support Agentic Commerce Protocol
diff --git a/includes/class-payment-information.php b/includes/class-payment-information.php
index 8548c671443..902b9dc8e54 100644
--- a/includes/class-payment-information.php
+++ b/includes/class-payment-information.php
@@ -16,7 +16,6 @@
use WCPay\Constants\Payment_Initiated_By;
use WCPay\Constants\Payment_Capture_Type;
use WCPay\Exceptions\Invalid_Payment_Method_Exception;
-use WCPay\Payment_Methods\CC_Payment_Gateway;
/**
* Mostly a wrapper containing information on a single payment.
@@ -127,19 +126,27 @@ class Payment_Information {
*/
private $error = null;
+ /**
+ * Whether the current order is processed by Agentic Commerce.
+ *
+ * @var bool
+ */
+ private $is_agentic_commerce_request = false;
+
/**
* Payment information constructor.
*
- * @param string $payment_method The ID of the payment method used for this payment.
- * @param \WC_Order $order The order object.
- * @param Payment_Type $payment_type The type of the payment.
- * @param \WC_Payment_Token $token The payment token used for this payment.
- * @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
- * @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately.
- * @param string $cvc_confirmation The CVC confirmation for this payment method.
- * @param string $fingerprint The attached fingerprint.
- * @param string $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
- * @param string $customer_id The WCPay Customer ID that owns the payment token.
+ * @param string $payment_method The ID of the payment method used for this payment.
+ * @param \WC_Order|null $order The order object.
+ * @param Payment_Type|null $payment_type The type of the payment.
+ * @param \WC_Payment_Token|null $token The payment token used for this payment.
+ * @param Payment_Initiated_By|null $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
+ * @param Payment_Capture_Type|null $manual_capture Indicates whether the payment will be only authorized or captured immediately.
+ * @param string|null $cvc_confirmation The CVC confirmation for this payment method.
+ * @param string $fingerprint The attached fingerprint.
+ * @param string|null $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
+ * @param string|null $customer_id The WCPay Customer ID that owns the payment token.
+ * @param bool $is_agentic_commerce_request Whether the current order is processed by Agentic Commerce.
*
* @throws Invalid_Payment_Method_Exception When no payment method is found in the provided request.
*/
@@ -153,7 +160,8 @@ public function __construct(
?string $cvc_confirmation = null,
string $fingerprint = '',
?string $payment_method_stripe_id = null,
- ?string $customer_id = null
+ ?string $customer_id = null,
+ bool $is_agentic_commerce_request = false
) {
if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) {
// If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used.
@@ -162,16 +170,17 @@ public function __construct(
'payment_method_not_provided'
);
}
- $this->payment_method = $payment_method;
- $this->order = $order;
- $this->token = $token;
- $this->payment_initiated_by = $payment_initiated_by ?? Payment_Initiated_By::CUSTOMER();
- $this->manual_capture = $manual_capture ?? Payment_Capture_Type::AUTOMATIC();
- $this->payment_type = $payment_type ?? Payment_Type::SINGLE();
- $this->cvc_confirmation = $cvc_confirmation;
- $this->fingerprint = $fingerprint;
- $this->payment_method_stripe_id = $payment_method_stripe_id;
- $this->customer_id = $customer_id;
+ $this->payment_method = $payment_method;
+ $this->order = $order;
+ $this->token = $token;
+ $this->payment_initiated_by = $payment_initiated_by ?? Payment_Initiated_By::CUSTOMER();
+ $this->manual_capture = $manual_capture ?? Payment_Capture_Type::AUTOMATIC();
+ $this->payment_type = $payment_type ?? Payment_Type::SINGLE();
+ $this->cvc_confirmation = $cvc_confirmation;
+ $this->fingerprint = $fingerprint;
+ $this->payment_method_stripe_id = $payment_method_stripe_id;
+ $this->customer_id = $customer_id;
+ $this->is_agentic_commerce_request = $is_agentic_commerce_request;
}
/**
@@ -266,7 +275,15 @@ public static function from_payment_request(
?Payment_Capture_Type $manual_capture = null,
?string $payment_method_stripe_id = null
): Payment_Information {
- $payment_method = self::get_payment_method_from_request( $request );
+ /**
+ * Psalm note: Class exists as a util in Woo core.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ $is_agentic_commerce_request = method_exists( '\\Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils', 'is_agentic_commerce_session' )
+ && \Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils::is_agentic_commerce_session();
+
+ $payment_method = self::get_payment_method_from_request( $request, $is_agentic_commerce_request );
$token = self::get_token_from_request( $request );
$cvc_confirmation = self::get_cvc_confirmation_from_request( $request );
$fingerprint = self::get_fingerprint_from_request( $request );
@@ -275,7 +292,7 @@ public static function from_payment_request(
$order->add_meta_data( 'is_woopay', true, true );
$order->save_meta_data();
}
- $payment_information = new Payment_Information( $payment_method, $order, $payment_type, $token, $payment_initiated_by, $manual_capture, $cvc_confirmation, $fingerprint, $payment_method_stripe_id );
+ $payment_information = new Payment_Information( $payment_method, $order, $payment_type, $token, $payment_initiated_by, $manual_capture, $cvc_confirmation, $fingerprint, $payment_method_stripe_id, null, $is_agentic_commerce_request );
if ( self::PAYMENT_METHOD_ERROR === $payment_method ) {
$error_message = $request['wcpay-payment-method-error-message'] ?? __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' );
@@ -291,11 +308,16 @@ public static function from_payment_request(
* Extracts the payment method from the provided request.
*
* @param array $request Associative array containing payment request information.
+ * @param bool $is_agentic_commerce_request Whether the current order is processed by Agentic Commerce.
*
* @return string
*/
- public static function get_payment_method_from_request( array $request ): string {
- foreach ( [ 'wcpay-payment-method', 'wcpay-payment-method-sepa' ] as $key ) {
+ public static function get_payment_method_from_request( array $request, bool $is_agentic_commerce_request = false ): string {
+ $keys_to_check = $is_agentic_commerce_request
+ ? [ 'wc-agentic_commerce-token' ]
+ : [ 'wcpay-payment-method', 'wcpay-payment-method-sepa' ];
+
+ foreach ( $keys_to_check as $key ) {
if ( ! empty( $request[ $key ] ) ) {
$normalized = wc_clean( $request[ $key ] );
return is_string( $normalized ) ? $normalized : '';
@@ -304,6 +326,15 @@ public static function get_payment_method_from_request( array $request ): string
return '';
}
+ /**
+ * Whether this payment is processed by Agentic Commerce.
+ *
+ * @return bool
+ */
+ public function is_agentic_commerce_request(): bool {
+ return $this->is_agentic_commerce_request;
+ }
+
/**
* Extract the payment token from the provided request.
*
diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php
index 78780e977c0..26b9b46e59e 100644
--- a/includes/class-wc-payment-gateway-wcpay.php
+++ b/includes/class-wc-payment-gateway-wcpay.php
@@ -325,6 +325,7 @@ public function __construct(
$this->supports = [
'products',
'refunds',
+ 'agentic_commerce',
];
if ( 'card' !== $this->stripe_id ) {
@@ -753,7 +754,9 @@ private function get_request_payment_data( \WP_REST_Request $request ) {
}
if ( ! empty( $request['payment_data'] ) ) {
foreach ( $request['payment_data'] as $data ) {
- $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
+ if ( isset( $data['key'], $data['value'] ) ) {
+ $payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
+ }
}
}
@@ -864,6 +867,24 @@ public function needs_setup() {
return parent::needs_setup() || ! empty( $account_status['error'] ) || ! $account_status['paymentsEnabled'];
}
+ /**
+ * Get the agentic commerce payment provider identifier.
+ *
+ * @return string Payment provider identifier.
+ */
+ public function get_agentic_commerce_provider() {
+ return 'stripe';
+ }
+
+ /**
+ * Get the supported payment methods for agentic commerce.
+ *
+ * @return array Array of supported payment methods.
+ */
+ public function get_agentic_commerce_payment_methods() {
+ return [ 'card' ];
+ }
+
/**
* Returns whether a store that is not in test mode needs to set https
* in the checkout
@@ -1127,8 +1148,14 @@ public function process_payment( $order_id ) {
'invalid_phone_number'
);
}
+
+ $payment_information = $this->prepare_payment_information( $order );
+
// Check if session exists and we're currently not processing a WooPay request before instantiating `Fraud_Prevention_Service`.
- if ( WC()->session && ! apply_filters( 'wcpay_is_woopay_store_api_request', false ) ) {
+ if ( WC()->session
+ && ! apply_filters( 'wcpay_is_woopay_store_api_request', false )
+ && ! $payment_information->is_agentic_commerce_request()
+ ) {
$fraud_prevention_service = Fraud_Prevention_Service::get_instance();
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( $fraud_prevention_service->is_enabled() && ! $fraud_prevention_service->verify_token( $_POST['wcpay-fraud-prevention-token'] ?? null ) ) {
@@ -1171,7 +1198,6 @@ public function process_payment( $order_id ) {
return $check_existing_intention;
}
- $payment_information = $this->prepare_payment_information( $order );
return $this->process_payment_for_order( WC()->cart, $payment_information );
} catch ( Exception $e ) {
// We set this variable to be used in following checks.
@@ -1194,6 +1220,24 @@ public function process_payment( $order_id ) {
if ( $blocked_by_fraud_rules ) {
$this->order_service->mark_order_blocked_for_fraud( $order, '', Intent_Status::CANCELED );
+ } elseif ( $e instanceof Process_Payment_Exception && 'rate_limiter_enabled' === $e->get_error_code() ) {
+ /**
+ * TODO: Move the contents of this into the Order_Service.
+ */
+ $note = sprintf(
+ WC_Payments_Utils::esc_interpolated_html(
+ /* translators: %1: the failed payment amount */
+ __(
+ 'A payment of %1$s failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.',
+ 'woocommerce-payments'
+ ),
+ [
+ 'strong' => '',
+ ]
+ ),
+ WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $order->get_total(), [ 'currency' => $order->get_currency() ] ), $order )
+ );
+ $order->add_order_note( $note );
} elseif ( ! empty( $payment_information ) ) {
/**
* TODO: Move the contents of this else into the Order_Service.
@@ -1243,26 +1287,6 @@ public function process_payment( $order_id ) {
$order->add_order_note( $note );
}
- if ( $e instanceof Process_Payment_Exception && 'rate_limiter_enabled' === $e->get_error_code() ) {
- /**
- * TODO: Move the contents of this into the Order_Service.
- */
- $note = sprintf(
- WC_Payments_Utils::esc_interpolated_html(
- /* translators: %1: the failed payment amount */
- __(
- 'A payment of %1$s failed to complete because of too many failed transactions. A rate limiter was enabled for the user to prevent more attempts temporarily.',
- 'woocommerce-payments'
- ),
- [
- 'strong' => '',
- ]
- ),
- WC_Payments_Explicit_Price_Formatter::get_explicit_price( wc_price( $order->get_total(), [ 'currency' => $order->get_currency() ] ), $order )
- );
- $order->add_order_note( $note );
- }
-
// This allows WC to check if WP_DEBUG mode is enabled before returning previous Exception and expose Exception class name to frontend.
add_filter( 'woocommerce_return_previous_exceptions', '__return_true' );
wc_add_notice( wp_strip_all_tags( WC_Payments_Utils::get_filtered_error_message( $e, $blocked_by_fraud_rules ) ), 'error' );
@@ -2117,6 +2141,8 @@ public function get_payment_method_types( $payment_information ): array {
$order = $payment_information->get_order();
$order_id = $order instanceof WC_Order ? $order->get_id() : null;
$payment_methods = $this->get_payment_methods_from_gateway_id( $token->get_gateway_id(), $order_id );
+ } elseif ( $payment_information->is_agentic_commerce_request() ) {
+ $payment_methods = $this->get_agentic_commerce_payment_methods();
}
return $payment_methods;
diff --git a/includes/core/server/request/class-create-intention.php b/includes/core/server/request/class-create-intention.php
index 917089eb5d7..64e9b9d2807 100644
--- a/includes/core/server/request/class-create-intention.php
+++ b/includes/core/server/request/class-create-intention.php
@@ -55,7 +55,7 @@ public function get_method(): string {
*/
public function set_payment_method( string $payment_method_id ) {
// Including the 'card' prefix to support subscription renewals using legacy payment method IDs.
- $this->validate_stripe_id( $payment_method_id, [ 'pm', 'src', 'card' ] );
+ $this->validate_stripe_id( $payment_method_id, [ 'pm', 'src', 'card', 'spt' ] );
$this->set_param( 'payment_method', $payment_method_id );
}
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index 95a27f48e4c..d901bde4ffb 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -962,6 +962,9 @@ public function test_exception_will_be_thrown_if_phone_number_is_invalid() {
$order = WC_Helper_Order::create_order();
$order->set_billing_phone( '+1123456789123456789123' );
$order->save();
+
+ $_POST['wcpay-payment-method'] = 'pm_mock';
+
$result = $this->card_gateway->process_payment( $order->get_id() );
$this->assertEquals( 'fail', $result['result'] );
$error_notices = WC()->session->get( 'wc_notices' );
@@ -3381,6 +3384,8 @@ public function test_process_payment_caches_mimimum_amount_and_displays_error_up
public function test_process_payment_rejects_if_missing_fraud_prevention_token() {
$order = WC_Helper_Order::create_order();
+ $_POST['wcpay-payment-method'] = 'pm_mock';
+
$fraud_prevention_service_mock = $this->get_fraud_prevention_service_mock();
$fraud_prevention_service_mock
@@ -3400,6 +3405,8 @@ public function test_process_payment_rejects_if_missing_fraud_prevention_token()
public function test_process_payment_rejects_if_invalid_fraud_prevention_token() {
$order = WC_Helper_Order::create_order();
+ $_POST['wcpay-payment-method'] = 'pm_mock';
+
$fraud_prevention_service_mock = $this->get_fraud_prevention_service_mock();
$fraud_prevention_service_mock
@@ -3467,9 +3474,13 @@ public function test_process_payment_marks_order_as_blocked_for_fraud() {
$error_message = "There's a problem with this payment. Please try again or use a different payment method.";
+ $mock_payment_information = $this->createMock( Payment_Information::class );
+ $mock_payment_information->method( 'is_agentic_commerce_request' )->willReturn( false );
+
$mock_wcpay_gateway
->expects( $this->once() )
- ->method( 'prepare_payment_information' );
+ ->method( 'prepare_payment_information' )
+ ->willReturn( $mock_payment_information );
$mock_wcpay_gateway
->expects( $this->once() )
->method( 'process_payment_for_order' )
@@ -3538,9 +3549,13 @@ public function test_process_payment_marks_order_as_blocked_for_fraud_avs_mismat
$error_message = "There's a problem with this payment. Please try again or use a different payment method.";
+ $mock_payment_information = $this->createMock( Payment_Information::class );
+ $mock_payment_information->method( 'is_agentic_commerce_request' )->willReturn( false );
+
$mock_wcpay_gateway
->expects( $this->once() )
- ->method( 'prepare_payment_information' );
+ ->method( 'prepare_payment_information' )
+ ->willReturn( $mock_payment_information );
$mock_wcpay_gateway
->expects( $this->once() )
->method( 'process_payment_for_order' )
@@ -3611,9 +3626,13 @@ public function test_process_payment_marks_order_as_blocked_for_postal_code_mism
$error_message = 'We couldn’t verify the postal code in your billing address. Make sure the information is current with your card issuing bank and try again.';
+ $mock_payment_information = $this->createMock( Payment_Information::class );
+ $mock_payment_information->method( 'is_agentic_commerce_request' )->willReturn( false );
+
$mock_wcpay_gateway
->expects( $this->once() )
- ->method( 'prepare_payment_information' );
+ ->method( 'prepare_payment_information' )
+ ->willReturn( $mock_payment_information );
$mock_wcpay_gateway
->expects( $this->once() )
->method( 'process_payment_for_order' )
@@ -3652,9 +3671,15 @@ public function test_process_payment_continues_if_valid_fraud_prevention_token()
->willReturn( false );
$mock_wcpay_gateway = $this->get_partial_mock_for_gateway( [ 'prepare_payment_information', 'process_payment_for_order' ] );
+
+ $mock_payment_information = $this->createMock( Payment_Information::class );
+ $mock_payment_information->method( 'is_agentic_commerce_request' )->willReturn( false );
+
$mock_wcpay_gateway
->expects( $this->once() )
- ->method( 'prepare_payment_information' );
+ ->method( 'prepare_payment_information' )
+ ->willReturn( $mock_payment_information );
+
$mock_wcpay_gateway
->expects( $this->once() )
->method( 'process_payment_for_order' );
@@ -3843,6 +3868,7 @@ private function get_partial_mock_for_gateway( array $methods = [], array $const
*/
public function test_no_payment_is_processed_for_woopay_preflight_check_request() {
$_POST['is-woopay-preflight-check'] = true;
+ $_POST['wcpay-payment-method'] = 'pm_mock';
// Arrange: Create an order to test with.
$order_data = [
@@ -3866,6 +3892,8 @@ public function test_no_payment_is_processed_for_woopay_preflight_check_request(
public function test_process_payment_rate_limiter_enabled_throw_exception() {
$order = WC_Helper_Order::create_order();
+ $_POST['wcpay-payment-method'] = 'pm_mock';
+
$this->mock_rate_limiter
->expects( $this->once() )
->method( 'is_limited' )