Commit 8935cfd5cde for woocommerce
commit 8935cfd5cdeb50f9bd20bea34cb59406eb829f6b
Author: Seghir Nadir <nadir.seghir@gmail.com>
Date: Thu Aug 27 11:59:47 2026 +0200
Remove the experimental Agentic Checkout API (#67914)
* Remove the experimental Agentic Checkout API
Delete the wc/agentic/v1 checkout session endpoints, their schema and
utilities, the ACP spec enums, and the agentic webhook manager, along
with the agentic_checkout feature flag that gated them.
PaymentGatewayFeature::AGENTIC_COMMERCE is kept and marked @deprecated:
it is a public constant that payment gateways reference, so removing it
would fatal them on an undefined constant. Nothing in core reads it now.
Also drops the agentic_checkout entry from the 10.5.0 autoload migration
so upgrading sites no longer create a dead option, and removes the 86
PHPStan baseline entries that pointed at the deleted files.
* Apply suggestions from code review
Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
diff --git a/.github/project-community-pr-assigner.yml b/.github/project-community-pr-assigner.yml
index 53f2ecc2381..c38d65d68b8 100644
--- a/.github/project-community-pr-assigner.yml
+++ b/.github/project-community-pr-assigner.yml
@@ -102,13 +102,13 @@
'plugins/woocommerce/src/{Abilities,Api,Caches,Caching,Database,Enums,Proxies,Utilities,Vendor}/**/*':
- team: rubik
-'plugins/woocommerce/src/Internal/{Abilities,AbilitiesApi,Agentic,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Traits,TransientFiles}/**/*':
+'plugins/woocommerce/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Traits,TransientFiles}/**/*':
- team: rubik
'plugins/woocommerce/src/Internal/{AddressProvider,POS}/**/*':
- team: woo-fse
-'plugins/woocommerce/src/Internal/Admin/{Agentic,Logging,Onboarding,Orders,Schedulers}/**/*':
+'plugins/woocommerce/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*':
- team: rubik
'plugins/woocommerce/src/Internal/Admin/*':
@@ -138,10 +138,10 @@
'plugins/woocommerce/tests/php/src/{Api,Caching,Database,Proxies,Utilities}/**/*':
- team: rubik
-'plugins/woocommerce/tests/php/src/Internal/{Abilities,AbilitiesApi,Agentic,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,LegacyPhpApi,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Telemetry,Traits,TransientFiles}/**/*':
+'plugins/woocommerce/tests/php/src/Internal/{Abilities,AbilitiesApi,Api,BatchProcessing,Caches,ComingSoon,CustomerEmailVerification,DataStores,DependencyManagement,Features,LegacyAssets,LegacyPhpApi,Logging,MCP,OrderReviews,Orders,OrderWithdrawal,PushNotifications,ReceiptRendering,RestApi,ShopperLists,StockNotifications,Telemetry,Traits,TransientFiles}/**/*':
- team: rubik
-'plugins/woocommerce/tests/php/src/Internal/Admin/{Agentic,Logging,Onboarding,Orders,Schedulers}/**/*':
+'plugins/woocommerce/tests/php/src/Internal/Admin/{Logging,Onboarding,Orders,Schedulers}/**/*':
- team: rubik
'plugins/woocommerce/tests/php/src/Internal/Admin/*':
diff --git a/plugins/woocommerce/changelog/remove-agentic-checkout-api b/plugins/woocommerce/changelog/remove-agentic-checkout-api
new file mode 100644
index 00000000000..652142a22cb
--- /dev/null
+++ b/plugins/woocommerce/changelog/remove-agentic-checkout-api
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Remove the experimental Agentic Checkout API.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 7cda07c2945..5ef90427e34 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -427,7 +427,6 @@ final class WooCommerce {
$container->get( EmailLogger::class )->register();
$container->get( VisualAttributeTermAdmin::class )->register();
$container->get( Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentsController::class )->register();
- $container->get( Automattic\WooCommerce\Internal\Admin\Agentic\AgenticController::class )->register();
$container->get( Automattic\WooCommerce\Internal\ProductFeed\ProductFeed::class )->register();
$container->get( Automattic\WooCommerce\Internal\PushNotifications\PushNotifications::class )->register();
$container->get( Automattic\WooCommerce\Internal\Orders\PointOfSaleEmailHandler::class )->register();
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 3b8402817a6..67875534a88 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -3352,7 +3352,6 @@ function wc_update_1050_enable_autoload_options() {
$feature_options = array(
'fulfillments' => 'woocommerce_feature_fulfillments_enabled',
'push_notifications' => 'woocommerce_feature_push_notifications_enabled',
- 'agentic_checkout' => 'woocommerce_feature_agentic_checkout_enabled',
'cart_checkout_blocks' => 'woocommerce_feature_cart_checkout_blocks_enabled',
);
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 7ce68d17b90..22941520532 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -54843,66 +54843,6 @@ parameters:
count: 1
path: src/Internal/Admin/ActivityPanels.php
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticController\:\:on_init\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticController.php
-
- -
- message: '#^@param WC_Order \$order does not accept actual type of parameter\: WC_Order\|WC_Order_Refund\.$#'
- identifier: parameter.phpDocType
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:handle_order_created\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:handle_order_refunded\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:handle_order_status_changed\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:init\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:mark_first_event_delivered\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Parameter \#1 \$order of method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookManager\:\:should_trigger_webhook\(\) expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
- identifier: argument.type
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Parameter \#2 \$order of method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookPayloadBuilder\:\:build_payload\(\) expects WC_Order, WC_Order\|WC_Order_Refund given\.$#'
- identifier: argument.type
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookManager.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Agentic\\AgenticWebhookPayloadBuilder\:\:init\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilder.php
-
-
message: '#^Method Automattic\\WooCommerce\\Internal\\Admin\\Analytics\:\:get_instance\(\) has no return type specified\.$#'
identifier: missingType.return
@@ -68082,216 +68022,6 @@ parameters:
count: 1
path: src/StoreApi/Routes/V1/AbstractTermsRoute.php
- -
- message: '#^Call to method to_rest_response\(\) on an unknown class Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Errors\\Error\.$#'
- identifier: class.notFound
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessions\:\:get_route_post_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessions\:\:requires_nonce\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
-
- -
- message: '#^Parameter \#1 \$cart of method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\CartSchema\:\:get_item_response\(\) expects WC_Cart, Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\AgenticCheckoutSession given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
-
- -
- message: '#^Access to protected property Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult\:\:\$status\.$#'
- identifier: property.protected
- count: 3
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Call to an undefined method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_request_payment_method\(\)\.$#'
- identifier: method.notFound
- count: 2
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Call to an undefined method WC_Session\:\:save_data\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Instanceof between Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult and Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult will always evaluate to true\.$#'
- identifier: instanceof.alwaysTrue
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_document_object_from_rest_request\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_request_payment_data\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_request_payment_method_id\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_request_payment_method_title\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:get_route_post_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:has_cart_token\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:is_authorized\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:persist_additional_fields_for_order\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:persist_additional_fields_for_order\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:prepare_item_for_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:process_payment\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:process_payment\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:process_without_payment\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:process_without_payment\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:requires_nonce\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:update_order_from_request\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsComplete\:\:update_order_from_request\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^One or more @param tags has an invalid name or invalid syntax\.$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^PHPDoc tag @param has invalid value \(bool The WP_DEBUG mode\.\)\: Unexpected token "The", expected variable at offset 115 on line 4$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Parameter \#1 \$cart of method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\CartSchema\:\:get_item_response\(\) expects WC_Cart, Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\AgenticCheckoutSession given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Parameter \#1 \$redirect_url of method Automattic\\WooCommerce\\StoreApi\\Payments\\PaymentResult\:\:set_redirect_url\(\) expects array, string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
-
- -
- message: '#^Call to method to_rest_response\(\) on an unknown class Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Errors\\Error\.$#'
- identifier: class.notFound
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsUpdate\:\:get_route_post_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsUpdate\:\:has_cart_token\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\CheckoutSessionsUpdate\:\:is_authorized\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
-
- -
- message: '#^Parameter \#1 \$cart of method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\CartSchema\:\:get_item_response\(\) expects WC_Cart, Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\AgenticCheckoutSession given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Messages\\Message\:\:use_markdown\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Routes/V1/Agentic/Messages/Message.php
-
-
message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Batch\:\:get_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
@@ -69414,96 +69144,6 @@ parameters:
count: 1
path: src/StoreApi/Schemas/V1/AbstractSchema.php
- -
- message: '#^Binary operation "\+" between string and string results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Binary operation "\-" between \(float\|int\) and string results in an error\.$#'
- identifier: binaryOp.invalid
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_product\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_product_id\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_subtotal\(\)\.$#'
- identifier: method.notFound
- count: 2
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_total\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_total_tax\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Order_Item\:\:get_variation_id\(\)\.$#'
- identifier: method.notFound
- count: 2
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Payment_Gateway\:\:get_agentic_commerce_payment_methods\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to an undefined method WC_Payment_Gateway\:\:get_agentic_commerce_provider\(\)\.$#'
- identifier: method.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Call to method format\(\) on an unknown class Automattic\\WooCommerce\\StoreApi\\Schemas\\FormatterInterface\.$#'
- identifier: class.notFound
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\Agentic\\CheckoutSessionSchema\:\:get_draft_order\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\Agentic\\CheckoutSessionSchema\:\:set_draft_order_id\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^PHPDoc tag @return has invalid value \(\\WC_Order\|null;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 99 on line 4$#'
- identifier: phpDoc.parseError
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
- -
- message: '#^Parameter \#1 \$order_object of method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\Agentic\\CheckoutSessionSchema\:\:is_valid_draft_order\(\) expects WC_Order, WC_Order\|WC_Order_Refund\|false given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
-
-
message: '#^Method Automattic\\WooCommerce\\StoreApi\\Schemas\\V1\\BillingAddressSchema\:\:sanitize_callback\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
identifier: missingType.generics
@@ -70038,162 +69678,6 @@ parameters:
count: 1
path: src/StoreApi/StoreApi.php
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:add_items_to_cart\(\) has invalid return type Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Errors\\Error\.$#'
- identifier: class.notFound
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:add_items_to_cart\(\) should return Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Errors\\Error\|null but returns Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Agentic\\Error\.$#'
- identifier: return.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:add_protocol_headers\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
- identifier: missingType.generics
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:clear_fulfillment_address\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:get_agentic_commerce_gateway\(\) should return WC_Payment_Gateway\|null but returns class\-string\|object\.$#'
- identifier: return.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:set_billing_address\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:set_buyer_data\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Method Automattic\\WooCommerce\\StoreApi\\Utilities\\AgenticCheckoutUtils\:\:set_fulfillment_address\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_address_1\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_address_2\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_city\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_country\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_first_name\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_last_name\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_phone\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_postcode\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_billing_state\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_address_1\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_address_2\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_city\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_country\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_first_name\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_last_name\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_postcode\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#1 \$value of method WC_Customer\:\:set_shipping_state\(\) expects string, array\|string given\.$#'
- identifier: argument.type
- count: 1
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
- -
- message: '#^Parameter \#2 \$str of function explode expects string, array\|string given\.$#'
- identifier: argument.type
- count: 2
- path: src/StoreApi/Utilities/AgenticCheckoutUtils.php
-
-
message: '#^@param int \$request_quantity does not accept actual type of parameter\: float\|int\.$#'
identifier: parameter.phpDocType
diff --git a/plugins/woocommerce/src/Enums/PaymentGatewayFeature.php b/plugins/woocommerce/src/Enums/PaymentGatewayFeature.php
index 42a05f60b0b..9b4996169f5 100644
--- a/plugins/woocommerce/src/Enums/PaymentGatewayFeature.php
+++ b/plugins/woocommerce/src/Enums/PaymentGatewayFeature.php
@@ -143,6 +143,9 @@ final class PaymentGatewayFeature {
/**
* Agentic Commerce feature.
+ *
+ * @deprecated 11.2.0 The Agentic Checkout API was removed from WooCommerce core; nothing reads this
+ * capability any more. Kept so gateways declaring it do not fatal on an undefined constant.
*/
public const AGENTIC_COMMERCE = 'agentic_commerce';
}
diff --git a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticController.php b/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticController.php
deleted file mode 100644
index 639a6c47190..00000000000
--- a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticController.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Admin\Agentic;
-
-use Automattic\Jetpack\Constants;
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;
-
-/**
- * AgenticController class
- *
- * Main controller for Agentic Commerce Protocol features.
- * Manages initialization of webhooks and future settings for the Agentic feature.
- *
- * @since 10.3.0
- */
-class AgenticController implements RegisterHooksInterface {
- /**
- * Register this class instance to the appropriate hooks.
- *
- * @internal
- */
- public function register() {
- // Don't register hooks during installation.
- if ( Constants::is_true( 'WC_INSTALLING' ) ) {
- return;
- }
-
- // We want to run on init for translations but before woocommerce_init so that
- // we can hook the new integration settings page. We should be able to simplify
- // this by just hooking here when we no longer need to check if the feature is enabled.
- add_action( 'before_woocommerce_init', array( $this, 'on_init' ) );
- }
-
- /**
- * Hook into WordPress on init.
- *
- * @internal
- */
- public function on_init() {
- // Bail if the feature is not enabled.
- if ( ! FeaturesUtil::feature_is_enabled( 'agentic_checkout' ) ) {
- return;
- }
-
- // Resolve webhook manager from container.
- wc_get_container()->get( AgenticWebhookManager::class )->register();
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookManager.php b/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookManager.php
deleted file mode 100644
index 19e03187721..00000000000
--- a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookManager.php
+++ /dev/null
@@ -1,299 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Admin\Agentic;
-
-use Automattic\WooCommerce\Enums\OrderStatus;
-use Automattic\WooCommerce\Internal\RegisterHooksInterface;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey;
-use WC_Order;
-use WC_Webhook;
-
-/**
- * AgenticWebhookManager class
- *
- * Integrates Agentic Commerce Protocol webhooks with WooCommerce's native webhook system.
- * Defines custom action topics and handles filtering/transformation for ACP compliance.
- *
- * @since 10.3.0
- */
-class AgenticWebhookManager implements RegisterHooksInterface {
- /**
- * Action that will be triggered for webhooks.
- *
- * @var string
- */
- const WEBHOOK_ACTION = 'woocommerce_agentic_order_changed';
-
- /**
- * Topic that will be used for webhooks.
- *
- * @var string
- */
- const WEBHOOK_TOPIC = 'action.' . self::WEBHOOK_ACTION;
-
- /**
- * Meta key to store if the first event has been delivered.
- *
- * @var string
- */
- const FIRST_EVENT_DELIVERED_META_KEY = '_acp_order_created_sent';
-
- /**
- * Payload builder instance.
- *
- * @var AgenticWebhookPayloadBuilder
- */
- private $payload_builder;
-
- /**
- * Initializes dependencies and hooks.
- *
- * @internal
- *
- * @param AgenticWebhookPayloadBuilder $payload_builder Payload builder instance.
- */
- final public function init( AgenticWebhookPayloadBuilder $payload_builder ) {
- $this->payload_builder = $payload_builder;
- }
-
- /**
- * Initialize hooks for webhook integration.
- *
- * @internal
- */
- public function register() {
-
- add_filter( 'woocommerce_webhook_topics', array( $this, 'register_webhook_topic_names' ) );
-
- // Hook into order lifecycle events to fire our custom actions.
- add_action( 'woocommerce_new_order', array( $this, 'handle_order_created' ), 999, 2 ); // Hook late to give a chance for other plugins to modify.
- add_action( 'woocommerce_order_status_changed', array( $this, 'handle_order_status_changed' ), 10, 4 );
- add_action( 'woocommerce_order_refunded', array( $this, 'handle_order_refunded' ), 10, 1 );
-
- // Customize webhook payload for our topics.
- add_filter( 'woocommerce_webhook_payload', array( $this, 'customize_webhook_payload' ), 10, 4 );
-
- // Customize webhook HTTP arguments for our topics.
- add_filter( 'woocommerce_webhook_http_args', array( $this, 'customize_webhook_http_args' ), 10, 3 );
-
- // When the webhook is delivered (or not), mark the first event as delivered.
- add_action( 'woocommerce_webhook_delivery', array( $this, 'mark_first_event_delivered' ), 10, 5 );
- }
-
- /**
- * Register webhook topic names for display in the UI.
- *
- * @param array $topics Existing topics.
- * @return array Modified topics.
- */
- public function register_webhook_topic_names( $topics ): array {
- $topics[ self::WEBHOOK_TOPIC ] = __( 'Agentic Commerce Protocol: Order created or updated', 'woocommerce' );
- return $topics;
- }
-
- /**
- * Handle order creation.
- *
- * @param int $order_id Order ID.
- * @param WC_Order $order Order object.
- */
- public function handle_order_created( $order_id, $order ) {
- if ( ! $this->should_trigger_webhook( $order ) ) {
- return;
- }
-
- /**
- * Fires when an Agentic order is updated or created.
- *
- * @since 10.3.0
- *
- * @param int $order_id Order ID.
- * @param WC_Order $order Order object.
- */
- do_action( self::WEBHOOK_ACTION, $order_id, $order );
- }
-
- /**
- * Handle order status changes.
- *
- * @param int $order_id Order ID.
- * @param string $old_status Old status.
- * @param string $new_status New status.
- * @param WC_Order $order Order object.
- */
- public function handle_order_status_changed( $order_id, $old_status, $new_status, $order ) {
- if ( ! $this->should_trigger_webhook( $order ) ) {
- return;
- }
-
- /**
- * Fires when an Agentic order status changes.
- *
- * @since 10.3.0
- *
- * @param int $order_id Order ID.
- * @param WC_Order $order Order object.
- */
- do_action( self::WEBHOOK_ACTION, $order_id, $order );
- }
-
- /**
- * Handle order refunds.
- *
- * @param int $order_id Order ID.
- */
- public function handle_order_refunded( $order_id ) {
- $order = wc_get_order( $order_id );
- if ( ! $order || ! $this->should_trigger_webhook( $order ) ) {
- return;
- }
-
- /**
- * Fires when an Agentic order is refunded.
- *
- * @since 10.3.0
- *
- * @param int $order_id Order ID.
- * @param WC_Order $order Order object.
- */
- do_action( self::WEBHOOK_ACTION, $order_id, $order );
- }
-
- /**
- * Check if webhook should be triggered for this order.
- *
- * @param WC_Order $order Order object.
- * @return bool True if webhook should be triggered.
- */
- private function should_trigger_webhook( $order ) {
- // Only trigger for orders with an Agentic checkout session ID.
- $checkout_session_id = $order->get_meta( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID );
- if ( empty( $checkout_session_id ) ) {
- return false;
- }
-
- // Don't trigger for draft orders.
- if (
- in_array(
- $order->get_status(),
- array(
- OrderStatus::CHECKOUT_DRAFT,
- OrderStatus::DRAFT,
- OrderStatus::AUTO_DRAFT,
- ),
- true
- )
- ) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Customize webhook payload for Agentic topics.
- *
- * @param array $payload Original payload.
- * @param string $resource_type Resource type.
- * @param int $resource_id Resource ID.
- * @param int $webhook_id Webhook ID.
- * @return array Modified payload.
- */
- public function customize_webhook_payload( $payload, $resource_type, $resource_id, $webhook_id ) {
- $webhook = wc_get_webhook( $webhook_id );
- if ( ! $webhook ) {
- return $payload;
- }
-
- $topic = $webhook->get_topic();
-
- // Check if this is one of our Agentic topics.
- if ( self::WEBHOOK_TOPIC !== $topic ) {
- return $payload;
- }
-
- // Get the order.
- $order = wc_get_order( $resource_id );
- if ( ! $order ) {
- return $payload;
- }
-
- $is_first_event = 'sent' !== $order->get_meta( self::FIRST_EVENT_DELIVERED_META_KEY );
- $event = $is_first_event ? 'order_create' : 'order_update';
-
- // Build ACP-compliant payload.
- return $this->payload_builder->build_payload( $event, $order );
- }
-
- /**
- * Customize webhook HTTP arguments for Agentic topics.
- *
- * @param array $http_args HTTP arguments.
- * @param mixed $arg First hook argument.
- * @param int $webhook_id Webhook ID.
- * @return array Modified HTTP arguments.
- */
- public function customize_webhook_http_args( $http_args, $arg, $webhook_id ) {
- $webhook = wc_get_webhook( $webhook_id );
- if ( ! $webhook ) {
- return $http_args;
- }
-
- $topic = $webhook->get_topic();
-
- // Check if this is one of our Agentic topics.
- if ( self::WEBHOOK_TOPIC !== $topic ) {
- return $http_args;
- }
-
- // Compute HMAC signature per ACP webhook spec using WooCommerce's built-in method.
- // The signature must be computed over the raw request body.
- if ( isset( $http_args['body'] ) && ! empty( $webhook->get_secret() ) ) {
- // Use WooCommerce's signature generation to ensure consistency.
- $signature = $webhook->generate_signature( $http_args['body'] );
-
- // Add Merchant-Signature header per ACP webhook specification.
- $http_args['headers']['Merchant-Signature'] = $signature;
- }
-
- return $http_args;
- }
-
- /**
- * Mark first event as delivered on successful webhook delivery.
- *
- * @param array $http_args HTTP request args.
- * @param mixed $response HTTP response.
- * @param float $duration Request duration.
- * @param int $arg First argument to the action (order_id).
- * @param int $webhook_id Webhook ID.
- */
- public function mark_first_event_delivered( $http_args, $response, $duration, $arg, $webhook_id ) {
- // Only proceed for successful responses.
- if ( is_wp_error( $response ) ) {
- return;
- }
- $code = wp_remote_retrieve_response_code( $response );
- if ( $code < 200 || $code >= 300 ) {
- return;
- }
-
- // Verify this is our webhook topic.
- $webhook = wc_get_webhook( $webhook_id );
- if ( ! $webhook || self::WEBHOOK_TOPIC !== $webhook->get_topic() ) {
- return;
- }
-
- // $arg contains the order_id from do_action( self::WEBHOOK_ACTION, $order_id, $order ).
- $order = wc_get_order( $arg );
- if ( ! $order ) {
- return;
- }
-
- if ( 'sent' !== $order->get_meta( self::FIRST_EVENT_DELIVERED_META_KEY ) ) {
- $order->update_meta_data( self::FIRST_EVENT_DELIVERED_META_KEY, 'sent' );
- $order->save();
- }
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilder.php b/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilder.php
deleted file mode 100644
index 1841ecfab66..00000000000
--- a/plugins/woocommerce/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilder.php
+++ /dev/null
@@ -1,179 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Internal\Admin\Agentic;
-
-use Automattic\WooCommerce\Enums\OrderStatus;
-use Automattic\WooCommerce\StoreApi\Formatters\MoneyFormatter;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\OrderStatus as ACPOrderStatus;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\RefundType;
-use WC_Logger_Interface;
-use WC_Order;
-use WC_Order_Refund;
-
-/**
- * AgenticWebhookPayloadBuilder class
- *
- * Builds webhook payloads for the Agentic Commerce Protocol following
- * the specification for order lifecycle events.
- *
- * @since 10.3.0
- */
-class AgenticWebhookPayloadBuilder {
- /**
- * Money formatter instance.
- *
- * @var MoneyFormatter
- */
- private $money_formatter;
-
- /**
- * Dependency initialization.
- *
- * @internal
- */
- final public function init() {
- $this->money_formatter = new MoneyFormatter();
- }
-
- /**
- * Build the webhook payload for an order event.
- *
- * @param string $event Event type ('order_create' or 'order_update').
- * @param WC_Order $order Order object.
- * @return array Webhook payload.
- */
- public function build_payload( string $event, WC_Order $order ): array {
- return array(
- 'type' => $event,
- 'data' => $this->build_order_data( $order ),
- );
- }
-
- /**
- * Build the order data for the webhook payload.
- *
- * @param WC_Order $order Order object.
- * @return array Order data.
- */
- private function build_order_data( WC_Order $order ): array {
- return array(
- 'type' => 'order',
- 'checkout_session_id' => $order->get_meta( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID ),
- 'permalink_url' => $order->get_checkout_order_received_url(),
- 'status' => $this->map_order_status( $order->get_status() ),
- 'refunds' => $this->build_refunds_data( $order ),
- );
- }
-
- /**
- * Map WooCommerce order status to ACP status.
- *
- * ACP statuses: created, manual_review, confirmed, canceled, shipped, fulfilled
- *
- * @param string $wc_status WooCommerce order status.
- * @return string ACP status.
- */
- private function map_order_status( string $wc_status ): string {
- $status_map = array(
- // WooCommerce status => ACP status.
- OrderStatus::PENDING => ACPOrderStatus::CREATED,
- OrderStatus::PROCESSING => ACPOrderStatus::CONFIRMED,
- OrderStatus::ON_HOLD => ACPOrderStatus::MANUAL_REVIEW,
- OrderStatus::COMPLETED => ACPOrderStatus::FULFILLED,
- OrderStatus::CANCELLED => ACPOrderStatus::CANCELED,
- OrderStatus::REFUNDED => ACPOrderStatus::FULFILLED, // Refunded orders are still fulfilled.
- OrderStatus::FAILED => ACPOrderStatus::CANCELED,
- );
-
- /**
- * Filter the WooCommerce to ACP order status mapping.
- *
- * Allows extensions to map custom WooCommerce order statuses to ACP order statuses.
- * The mapped status must be one of: created, manual_review, confirmed, canceled, shipped, fulfilled.
- *
- * @see Automattic\WooCommerce\Internal\Agentic\Enums\Specs\OrderStatus
- *
- * @since 10.3.0
- *
- * @param array $status_map Associative array of WooCommerce status => ACP status.
- * @param string $wc_status The WooCommerce order status being mapped.
- */
- $status_map = apply_filters( 'woocommerce_agentic_webhook_order_status_map', $status_map, $wc_status );
-
- // Get mapped status or default to 'created'.
- $mapped_status = isset( $status_map[ $wc_status ] ) ? $status_map[ $wc_status ] : ACPOrderStatus::CREATED;
-
- // Validate the mapped status is a valid ACP status.
- if ( ! ACPOrderStatus::is_valid( $mapped_status ) ) {
- // Log a warning for invalid status but continue with fallback.
- wc_get_logger()->warning(
- sprintf(
- 'Invalid ACP order status "%s" returned by woocommerce_agentic_webhook_order_status_map filter for WooCommerce status "%s". Using "created" as fallback.',
- $mapped_status,
- $wc_status
- ),
- array( 'source' => 'agentic-webhooks' )
- );
- return ACPOrderStatus::CREATED;
- }
-
- return $mapped_status;
- }
-
- /**
- * Build refunds data for the order.
- *
- * @param WC_Order $order Order object.
- * @return array Array of refunds.
- */
- private function build_refunds_data( WC_Order $order ): array {
- return array_map(
- array( $this, 'build_single_refund_data' ),
- $order->get_refunds()
- );
- }
-
- /**
- * Build data for a single refund.
- *
- * @param WC_Order_Refund $refund Refund object.
- * @return array Refund data.
- */
- private function build_single_refund_data( WC_Order_Refund $refund ): array {
- $refund_type = $this->determine_refund_type( $refund );
- $amount = abs( (float) $refund->get_total() ); // Get absolute value as refunds are negative.
-
- // Convert amount to minor units using MoneyFormatter (respects store currency decimals).
- $amount_in_minor_units = (int) $this->money_formatter->format( $amount );
-
- return array(
- 'type' => $refund_type,
- 'amount' => $amount_in_minor_units,
- );
- }
-
- /**
- * Determine the refund type.
- *
- * @param WC_Order_Refund $refund Refund object.
- * @return string Refund type ('store_credit' or 'original_payment').
- */
- private function determine_refund_type( WC_Order_Refund $refund ): string {
- // Default to original payment method.
- $refund_type = RefundType::ORIGINAL_PAYMENT;
-
- /**
- * Filter the refund type for Agentic webhooks.
- *
- * This allows extensions to specify when a refund is store credit.
- * By default, all refunds are assumed to be original payment method.
- *
- * @since 10.4.0
- * @param string $refund_type The refund type ('store_credit' or 'original_payment').
- * @param WC_Order_Refund $refund The refund object.
- */
- return apply_filters( 'woocommerce_agentic_webhook_refund_type', $refund_type, $refund );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/CheckoutSessionStatus.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/CheckoutSessionStatus.php
deleted file mode 100644
index 5b4b1608cb4..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/CheckoutSessionStatus.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Checkout session status values as defined in the Agentic Commerce Protocol.
- */
-class CheckoutSessionStatus {
- /**
- * Session is not ready for payment (missing required information).
- */
- const NOT_READY_FOR_PAYMENT = 'not_ready_for_payment';
-
- /**
- * Session is ready for payment.
- */
- const READY_FOR_PAYMENT = 'ready_for_payment';
-
- /**
- * Session has been completed (payment successful).
- */
- const COMPLETED = 'completed';
-
- /**
- * Session has been canceled.
- */
- const CANCELED = 'canceled';
-
- /**
- * Session is in progress (payment initiated but not complete).
- */
- const IN_PROGRESS = 'in_progress';
-
- /**
- * Allowed statuses for update operations.
- */
- const ALLOWED_STATUSES_FOR_UPDATE = array( self::NOT_READY_FOR_PAYMENT, self::READY_FOR_PAYMENT );
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorCode.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorCode.php
deleted file mode 100644
index f9aadcd2646..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorCode.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Error codes for message errors as defined in the Agentic Commerce Protocol.
- */
-class ErrorCode {
- /**
- * Required field is missing.
- */
- const MISSING = 'missing';
-
- /**
- * Field value is invalid.
- */
- const INVALID = 'invalid';
-
- /**
- * Product is out of stock.
- */
- const OUT_OF_STOCK = 'out_of_stock';
-
- /**
- * Payment was declined.
- */
- const PAYMENT_DECLINED = 'payment_declined';
-
- /**
- * User sign-in is required.
- */
- const REQUIRES_SIGN_IN = 'requires_sign_in';
-
- /**
- * 3D Secure authentication is required.
- */
- const REQUIRES_3DS = 'requires_3ds';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorType.php
deleted file mode 100644
index b2878650682..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/ErrorType.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Error types as defined in the Agentic Commerce Protocol.
- */
-class ErrorType {
- /**
- * Invalid request.
- */
- const INVALID_REQUEST = 'invalid_request';
-
- /**
- * Request not idempotent.
- */
- const REQUEST_NOT_IDEMPOTENT = 'request_not_idempotent';
-
- /**
- * Processing error.
- */
- const PROCESSING_ERROR = 'processing_error';
-
- /**
- * Service unavailable.
- */
- const SERVICE_UNAVAILABLE = 'service_unavailable';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/FulfillmentType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/FulfillmentType.php
deleted file mode 100644
index 357838e1526..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/FulfillmentType.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Fulfillment types as defined in the Agentic Commerce Protocol.
- */
-class FulfillmentType {
- /**
- * Physical shipping.
- */
- const SHIPPING = 'shipping';
-
- /**
- * Digital delivery.
- */
- const DIGITAL = 'digital';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/LinkType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/LinkType.php
deleted file mode 100644
index cbc24d8c50d..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/LinkType.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Link types as defined in the Agentic Commerce Protocol.
- */
-class LinkType {
- /**
- * Terms of use/service.
- */
- const TERMS_OF_USE = 'terms_of_use';
-
- /**
- * Privacy policy.
- */
- const PRIVACY_POLICY = 'privacy_policy';
-
- /**
- * Seller shop policies.
- */
- const SELLER_SHOP_POLICIES = 'seller_shop_policies';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageContentType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageContentType.php
deleted file mode 100644
index 3b2e0d51219..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageContentType.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Content types for messages as defined in the Agentic Commerce Protocol.
- */
-class MessageContentType {
- /**
- * Plain text content.
- */
- const PLAIN = 'plain';
-
- /**
- * Markdown formatted content.
- */
- const MARKDOWN = 'markdown';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageType.php
deleted file mode 100644
index 9cc2bec78a7..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/MessageType.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Message types as defined in the Agentic Commerce Protocol.
- */
-class MessageType {
- /**
- * Informational message.
- */
- const INFO = 'info';
-
- /**
- * Warning message (deprecated in favor of info).
- */
- const WARNING = 'warning';
-
- /**
- * Error message.
- */
- const ERROR = 'error';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/OrderStatus.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/OrderStatus.php
deleted file mode 100644
index bf4a7e60e18..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/OrderStatus.php
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Order status values as defined in the Agentic Commerce Protocol.
- *
- * @since 10.3.0
- */
-class OrderStatus {
- /**
- * Order has been created.
- */
- const CREATED = 'created';
-
- /**
- * Order requires manual review.
- */
- const MANUAL_REVIEW = 'manual_review';
-
- /**
- * Order has been confirmed.
- */
- const CONFIRMED = 'confirmed';
-
- /**
- * Order has been canceled.
- */
- const CANCELED = 'canceled';
-
- /**
- * Order has been shipped.
- */
- const SHIPPED = 'shipped';
-
- /**
- * Order has been fulfilled.
- */
- const FULFILLED = 'fulfilled';
-
- /**
- * Get all valid order statuses.
- *
- * @return array Array of valid order status values.
- */
- public static function get_all() {
- return array(
- self::CREATED,
- self::MANUAL_REVIEW,
- self::CONFIRMED,
- self::CANCELED,
- self::SHIPPED,
- self::FULFILLED,
- );
- }
-
- /**
- * Check if a status is valid.
- *
- * @param string $status Status to check.
- * @return bool True if valid, false otherwise.
- */
- public static function is_valid( $status ) {
- return in_array( $status, self::get_all(), true );
- }
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentMethod.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentMethod.php
deleted file mode 100644
index ba647985bdb..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentMethod.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Payment methods as defined in the Agentic Commerce Protocol.
- */
-class PaymentMethod {
- /**
- * Card payment method.
- */
- const CARD = 'card';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentProvider.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentProvider.php
deleted file mode 100644
index 5397b71ae40..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/PaymentProvider.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Payment provider identifiers as defined in the Agentic Commerce Protocol.
- */
-class PaymentProvider {
- /**
- * Stripe payment provider.
- */
- const STRIPE = 'stripe';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/RefundType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/RefundType.php
deleted file mode 100644
index d4dae0070d6..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/RefundType.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Refund types as defined in the Agentic Commerce Protocol.
- */
-class RefundType {
- /**
- * Refund to store credit.
- */
- const STORE_CREDIT = 'store_credit';
-
- /**
- * Refund to original payment method.
- */
- const ORIGINAL_PAYMENT = 'original_payment';
-}
diff --git a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/TotalType.php b/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/TotalType.php
deleted file mode 100644
index 36dc153cc4f..00000000000
--- a/plugins/woocommerce/src/Internal/Agentic/Enums/Specs/TotalType.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\Internal\Agentic\Enums\Specs;
-
-/**
- * Total types as defined in the Agentic Commerce Protocol.
- */
-class TotalType {
- /**
- * Base amount of all items before discounts.
- */
- const ITEMS_BASE_AMOUNT = 'items_base_amount';
-
- /**
- * Total discount on items.
- */
- const ITEMS_DISCOUNT = 'items_discount';
-
- /**
- * Subtotal after item discounts.
- */
- const SUBTOTAL = 'subtotal';
-
- /**
- * Additional discount applied to order.
- */
- const DISCOUNT = 'discount';
-
- /**
- * Fulfillment/shipping cost.
- */
- const FULFILLMENT = 'fulfillment';
-
- /**
- * Tax amount.
- */
- const TAX = 'tax';
-
- /**
- * Additional fee.
- */
- const FEE = 'fee';
-
- /**
- * Final total amount.
- */
- const TOTAL = 'total';
-}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index e067fb01c69..b8693dc6266 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -623,18 +623,6 @@ class FeaturesController {
'disable_ui' => false,
'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
),
- 'agentic_checkout' => array(
- 'name' => __( 'Agentic Checkout API', 'woocommerce' ),
- 'description' => __(
- 'Enable the Agentic Checkout API for AI-powered checkout experiences (e.g., ChatGPT). This adds REST API endpoints that allow AI agents to create and manage checkout sessions.',
- 'woocommerce'
- ),
- 'enabled_by_default' => false,
- 'is_experimental' => true,
- 'disable_ui' => true,
- 'skip_compatibility_checks' => true,
- 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
- ),
'dual_code_graphql_api' => array(
'name' => __( 'Dual Code & GraphQL API', 'woocommerce' ),
'description' => __(
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/AgenticCheckoutSession.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/AgenticCheckoutSession.php
deleted file mode 100644
index 97a9dfa5f29..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/AgenticCheckoutSession.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages\Messages;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use WC_Cart;
-
-/**
- * AgenticCheckoutSession class.
- *
- * Wrapper for all things, associated with an agentic checkout session.
- * This class manages the cart and error handling for agentic checkout processes.
- */
-final class AgenticCheckoutSession {
- /**
- * The WooCommerce cart instance.
- *
- * @var WC_Cart
- */
- private $cart;
-
- /**
- * Error messages handler for the checkout session.
- *
- * @var Messages
- */
- private $messages;
-
- /**
- * The checkout session ID.
- *
- * @var string
- */
- private $id;
-
- /**
- * Constructor.
- *
- * @param WC_Cart $cart The WooCommerce cart instance.
- */
- public function __construct( WC_Cart $cart ) {
- $this->cart = $cart;
- $this->messages = new Messages();
- $this->id = $this->get_or_set_checkout_session_id();
- }
-
- /**
- * Gets the cart instance.
- *
- * @return WC_Cart The WooCommerce cart instance.
- */
- public function get_cart(): WC_Cart {
- return $this->cart;
- }
-
- /**
- * Gets the messages collection.
- *
- * @return Messages The messages handler instance.
- */
- public function get_messages(): Messages {
- return $this->messages;
- }
-
- /**
- * Gets the checkout session ID.
- *
- * @return string The checkout session ID.
- */
- public function get_id(): string {
- return $this->id;
- }
-
- /**
- * Get the checkout session ID. If it does not exist, generate a cart token for it and save to the current session.
- *
- * @return string Checkout Session ID stored in the current session.
- */
- private function get_or_set_checkout_session_id(): string {
- $wc_session = WC()->session;
- if ( null === $wc_session ) {
- return '';
- }
-
- $session_id = $wc_session->get( SessionKey::AGENTIC_CHECKOUT_SESSION_ID );
- if ( null === $session_id ) {
- $session_id = CartTokenUtils::get_cart_token( (string) $wc_session->get_customer_id() );
- $wc_session->set( SessionKey::AGENTIC_CHECKOUT_SESSION_ID, $session_id );
- }
-
- return $session_id;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
deleted file mode 100644
index c640c1729c5..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessions.php
+++ /dev/null
@@ -1,178 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Error;
-use Automattic\WooCommerce\StoreApi\SchemaController;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\Agentic\CheckoutSessionSchema;
-use Automattic\WooCommerce\StoreApi\Utilities\CartController;
-use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
-use Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\AgenticCheckoutSession;
-
-/**
- * CheckoutSessions class.
- *
- * Handles the Agentic Checkout API checkout sessions endpoint.
- * This endpoint allows AI agents to create and manage checkout sessions.
- */
-class CheckoutSessions extends AbstractCartRoute {
- /**
- * The route identifier.
- *
- * @var string
- */
- const IDENTIFIER = 'agentic-checkout-sessions';
-
- /**
- * The route's schema type.
- *
- * @var string
- */
- const SCHEMA_TYPE = CheckoutSessionSchema::IDENTIFIER;
-
- /**
- * Cart controller for managing cart operations.
- *
- * @var CartController
- */
- protected $cart_controller;
-
- /**
- * Constructor.
- *
- * @param SchemaController $schema_controller Schema Controller instance.
- * @param AbstractSchema $schema Schema class instance.
- */
- public function __construct( $schema_controller, $schema ) {
- parent::__construct( $schema_controller, $schema );
- $this->order_controller = new OrderController();
- $this->cart_controller = new CartController();
- }
-
- /**
- * Get the path of this REST route.
- *
- * @return string
- */
- public function get_path() {
- return $this->get_path_regex();
- }
-
- /**
- * Get the path regex for this REST route.
- *
- * @return string
- */
- public static function get_path_regex() {
- return '/checkout_sessions';
- }
-
- /**
- * Get method arguments for this REST route.
- *
- * @return array An array of endpoints.
- */
- public function get_args() {
- return [
- [
- 'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => [ $this, 'get_response' ],
- 'permission_callback' => [ $this, 'is_authorized' ],
- 'args' => $this->get_create_params(),
- ],
- 'schema' => [ $this->schema, 'get_public_item_schema' ],
- ];
- }
-
- /**
- * Get the parameters for creating a checkout session.
- *
- * @return array Parameters array.
- */
- protected function get_create_params() {
- $params = AgenticCheckoutUtils::get_shared_params();
- $params['items'] = array_merge(
- $params['items'],
- [
- 'required' => true,
- 'minItems' => 1,
- ]
- );
- return $params;
- }
-
- /**
- * Check if the request is authorized.
- *
- * Validates that the request is signed with Jetpack blog token.
- *
- * @return bool|\WP_Error True if authorized, WP_Error otherwise.
- */
- public function is_authorized() {
- return AgenticCheckoutUtils::validate_jetpack_request();
- }
-
- /**
- * Check if a nonce is required for the route.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool False, Jetpack blog token auth used instead.
- */
- protected function requires_nonce( \WP_REST_Request $request ) {
- // Uses Jetpack blog token authentication via is_authorized().
- return false;
- }
-
- /**
- * Handle the request and return a valid response for this endpoint.
- *
- * @param \WP_REST_Request $request Request object.
- * @return \WP_REST_Response
- */
- protected function get_route_post_response( \WP_REST_Request $request ) {
- $checkout_session = new AgenticCheckoutSession( $this->cart_controller->get_cart_instance() );
-
- // Clear existing cart to start fresh for POST requests.
- $this->cart_controller->empty_cart();
-
- // Add items to cart.
- $items = $request->get_param( 'items' );
- $error = AgenticCheckoutUtils::add_items_to_cart( $items, $this->cart_controller, $checkout_session->get_messages() );
- // Halt for critical errors.
- if ( $error instanceof Error ) {
- return $error->to_rest_response();
- }
-
- // Set buyer information.
- $buyer = $request->get_param( 'buyer' );
- if ( $buyer ) {
- AgenticCheckoutUtils::set_buyer_data( $buyer, WC()->customer );
- }
-
- // Set fulfillment address.
- $address = $request->get_param( 'fulfillment_address' );
- if ( $address ) {
- AgenticCheckoutUtils::set_fulfillment_address( $address, WC()->customer );
- } else {
- // Clear address when not provided (POST creates fresh session).
- AgenticCheckoutUtils::clear_fulfillment_address( WC()->customer );
- }
-
- // Calculate totals.
- try {
- $this->cart_controller->calculate_totals();
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::processing_error( 'totals_calculation_error', $message )->to_rest_response();
- }
-
- // Build response from canonical cart schema.
- $response = $this->schema->get_item_response( $checkout_session );
-
- // Add protocol headers.
- return AgenticCheckoutUtils::add_protocol_headers( rest_ensure_response( $response ), $request );
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
deleted file mode 100644
index 2fa8230efc8..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsComplete.php
+++ /dev/null
@@ -1,415 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorCode;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\CheckoutSessionStatus;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Error;
-use Automattic\WooCommerce\StoreApi\SchemaController;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\Agentic\CheckoutSessionSchema;
-use Automattic\WooCommerce\StoreApi\Utilities\CartController;
-use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
-use Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
-use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
-use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
-
-/**
- * CheckoutSessionsComplete class.
- *
- * Handles the Agentic Checkout API checkout sessions complete endpoint.
- * This endpoint allows AI agents to complete checkout sessions with payment.
- */
-class CheckoutSessionsComplete extends AbstractCartRoute {
- use CheckoutTrait;
-
- /**
- * The route identifier.
- *
- * @var string
- */
- const IDENTIFIER = 'agentic-checkout-sessions-complete';
-
- /**
- * The route's schema type.
- *
- * @var string
- */
- const SCHEMA_TYPE = CheckoutSessionSchema::IDENTIFIER;
-
- /**
- * Order controller for managing orders.
- *
- * @var OrderController
- */
- protected $order_controller;
-
- /**
- * Cart controller for managing cart operations.
- *
- * @var CartController
- */
- protected $cart_controller;
-
- /**
- * The order object for the current request.
- *
- * @var \WC_Order|null
- */
- protected $order;
-
- /**
- * Constructor.
- *
- * @param SchemaController $schema_controller Schema Controller instance.
- * @param AbstractSchema $schema Schema class instance.
- */
- public function __construct( $schema_controller, $schema ) {
- parent::__construct( $schema_controller, $schema );
- $this->order_controller = new OrderController();
- $this->cart_controller = new CartController();
- }
-
- /**
- * Get the path of this REST route.
- *
- * @return string
- */
- public function get_path() {
- return self::get_path_regex();
- }
-
- /**
- * Get the path regex for this REST route.
- *
- * @return string
- */
- public static function get_path_regex() {
- return '/checkout_sessions/(?P<checkout_session_id>[a-zA-Z0-9._-]+)/complete';
- }
-
- /**
- * Get method arguments for this REST route.
- *
- * @return array An array of endpoints.
- */
- public function get_args() {
- return [
- 'args' => [
- 'checkout_session_id' => [
- 'description' => __( 'The checkout session ID (Cart-Token JWT).', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- [
- 'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => [ $this, 'get_response' ],
- 'permission_callback' => [ $this, 'is_authorized' ],
- 'args' => $this->get_complete_params(),
- ],
- 'schema' => [ $this->schema, 'get_public_item_schema' ],
- ];
- }
-
- /**
- * Get the parameters for completing a checkout session.
- *
- * @return array Parameters array.
- */
- protected function get_complete_params() {
- $shared_params = AgenticCheckoutUtils::get_shared_params();
-
- return [
- 'buyer' => $shared_params['buyer'],
- 'payment_data' => [
- 'description' => __( 'Payment data including token and provider.', 'woocommerce' ),
- 'type' => 'object',
- 'properties' => [
- 'token' => [
- 'description' => __( 'Payment token from the payment provider.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'provider' => [
- 'description' => __( 'Payment provider identifier.', 'woocommerce' ),
- 'type' => 'string',
- 'enum' => [ 'stripe' ],
- ],
- 'billing_address' => $shared_params['fulfillment_address'],
- ],
- 'required' => [ 'token', 'provider' ],
- ],
- ];
- }
-
- /**
- * Check if the request is authorized.
- *
- * Validates Jetpack blog token and cart token validity.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool|\WP_Error True if authorized, WP_Error otherwise.
- */
- public function is_authorized( \WP_REST_Request $request ) {
- // Check Jetpack blog token authentication.
- $auth_check = AgenticCheckoutUtils::validate_jetpack_request();
- if ( is_wp_error( $auth_check ) ) {
- return $auth_check;
- }
-
- // Additional check for cart token validity.
- if ( ! $this->has_cart_token( $request ) ) {
- return new \WP_Error(
- 'woocommerce_rest_invalid_checkout_session',
- __( 'Invalid or expired checkout session ID.', 'woocommerce' ),
- array( 'status' => 404 )
- );
- }
-
- return true;
- }
-
- /**
- * Use the checkout_session_id as Cart-Token, and set the respective values to HTTP header and request.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool|null
- */
- protected function has_cart_token( \WP_REST_Request $request ) {
- $session_id = $request->get_param( 'checkout_session_id' );
- if ( is_null( $this->has_cart_token ) ) {
- $this->has_cart_token = CartTokenUtils::validate_cart_token( $session_id );
- }
-
- // This allows the session will be loaded later without any further intervention.
- if ( true === $this->has_cart_token ) {
- $request->set_header( 'Cart-Token', $session_id );
- // phpcs:ignore WooCommerceStoreApi.StoreApi.CartTokenSource.ServerSuperglobalWrite -- $session_id was validated above; this keeps the consumed token in sync with the validated one.
- $_SERVER['HTTP_CART_TOKEN'] = $session_id;
- }
-
- return $this->has_cart_token;
- }
-
- /**
- * Check if a nonce is required for the route.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool False, Jetpack blog token auth used instead.
- */
- protected function requires_nonce( \WP_REST_Request $request ) {
- // Uses Jetpack blog token authentication via is_authorized().
- return false;
- }
-
- /**
- * Handle the request and return a valid response for this endpoint.
- *
- * @param \WP_REST_Request $request Request object.
- * @return \WP_REST_Response|\WP_Error
- */
- protected function get_route_post_response( \WP_REST_Request $request ) {
- $checkout_session = new AgenticCheckoutSession( $this->cart_controller->get_cart_instance() );
-
- AgenticCheckoutUtils::validate( $checkout_session );
-
- /**
- * Verify checkout session is ready for payment.
- */
- $current_status = AgenticCheckoutUtils::calculate_status( $checkout_session );
- if ( CheckoutSessionStatus::READY_FOR_PAYMENT !== $current_status ) {
- $message = sprintf(
- /* translators: %s: current session status */
- __( 'Checkout session is not ready for payment. Current status: %s', 'woocommerce' ),
- $current_status
- );
- return Error::invalid_request( ErrorCode::INVALID, $message )->to_rest_response();
- }
-
- /**
- * Set buyer data if exists.
- */
- $buyer = $request->get_param( 'buyer' );
- if ( null !== $buyer ) {
- AgenticCheckoutUtils::set_buyer_data( $buyer, WC()->customer );
- }
-
- /**
- * Set billing address from payment_data if provided.
- */
- $payment_data = $request->get_param( 'payment_data' );
- if ( isset( $payment_data['billing_address'] ) ) {
- AgenticCheckoutUtils::set_billing_address( $payment_data['billing_address'], WC()->customer );
- }
-
- try {
- /**
- * Before triggering validation, ensure totals are current and in turn, things such as shipping costs are present.
- * This is so plugins that validate other cart data (e.g. conditional shipping and payments) can access this data.
- */
- $this->cart_controller->calculate_totals();
-
- /**
- * Validate that the cart is not empty.
- */
- $this->cart_controller->validate_cart_not_empty();
-
- /**
- * Validate items and fix violations before the order is processed.
- */
- $this->cart_controller->validate_cart();
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::processing_error( ErrorCode::INVALID, $message )->to_rest_response();
- }
-
- /**
- * Similar to Checkout::create_or_update_draft_order.
- * Can move this to CheckoutTrait to share between Checkout.php and this controller.
- */
- $this->order = $this->get_draft_order();
- if ( ! $this->order ) {
- $this->order = $this->order_controller->create_order_from_cart();
- } else {
- $this->order_controller->update_order_from_cart( $this->order, true );
- }
-
- /**
- * Stores the checkout session ID to the order meta.
- */
- $this->order->update_meta_data( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID, $request->get_param( 'checkout_session_id' ) );
- $this->order->save_meta_data();
-
- /**
- * Validate updated order before payment is attempted.
- */
- try {
- $this->order_controller->validate_order_before_payment( $this->order );
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::invalid_request( ErrorCode::INVALID, $message )->to_rest_response();
- }
-
- try {
- wc_reserve_stock_for_order( $this->order );
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::invalid_request( ErrorCode::INVALID, $message )->to_rest_response();
- }
-
- // Set the order status to 'pending' as an initial step.
- $this->order->update_status( 'pending' );
-
- /**
- * Process payment (reuse CheckoutTrait).
- */
- $payment_result = new PaymentResult();
-
- try {
- /**
- * Set IN_PROGRESS status to prevent concurrent payment attempts.
- * Save this status right away so that any concurrent request will not be able to access the payment process.
- */
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, true );
- WC()->session->save_data();
-
- $this->process_payment( $request, $payment_result );
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::processing_error( ErrorCode::INVALID, $message )->to_rest_response();
- } finally {
- /**
- * Clear IN_PROGRESS status after payment attempt.
- * Do not save session here as it will be done after the shutdown.
- */
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, false );
- }
-
- /**
- * If payment failed, return error.
- */
- if ( 'failure' === $payment_result->status || 'error' === $payment_result->status ) {
- // Clear IN_PROGRESS status to allow retry.
- $message = $payment_result->message ?? __( 'Payment was declined.', 'woocommerce' );
- $message = wp_specialchars_decode( $message, ENT_QUOTES );
- return Error::processing_error( ErrorCode::PAYMENT_DECLINED, $message )->to_rest_response();
- }
-
- /**
- * Store the completed order ID into the session. This will prevent new orders in this session.
- */
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID, $this->order->get_id() );
-
- /**
- * Build response from canonical cart schema.
- */
- $response_data = $this->schema->get_item_response( $checkout_session );
- $response = rest_ensure_response( $response_data );
-
- return AgenticCheckoutUtils::add_protocol_headers( $response, $request );
- }
-
- /**
- * Gets and formats payment request data for CheckoutTrait.
- *
- * Transforms agentic payment_data format to Store API format.
- *
- * @param \WP_REST_Request $request Request object.
- * @return array
- */
- private function get_request_payment_data( \WP_REST_Request $request ) {
- $payment_data = [];
- $agentic_data = $request->get_param( 'payment_data' );
-
- if ( ! $agentic_data ) {
- return $payment_data;
- }
-
- // Transform agentic format to Store API payment_data format.
- if ( isset( $agentic_data['token'] ) ) {
- $payment_data['wc-agentic_commerce-token'] = wc_clean( $agentic_data['token'] );
- }
-
- if ( isset( $agentic_data['provider'] ) ) {
- $payment_data['wc-agentic_commerce-provider'] = wc_clean( $agentic_data['provider'] );
- }
-
- return $payment_data;
- }
-
- /**
- * Gets the chosen payment method (gateway) ID for CheckoutTrait.
- *
- * @param \WP_REST_Request $request Request object.
- * @return string
- * @throws RouteException If no payment gateway is available.
- */
- private function get_request_payment_method_id( \WP_REST_Request $request ) {
- $available_gateways = WC()->payment_gateways()->get_available_payment_gateways();
-
- if ( empty( $available_gateways ) ) {
- throw new RouteException(
- 'woocommerce_checkout_session_no_payment_gateway_available',
- esc_html__( 'No payment gateway available.', 'woocommerce' ),
- 400
- );
- }
-
- // Look for gateway with agentic_commerce capability.
- $gateway = AgenticCheckoutUtils::get_agentic_commerce_gateway( $available_gateways );
-
- if ( null === $gateway ) {
- throw new RouteException(
- 'woocommerce_checkout_session_no_agentic_payment_gateway_available',
- esc_html__( 'No agentic-supported payment gateway available.', 'woocommerce' ),
- 400
- );
- }
-
- return $gateway->id;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
deleted file mode 100644
index 22512e3c451..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/CheckoutSessionsUpdate.php
+++ /dev/null
@@ -1,244 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractCartRoute;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\CheckoutSessionStatus;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorCode;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Error;
-use Automattic\WooCommerce\StoreApi\SchemaController;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\Agentic\CheckoutSessionSchema;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\AgenticCheckoutSession;
-use Automattic\WooCommerce\StoreApi\Utilities\CartController;
-use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
-use Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils;
-
-/**
- * CheckoutSessionsUpdate class.
- *
- * Handles the Agentic Checkout API checkout sessions update endpoint.
- * This endpoint allows AI agents to update existing checkout sessions.
- */
-class CheckoutSessionsUpdate extends AbstractCartRoute {
- /**
- * The route identifier.
- *
- * @var string
- */
- const IDENTIFIER = 'agentic-checkout-sessions-update';
-
- /**
- * The route's schema type.
- *
- * @var string
- */
- const SCHEMA_TYPE = CheckoutSessionSchema::IDENTIFIER;
-
- /**
- * Cart controller for managing cart operations.
- *
- * @var CartController
- */
- protected $cart_controller;
-
- /**
- * Constructor.
- *
- * @param SchemaController $schema_controller Schema Controller instance.
- * @param AbstractSchema $schema Schema class instance.
- */
- public function __construct( $schema_controller, $schema ) {
- parent::__construct( $schema_controller, $schema );
- $this->order_controller = new OrderController();
- $this->cart_controller = new CartController();
- }
-
- /**
- * Get the path of this REST route.
- *
- * @return string
- */
- public function get_path() {
- return self::get_path_regex();
- }
-
- /**
- * Get the path regex for this REST route.
- *
- * @return string
- */
- public static function get_path_regex() {
- return '/checkout_sessions/(?P<checkout_session_id>[a-zA-Z0-9._-]+)';
- }
-
- /**
- * Get method arguments for this REST route.
- *
- * @return array An array of endpoints.
- */
- public function get_args() {
- return [
- 'args' => [
- 'checkout_session_id' => [
- 'description' => __( 'The checkout session ID (Cart-Token JWT).', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- [
- 'methods' => \WP_REST_Server::CREATABLE,
- 'callback' => [ $this, 'get_response' ],
- 'permission_callback' => [ $this, 'is_authorized' ],
- 'args' => $this->get_update_params(),
- ],
- 'schema' => [ $this->schema, 'get_public_item_schema' ],
- ];
- }
-
- /**
- * Get the parameters for updating a checkout session.
- *
- * @return array Parameters array.
- */
- protected function get_update_params() {
- $params = AgenticCheckoutUtils::get_shared_params();
-
- $params['fulfillment_option_id'] = [
- 'description' => __( 'Selected fulfillment option ID.', 'woocommerce' ),
- 'type' => 'string',
- ];
-
- return $params;
- }
-
- /**
- * Check if the request is authorized.
- *
- * Validates Jetpack blog token and cart token validity.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool|\WP_Error True if authorized, WP_Error otherwise.
- */
- public function is_authorized( \WP_REST_Request $request ) {
- // Check Jetpack blog token authentication.
- $auth_check = AgenticCheckoutUtils::validate_jetpack_request();
- if ( is_wp_error( $auth_check ) ) {
- return $auth_check;
- }
-
- // Additional check for cart token validity.
- if ( ! $this->has_cart_token( $request ) ) {
- return new \WP_Error(
- 'woocommerce_rest_invalid_checkout_session',
- __( 'Invalid or expired checkout session ID.', 'woocommerce' ),
- array( 'status' => 404 )
- );
- }
-
- return true;
- }
-
- /**
- * Use the checkout_session_id as Cart-Token, and set the respective values to HTTP header and request.
- *
- * @param \WP_REST_Request $request Request object.
- * @return bool|null
- */
- protected function has_cart_token( \WP_REST_Request $request ) {
- $session_id = $request->get_param( 'checkout_session_id' );
- if ( is_null( $this->has_cart_token ) ) {
- $this->has_cart_token = CartTokenUtils::validate_cart_token( $session_id );
- }
-
- // This allows the session will be loaded later without any further intervention.
- if ( true === $this->has_cart_token ) {
- $request->set_header( 'Cart-Token', $session_id );
- // phpcs:ignore WooCommerceStoreApi.StoreApi.CartTokenSource.ServerSuperglobalWrite -- $session_id was validated above; this keeps the consumed token in sync with the validated one.
- $_SERVER['HTTP_CART_TOKEN'] = $session_id;
- }
-
- return $this->has_cart_token;
- }
-
- /**
- * Handle the request and return a valid response for this endpoint.
- *
- * @param \WP_REST_Request $request Request object.
- * @return \WP_REST_Response|\WP_Error
- */
- protected function get_route_post_response( \WP_REST_Request $request ) {
- $cart = $this->cart_controller->get_cart_instance();
- $checkout_session = new AgenticCheckoutSession( $cart );
-
- $current_status = AgenticCheckoutUtils::calculate_status( $checkout_session );
- if ( ! in_array( $current_status, CheckoutSessionStatus::ALLOWED_STATUSES_FOR_UPDATE, true ) ) {
- $allowed_statuses = implode( ', ', CheckoutSessionStatus::ALLOWED_STATUSES_FOR_UPDATE );
- $message = sprintf(
- /* translators: 1: current session status, 2: allowed statuses */
- __( 'Checkout session cannot be updated. Current status: %1$s. Allowed statuses: %2$s', 'woocommerce' ),
- $current_status,
- $allowed_statuses
- );
- return Error::invalid_request( ErrorCode::INVALID, $message )->to_rest_response();
- }
-
- // Update items if provided.
- $items = $request->get_param( 'items' );
- if ( null !== $items ) {
- // Clear existing cart items and replace with new ones.
- $this->cart_controller->empty_cart();
-
- $error = AgenticCheckoutUtils::add_items_to_cart(
- $items,
- $this->cart_controller,
- $checkout_session->get_messages()
- );
- if ( $error instanceof Error ) {
- return $error->to_rest_response();
- }
- }
-
- // Update buyer information if provided.
- $buyer = $request->get_param( 'buyer' );
- if ( null !== $buyer ) {
- AgenticCheckoutUtils::set_buyer_data( $buyer, WC()->customer );
- }
-
- // Update fulfillment address if provided.
- $address = $request->get_param( 'fulfillment_address' );
- if ( null !== $address ) {
- AgenticCheckoutUtils::set_fulfillment_address( $address, WC()->customer );
- }
-
- // Update selected shipping method if provided.
- $fulfillment_option_id = $request->get_param( 'fulfillment_option_id' );
- if ( null !== $fulfillment_option_id ) {
- $option_id = wc_clean( (string) $fulfillment_option_id );
- $packages = WC()->shipping()->get_packages();
- foreach ( $packages as $package ) {
- foreach ( (array) ( $package['rates'] ?? array() ) as $rate ) {
- if ( $rate->get_id() === $option_id ) {
- WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, array( $option_id ) );
- break 2;
- }
- }
- }
- }
-
- // Calculate totals after all updates.
- try {
- $this->cart_controller->calculate_totals();
- } catch ( \Exception $e ) {
- $message = wp_specialchars_decode( $e->getMessage(), ENT_QUOTES );
- return Error::processing_error( 'totals_calculation_error', $message )->to_rest_response();
- }
-
- // Build response from canonical cart schema.
- $response = $this->schema->get_item_response( $checkout_session );
-
- // Add protocol headers.
- return AgenticCheckoutUtils::add_protocol_headers( rest_ensure_response( $response ), $request );
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/OrderMetaKey.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/OrderMetaKey.php
deleted file mode 100644
index 24ee5e6ff78..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/OrderMetaKey.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums;
-
-/**
- * Order meta keys used in Agentic Checkout.
- */
-class OrderMetaKey {
- /**
- * Agentic checkout session ID for this order.
- */
- const AGENTIC_CHECKOUT_SESSION_ID = '_agentic_checkout_session_id';
-
- /**
- * Meta key for canceled checkout order.
- */
- const AGENTIC_CHECKOUT_CANCELED = '_agentic_checkout_canceled';
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/SessionKey.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/SessionKey.php
deleted file mode 100644
index e4b72d37f17..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Enums/SessionKey.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums;
-
-/**
- * Session keys used in Agentic Checkout.
- */
-class SessionKey {
- /**
- * Chosen shipping methods. This is not specific to Agentic Checkout.
- */
- const CHOSEN_SHIPPING_METHODS = 'chosen_shipping_methods';
-
- /**
- * Agentic session ID stored in WC session.
- */
- const AGENTIC_CHECKOUT_SESSION_ID = 'agentic_checkout_session_id';
-
- /**
- * Completed order ID.
- */
- const AGENTIC_CHECKOUT_COMPLETED_ORDER_ID = 'agentic_checkout_completed_order_id';
-
- /**
- * Whether payment is in progress.
- */
- const AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS = 'agentic_checkout_payment_in_progress';
-
- /**
- * Provider ID that authenticated the request.
- */
- const AGENTIC_CHECKOUT_PROVIDER_ID = 'agentic_checkout_provider_id';
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Error.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Error.php
deleted file mode 100644
index 684a19591e6..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Error.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic;
-
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorType;
-use WP_REST_Response;
-
-/**
- * Error class.
- *
- * Represents an error object as defined in the Agentic Commerce Protocol.
- * This class handles API-level errors with type, code, message, and optional param.
- */
-class Error {
- /**
- * The error type.
- *
- * @var string
- */
- private $type;
-
- /**
- * Implementation-defined error code.
- *
- * @var string
- */
- private $code;
-
- /**
- * Human-readable error message.
- *
- * @var string
- */
- private $message;
-
- /**
- * RFC 9535 JSONPath to the problematic parameter (optional).
- *
- * @var string|null
- */
- private $param;
-
- /**
- * Constructor.
- *
- * @param string $type Error type from ErrorType enum.
- * @param string $code Implementation-defined error code.
- * @param string $message Human-readable error message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- */
- private function __construct( $type, $code, $message, $param = null ) {
- $this->type = $type;
- $this->code = $code;
- $this->message = $message;
- $this->param = $param;
- }
-
- /**
- * Create an invalid request error.
- *
- * @param string $code Implementation-defined error code.
- * @param string $message Human-readable error message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return Error
- */
- public static function invalid_request( $code, $message, $param = null ) {
- return new self( ErrorType::INVALID_REQUEST, $code, $message, $param );
- }
-
- /**
- * Create a request not idempotent error.
- *
- * @param string $code Implementation-defined error code.
- * @param string $message Human-readable error message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return Error
- */
- public static function request_not_idempotent( $code, $message, $param = null ) {
- return new self( ErrorType::REQUEST_NOT_IDEMPOTENT, $code, $message, $param );
- }
-
- /**
- * Create a processing error.
- *
- * @param string $code Implementation-defined error code.
- * @param string $message Human-readable error message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return Error
- */
- public static function processing_error( $code, $message, $param = null ) {
- return new self( ErrorType::PROCESSING_ERROR, $code, $message, $param );
- }
-
- /**
- * Create a service unavailable error.
- *
- * @param string $code Implementation-defined error code.
- * @param string $message Human-readable error message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return Error
- */
- public static function service_unavailable( $code, $message, $param = null ) {
- return new self( ErrorType::SERVICE_UNAVAILABLE, $code, $message, $param );
- }
-
- /**
- * Convert the error to a WP_REST_Response.
- *
- * @return WP_REST_Response WordPress REST API response object
- */
- public function to_rest_response() {
- $data = array(
- 'type' => $this->type,
- 'code' => $this->code,
- 'message' => $this->message,
- );
-
- if ( null !== $this->param ) {
- $data['param'] = $this->param;
- }
-
- $status_code = $this->get_http_status_code();
-
- return new WP_REST_Response( $data, $status_code );
- }
-
- /**
- * Determine HTTP status code based on error type.
- *
- * @return int HTTP status code
- */
- private function get_http_status_code() {
- switch ( $this->type ) {
- case ErrorType::INVALID_REQUEST:
- return 400;
- case ErrorType::REQUEST_NOT_IDEMPOTENT:
- return 409;
- case ErrorType::PROCESSING_ERROR:
- return 500;
- case ErrorType::SERVICE_UNAVAILABLE:
- return 503;
- default:
- return 500;
- }
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Message.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Message.php
deleted file mode 100644
index 09d3d5232f4..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Message.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages;
-
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\MessageContentType;
-
-/**
- * Base class for error and info messages.
- */
-abstract class Message {
- /**
- * Content type for the error message.
- *
- * Defaults to plain, but could also be markdown.
- *
- * @var string
- */
- protected $content_type = MessageContentType::PLAIN;
-
- /**
- * Error content/message.
- *
- * @var string
- */
- protected $content;
-
- /**
- * RFC 9535 JSONPath to the problematic parameter (optional).
- *
- * @var string|null
- */
- protected $param;
-
- /**
- * Check if the message is an error.
- *
- * @return bool True if the message is an error, false otherwise.
- */
- abstract public function is_error(): bool;
-
- /**
- * Convert the message to an array.
- *
- * @return array A message for the `messages` array of the response.
- */
- abstract public function to_array(): array;
-
- /**
- * Use markdown content type for the content of the error.
- */
- public function use_markdown() {
- $this->content_type = MessageContentType::MARKDOWN;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageError.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageError.php
deleted file mode 100644
index f2cd6a9120f..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageError.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages;
-
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorCode;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\MessageType;
-
-/**
- * MessageError class.
- *
- * Represents a message error object as defined in the Agentic Commerce Protocol.
- * This class handles message-level errors with type, code, content_type, content, and optional param.
- */
-class MessageError extends Message {
- /**
- * The error type (always 'error' for message errors).
- *
- * @var string
- */
- private $type = MessageType::ERROR;
-
- /**
- * Error code from ErrorCode enum.
- *
- * @var string
- */
- private $code;
-
- /**
- * Constructor.
- *
- * @param string $code Error code from ErrorCode enum.
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- */
- public function __construct( string $code, string $content, ?string $param = null ) {
- $this->code = $code;
- $this->content = $content;
- $this->param = $param;
- }
-
- /**
- * Create a missing field error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function missing( $content, $param = null ) {
- return new self( ErrorCode::MISSING, $content, $param );
- }
-
- /**
- * Create an invalid field error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function invalid( $content, $param = null ) {
- return new self( ErrorCode::INVALID, $content, $param );
- }
-
- /**
- * Create an out of stock error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function out_of_stock( $content, $param = null ) {
- return new self( ErrorCode::OUT_OF_STOCK, $content, $param );
- }
-
- /**
- * Create a payment declined error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function payment_declined( $content, $param = null ) {
- return new self( ErrorCode::PAYMENT_DECLINED, $content, $param );
- }
-
- /**
- * Create a requires sign in error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function requires_sign_in( $content, $param = null ) {
- return new self( ErrorCode::REQUIRES_SIGN_IN, $content, $param );
- }
-
- /**
- * Create a requires 3DS error.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- * @return MessageError
- */
- public static function requires_3ds( $content, $param = null ) {
- return new self( ErrorCode::REQUIRES_3DS, $content, $param );
- }
-
- /**
- * Check if the message is an error.
- *
- * @return bool True if the message is an error, false otherwise.
- */
- public function is_error(): bool {
- return true;
- }
-
- /**
- * Convert the error to an array.
- *
- * @return array A message for the `messages` array of the response.
- */
- public function to_array(): array {
- $data = array(
- 'type' => $this->type,
- 'code' => $this->code,
- 'content_type' => $this->content_type,
- 'content' => $this->content,
- );
-
- if ( null !== $this->param ) {
- $data['param'] = $this->param;
- }
-
- return $data;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageInfo.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageInfo.php
deleted file mode 100644
index c857303aad0..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/MessageInfo.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages;
-
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\MessageType;
-
-/**
- * MessageInfo class.
- *
- * Represents an info message object as defined in the Agentic Commerce Protocol.
- */
-class MessageInfo extends Message {
- /**
- * The error type (always 'error' for message errors).
- *
- * @var string
- */
- private $type = MessageType::INFO;
-
- /**
- * Constructor.
- *
- * @param string $content Error content/message.
- * @param string|null $param RFC 9535 JSONPath (optional).
- */
- public function __construct( $content, $param = null ) {
- $this->content = $content;
- $this->param = $param;
- }
-
- /**
- * Check if the message is an error.
- *
- * @return bool True if the message is an error, false otherwise.
- */
- public function is_error(): bool {
- return false;
- }
-
- /**
- * Convert the error to an array.
- *
- * @return array A message for the `messages` array of the response.
- */
- public function to_array(): array {
- $data = array(
- 'type' => $this->type,
- 'content_type' => $this->content_type,
- 'content' => $this->content,
- );
-
- if ( null !== $this->param ) {
- $data['param'] = $this->param;
- }
-
- return $data;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Messages.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Messages.php
deleted file mode 100644
index 5e3262e8f4b..00000000000
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Agentic/Messages/Messages.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages;
-
-/**
- * Class Messages
- *
- * Manages error & info messages for the agentic checkout process.
- */
-class Messages {
- /**
- * Array of messages.
- *
- * @var Message[]
- */
- private $messages = array();
-
- /**
- * Add a message.
- *
- * @param Message $message The message to add.
- * @return void
- */
- public function add( Message $message ): void {
- $this->messages[] = $message;
- }
-
- /**
- * Check if there are any error messages.
- *
- * @return bool True if there are error messages, false otherwise.
- */
- public function has_errors(): bool {
- foreach ( $this->messages as $message ) {
- if ( $message->is_error() ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Get all error messages, formatted as per the ACP spec.
- *
- * @return array that is ready for the response.
- */
- public function get_formatted_messages(): array {
- return array_map(
- function ( Message $message ) {
- return $message->to_array();
- },
- $this->messages
- );
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/RoutesController.php b/plugins/woocommerce/src/StoreApi/RoutesController.php
index 6de8e81baed..9f9eb40af1d 100644
--- a/plugins/woocommerce/src/StoreApi/RoutesController.php
+++ b/plugins/woocommerce/src/StoreApi/RoutesController.php
@@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;
/**
* RoutesController class.
@@ -83,12 +82,6 @@ class RoutesController {
Routes\V1\ShopperListItems::IDENTIFIER => Routes\V1\ShopperListItems::class,
Routes\V1\ShopperListItemsByKey::IDENTIFIER => Routes\V1\ShopperListItemsByKey::class,
],
- 'agentic' => [
- // Agentic Commerce Protocol endpoints.
- Routes\V1\Agentic\CheckoutSessions::IDENTIFIER => Routes\V1\Agentic\CheckoutSessions::class,
- Routes\V1\Agentic\CheckoutSessionsUpdate::IDENTIFIER => Routes\V1\Agentic\CheckoutSessionsUpdate::class,
- Routes\V1\Agentic\CheckoutSessionsComplete::IDENTIFIER => Routes\V1\Agentic\CheckoutSessionsComplete::class,
- ],
];
}
@@ -103,10 +96,6 @@ class RoutesController {
if ( wc_get_container()->get( ShopperListsController::class )->is_enabled() ) {
$this->register_routes( 'shopper_lists', self::$api_namespace . '/v1' );
}
-
- if ( FeaturesUtil::feature_is_enabled( 'agentic_checkout' ) ) {
- $this->register_routes( 'agentic', 'wc/agentic/v1' );
- }
}
/**
diff --git a/plugins/woocommerce/src/StoreApi/SchemaController.php b/plugins/woocommerce/src/StoreApi/SchemaController.php
index bac2e6cf1bf..8705a2f15c5 100644
--- a/plugins/woocommerce/src/StoreApi/SchemaController.php
+++ b/plugins/woocommerce/src/StoreApi/SchemaController.php
@@ -59,7 +59,6 @@ class SchemaController {
Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class,
Schemas\V1\ShopperListSchema::IDENTIFIER => Schemas\V1\ShopperListSchema::class,
Schemas\V1\ShopperListItemSchema::IDENTIFIER => Schemas\V1\ShopperListItemSchema::class,
- Schemas\V1\Agentic\CheckoutSessionSchema::IDENTIFIER => Schemas\V1\Agentic\CheckoutSessionSchema::class,
],
];
}
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
deleted file mode 100644
index 3d0435a92a4..00000000000
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/Agentic/CheckoutSessionSchema.php
+++ /dev/null
@@ -1,875 +0,0 @@
-<?php
-/**
- * CheckoutSessionSchema class.
- *
- * @package Automattic\WooCommerce\StoreApi\Schemas\V1\Agentic
- */
-
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Schemas\V1\Agentic;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\CheckoutSessionStatus;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\MessageType;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\MessageContentType;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\FulfillmentType;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\TotalType;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\LinkType;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\PaymentMethod;
-use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\AgenticCheckoutSession;
-use Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
-use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
-use WC_Order;
-
-/**
- * Handles the schema for Agentic Checkout API checkout sessions.
- * This schema formats WooCommerce cart/order data according to the
- * Agentic Commerce Protocol specification.
- *
- * @internal The specification for agentic requests is subject to abrupt changes; backwards compatibility cannot be guaranteed.
- */
-class CheckoutSessionSchema extends AbstractSchema {
- use DraftOrderTrait;
-
- /**
- * The schema item name.
- *
- * @var string
- */
- protected $title = 'agentic_checkout_session';
-
- /**
- * The schema item identifier.
- *
- * @var string
- */
- const IDENTIFIER = 'agentic-checkout-session';
-
- /**
- * Checkout session schema properties.
- *
- * @return array
- */
- public function get_properties() {
- return [
- 'id' => [
- 'description' => __( 'Unique identifier for the checkout session.', 'woocommerce' ),
- 'type' => 'string',
- 'context' => [ 'view', 'edit' ],
- 'readonly' => true,
- ],
- 'buyer' => [
- 'description' => __( 'Buyer information.', 'woocommerce' ),
- 'type' => [ 'object', 'null' ],
- 'context' => [ 'view', 'edit' ],
- 'properties' => [
- 'first_name' => [
- 'description' => __( 'First name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'last_name' => [
- 'description' => __( 'Last name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'email' => [
- 'description' => __( 'Email address.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'phone_number' => [
- 'description' => __( 'Phone number.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- ],
- 'payment_provider' => [
- 'description' => __( 'Payment provider information.', 'woocommerce' ),
- 'type' => [ 'object', 'null' ],
- 'context' => [ 'view', 'edit' ],
- 'properties' => [
- 'provider' => [
- 'description' => __( 'Payment provider identifier.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'supported_payment_methods' => [
- 'description' => __( 'List of supported payment methods.', 'woocommerce' ),
- 'type' => 'array',
- 'items' => [
- 'type' => 'string',
- ],
- ],
- ],
- ],
- 'status' => [
- 'description' => __( 'Status of the checkout session.', 'woocommerce' ),
- 'type' => 'string',
- 'context' => [ 'view', 'edit' ],
- 'enum' => [
- CheckoutSessionStatus::NOT_READY_FOR_PAYMENT,
- CheckoutSessionStatus::READY_FOR_PAYMENT,
- CheckoutSessionStatus::COMPLETED,
- CheckoutSessionStatus::CANCELED,
- ],
- 'readonly' => true,
- ],
- 'currency' => [
- 'description' => __( 'Currency code (ISO 4217).', 'woocommerce' ),
- 'type' => 'string',
- 'context' => [ 'view', 'edit' ],
- 'readonly' => true,
- ],
- 'line_items' => [
- 'description' => __( 'Line items in the checkout session.', 'woocommerce' ),
- 'type' => 'array',
- 'context' => [ 'view', 'edit' ],
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'id' => [
- 'description' => __( 'Line item ID.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'item' => [
- 'description' => __( 'Product item details.', 'woocommerce' ),
- 'type' => 'object',
- 'properties' => [
- 'id' => [
- 'description' => __( 'Product ID.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'quantity' => [
- 'description' => __( 'Quantity.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- ],
- ],
- 'base_amount' => [
- 'description' => __( 'Base amount in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'discount' => [
- 'description' => __( 'Discount amount in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'subtotal' => [
- 'description' => __( 'Subtotal in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'tax' => [
- 'description' => __( 'Tax amount in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'total' => [
- 'description' => __( 'Total amount in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- ],
- ],
- ],
- 'fulfillment_address' => [
- 'description' => __( 'Fulfillment/shipping address.', 'woocommerce' ),
- 'type' => [ 'object', 'null' ],
- 'context' => [ 'view', 'edit' ],
- 'properties' => [
- 'name' => [
- 'description' => __( 'Full name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'line_one' => [
- 'description' => __( 'Address line 1.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'line_two' => [
- 'description' => __( 'Address line 2.', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'city' => [
- 'description' => __( 'City.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'state' => [
- 'description' => __( 'State/province.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'country' => [
- 'description' => __( 'Country code (ISO 3166-1 alpha-2).', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'postal_code' => [
- 'description' => __( 'Postal/ZIP code.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- ],
- 'fulfillment_options' => [
- 'description' => __( 'Available fulfillment options.', 'woocommerce' ),
- 'type' => 'array',
- 'context' => [ 'view', 'edit' ],
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'description' => __( 'Fulfillment type.', 'woocommerce' ),
- 'type' => 'string',
- 'enum' => [ FulfillmentType::SHIPPING, FulfillmentType::DIGITAL ],
- ],
- 'id' => [
- 'description' => __( 'Fulfillment option ID.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'title' => [
- 'description' => __( 'Title.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'subtitle' => [
- 'description' => __( 'Subtitle.', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'carrier' => [
- 'description' => __( 'Carrier name.', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'earliest_delivery_time' => [
- 'description' => __( 'Earliest delivery time (ISO 8601).', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'latest_delivery_time' => [
- 'description' => __( 'Latest delivery time (ISO 8601).', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'subtotal' => [
- 'description' => __( 'Subtotal in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'tax' => [
- 'description' => __( 'Tax in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- 'total' => [
- 'description' => __( 'Total in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- ],
- ],
- ],
- 'fulfillment_option_id' => [
- 'description' => __( 'Selected fulfillment option ID.', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- 'context' => [ 'view', 'edit' ],
- ],
- 'totals' => [
- 'description' => __( 'Order totals breakdown.', 'woocommerce' ),
- 'type' => 'array',
- 'context' => [ 'view', 'edit' ],
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'description' => __( 'Total type.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'display_text' => [
- 'description' => __( 'Display text.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'amount' => [
- 'description' => __( 'Amount in cents.', 'woocommerce' ),
- 'type' => 'integer',
- ],
- ],
- ],
- ],
- 'messages' => [
- 'description' => __( 'Messages (info, warnings, errors).', 'woocommerce' ),
- 'type' => 'array',
- 'context' => [ 'view', 'edit' ],
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'description' => __( 'Message type.', 'woocommerce' ),
- 'type' => 'string',
- 'enum' => [ MessageType::INFO, MessageType::WARNING, MessageType::ERROR ],
- ],
- 'param' => [
- 'description' => __( 'JSON path to the related field.', 'woocommerce' ),
- 'type' => [ 'string', 'null' ],
- ],
- 'content_type' => [
- 'description' => __( 'Content type.', 'woocommerce' ),
- 'type' => 'string',
- 'enum' => [ MessageContentType::PLAIN, MessageContentType::MARKDOWN ],
- ],
- 'content' => [
- 'description' => __( 'Message content.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- ],
- ],
- 'links' => [
- 'description' => __( 'Related links.', 'woocommerce' ),
- 'type' => 'array',
- 'context' => [ 'view', 'edit' ],
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'type' => [
- 'description' => __( 'Link type.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'url' => [
- 'description' => __( 'URL.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- ],
- ],
- ];
- }
-
- /**
- * Convert a WooCommerce cart to the Agentic Checkout session format.
- *
- * @param AgenticCheckoutSession $checkout_session Checkout session object.
- * @return array Formatted checkout session data.
- */
- public function get_item_response( $checkout_session ) {
- $cart = $checkout_session->get_cart();
-
- // If validation already went through and we have errors, no need to repeat them.
- if ( ! $checkout_session->get_messages()->has_errors() ) {
- // Validate the checkout session. Messages will be added to the collection, if any.
- AgenticCheckoutUtils::validate( $checkout_session );
- }
-
- $completed_order = WC()->session
- ? wc_get_order( WC()->session->get( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID ) )
- : null;
-
- // Get line items from cart, or from completed order if cart is empty.
- $cart_items = $cart->get_cart();
- $line_items = $completed_order instanceof WC_Order
- ? $this->format_line_items_from_order( $completed_order )
- : $this->format_line_items_from_cart( $cart_items );
-
- $response = [
- 'id' => $checkout_session->get_id(),
- 'buyer' => $completed_order instanceof WC_Order
- ? $this->format_buyer_from_order( $completed_order )
- : $this->format_buyer(),
- 'payment_provider' => $this->format_payment_provider(),
- 'status' => AgenticCheckoutUtils::calculate_status( $checkout_session ),
- 'currency' => $completed_order instanceof WC_Order
- ? strtolower( $completed_order->get_currency() )
- : strtolower( get_woocommerce_currency() ),
- 'line_items' => $line_items,
- 'fulfillment_address' => $completed_order instanceof WC_Order
- ? $this->format_fulfillment_address_from_order( $completed_order )
- : $this->format_fulfillment_address(),
- 'fulfillment_options' => $completed_order instanceof WC_Order
- ? $this->format_fulfillment_options_from_order( $completed_order )
- : $this->format_fulfillment_options(),
- 'fulfillment_option_id' => $completed_order instanceof WC_Order
- ? $this->get_selected_fulfillment_option_id_from_order( $completed_order )
- : $this->get_selected_fulfillment_option_id(),
- 'totals' => $completed_order instanceof WC_Order
- ? $this->format_totals_from_order( $completed_order )
- : $this->format_totals( $cart ),
- 'messages' => $checkout_session->get_messages()->get_formatted_messages(),
- 'links' => $this->get_links(),
- ];
-
- // Add order data if a completed order exists.
- if ( $completed_order instanceof WC_Order ) {
- $response['order'] = [
- 'id' => (string) $completed_order->get_id(),
- 'checkout_session_id' => $checkout_session->get_id(),
- 'permalink_url' => $completed_order->get_checkout_order_received_url(),
- ];
- }
-
- return $response;
- }
-
- /**
- * Format buyer information.
- *
- * @return array|null Buyer data or null.
- */
- protected function format_buyer() {
- $customer = WC()->customer;
-
- if ( ! $customer ) {
- return null;
- }
-
- $first_name = $customer->get_billing_first_name() ? $customer->get_billing_first_name() : $customer->get_shipping_first_name();
- $last_name = $customer->get_billing_last_name() ? $customer->get_billing_last_name() : $customer->get_shipping_last_name();
- $email = $customer->get_billing_email();
-
- if ( ! $first_name && ! $last_name && ! $email ) {
- return null;
- }
-
- return [
- 'first_name' => $first_name ? $first_name : '',
- 'last_name' => $last_name ? $last_name : '',
- 'email' => $email ? $email : '',
- 'phone_number' => $customer->get_billing_phone() ? $customer->get_billing_phone() : '',
- ];
- }
-
- /**
- * Format buyer information from order.
- *
- * @param \WC_Order $order Order object.
- * @return array|null Buyer data or null.
- */
- protected function format_buyer_from_order( $order ) {
- $first_name = $order->get_billing_first_name() ? $order->get_billing_first_name() : $order->get_shipping_first_name();
- $last_name = $order->get_billing_last_name() ? $order->get_billing_last_name() : $order->get_shipping_last_name();
- $email = $order->get_billing_email();
-
- if ( ! $first_name && ! $last_name && ! $email ) {
- return null;
- }
-
- return [
- 'first_name' => $first_name ? $first_name : '',
- 'last_name' => $last_name ? $last_name : '',
- 'email' => $email ? $email : '',
- 'phone_number' => $order->get_billing_phone() ? $order->get_billing_phone() : '',
- ];
- }
-
- /**
- * Format payment provider information.
- *
- * @return array|null Payment provider data or null.
- */
- protected function format_payment_provider() {
- $available_gateways = WC()->payment_gateways()->get_available_payment_gateways();
-
- if ( empty( $available_gateways ) ) {
- return null;
- }
-
- // Look for gateway with agentic_commerce capability.
- $gateway = AgenticCheckoutUtils::get_agentic_commerce_gateway( $available_gateways );
-
- if ( null !== $gateway ) {
- return [
- 'provider' => $gateway->get_agentic_commerce_provider(),
- 'supported_payment_methods' => $gateway->get_agentic_commerce_payment_methods(),
- ];
- }
-
- return [
- 'provider' => 'stripe',
- 'supported_payment_methods' => [ PaymentMethod::CARD ], // Default, can be expanded.
- ];
- }
-
- /**
- * Convert amount from decimal to cents.
- *
- * @param string|float $amount Amount in decimal.
- * @return int Amount in cents.
- */
- protected function amount_to_cents( $amount ) {
- return (int) $this->extend->get_formatter( 'money' )->format(
- $amount
- );
- }
-
- /**
- * Format line items from cart.
- *
- * @param array $cart_items Cart items array.
- * @return array Formatted line items.
- */
- protected function format_line_items_from_cart( $cart_items ) {
- $items = [];
-
- foreach ( $cart_items as $cart_item_key => $cart_item ) {
- $product = $cart_item['data'];
- $quantity = $cart_item['quantity'];
- $base_amount = $this->amount_to_cents( $product->get_price() * $quantity );
- $discount = $this->amount_to_cents( $cart_item['line_subtotal'] - $cart_item['line_total'] );
- $subtotal = $base_amount - $discount;
- $tax = $this->amount_to_cents( $cart_item['line_tax'] );
- $total = $subtotal + $tax;
-
- $items[] = [
- 'id' => (string) $cart_item_key,
- 'item' => [
- 'id' => (string) $product->get_id(),
- 'quantity' => $quantity,
- ],
- 'base_amount' => $base_amount,
- 'discount' => $discount,
- 'subtotal' => $subtotal,
- 'tax' => $tax,
- 'total' => $total,
- ];
- }
-
- return $items;
- }
-
- /**
- * Format line items from order.
- *
- * @param \WC_Order $order Order object.
- * @return array Formatted line items.
- */
- protected function format_line_items_from_order( $order ) {
- $items = [];
-
- foreach ( $order->get_items() as $item_id => $item ) {
- $quantity = $item->get_quantity();
- $base_amount = $this->amount_to_cents( $item->get_subtotal() );
- $discount = $this->amount_to_cents( $item->get_subtotal() - $item->get_total() );
- $subtotal = $base_amount - $discount;
- $tax = $this->amount_to_cents( $item->get_total_tax() );
- $total = $subtotal + $tax;
-
- // Use product_id from the order item, with variation_id as fallback.
- $item_product_id = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
-
- $items[] = [
- 'id' => (string) $item_id,
- 'item' => [
- 'id' => (string) $item_product_id,
- 'quantity' => $quantity,
- ],
- 'base_amount' => $base_amount,
- 'discount' => $discount,
- 'subtotal' => $subtotal,
- 'tax' => $tax,
- 'total' => $total,
- ];
- }
-
- return $items;
- }
-
- /**
- * Format fulfillment address.
- *
- * @return array|null Address data or null.
- */
- protected function format_fulfillment_address() {
- $customer = WC()->customer;
-
- if ( ! $customer || ! $customer->get_shipping_address_1() ) {
- return null;
- }
-
- return $this->build_address_array(
- $customer->get_shipping_first_name(),
- $customer->get_shipping_last_name(),
- $customer->get_shipping_address_1(),
- $customer->get_shipping_address_2(),
- $customer->get_shipping_city(),
- $customer->get_shipping_state(),
- $customer->get_shipping_country(),
- $customer->get_shipping_postcode()
- );
- }
-
- /**
- * Format fulfillment address from order.
- *
- * @param \WC_Order $order Order object.
- * @return array|null Address data or null.
- */
- protected function format_fulfillment_address_from_order( $order ) {
- if ( ! $order->get_shipping_address_1() ) {
- return null;
- }
-
- return $this->build_address_array(
- $order->get_shipping_first_name(),
- $order->get_shipping_last_name(),
- $order->get_shipping_address_1(),
- $order->get_shipping_address_2(),
- $order->get_shipping_city(),
- $order->get_shipping_state(),
- $order->get_shipping_country(),
- $order->get_shipping_postcode()
- );
- }
-
- /**
- * Build address array from components.
- *
- * @param string $first_name First name.
- * @param string $last_name Last name.
- * @param string $address_1 Address line 1.
- * @param string $address_2 Address line 2.
- * @param string $city City.
- * @param string $state State.
- * @param string $country Country.
- * @param string $postcode Postcode.
- * @return array Address array.
- */
- protected function build_address_array( $first_name, $last_name, $address_1, $address_2, $city, $state, $country, $postcode ) {
- $name = trim( $first_name . ' ' . $last_name );
-
- return [
- 'name' => $name ? $name : 'Customer',
- 'line_one' => $address_1,
- 'line_two' => $address_2 ? $address_2 : '',
- 'city' => $city,
- 'state' => $state,
- 'country' => $country,
- 'postal_code' => $postcode,
- ];
- }
-
- /**
- * Format fulfillment options (shipping methods).
- *
- * @return array Fulfillment options.
- */
- protected function format_fulfillment_options() {
- $options = [];
- $packages = WC()->shipping()->get_packages();
-
- foreach ( $packages as $package ) {
- if ( empty( $package['rates'] ) ) {
- continue;
- }
-
- foreach ( $package['rates'] as $rate ) {
- $options[] = [
- 'type' => FulfillmentType::SHIPPING,
- 'id' => $rate->get_id(),
- 'title' => $rate->get_label(),
- 'subtitle' => null,
- 'carrier' => $rate->get_method_id(),
- 'earliest_delivery_time' => null,
- 'latest_delivery_time' => null,
- 'subtotal' => $this->amount_to_cents( $rate->get_cost() ),
- 'tax' => $this->amount_to_cents( $rate->get_shipping_tax() ),
- 'total' => $this->amount_to_cents( $rate->get_cost() + $rate->get_shipping_tax() ),
- ];
- }
- }
-
- return $options;
- }
-
- /**
- * Format fulfillment options from order.
- *
- * @param \WC_Order $order Order object.
- * @return array Fulfillment options.
- */
- protected function format_fulfillment_options_from_order( $order ) {
- $options = [];
- $shipping_methods = $order->get_shipping_methods();
-
- foreach ( $shipping_methods as $item ) {
- $options[] = [
- 'type' => FulfillmentType::SHIPPING,
- 'id' => $item->get_method_id() . ':' . $item->get_instance_id(),
- 'title' => $item->get_name(),
- 'subtitle' => null,
- 'carrier' => $item->get_method_id(),
- 'earliest_delivery_time' => null,
- 'latest_delivery_time' => null,
- 'subtotal' => $this->amount_to_cents( $item->get_total() ),
- 'tax' => $this->amount_to_cents( $item->get_total_tax() ),
- 'total' => $this->amount_to_cents( $item->get_total() + $item->get_total_tax() ),
- ];
- }
-
- return $options;
- }
-
- /**
- * Get selected fulfillment option ID.
- *
- * @return string|null Selected option ID or null.
- */
- protected function get_selected_fulfillment_option_id() {
- $chosen_methods = WC()->session->get( SessionKey::CHOSEN_SHIPPING_METHODS );
-
- return ! empty( $chosen_methods[0] ) ? $chosen_methods[0] : null;
- }
-
- /**
- * Get selected fulfillment option ID from order.
- *
- * @param \WC_Order $order Order object.
- * @return string|null Selected option ID or null.
- */
- protected function get_selected_fulfillment_option_id_from_order( $order ) {
- $shipping_methods = $order->get_shipping_methods();
- if ( empty( $shipping_methods ) ) {
- return null;
- }
-
- $shipping_method = reset( $shipping_methods );
- return $shipping_method->get_method_id() . ':' . $shipping_method->get_instance_id();
- }
-
- /**
- * Format totals array.
- *
- * @param \WC_Cart $cart Cart object.
- * @return array Totals array.
- */
- protected function format_totals( $cart ) {
- $totals = [];
-
- // Items base amount.
- $items_base = 0;
- foreach ( $cart->get_cart() as $cart_item ) {
- $product = $cart_item['data'];
- $items_base += $product->get_price() * $cart_item['quantity'];
- }
- $totals[] = [
- 'type' => TotalType::ITEMS_BASE_AMOUNT,
- 'display_text' => __( 'Items Base Amount', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $items_base ),
- ];
-
- // Items discount.
- $discount = $cart->get_cart_discount_total();
- $totals[] = [
- 'type' => TotalType::ITEMS_DISCOUNT,
- 'display_text' => __( 'Items Discount', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $discount ),
- ];
-
- // Subtotal.
- $totals[] = [
- 'type' => TotalType::SUBTOTAL,
- 'display_text' => __( 'Subtotal', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $cart->get_subtotal() - $discount ),
- ];
-
- // Fulfillment (shipping).
- $totals[] = [
- 'type' => TotalType::FULFILLMENT,
- 'display_text' => __( 'Shipping', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $cart->get_shipping_total() ),
- ];
-
- // Tax.
- $totals[] = [
- 'type' => TotalType::TAX,
- 'display_text' => __( 'Tax', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $cart->get_total_tax() ),
- ];
-
- // Total.
- $totals[] = [
- 'type' => TotalType::TOTAL,
- 'display_text' => __( 'Total', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $cart->get_total( 'edit' ) ),
- ];
-
- return $totals;
- }
-
- /**
- * Format totals array from order.
- *
- * @param \WC_Order $order Order object.
- * @return array Totals array.
- */
- protected function format_totals_from_order( $order ) {
- $totals = [];
-
- // Items base amount.
- $items_base = 0;
- foreach ( $order->get_items() as $item ) {
- $product = $item->get_product();
- $items_base += $product->get_price() * $item->get_quantity();
- }
- $totals[] = [
- 'type' => TotalType::ITEMS_BASE_AMOUNT,
- 'display_text' => __( 'Items Base Amount', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $items_base ),
- ];
-
- // Items discount.
- $discount = $order->get_discount_total();
- $totals[] = [
- 'type' => TotalType::ITEMS_DISCOUNT,
- 'display_text' => __( 'Items Discount', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $discount ),
- ];
-
- // Subtotal.
- $totals[] = [
- 'type' => TotalType::SUBTOTAL,
- 'display_text' => __( 'Subtotal', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $items_base - $discount ),
- ];
-
- // Fulfillment (shipping).
- $totals[] = [
- 'type' => TotalType::FULFILLMENT,
- 'display_text' => __( 'Shipping', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $order->get_shipping_total() ),
- ];
-
- // Tax.
- $totals[] = [
- 'type' => TotalType::TAX,
- 'display_text' => __( 'Tax', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $order->get_total_tax() ),
- ];
-
- // Total.
- $totals[] = [
- 'type' => TotalType::TOTAL,
- 'display_text' => __( 'Total', 'woocommerce' ),
- 'amount' => $this->amount_to_cents( $order->get_total() ),
- ];
-
- return $totals;
- }
-
- /**
- * Get links for the session.
- *
- * @return array Links array.
- */
- protected function get_links() {
- $links = [];
-
- // Terms of use.
- $terms_page_id = wc_terms_and_conditions_page_id();
- if ( $terms_page_id ) {
- $permalink = get_permalink( $terms_page_id );
- if ( $permalink ) {
- $links[] = [
- 'type' => LinkType::TERMS_OF_USE,
- 'url' => $permalink,
- ];
- }
- }
-
- // Privacy policy.
- $privacy_page_id = get_option( 'wp_page_for_privacy_policy' );
- if ( $privacy_page_id ) {
- $permalink = get_permalink( $privacy_page_id );
- if ( $permalink ) {
- $links[] = [
- 'type' => LinkType::PRIVACY_POLICY,
- 'url' => $permalink,
- ];
- }
- }
-
- return $links;
- }
-}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php b/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php
deleted file mode 100644
index b51d064fa02..00000000000
--- a/plugins/woocommerce/src/StoreApi/Utilities/AgenticCheckoutUtils.php
+++ /dev/null
@@ -1,463 +0,0 @@
-<?php
-declare(strict_types=1);
-namespace Automattic\WooCommerce\StoreApi\Utilities;
-
-use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\CheckoutSessionStatus;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorCode;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Errors\Error;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Error as AgenticError;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\AgenticCheckoutSession;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages\MessageError;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Messages\Messages;
-
-/**
- * AgenticCheckoutUtils class.
- *
- * Utility class for shared Agentic Checkout API functionality.
- */
-class AgenticCheckoutUtils {
-
- /**
- * Get the shared parameters schema for checkout session requests.
- *
- * @return array Parameters array.
- */
- public static function get_shared_params() {
- return [
- 'items' => [
- 'description' => __( 'Line items to add to the cart.', 'woocommerce' ),
- 'type' => 'array',
- 'items' => [
- 'type' => 'object',
- 'properties' => [
- 'id' => [
- 'description' => __( 'Product ID.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'quantity' => [
- 'description' => __( 'Quantity.', 'woocommerce' ),
- 'type' => 'integer',
- 'minimum' => 1,
- ],
- ],
- 'required' => [ 'id', 'quantity' ],
- ],
- ],
- 'buyer' => [
- 'description' => __( 'Buyer information.', 'woocommerce' ),
- 'type' => 'object',
- 'properties' => [
- 'first_name' => [
- 'description' => __( 'First name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'last_name' => [
- 'description' => __( 'Last name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'email' => [
- 'description' => __( 'Email address.', 'woocommerce' ),
- 'type' => 'string',
- 'format' => 'email',
- ],
- 'phone_number' => [
- 'description' => __( 'Phone number.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- ],
- 'fulfillment_address' => [
- 'description' => __( 'Fulfillment/shipping address.', 'woocommerce' ),
- 'type' => 'object',
- 'properties' => [
- 'name' => [
- 'description' => __( 'Full name.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'line_one' => [
- 'description' => __( 'Address line 1.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'line_two' => [
- 'description' => __( 'Address line 2.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'city' => [
- 'description' => __( 'City.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'state' => [
- 'description' => __( 'State/province.', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'country' => [
- 'description' => __( 'Country code (ISO 3166-1 alpha-2).', 'woocommerce' ),
- 'type' => 'string',
- ],
- 'postal_code' => [
- 'description' => __( 'Postal/ZIP code.', 'woocommerce' ),
- 'type' => 'string',
- ],
- ],
- 'required' => [ 'line_one', 'city', 'country', 'postal_code' ],
- ],
- ];
- }
-
- /**
- * Add items to cart from request.
- *
- * @param array $items Items array from request.
- * @param CartController $cart_controller Cart controller instance.
- * @param Messages $messages Error messages instance.
- * @return Error|null Returns error response on failure, null on success.
- */
- public static function add_items_to_cart( $items, $cart_controller, $messages ) {
- foreach ( $items as $item_index => $item ) {
- if ( ! ctype_digit( $item['id'] ) ) {
- return AgenticError::invalid_request(
- 'invalid_product_id',
- __( 'Product ID must be numeric.', 'woocommerce' ),
- '$.items[' . $item_index . '].id'
- );
- }
-
- $product_id = (int) $item['id'];
- $quantity = (int) $item['quantity'];
-
- try {
- $cart_controller->add_to_cart(
- [
- 'id' => $product_id,
- 'quantity' => $quantity,
- ]
- );
- } catch ( RouteException $exception ) {
- $message = wp_specialchars_decode( $exception->getMessage(), ENT_QUOTES );
- $param = '$.items[' . $item_index . ']';
- $message_error = null;
-
- // Map WooCommerce error codes to Agentic Commerce Protocol error codes.
- switch ( $exception->getErrorCode() ) {
- case 'woocommerce_rest_product_out_of_stock':
- case 'woocommerce_rest_product_partially_out_of_stock':
- $message_error = MessageError::out_of_stock( $message, $param );
- break;
- }
-
- if ( null !== $message_error ) {
- $messages->add( $message_error );
- } else {
- // The error code is generally applicable only to MessageErrors, but we can use it here as well.
- return AgenticError::invalid_request( ErrorCode::INVALID, $message, $param );
- }
- }
- }
-
- return null;
- }
-
- /**
- * Set buyer data on customer.
- *
- * @param array $buyer Buyer data.
- * @param \WC_Customer $customer Customer instance.
- */
- public static function set_buyer_data( $buyer, $customer ) {
- if ( isset( $buyer['first_name'] ) ) {
- $first_name = wc_clean( $buyer['first_name'] );
- $customer->set_billing_first_name( $first_name );
- $customer->set_shipping_first_name( $first_name );
- }
-
- if ( isset( $buyer['last_name'] ) ) {
- $last_name = wc_clean( $buyer['last_name'] );
- $customer->set_billing_last_name( $last_name );
- $customer->set_shipping_last_name( $last_name );
- }
-
- if ( isset( $buyer['email'] ) ) {
- $email = sanitize_email( $buyer['email'] );
- if ( is_email( $email ) ) {
- $customer->set_billing_email( $email );
- }
- }
-
- if ( isset( $buyer['phone_number'] ) ) {
- $phone = wc_clean( $buyer['phone_number'] );
- $customer->set_billing_phone( $phone );
- }
-
- $customer->save();
- }
-
- /**
- * Set fulfillment address on customer.
- *
- * @param array $address Address data.
- * @param \WC_Customer $customer Customer instance.
- */
- public static function set_fulfillment_address( $address, $customer ) {
- // Only parse and set name if provided and non-empty.
- if ( ! empty( $address['name'] ) ) {
- $name = wc_clean( $address['name'] );
- $name_parts = explode( ' ', $name, 2 );
- $first_name = $name_parts[0];
- $last_name = isset( $name_parts[1] ) ? $name_parts[1] : '';
-
- // Set shipping names.
- $customer->set_shipping_first_name( $first_name );
- $customer->set_shipping_last_name( $last_name );
- } else {
- // Preserve existing shipping names.
- $first_name = $customer->get_shipping_first_name();
- $last_name = $customer->get_shipping_last_name();
- }
-
- // Sanitize all address fields.
- $line_one = wc_clean( $address['line_one'] ?? '' );
- $line_two = wc_clean( $address['line_two'] ?? '' );
- $city = wc_clean( $address['city'] ?? '' );
- $state = wc_clean( $address['state'] ?? '' );
- $postal_code = wc_clean( $address['postal_code'] ?? '' );
- $country = wc_clean( $address['country'] ?? '' );
-
- // Set shipping address fields.
- $customer->set_shipping_address_1( $line_one );
- $customer->set_shipping_address_2( $line_two );
- $customer->set_shipping_city( $city );
- $customer->set_shipping_state( $state );
- $customer->set_shipping_postcode( $postal_code );
- $customer->set_shipping_country( $country );
-
- // Also set as billing address if not already set.
- if ( ! $customer->get_billing_address_1() ) {
- // For billing, only set names if provided or use existing billing names.
- if ( ! empty( $address['name'] ) ) {
- $customer->set_billing_first_name( $first_name );
- $customer->set_billing_last_name( $last_name );
- }
- $customer->set_billing_address_1( $line_one );
- $customer->set_billing_address_2( $line_two );
- $customer->set_billing_city( $city );
- $customer->set_billing_state( $state );
- $customer->set_billing_postcode( $postal_code );
- $customer->set_billing_country( $country );
- }
-
- $customer->save();
- }
-
- /**
- * Clear fulfillment address from customer.
- *
- * @param \WC_Customer $customer Customer instance.
- */
- public static function clear_fulfillment_address( $customer ) {
- // Clear shipping address.
- $customer->set_shipping_first_name( '' );
- $customer->set_shipping_last_name( '' );
- $customer->set_shipping_address_1( '' );
- $customer->set_shipping_address_2( '' );
- $customer->set_shipping_city( '' );
- $customer->set_shipping_state( '' );
- $customer->set_shipping_postcode( '' );
- $customer->set_shipping_country( '' );
-
- $customer->save();
- }
-
- /**
- * Set billing address on customer.
- *
- * @param array $address Address data.
- * @param \WC_Customer $customer Customer instance.
- */
- public static function set_billing_address( $address, $customer ) {
- // Only parse and set name if provided and non-empty.
- if ( ! empty( $address['name'] ) ) {
- $name = wc_clean( $address['name'] );
- $name_parts = explode( ' ', $name, 2 );
- $first_name = $name_parts[0];
- $last_name = isset( $name_parts[1] ) ? $name_parts[1] : '';
-
- // Set billing names.
- $customer->set_billing_first_name( $first_name );
- $customer->set_billing_last_name( $last_name );
- }
-
- // Sanitize all address fields.
- $line_one = wc_clean( $address['line_one'] ?? '' );
- $line_two = wc_clean( $address['line_two'] ?? '' );
- $city = wc_clean( $address['city'] ?? '' );
- $state = wc_clean( $address['state'] ?? '' );
- $postal_code = wc_clean( $address['postal_code'] ?? '' );
- $country = wc_clean( $address['country'] ?? '' );
-
- // Set billing address fields.
- $customer->set_billing_address_1( $line_one );
- $customer->set_billing_address_2( $line_two );
- $customer->set_billing_city( $city );
- $customer->set_billing_state( $state );
- $customer->set_billing_postcode( $postal_code );
- $customer->set_billing_country( $country );
-
- $customer->save();
- }
-
- /**
- * Add Agentic Commerce Protocol headers to response.
- *
- * @param \WP_REST_Response $response Response object.
- * @param \WP_REST_Request $request Request object.
- * @return \WP_REST_Response Response with headers.
- */
- public static function add_protocol_headers( \WP_REST_Response $response, \WP_REST_Request $request ) {
- // Echo Idempotency-Key header if provided.
- $idempotency_key = $request->get_header( 'Idempotency-Key' );
- if ( $idempotency_key ) {
- $response->header( 'Idempotency-Key', $idempotency_key );
- }
-
- // Echo Request-Id header if provided.
- $request_id = $request->get_header( 'Request-Id' );
- if ( $request_id ) {
- $response->header( 'Request-Id', $request_id );
- }
-
- return $response;
- }
-
- /**
- * Validate that the request is signed with Jetpack blog token.
- *
- * @since 10.6.0
- *
- * @return true|\WP_Error True if valid, WP_Error otherwise.
- */
- public static function validate_jetpack_request() {
- if ( class_exists( 'Automattic\Jetpack\Connection\Rest_Authentication' ) ) {
- if ( \Automattic\Jetpack\Connection\Rest_Authentication::is_signed_with_blog_token() ) {
- return true;
- }
- }
-
- return new \WP_Error(
- 'rest_forbidden',
- __( 'This endpoint requires Jetpack blog token authentication.', 'woocommerce' ),
- array( 'status' => 401 )
- );
- }
-
- /**
- * Validates a session.
- *
- * @param AgenticCheckoutSession $checkout_session Checkout session object.
- * @return void
- */
- public static function validate( AgenticCheckoutSession $checkout_session ): void {
- $messages = $checkout_session->get_messages();
-
- // Check if ready for payment.
- $needs_shipping = $checkout_session->get_cart()->needs_shipping();
- $has_address = WC()->customer && WC()->customer->get_shipping_address_1();
-
- // Add info message if shipping is needed.
- if ( $needs_shipping && ! $has_address ) {
- $messages->add(
- MessageError::missing(
- __( 'Shipping address required.', 'woocommerce' ),
- '$.fulfillment_address'
- )
- );
- }
-
- // Check if valid shipping method is selected (not just empty strings).
- $chosen_methods = WC()->session ? WC()->session->get( SessionKey::CHOSEN_SHIPPING_METHODS ) : null;
- $has_shipping = ! empty( $chosen_methods ) && ! empty( array_filter( $chosen_methods ) );
-
- if ( $needs_shipping && ! $has_shipping ) {
- $messages->add(
- MessageError::missing(
- __( 'No shipping method selected.', 'woocommerce' ),
- '$.fulfillment_option_id'
- )
- );
- }
- }
-
- /**
- * Calculate the status of the checkout session.
- *
- * @param AgenticCheckoutSession $checkout_session Checkout session object.
- *
- * @return string Status value.
- */
- public static function calculate_status( AgenticCheckoutSession $checkout_session ): string {
- $wc_session = WC()->session;
- if ( null === $wc_session ) {
- return CheckoutSessionStatus::CANCELED;
- }
-
- if ( $wc_session->get( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID ) ) {
- return CheckoutSessionStatus::COMPLETED;
- }
-
- if ( $wc_session->get( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS ) ) {
- return CheckoutSessionStatus::IN_PROGRESS;
- }
-
- // Check for validation errors.
- if (
- $checkout_session->get_messages()->has_errors()
- // Once we switch to using the CartController everywhere, there should be no notices and need for this.
- || ! empty( wc_get_notices( 'error' ) )
- ) {
- return CheckoutSessionStatus::NOT_READY_FOR_PAYMENT;
- }
-
- return CheckoutSessionStatus::READY_FOR_PAYMENT;
- }
-
- /**
- * Get the agentic commerce payment gateway from available gateways.
- *
- * Finds the first gateway that supports agentic commerce and has the required methods.
- *
- * @param array $available_gateways Array of available payment gateways.
- * @return \WC_Payment_Gateway|null The agentic commerce gateway or null if not found.
- */
- public static function get_agentic_commerce_gateway( $available_gateways ) {
- if ( empty( $available_gateways ) ) {
- return null;
- }
-
- foreach ( $available_gateways as $gateway ) {
- if ( $gateway->supports( \Automattic\WooCommerce\Enums\PaymentGatewayFeature::AGENTIC_COMMERCE )
- && method_exists( $gateway, 'get_agentic_commerce_provider' )
- && method_exists( $gateway, 'get_agentic_commerce_payment_methods' )
- ) {
- return $gateway;
- }
- }
-
- return null;
- }
-
- /**
- * Whether the current request is within Agentic Commerce session.
- *
- * @return bool
- */
- public static function is_agentic_commerce_session(): bool {
- $wc_session = WC()->session;
- if ( null === $wc_session ) {
- return false;
- }
-
- return ! empty( $wc_session->get( SessionKey::AGENTIC_CHECKOUT_SESSION_ID ) );
- }
-}
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
index f16298dd7b9..f1448ee99fc 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-session-test.php
@@ -312,7 +312,7 @@ class WC_Customer_Data_Store_Session_Test extends WC_Unit_Test_Case {
/**
* Ensure backslashes in customer fields survive a session save/read round-trip.
*
- * Reproduces the Agentic Checkout flow where one request stores the address in the session and a
+ * Covers the multi-request flow where one request stores the address in the session and a
* follow-up request reads it back: the session store must not wp_unslash() the data on read, or
* real backslashes the buyer typed get stripped (session data is stored raw, never magic-quoted).
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php
deleted file mode 100644
index 49a4cc094b9..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php
+++ /dev/null
@@ -1,1431 +0,0 @@
-<?php
-/**
- * Agentic Checkout Sessions Tests.
- *
- * @package Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes
- */
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Enums\ProductStockStatus;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\ErrorCode;
-use Automattic\WooCommerce\StoreApi\RoutesController;
-use Automattic\WooCommerce\Tests\Internal\Admin\Agentic\AgenticTestHelpers;
-
-/**
- * CheckoutSessions Controller Tests.
- */
-class CheckoutSessions extends ControllerTestCase {
- use AgenticTestHelpers;
-
- /**
- * Product IDs shared by the class.
- *
- * @var int[]
- */
- private static $product_ids = array();
-
- /**
- * Products created for tests.
- *
- * @var array
- */
- protected $products = array();
-
- /**
- * Create immutable product rows shared by all test methods.
- */
- public static function wpSetUpBeforeClass(): void {
- self::$product_ids = array_map(
- fn( $product ) => $product->get_id(),
- self::create_class_fixture_products(
- array(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- ),
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 20,
- 'weight' => 5,
- ),
- array(
- 'name' => 'Virtual Product',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 15,
- 'virtual' => true,
- ),
- ),
- )
- );
- }
-
- /**
- * Delete class products through WooCommerce data stores.
- */
- public static function wpTearDownAfterClass(): void {
- self::delete_class_fixture_products( self::$product_ids );
- }
-
- /**
- * Setup test product data. Called before every test.
- */
- protected function setUp(): void {
- parent::setUp();
-
- // Reset customer and cart FIRST before anything else.
- wc_empty_cart();
- $this->reset_customer_state();
-
- // Clear all session data early to ensure clean state.
- if ( WC()->session ) {
- WC()->session->destroy_session();
- }
-
- // Enable the agentic_checkout feature.
- update_option( 'woocommerce_feature_agentic_checkout_enabled', 'yes' );
-
- // Set up Jetpack blog token authentication.
- $this->mock_jetpack_blog_token_auth();
-
- $fixtures = new FixtureData();
- $fixtures->shipping_add_flat_rate();
-
- $this->products = array_map( 'wc_get_product', self::$product_ids );
-
- wc_get_container()->get( RoutesController::class )->register_all_routes();
- }
-
- /**
- * Tear down test.
- */
- protected function tearDown(): void {
- parent::tearDown();
- delete_option( 'woocommerce_feature_agentic_checkout_enabled' );
-
- // Clear session data.
- WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, null );
-
- // Reset customer state to clean state.
- $this->reset_customer_state();
-
- // Reset Jetpack auth state.
- $this->reset_jetpack_auth_state();
- }
-
- /**
- * Resets customer state and remove any existing data from previous tests.
- */
- private function reset_customer_state() {
- // Clear all customer data fields.
- $customer = WC()->customer;
-
- // Clear billing fields.
- $customer->set_billing_first_name( '' );
- $customer->set_billing_last_name( '' );
- $customer->set_billing_company( '' );
- $customer->set_billing_address_1( '' );
- $customer->set_billing_address_2( '' );
- $customer->set_billing_city( '' );
- $customer->set_billing_state( '' );
- $customer->set_billing_postcode( '' );
- $customer->set_billing_country( '' );
- $customer->set_billing_email( '' );
- $customer->set_billing_phone( '' );
-
- // Clear shipping fields.
- $customer->set_shipping_first_name( '' );
- $customer->set_shipping_last_name( '' );
- $customer->set_shipping_company( '' );
- $customer->set_shipping_address_1( '' );
- $customer->set_shipping_address_2( '' );
- $customer->set_shipping_city( '' );
- $customer->set_shipping_state( '' );
- $customer->set_shipping_postcode( '' );
- $customer->set_shipping_country( '' );
-
- $customer->save();
- }
-
- /**
- * Helper: Create base checkout session request data.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Request data.
- */
- private function create_checkout_request( $overrides = array() ) {
- $defaults = array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 1,
- ),
- ),
- );
- return array_merge_recursive( $defaults, $overrides );
- }
-
- /**
- * Helper: Get test fulfillment address.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Address data.
- */
- private function get_test_address( $overrides = array() ) {
- $defaults = array(
- 'name' => 'John Doe',
- 'line_one' => '555 Golden Gate Avenue',
- 'line_two' => '',
- 'city' => 'San Francisco',
- 'state' => 'CA',
- 'country' => 'US',
- 'postal_code' => '94102',
- );
- return array_merge( $defaults, $overrides );
- }
-
- /**
- * Helper: Get test buyer information.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Buyer data.
- */
- private function get_test_buyer( $overrides = array() ) {
- $defaults = array(
- 'first_name' => 'Jane',
- 'last_name' => 'Smith',
- 'email' => 'jane@example.com',
- 'phone_number' => '+1234567890',
- );
- return array_merge( $defaults, $overrides );
- }
-
- /**
- * Helper: Create and dispatch a checkout session request.
- *
- * @param array $body_params Request body parameters.
- * @return \WP_REST_Response Response object.
- */
- private function create_session( $body_params ) {
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $body_params );
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Assert that totals array has the correct structure.
- *
- * @param array $totals The totals array to validate.
- */
- private function assertValidTotalsStructure( $totals ) {
- $this->assertIsArray( $totals );
- $this->assertNotEmpty( $totals );
-
- // Verify required total types exist.
- $total_types = array_column( $totals, 'type' );
- $this->assertContains( 'items_base_amount', $total_types );
- $this->assertContains( 'subtotal', $total_types );
- $this->assertContains( 'total', $total_types );
-
- // Verify each total has required fields.
- foreach ( $totals as $total ) {
- $this->assertArrayHasKey( 'type', $total );
- $this->assertArrayHasKey( 'display_text', $total );
- $this->assertArrayHasKey( 'amount', $total );
- $this->assertIsInt( $total['amount'] );
- }
- }
-
- /**
- * Assert that session ID is a valid Cart-Token (JWT format).
- *
- * @param string $session_id The session ID to validate.
- */
- private function assertValidSessionId( $session_id ) {
- $this->assertNotEmpty( $session_id );
- $this->assertIsString( $session_id );
-
- // JWT tokens have 3 parts separated by dots.
- $parts = explode( '.', $session_id );
- $this->assertCount( 3, $parts, 'Session ID should be a JWT token with 3 parts' );
-
- // Validate that it's a valid Cart-Token.
- $is_valid = \Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils::validate_cart_token( $session_id );
- $this->assertTrue( $is_valid, 'Session ID should be a valid Cart-Token' );
-
- // Extract and verify payload.
- $payload = \Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils::get_cart_token_payload( $session_id );
- $this->assertIsArray( $payload );
- $this->assertArrayHasKey( 'user_id', $payload );
- $this->assertArrayHasKey( 'exp', $payload );
- $this->assertArrayHasKey( 'iss', $payload );
- $this->assertEquals( 'store-api', $payload['iss'] );
-
- // Verify customer ID matches.
- $this->assertEquals( (string) WC()->session->get_customer_id(), $payload['user_id'] );
- }
-
- /**
- * Test creating a checkout session with items only.
- */
- public function test_create_checkout_session_with_items() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 2,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
- $this->assertArrayHasKey( 'id', $data );
- $this->assertArrayHasKey( 'status', $data );
- $this->assertArrayHasKey( 'line_items', $data );
- $this->assertArrayHasKey( 'currency', $data );
- $this->assertArrayHasKey( 'totals', $data );
- $this->assertArrayHasKey( 'fulfillment_options', $data );
- $this->assertArrayHasKey( 'messages', $data );
- $this->assertArrayHasKey( 'links', $data );
-
- // Verify line items.
- $this->assertCount( 1, $data['line_items'] );
- $this->assertEquals( (string) $this->products[0]->get_id(), $data['line_items'][0]['item']['id'] );
- $this->assertEquals( 2, $data['line_items'][0]['item']['quantity'] );
-
- // Verify status (should be not_ready_for_payment without address).
- $this->assertEquals( 'not_ready_for_payment', $data['status'] );
-
- // Verify amounts are in cents (integers).
- $this->assertIsInt( $data['line_items'][0]['base_amount'] );
- $this->assertIsInt( $data['line_items'][0]['total'] );
- $this->assertEquals( 2000, $data['line_items'][0]['base_amount'] ); // $10 * 2 = $20 = 2000 cents
-
- // Verify session ID is valid.
- $this->assertValidSessionId( $data['id'] );
- }
-
- /**
- * Test creating a checkout session with address.
- */
- public function test_create_checkout_session_with_address() {
- $test_address = $this->get_test_address( array( 'line_two' => 'Apt 401' ) );
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $test_address,
- )
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
-
- // Verify address is set.
- $this->assertArrayHasKey( 'fulfillment_address', $data );
- $this->assertNotNull( $data['fulfillment_address'] );
- $this->assertEquals( 'John Doe', $data['fulfillment_address']['name'] );
- $this->assertEquals( '555 Golden Gate Avenue', $data['fulfillment_address']['line_one'] );
- $this->assertEquals( 'Apt 401', $data['fulfillment_address']['line_two'] );
- $this->assertEquals( 'San Francisco', $data['fulfillment_address']['city'] );
- $this->assertEquals( 'CA', $data['fulfillment_address']['state'] );
- $this->assertEquals( 'US', $data['fulfillment_address']['country'] );
- $this->assertEquals( '94102', $data['fulfillment_address']['postal_code'] );
-
- // Verify fulfillment options are available.
- $this->assertNotEmpty( $data['fulfillment_options'] );
- $this->assertIsArray( $data['fulfillment_options'] );
- }
-
- /**
- * Test creating a checkout session with buyer info.
- */
- public function test_create_checkout_session_with_buyer() {
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
-
- // Verify buyer info is set.
- $this->assertArrayHasKey( 'buyer', $data );
- $this->assertNotNull( $data['buyer'] );
- $this->assertEquals( 'Jane', $data['buyer']['first_name'] );
- $this->assertEquals( 'Smith', $data['buyer']['last_name'] );
- $this->assertEquals( 'jane@example.com', $data['buyer']['email'] );
- $this->assertEquals( '+1234567890', $data['buyer']['phone_number'] );
- }
-
- /**
- * Test status calculation for not_ready_for_payment.
- */
- public function test_status_not_ready_for_payment() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- // Without address and shipping method, should be not_ready_for_payment.
- $this->assertEquals( 'not_ready_for_payment', $data['status'] );
- }
-
- /**
- * Test status calculation for ready_for_payment.
- */
- public function test_status_ready_for_payment() {
- // Get shipping methods first.
- wc()->customer->set_shipping_address_1( '555 Golden Gate Avenue' );
- wc()->customer->set_shipping_city( 'San Francisco' );
- wc()->customer->set_shipping_state( 'CA' );
- wc()->customer->set_shipping_postcode( '94102' );
- wc()->customer->set_shipping_country( 'US' );
- wc()->cart->add_to_cart( $this->products[0]->get_id(), 1 );
- wc()->cart->calculate_shipping();
-
- $packages = wc()->shipping()->get_packages();
- $shipping_method_id = ! empty( $packages[0]['rates'] ) ? array_key_first( $packages[0]['rates'] ) : null;
- wc_empty_cart();
-
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'fulfillment_option_id' => $shipping_method_id,
- )
- )
- );
-
- $data = $response->get_data();
-
- // With address and shipping method, should be ready_for_payment.
- $this->assertEquals( 'ready_for_payment', $data['status'] );
- }
-
- /**
- * Test invalid product ID returns error.
- */
- public function test_invalid_product_returns_error() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => '999999',
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 400, $response->get_status() );
- $this->assertArrayHasKey( 'code', $data );
- $this->assertEquals( 'invalid', $data['code'] );
- }
-
- /**
- * Test out of stock product returns error.
- */
- public function test_out_of_stock_product_returns_error() {
- // Set product out of stock.
- $this->products[0]->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
- $this->products[0]->save();
-
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
- $this->assertArrayHasKey( 'messages', $data );
- $this->assertEquals( 'error', $data['messages'][0]['type'] );
- $this->assertEquals( 'out_of_stock', $data['messages'][0]['code'] );
- }
-
- /**
- * Test virtual product doesn't require shipping address.
- */
- public function test_virtual_product_ready_for_payment_without_address() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[2]->get_id(), // Virtual product.
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
- // Virtual product should be ready_for_payment without address.
- $this->assertEquals( 'ready_for_payment', $data['status'] );
- }
-
- /**
- * Test totals array format.
- */
- public function test_totals_array_format() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- // Use the assertion helper method.
- $this->assertValidTotalsStructure( $data['totals'] );
- }
-
- /**
- * Test payment provider is included.
- */
- public function test_payment_provider_included() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- // Should have payment_provider even if null.
- $this->assertArrayHasKey( 'payment_provider', $data );
- }
-
- /**
- * Test links array includes terms and privacy.
- */
- public function test_links_array() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- $this->assertIsArray( $data['links'] );
-
- // Verify each link has required fields.
- foreach ( $data['links'] as $link ) {
- $this->assertArrayHasKey( 'type', $link );
- $this->assertArrayHasKey( 'url', $link );
- $this->assertIsString( $link['type'] );
- $this->assertIsString( $link['url'] );
- }
- }
-
- /**
- * Test currency format is lowercase.
- */
- public function test_currency_format_is_lowercase() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- // Currency should be lowercase (e.g., "usd" not "USD").
- $this->assertArrayHasKey( 'currency', $data );
- $this->assertSame( strtolower( $data['currency'] ), $data['currency'] );
- }
-
- /**
- * Test address line_two returns empty string when not set.
- */
- public function test_address_line_two_empty_string() {
- $address_without_line_two = $this->get_test_address();
- unset( $address_without_line_two['line_two'] ); // Remove line_two to test empty case.
-
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $address_without_line_two,
- )
- )
- );
-
- $data = $response->get_data();
-
- // line_two should be empty string, not null.
- $this->assertArrayHasKey( 'fulfillment_address', $data );
- $this->assertNotNull( $data['fulfillment_address'] );
- $this->assertArrayHasKey( 'line_two', $data['fulfillment_address'] );
- $this->assertSame( '', $data['fulfillment_address']['line_two'] );
- $this->assertNotNull( $data['fulfillment_address']['line_two'] ); // Explicitly not null.
- }
-
- /**
- * Test address line_two preserves value when provided.
- */
- public function test_address_line_two_with_value() {
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address( array( 'line_two' => 'Apt 401' ) ),
- )
- )
- );
-
- $data = $response->get_data();
-
- // line_two should preserve the provided value.
- $this->assertEquals( 'Apt 401', $data['fulfillment_address']['line_two'] );
- }
-
- /**
- * Test session_id is a valid Cart-Token (JWT format).
- */
- public function test_session_id_is_cart_token() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
- $this->assertArrayHasKey( 'id', $data );
-
- // Use the assertion helper method.
- $this->assertValidSessionId( $data['id'] );
- }
-
- /**
- * Helper: Update an existing checkout session.
- *
- * @param string $session_id The session ID (Cart-Token).
- * @param array $body_params Request body parameters.
- * @return \WP_REST_Response Response object.
- */
- private function update_session( $session_id, $body_params ) {
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id );
- $request->set_body_params( $body_params );
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Test updating a checkout session with new items.
- */
- public function test_update_checkout_session_items() {
- // Create initial session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update with different items.
- $update_response = $this->update_session(
- $session_id,
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[1]->get_id(),
- 'quantity' => 3,
- ),
- ),
- )
- );
-
- $update_data = $update_response->get_data();
-
- $this->assertEquals( 200, $update_response->get_status() );
- $this->assertEquals( $session_id, $update_data['id'] ); // Session ID should remain the same.
- $this->assertCount( 1, $update_data['line_items'] );
- $this->assertEquals( (string) $this->products[1]->get_id(), $update_data['line_items'][0]['item']['id'] );
- $this->assertEquals( 3, $update_data['line_items'][0]['item']['quantity'] );
- $this->assertEquals( 6000, $update_data['line_items'][0]['base_amount'] ); // $20 * 3 = $60 = 6000 cents.
- }
-
- /**
- * Test updating a checkout session with buyer info.
- */
- public function test_update_checkout_session_buyer_info() {
- // Create initial session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update with buyer info.
- $update_response = $this->update_session(
- $session_id,
- array(
- 'buyer' => array(
- 'first_name' => 'John',
- 'last_name' => 'Doe',
- 'email' => 'john.doe@example.com',
- 'phone_number' => '+9876543210',
- ),
- )
- );
-
- $update_data = $update_response->get_data();
-
- $this->assertEquals( 200, $update_response->get_status() );
- $this->assertEquals( 'John', $update_data['buyer']['first_name'] );
- $this->assertEquals( 'Doe', $update_data['buyer']['last_name'] );
- $this->assertEquals( 'john.doe@example.com', $update_data['buyer']['email'] );
- $this->assertEquals( '+9876543210', $update_data['buyer']['phone_number'] );
- }
-
- /**
- * Test updating a checkout session with fulfillment address.
- */
- public function test_update_checkout_session_address() {
- // Create initial session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update with address.
- $new_address = array(
- 'name' => 'Alice Johnson',
- 'line_one' => '123 Market Street',
- 'line_two' => 'Suite 200',
- 'city' => 'Los Angeles',
- 'state' => 'CA',
- 'country' => 'US',
- 'postal_code' => '90001',
- );
- $update_response = $this->update_session(
- $session_id,
- array(
- 'fulfillment_address' => $new_address,
- )
- );
-
- $update_data = $update_response->get_data();
-
- $this->assertEquals( 200, $update_response->get_status() );
- $this->assertEquals( 'Alice Johnson', $update_data['fulfillment_address']['name'] );
- $this->assertEquals( '123 Market Street', $update_data['fulfillment_address']['line_one'] );
- $this->assertEquals( 'Suite 200', $update_data['fulfillment_address']['line_two'] );
- $this->assertEquals( 'Los Angeles', $update_data['fulfillment_address']['city'] );
- $this->assertNotEmpty( $update_data['fulfillment_options'] ); // Should have shipping options.
- }
-
- /**
- * Test updating a checkout session with shipping method.
- */
- public function test_update_checkout_session_shipping_method() {
- // Create initial session with address.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Get first available shipping method.
- $this->assertNotEmpty( $create_data['fulfillment_options'] );
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- // Update with shipping method.
- $update_response = $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- $update_data = $update_response->get_data();
-
- $this->assertEquals( 200, $update_response->get_status() );
- $this->assertEquals( $shipping_method_id, $update_data['fulfillment_option_id'] );
- $this->assertEquals( 'ready_for_payment', $update_data['status'] ); // Should be ready with address + shipping.
- }
-
- /**
- * Test partial update preserves existing data.
- */
- public function test_update_checkout_session_partial_update() {
- // Create initial session with buyer and address.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'buyer' => $this->get_test_buyer(),
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update only buyer email, should preserve other fields.
- $update_response = $this->update_session(
- $session_id,
- array(
- 'buyer' => array(
- 'email' => 'newemail@example.com',
- ),
- )
- );
-
- $update_data = $update_response->get_data();
-
- $this->assertEquals( 200, $update_response->get_status() );
- // Email should be updated.
- $this->assertEquals( 'newemail@example.com', $update_data['buyer']['email'] );
- // Note: WooCommerce merges buyer data differently - partial updates may override all buyer fields.
- // This is implementation-specific behavior.
- $this->assertArrayHasKey( 'first_name', $update_data['buyer'] );
- $this->assertArrayHasKey( 'last_name', $update_data['buyer'] );
- // Address should be preserved.
- $this->assertEquals( 'John Doe', $update_data['fulfillment_address']['name'] );
- }
-
- /**
- * Test updating with invalid session ID returns error.
- */
- public function test_update_checkout_session_invalid_token() {
- $invalid_token = 'invalid.token.here';
-
- $update_response = $this->update_session(
- $invalid_token,
- array(
- 'buyer' => array(
- 'first_name' => 'Test',
- ),
- )
- );
-
- // Should return 404 when token is invalid (session not found).
- $this->assertEquals( 404, $update_response->get_status() );
- }
-
- /**
- * Test updating with empty request body succeeds.
- */
- public function test_update_checkout_session_empty_body() {
- // Create initial session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update with empty body (should just return current state).
- $update_response = $this->update_session( $session_id, array() );
-
- $this->assertEquals( 200, $update_response->get_status() );
- $update_data = $update_response->get_data();
- $this->assertEquals( $session_id, $update_data['id'] );
- }
-
- /**
- * Test session ID persists across multiple updates.
- */
- public function test_session_id_persists_across_updates() {
- // Create initial session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $original_id = $create_data['id'];
-
- // First update.
- $update1_response = $this->update_session(
- $original_id,
- array(
- 'buyer' => $this->get_test_buyer(),
- )
- );
-
- $update1_data = $update1_response->get_data();
-
- // Second update.
- $update2_response = $this->update_session(
- $original_id,
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- );
-
- $update2_data = $update2_response->get_data();
-
- // Session ID should remain the same across all updates.
- $this->assertEquals( $original_id, $update1_data['id'] );
- $this->assertEquals( $original_id, $update2_data['id'] );
- }
-
- /**
- * Test creating session with zero quantity returns error.
- */
- public function test_create_session_with_zero_quantity() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 0,
- ),
- ),
- )
- );
-
- $this->assertEquals( 400, $response->get_status() );
- }
-
- /**
- * Test creating session with negative quantity returns error.
- */
- public function test_create_session_with_negative_quantity() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => -1,
- ),
- ),
- )
- );
-
- $this->assertEquals( 400, $response->get_status() );
- }
-
- /**
- * Test creating session with invalid email format returns error.
- */
- public function test_create_session_with_invalid_email() {
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'buyer' => array(
- 'email' => 'not-an-email',
- ),
- )
- )
- );
-
- $this->assertEquals( 400, $response->get_status() );
- $data = $response->get_data();
- // Check error message contains reference to invalid parameter.
- $this->assertStringContainsString( 'buyer', strtolower( $data['message'] ) );
- }
-
- /**
- * Test creating session with invalid country code.
- */
- public function test_create_session_with_invalid_country() {
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => array(
- 'line_one' => '123 Test St',
- 'city' => 'Test City',
- 'country' => 'ZZ', // Use a valid ISO code that's not a real country.
- 'postal_code' => '12345',
- ),
- )
- )
- );
-
- // WooCommerce validates country codes - invalid ones should be rejected or normalized.
- $data = $response->get_data();
- if ( 200 === $response->get_status() ) {
- // If accepted, check that we have a valid country in the response.
- $this->assertArrayHasKey( 'fulfillment_address', $data );
- $this->assertArrayHasKey( 'country', $data['fulfillment_address'] );
- // Should be normalized to a valid 2-letter code.
- $this->assertEquals( 2, strlen( $data['fulfillment_address']['country'] ) );
- } else {
- // If rejected, should be 400 error.
- $this->assertEquals( 400, $response->get_status() );
- }
- }
-
- /**
- * Test creating session with duplicate items (same product ID twice).
- */
- public function test_create_session_with_duplicate_items() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 2,
- ),
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 3,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- if ( 200 === $response->get_status() ) {
- // Should combine quantities or handle duplicates gracefully.
- $this->assertCount( 1, $data['line_items'] );
- // Total quantity should be 5 (2 + 3).
- $this->assertEquals( 5, $data['line_items'][0]['item']['quantity'] );
- }
- }
-
- /**
- * Test creating session with mixed virtual and physical products.
- */
- public function test_create_session_with_mixed_products() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(), // Physical.
- 'quantity' => 1,
- ),
- array(
- 'id' => (string) $this->products[2]->get_id(), // Virtual.
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
- $this->assertCount( 2, $data['line_items'] );
- // Cart should need shipping due to physical product.
- $this->assertEquals( 'not_ready_for_payment', $data['status'] ); // No address provided.
- }
-
- /**
- * Test response headers include Idempotency-Key when provided.
- */
- public function test_response_headers_idempotency_key() {
- $idempotency_key = 'test-idempotency-123';
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $this->create_checkout_request() );
- $request->set_header( 'Idempotency-Key', $idempotency_key );
-
- $response = rest_get_server()->dispatch( $request );
- $headers = $response->get_headers();
-
- $this->assertArrayHasKey( 'Idempotency-Key', $headers );
- $this->assertEquals( $idempotency_key, $headers['Idempotency-Key'] );
- }
-
- /**
- * Test response headers include Request-Id when provided.
- */
- public function test_response_headers_request_id() {
- $request_id = 'req_' . uniqid();
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $this->create_checkout_request() );
- $request->set_header( 'Request-Id', $request_id );
-
- $response = rest_get_server()->dispatch( $request );
- $headers = $response->get_headers();
-
- $this->assertArrayHasKey( 'Request-Id', $headers );
- $this->assertEquals( $request_id, $headers['Request-Id'] );
- }
-
- /**
- * Test error response format matches ACP spec.
- */
- public function test_error_response_format() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => 'non-numeric-id',
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 400, $response->get_status() );
- $this->assertArrayHasKey( 'type', $data );
- $this->assertArrayHasKey( 'code', $data );
- $this->assertArrayHasKey( 'message', $data );
- $this->assertArrayHasKey( 'param', $data );
- // Param should use JSON path notation.
- $this->assertStringStartsWith( '$.', $data['param'] );
- }
-
- /**
- * Test shipping calculation for different addresses.
- */
- public function test_shipping_calculated_for_address() {
- // Create session with US address.
- $us_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $us_data = $us_response->get_data();
-
- $this->assertNotEmpty( $us_data['fulfillment_options'] );
-
- // Verify shipping option has required fields.
- $shipping_option = $us_data['fulfillment_options'][0];
- $this->assertArrayHasKey( 'type', $shipping_option );
- $this->assertArrayHasKey( 'id', $shipping_option );
- $this->assertArrayHasKey( 'title', $shipping_option );
- $this->assertArrayHasKey( 'subtotal', $shipping_option );
- $this->assertArrayHasKey( 'tax', $shipping_option );
- $this->assertArrayHasKey( 'total', $shipping_option );
- $this->assertEquals( 'shipping', $shipping_option['type'] );
- }
-
- /**
- * Test draft order is created and persists.
- */
- public function test_draft_order_created() {
- $response = $this->create_session( $this->create_checkout_request() );
- $data = $response->get_data();
-
- $this->assertEquals( 200, $response->get_status() );
-
- // Check that draft order ID is stored in session.
- // Note: The implementation may or may not create draft orders immediately.
- $draft_order_id = WC()->session->get( 'agentic_draft_order_id' );
-
- if ( $draft_order_id ) {
- $this->assertIsNumeric( $draft_order_id );
- // Verify order exists and is pending.
- $order = wc_get_order( $draft_order_id );
- $this->assertInstanceOf( \WC_Order::class, $order );
- $this->assertEquals( 'pending', $order->get_status() );
- } else {
- // Draft order creation may be deferred until address is provided.
- $this->assertNull( $draft_order_id );
- }
- }
-
- /**
- * Test concurrent session creation (each creates new session).
- */
- public function test_concurrent_session_creation() {
- // Create first session.
- $response1 = $this->create_session( $this->create_checkout_request() );
- $data1 = $response1->get_data();
- $session1 = $data1['id'];
-
- // Clear cart and session to simulate new session.
- wc_empty_cart();
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_SESSION_ID, null );
- WC()->session->set( 'agentic_draft_order_id', null );
-
- // Create second session.
- $response2 = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[1]->get_id(),
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data2 = $response2->get_data();
- $session2 = $data2['id'];
-
- // Note: The implementation persists session ID, so they may be the same.
- // This test documents the actual behavior rather than expected behavior.
- if ( $session1 === $session2 ) {
- // If sessions are the same, it means session ID is persisted.
- $this->assertEquals( $session1, $session2 );
- } else {
- // If different, new session was created.
- $this->assertNotEquals( $session1, $session2 );
- }
- }
-
- /**
- * Test non-numeric product ID returns proper error.
- */
- public function test_non_numeric_product_id() {
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => 'SKU-123', // Non-numeric ID.
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- $this->assertEquals( 400, $response->get_status() );
- $this->assertEquals( 'invalid_product_id', $data['code'] );
- $this->assertStringContainsString( 'numeric', $data['message'] );
- }
-
- /**
- * Test empty items array returns validation error.
- */
- public function test_empty_items_array() {
- $response = $this->create_session(
- array(
- 'items' => array(),
- )
- );
-
- $this->assertEquals( 400, $response->get_status() );
- }
-
- /**
- * Test missing items parameter returns validation error.
- */
- public function test_missing_items_parameter() {
- $response = $this->create_session(
- array(
- 'buyer' => $this->get_test_buyer(),
- )
- );
-
- $this->assertEquals( 400, $response->get_status() );
- }
-
- /**
- * Test that amounts are always in cents (integers).
- */
- public function test_amounts_in_cents() {
- $response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $data = $response->get_data();
-
- // Check line item amounts.
- foreach ( $data['line_items'] as $item ) {
- $this->assertIsInt( $item['base_amount'] );
- $this->assertIsInt( $item['discount'] );
- $this->assertIsInt( $item['subtotal'] );
- $this->assertIsInt( $item['tax'] );
- $this->assertIsInt( $item['total'] );
- }
-
- // Check totals amounts.
- foreach ( $data['totals'] as $total ) {
- $this->assertIsInt( $total['amount'] );
- }
-
- // Check fulfillment options amounts (may be strings in current implementation).
- foreach ( $data['fulfillment_options'] as $option ) {
- // Convert to int to verify they're numeric at least.
- $this->assertIsNumeric( $option['subtotal'] );
- $this->assertIsNumeric( $option['tax'] );
- $this->assertIsNumeric( $option['total'] );
- }
- }
-
- /**
- * Test calculate_status returns NOT_READY_FOR_PAYMENT when out_of_stock MessageError is present.
- */
- public function test_calculate_status_out_of_stock_returns_not_ready_for_payment() {
- // Create a product and set it out of stock.
- $fixtures = new FixtureData();
- $product = $fixtures->get_simple_product(
- array(
- 'name' => 'Out of Stock Product',
- 'stock_status' => ProductStockStatus::OUT_OF_STOCK,
- 'regular_price' => 10,
- )
- );
-
- // Add the out of stock product to cart.
- $response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $product->get_id(),
- 'quantity' => 1,
- ),
- ),
- )
- );
-
- $data = $response->get_data();
-
- // Should return NOT_READY_FOR_PAYMENT due to out of stock error.
- $this->assertEquals( 'not_ready_for_payment', $data['status'] );
-
- // Verify there are error messages with the right code.
- $this->assertTrue( $this->does_response_contain_error_message_with_code( $data, ErrorCode::OUT_OF_STOCK ) );
- }
-
- /**
- * Test calculate_status returns NOT_READY_FOR_PAYMENT when shipping address is missing.
- */
- public function test_calculate_status_missing_shipping_address_returns_not_ready_for_payment() {
- // Create session with physical product that needs shipping but no address.
- $data = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(), // Physical product.
- 'quantity' => 1,
- ),
- ),
- )
- )->get_data();
-
- // Should return NOT_READY_FOR_PAYMENT due to missing shipping address.
- $this->assertEquals( 'not_ready_for_payment', $data['status'] );
- // Verify there are error messages about missing shipping address.
- $this->assertTrue( $this->does_response_contain_error_message_with_code( $data, ErrorCode::MISSING ) );
- }
-
- /**
- * Test calculate_status returns NOT_READY_FOR_PAYMENT when shipping method is missing.
- * This test verifies that when we have a physical product that needs shipping,
- * but no shipping method is selected, the status should be NOT_READY_FOR_PAYMENT.
- */
- public function test_calculate_status_missing_shipping_method_returns_not_ready_for_payment() {
- // This should trigger both "missing shipping address" and "missing shipping method" errors.
- $data = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(), // Physical product.
- 'quantity' => 1,
- ),
- ),
- )
- )->get_data();
-
- // Should return NOT_READY_FOR_PAYMENT due to missing shipping address and method.
- $this->assertEquals( 'not_ready_for_payment', $data['status'] );
-
- // Verify there are error messages about missing shipping address and method.
- $this->assertTrue( $this->does_response_contain_error_message_with_code( $data, ErrorCode::MISSING ) );
- }
-
- /**
- * Check if the response contains an error message with the given code.
- *
- * @param array $response The response data.
- * @param string $code The error code to check for.
- * @return bool True if the response contains an error message with the given code, false otherwise.
- */
- private function does_response_contain_error_message_with_code( array $response, string $code ): bool {
- $this->assertNotEmpty( $response['messages'] );
- foreach ( $response['messages'] as $message ) {
- if ( 'error' === $message['type'] && $message['code'] === $code ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Test updating a completed session returns error.
- *
- * This tests the new status validation added in the complete-agentic-commerce branch.
- */
- public function test_update_completed_session_returns_error() {
- // Create a session and mark it as completed by setting the completed order ID in session.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Simulate a completed session by setting the completed order ID.
- // This makes the status calculation return 'completed'.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID, 123 );
-
- // Try to update the completed session.
- $update_response = $this->update_session(
- $session_id,
- array(
- 'buyer' => array(
- 'first_name' => 'Test',
- ),
- )
- );
-
- // Should return 400 error.
- $this->assertEquals( 400, $update_response->get_status() );
-
- // Verify error response format.
- $data = $update_response->get_data();
- $this->assertArrayHasKey( 'type', $data );
- $this->assertArrayHasKey( 'code', $data );
- $this->assertArrayHasKey( 'message', $data );
- $this->assertStringContainsString( 'cannot be updated', $data['message'] );
- }
-
- /**
- * Test that creating a checkout session without Jetpack authentication fails.
- */
- public function test_create_session_without_jetpack_auth_fails() {
- // Clear Jetpack authentication to simulate unauthenticated request.
- $this->mock_jetpack_auth_failure();
-
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $this->create_checkout_request() );
- $response = rest_get_server()->dispatch( $request );
-
- // Should return 401 error.
- $this->assertEquals( 401, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertArrayHasKey( 'code', $data );
- $this->assertEquals( 'rest_forbidden', $data['code'] );
- $this->assertStringContainsString( 'Jetpack blog token', $data['message'] );
- }
-
- /**
- * Test that updating a checkout session without Jetpack authentication fails.
- */
- public function test_update_session_without_jetpack_auth_fails() {
- // Create a session first with valid auth.
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Clear Jetpack authentication to simulate unauthenticated request.
- $this->mock_jetpack_auth_failure();
-
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id );
- $request->set_body_params(
- array(
- 'buyer' => array(
- 'first_name' => 'Test',
- ),
- )
- );
- $response = rest_get_server()->dispatch( $request );
-
- // Should return 401 error.
- $this->assertEquals( 401, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertArrayHasKey( 'code', $data );
- $this->assertEquals( 'rest_forbidden', $data['code'] );
- }
-
- /**
- * Test that Jetpack authentication works when re-established after failure.
- */
- public function test_jetpack_auth_recovery() {
- // First request without auth should fail.
- $this->mock_jetpack_auth_failure();
-
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $this->create_checkout_request() );
- $response = rest_get_server()->dispatch( $request );
-
- $this->assertEquals( 401, $response->get_status() );
-
- // Re-establish Jetpack authentication.
- $this->mock_jetpack_blog_token_auth();
-
- // Second request with auth should succeed.
- $response = $this->create_session( $this->create_checkout_request() );
- $this->assertEquals( 200, $response->get_status() );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php
deleted file mode 100644
index 99b81325157..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php
+++ /dev/null
@@ -1,1007 +0,0 @@
-<?php
-/**
- * Agentic Checkout Sessions Complete Tests.
- *
- * @package Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes
- */
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Automattic\WooCommerce\Enums\ProductStockStatus;
-use Automattic\WooCommerce\StoreApi\RoutesController;
-use Automattic\WooCommerce\Tests\Internal\Admin\Agentic\AgenticTestHelpers;
-
-/**
- * CheckoutSessionsComplete Controller Tests.
- */
-class CheckoutSessionsComplete extends ControllerTestCase {
- use AgenticTestHelpers;
-
- /**
- * Product IDs shared by the class.
- *
- * @var int[]
- */
- private static $product_ids = array();
-
- /**
- * Create immutable products shared by all test methods.
- */
- public static function wpSetUpBeforeClass(): void {
- $products = self::create_class_fixture_products(
- array(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- ),
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 20,
- 'weight' => 5,
- ),
- )
- );
-
- self::$product_ids = array_map( static fn( $product ) => $product->get_id(), $products );
- }
-
- /**
- * Delete class products through WooCommerce data stores.
- */
- public static function wpTearDownAfterClass(): void {
- self::delete_class_fixture_products( self::$product_ids );
- }
-
- /**
- * Products created for tests.
- *
- * @var array
- */
- protected $products = array();
-
- /**
- * Mock payment gateway instance.
- *
- * @var MockAgenticPaymentGateway
- */
- protected $mock_gateway;
-
- /**
- * Option state to restore after each test.
- *
- * @var array<string, array{exists: bool, value: mixed}>
- */
- private $option_state = array();
-
- /**
- * Setup test product data. Called before every test.
- */
- protected function setUp(): void {
- parent::setUp();
- $this->snapshot_option_state( 'woocommerce_checkout_phone_field' );
- $this->snapshot_option_state( 'woocommerce_feature_agentic_checkout_enabled' );
- update_option( 'woocommerce_checkout_phone_field', 'optional' );
-
- // Reset customer and cart FIRST before anything else.
- wc_empty_cart();
- $this->reset_customer_state();
-
- // Clear all session data early to ensure clean state.
- if ( WC()->session ) {
- WC()->session->destroy_session();
- }
-
- // Enable the agentic_checkout feature.
- update_option( 'woocommerce_feature_agentic_checkout_enabled', 'yes' );
-
- // Set up Jetpack blog token authentication.
- $this->mock_jetpack_blog_token_auth();
-
- $fixtures = new FixtureData();
- $fixtures->shipping_add_flat_rate();
-
- $this->products = array_map( 'wc_get_product', self::$product_ids );
-
- // Register mock agentic payment gateway.
- $this->mock_gateway = new MockAgenticPaymentGateway();
- add_filter( 'woocommerce_payment_gateways', array( $this, 'add_mock_gateway' ) );
- add_filter( 'woocommerce_available_payment_gateways', array( $this, 'add_mock_gateway' ) );
-
- wc_get_container()->get( RoutesController::class )->register_all_routes();
- }
-
- /**
- * Tear down test.
- */
- protected function tearDown(): void {
- try {
- // Clear session data.
- WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, null );
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_SESSION_ID, null );
-
- // Reset customer state to clean state.
- $this->reset_customer_state();
-
- // Reset Jetpack auth state.
- $this->reset_jetpack_auth_state();
-
- } finally {
- remove_filter( 'woocommerce_payment_gateways', array( $this, 'add_mock_gateway' ) );
- remove_filter( 'woocommerce_available_payment_gateways', array( $this, 'add_mock_gateway' ) );
- try {
- parent::tearDown();
- } finally {
- $this->restore_option_state();
- }
- }
- }
-
- /**
- * Capture an option's exact existence and value before changing it.
- *
- * @param string $option_name Option name.
- */
- private function snapshot_option_state( string $option_name ): void {
- $missing_option = new \stdClass();
- $value = get_option( $option_name, $missing_option );
-
- $this->option_state[ $option_name ] = array(
- 'exists' => $missing_option !== $value,
- 'value' => $value,
- );
- }
-
- /**
- * Restore options changed by the test, including their original absence.
- */
- private function restore_option_state(): void {
- foreach ( $this->option_state as $option_name => $state ) {
- wp_cache_delete( $option_name, 'options' );
- wp_cache_delete( 'alloptions', 'options' );
- wp_cache_delete( 'notoptions', 'options' );
- if ( $state['exists'] ) {
- update_option( $option_name, $state['value'] );
- } else {
- delete_option( $option_name );
- }
- }
-
- $this->option_state = array();
- }
-
- /**
- * Add mock gateway to available gateways.
- *
- * @param array $gateways Existing gateways.
- * @return array Modified gateways.
- */
- public function add_mock_gateway( $gateways ) {
- $gateways[ MockAgenticPaymentGateway::GATEWAY_ID ] = $this->mock_gateway;
- return $gateways;
- }
-
- /**
- * Resets customer state and remove any existing data from previous tests.
- */
- private function reset_customer_state() {
- // Clear all customer data fields.
- $customer = WC()->customer;
-
- // Clear billing fields.
- $customer->set_billing_first_name( '' );
- $customer->set_billing_last_name( '' );
- $customer->set_billing_company( '' );
- $customer->set_billing_address_1( '' );
- $customer->set_billing_address_2( '' );
- $customer->set_billing_city( '' );
- $customer->set_billing_state( '' );
- $customer->set_billing_postcode( '' );
- $customer->set_billing_country( '' );
- $customer->set_billing_email( '' );
- $customer->set_billing_phone( '' );
-
- // Clear shipping fields.
- $customer->set_shipping_first_name( '' );
- $customer->set_shipping_last_name( '' );
- $customer->set_shipping_company( '' );
- $customer->set_shipping_address_1( '' );
- $customer->set_shipping_address_2( '' );
- $customer->set_shipping_city( '' );
- $customer->set_shipping_state( '' );
- $customer->set_shipping_postcode( '' );
- $customer->set_shipping_country( '' );
-
- $customer->save();
- }
-
- /**
- * Helper: Create base checkout session request data.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Request data.
- */
- private function create_checkout_request( $overrides = array() ) {
- $defaults = array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 1,
- ),
- ),
- );
- return array_merge_recursive( $defaults, $overrides );
- }
-
- /**
- * Helper: Get test fulfillment address.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Address data.
- */
- private function get_test_address( $overrides = array() ) {
- $defaults = array(
- 'name' => 'John Doe',
- 'line_one' => '555 Golden Gate Avenue',
- 'line_two' => '',
- 'city' => 'San Francisco',
- 'state' => 'CA',
- 'country' => 'US',
- 'postal_code' => '94102',
- );
- return array_merge( $defaults, $overrides );
- }
-
- /**
- * Helper: Get test buyer information.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Buyer data.
- */
- private function get_test_buyer( $overrides = array() ) {
- $defaults = array(
- 'first_name' => 'Jane',
- 'last_name' => 'Smith',
- 'email' => 'jane@example.com',
- 'phone_number' => '+1234567890',
- );
- return array_merge( $defaults, $overrides );
- }
-
- /**
- * Helper: Create and dispatch a checkout session request.
- *
- * @param array $body_params Request body parameters.
- * @return \WP_REST_Response Response object.
- */
- private function create_session( $body_params ) {
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions' );
- $request->set_body_params( $body_params );
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Helper: Update an existing checkout session.
- *
- * @param string $session_id The session ID (Cart-Token).
- * @param array $body_params Request body parameters.
- * @return \WP_REST_Response Response object.
- */
- private function update_session( $session_id, $body_params ) {
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id );
- $request->set_body_params( $body_params );
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Helper: Complete a checkout session.
- *
- * @param string $session_id The session ID (Cart-Token).
- * @param array $body_params Request body parameters.
- * @return \WP_REST_Response Response object.
- */
- private function complete_session( $session_id, $body_params ) {
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id . '/complete' );
- $request->set_body_params( $body_params );
- return rest_get_server()->dispatch( $request );
- }
-
- /**
- * Helper: Get payment data for completing checkout.
- *
- * @param array $overrides Optional array to override default values.
- * @return array Payment data.
- */
- private function get_payment_data( $overrides = array() ) {
- $defaults = array(
- 'token' => 'spt_test_123456789',
- 'provider' => 'stripe',
- );
- return array_merge( $defaults, $overrides );
- }
-
- /**
- * Test completing a checkout session successfully.
- */
- public function test_complete_checkout_session_success() {
- $create_response = $this->create_session(
- array(
- 'items' => array(
- array(
- 'id' => (string) $this->products[0]->get_id(),
- 'quantity' => 2,
- ),
- array(
- 'id' => (string) $this->products[1]->get_id(),
- 'quantity' => 1,
- ),
- ),
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Update with shipping method to make it ready for payment.
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Complete the checkout.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $complete_data = $complete_response->get_data();
-
- // Verify successful completion.
- $this->assertEquals( 200, $complete_response->get_status() );
-
- $this->assertEquals( 'completed', $complete_data['status'] );
- $this->assertArrayHasKey( 'id', $complete_data );
- $this->assertArrayHasKey( 'order', $complete_data );
-
- // Verify order object structure matches schema.
- $order_data = $complete_data['order'];
- $this->assertArrayHasKey( 'id', $order_data );
- $this->assertArrayHasKey( 'checkout_session_id', $order_data );
- $this->assertArrayHasKey( 'permalink_url', $order_data );
- $this->assertEquals( $session_id, $order_data['checkout_session_id'] );
- $this->assertIsString( $order_data['id'] );
- $this->assertIsString( $order_data['permalink_url'] );
-
- // Verify order was created with correct items.
- $this->assertCount( 2, $complete_data['line_items'] );
- $this->assertEquals( (string) $this->products[0]->get_id(), $complete_data['line_items'][0]['item']['id'] );
- $this->assertEquals( 2, $complete_data['line_items'][0]['item']['quantity'] );
-
- // Verify order ID is stored in session.
- $stored_order_id = WC()->session->get( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID );
- $this->assertNotNull( $stored_order_id );
- $this->assertIsNumeric( $stored_order_id );
- $this->assertEquals( $order_data['id'], (string) $stored_order_id );
-
- // Verify order exists in database.
- $order = wc_get_order( $stored_order_id );
- $this->assertInstanceOf( \WC_Order::class, $order );
- }
-
- /**
- * Test completing checkout without ready_for_payment status fails.
- */
- public function test_complete_checkout_session_not_ready() {
- // Create session without address (not ready for payment).
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Try to complete checkout.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 400, $complete_response->get_status() );
- $complete_data = $complete_response->get_data();
- $this->assertArrayHasKey( 'code', $complete_data );
- $this->assertStringContainsString( 'not ready for payment', strtolower( $complete_data['message'] ) );
- }
-
- /**
- * Test completing checkout with invalid session ID fails.
- */
- public function test_complete_checkout_session_invalid_id() {
- $invalid_session_id = 'invalid.token.here';
-
- $complete_response = $this->complete_session(
- $invalid_session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 404, $complete_response->get_status() );
- }
-
- /**
- * Test completing checkout without payment data fails.
- */
- public function test_complete_checkout_session_missing_payment_data() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Try to complete without payment_data.
- $complete_response = $this->complete_session( $session_id, array() );
-
- $this->assertEquals( 400, $complete_response->get_status() );
- }
-
- /**
- * Test completing checkout with missing payment token fails.
- */
- public function test_complete_checkout_session_missing_token() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Try to complete with payment_data missing token.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => array(
- 'provider' => 'stripe',
- ),
- )
- );
-
- $this->assertEquals( 400, $complete_response->get_status() );
- }
-
- /**
- * Test completing checkout with missing payment provider fails.
- */
- public function test_complete_checkout_session_missing_provider() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Try to complete with payment_data missing provider.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => array(
- 'token' => 'tok_test_123',
- ),
- )
- );
-
- $this->assertEquals( 400, $complete_response->get_status() );
- }
-
- /**
- * Test completing checkout with billing address in payment_data.
- */
- public function test_complete_checkout_session_with_billing_address() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Complete with billing address.
- $billing_address = $this->get_test_address(
- array(
- 'name' => 'Billing Name',
- 'city' => 'Los Angeles',
- )
- );
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => array_merge(
- $this->get_payment_data(),
- array(
- 'billing_address' => $billing_address,
- )
- ),
- )
- );
-
- $this->assertEquals( 200, $complete_response->get_status() );
-
- // If payment succeeded, verify order was created with billing address.
- $complete_data = $complete_response->get_data();
- if ( isset( $complete_data['order']['id'] ) ) {
- $order = wc_get_order( $complete_data['order']['id'] );
- $this->assertEquals( 'Los Angeles', $order->get_billing_city() );
- }
- }
-
- /**
- * Test completing checkout with buyer info updates order.
- */
- public function test_complete_checkout_session_with_buyer_info() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Complete with buyer info.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'buyer' => $this->get_test_buyer(),
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 200, $complete_response->get_status() );
-
- // Verify buyer info in response.
- $complete_data = $complete_response->get_data();
- $this->assertEquals( 'Jane', $complete_data['buyer']['first_name'] );
- $this->assertEquals( 'Smith', $complete_data['buyer']['last_name'] );
- $this->assertEquals( 'jane@example.com', $complete_data['buyer']['email'] );
- }
-
- /**
- * Test completing checkout reserves stock.
- */
- public function test_complete_checkout_session_reserves_stock() {
- // Set product to have limited stock.
- $this->products[0]->set_manage_stock( true );
- $this->products[0]->set_stock_quantity( 5 );
- $this->products[0]->save();
-
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Complete checkout.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 200, $complete_response->get_status() );
-
- // If payment succeeded, verify stock was reserved.
- $complete_data = $complete_response->get_data();
- if ( isset( $complete_data['order']['id'] ) ) {
- $order = wc_get_order( $complete_data['order']['id'] );
- $this->assertInstanceOf( \WC_Order::class, $order );
- // Stock should be reserved/reduced.
- $this->assertGreaterThan( 0, $order->get_item_count() );
- }
- }
-
- /**
- * Test completing checkout with insufficient stock fails.
- */
- public function test_complete_checkout_session_out_of_stock() {
- // Create session with address and buyer.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Set product to have insufficient stock.
- $this->products[0]->set_manage_stock( true );
- $this->products[0]->set_stock_quantity( 0 );
- $this->products[0]->save();
-
- // Try to complete checkout - this should fail during validation.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- // Complete should fail due to stock validation.
- $this->assertEquals( 400, $complete_response->get_status() );
-
- // Verify error response contains stock-related message.
- $complete_data = $complete_response->get_data();
- $this->assertArrayHasKey( 'type', $complete_data );
- $this->assertEquals( 'invalid_request', $complete_data['type'] );
- $this->assertArrayHasKey( 'code', $complete_data );
- $this->assertEquals( 'invalid', $complete_data['code'] );
- $this->assertArrayHasKey( 'message', $complete_data );
- $this->assertStringContainsString( 'out of stock', strtolower( $complete_data['message'] ) );
- }
-
- /**
- * Test error response format matches ACP spec.
- */
- public function test_complete_error_response_format() {
- // Create session without address (not ready).
- $create_response = $this->create_session( $this->create_checkout_request() );
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
-
- // Try to complete.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $data = $complete_response->get_data();
-
- $this->assertEquals( 400, $complete_response->get_status() );
- $this->assertArrayHasKey( 'type', $data );
- $this->assertArrayHasKey( 'code', $data );
- $this->assertArrayHasKey( 'message', $data );
- }
-
- /**
- * Test completing checkout calculates totals before validation.
- */
- public function test_complete_checkout_session_calculates_totals() {
- // Create ready-for-payment session.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Complete checkout.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 200, $complete_response->get_status() );
-
- // If payment succeeded, verify totals are present and calculated.
- $complete_data = $complete_response->get_data();
- $this->assertArrayHasKey( 'totals', $complete_data );
- $this->assertNotEmpty( $complete_data['totals'] );
-
- // Verify total amount is greater than 0.
- $total_obj = array_filter(
- $complete_data['totals'],
- function ( $total ) {
- return 'total' === $total['type'];
- }
- );
- $this->assertNotEmpty( $total_obj );
- $total = reset( $total_obj );
- $this->assertGreaterThan( 0, $total['amount'] );
- }
-
- /**
- * Test that completing a checkout session without Jetpack authentication fails.
- */
- public function test_complete_session_without_jetpack_auth_fails() {
- // Create a ready-for-payment session first with valid auth.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Clear Jetpack authentication to simulate unauthenticated request.
- $this->mock_jetpack_auth_failure();
-
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id . '/complete' );
- $request->set_body_params(
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
- $response = rest_get_server()->dispatch( $request );
-
- // Should return 401 error.
- $this->assertEquals( 401, $response->get_status() );
-
- $data = $response->get_data();
- $this->assertArrayHasKey( 'code', $data );
- $this->assertEquals( 'rest_forbidden', $data['code'] );
- $this->assertStringContainsString( 'Jetpack blog token', $data['message'] );
- }
-
- /**
- * Test that checkout completion works after Jetpack auth is re-established.
- */
- public function test_complete_session_jetpack_auth_recovery() {
- // Create a ready-for-payment session first with valid auth.
- $create_response = $this->create_session(
- $this->create_checkout_request(
- array(
- 'fulfillment_address' => $this->get_test_address(),
- 'buyer' => $this->get_test_buyer(),
- )
- )
- );
-
- $create_data = $create_response->get_data();
- $session_id = $create_data['id'];
- $shipping_method_id = $create_data['fulfillment_options'][0]['id'];
-
- $this->update_session(
- $session_id,
- array(
- 'fulfillment_option_id' => $shipping_method_id,
- )
- );
-
- // Clear Jetpack authentication - complete should fail.
- $this->mock_jetpack_auth_failure();
-
- $request = new \WP_REST_Request( 'POST', '/wc/agentic/v1/checkout_sessions/' . $session_id . '/complete' );
- $request->set_body_params(
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
- $response = rest_get_server()->dispatch( $request );
-
- $this->assertEquals( 401, $response->get_status() );
-
- // Re-establish Jetpack authentication.
- $this->mock_jetpack_blog_token_auth();
-
- // Complete should now succeed.
- $complete_response = $this->complete_session(
- $session_id,
- array(
- 'payment_data' => $this->get_payment_data(),
- )
- );
-
- $this->assertEquals( 200, $complete_response->get_status() );
- $complete_data = $complete_response->get_data();
- $this->assertEquals( 'completed', $complete_data['status'] );
- }
-}
-
-// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound, Squiz.Classes.ClassFileName.NoMatch, SlevomatCodingStandard.Files.TypeNameMatchesFileName.NoMatchBetweenTypeNameAndFileName
-
-/**
- * Mock Agentic Payment Gateway for testing.
- *
- * This gateway supports the agentic_commerce feature and is used
- * in CheckoutSessionsComplete tests.
- */
-class MockAgenticPaymentGateway extends \WC_Payment_Gateway {
- public const GATEWAY_ID = 'mock_agentic_payment_gateway';
- /**
- * Constructor for the gateway.
- */
- public function __construct() {
- $this->enabled = 'yes';
- $this->id = self::GATEWAY_ID;
- $this->has_fields = false;
- $this->method_title = 'Mock Agentic Gateway';
- $this->method_description = 'Mock Gateway for agentic commerce testing';
- $this->supports = array(
- \Automattic\WooCommerce\Enums\PaymentGatewayFeature::PRODUCTS,
- \Automattic\WooCommerce\Enums\PaymentGatewayFeature::AGENTIC_COMMERCE,
- );
-
- $this->init_form_fields();
- $this->init_settings();
- }
-
- /**
- * Initialise Gateway Settings Form Fields.
- */
- public function init_form_fields() {
- $this->form_fields = array(
- 'enabled' => array(
- 'title' => '',
- 'type' => 'checkbox',
- 'label' => '',
- 'default' => 'yes',
- ),
- );
- }
-
- /**
- * Get the agentic commerce provider name.
- *
- * @return string Provider name.
- */
- public function get_agentic_commerce_provider() {
- return 'stripe';
- }
-
- /**
- * Get supported payment methods for agentic commerce.
- *
- * @return array List of supported payment methods.
- */
- public function get_agentic_commerce_payment_methods() {
- return array( 'card' );
- }
-
- /**
- * Process payment for agentic commerce.
- *
- * @param int $order_id Order ID.
- * @return array Payment result.
- */
- public function process_payment( $order_id ) {
- $order = wc_get_order( $order_id );
-
- // Simulate successful payment processing.
- $order->payment_complete();
- $order->add_order_note( 'Mock agentic payment completed successfully.' );
-
- return array(
- 'result' => 'success',
- 'redirect' => $this->get_return_url( $order ),
- );
- }
-
- /**
- * Validate fields before processing payment.
- *
- * @return bool Whether fields are valid.
- */
- public function validate_fields() {
- return true;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/AgenticCheckoutUtilsTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/AgenticCheckoutUtilsTest.php
deleted file mode 100644
index e63bfb9df2d..00000000000
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/AgenticCheckoutUtilsTest.php
+++ /dev/null
@@ -1,256 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Utilities;
-
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\AgenticCheckoutSession;
-use Automattic\WooCommerce\StoreApi\Utilities\AgenticCheckoutUtils;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\SessionKey;
-use Automattic\WooCommerce\Internal\Agentic\Enums\Specs\CheckoutSessionStatus;
-use Automattic\WooCommerce\Tests\Internal\Admin\Agentic\AgenticTestHelpers;
-
-/**
- * Tests for AgenticCheckoutUtils class.
- */
-class AgenticCheckoutUtilsTest extends \WC_Unit_Test_Case {
- use AgenticTestHelpers;
-
- /**
- * Setup cart and session data.
- */
- public function setUp(): void {
- parent::setUp();
-
- // Reset cart FIRST before anything else.
- wc_empty_cart();
-
- // Clear all session data early to ensure clean state.
- if ( WC()->session ) {
- WC()->session->destroy_session();
- }
- }
-
- /**
- * Tear down test.
- */
- public function tearDown(): void {
- parent::tearDown();
-
- // Clear session data.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, null );
-
- // Reset Jetpack auth state.
- $this->reset_jetpack_auth_state();
- }
-
- /**
- * Test that calculate_status returns IN_PROGRESS when payment is in progress.
- */
- public function test_calculate_status_returns_in_progress_when_payment_in_progress() {
- $checkout_session = new AgenticCheckoutSession( WC()->cart );
- $cart = $checkout_session->get_cart();
-
- // Add a product to the cart.
- $product = \WC_Helper_Product::create_simple_product();
- $cart->add_to_cart( $product->get_id(), 1 );
-
- // Set the payment in progress flag.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, true );
-
- // Calculate status.
- $status = AgenticCheckoutUtils::calculate_status( $checkout_session );
-
- // Assert that status is IN_PROGRESS.
- $this->assertEquals( CheckoutSessionStatus::IN_PROGRESS, $status );
- }
-
- /**
- * Test that calculate_status returns COMPLETED when order is completed.
- */
- public function test_calculate_status_returns_completed_when_order_completed() {
- $checkout_session = new AgenticCheckoutSession( WC()->cart );
- $cart = $checkout_session->get_cart();
-
- // Set completed order ID.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_COMPLETED_ORDER_ID, 123 );
-
- // Calculate status.
- $status = AgenticCheckoutUtils::calculate_status( $checkout_session );
-
- // Assert that status is COMPLETED (takes precedence over IN_PROGRESS).
- $this->assertEquals( CheckoutSessionStatus::COMPLETED, $status );
- }
-
- /**
- * Test that IN_PROGRESS status has correct priority even though cart is otherwise ready.
- */
- public function test_in_progress_status_priority() {
- $checkout_session = new AgenticCheckoutSession( WC()->cart );
- $cart = $checkout_session->get_cart();
-
- // Add a product to the cart.
- $product = \WC_Helper_Product::create_simple_product();
- $cart->add_to_cart( $product->get_id(), 1 );
-
- // Set up shipping.
- WC()->customer->set_shipping_country( 'US' );
- WC()->customer->set_shipping_state( 'CA' );
- WC()->customer->set_shipping_postcode( '90210' );
- WC()->customer->set_shipping_city( 'Los Angeles' );
- WC()->customer->set_shipping_address_1( '123 Main St' );
-
- // Set payment in progress flag.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, true );
-
- // Calculate status - should be IN_PROGRESS even though cart is otherwise ready.
- $status = AgenticCheckoutUtils::calculate_status( $checkout_session );
-
- // Assert that status is IN_PROGRESS.
- $this->assertEquals( CheckoutSessionStatus::IN_PROGRESS, $status );
- }
-
- /**
- * Test that calculate_status returns READY_FOR_PAYMENT after IN_PROGRESS flag is cleared.
- */
- public function test_calculate_status_ready_after_in_progress_cleared() {
- // Set up cart and session.
- $checkout_session = new AgenticCheckoutSession( WC()->cart );
- $cart = $checkout_session->get_cart();
- $cart->empty_cart();
-
- // Add a product to the cart.
- $product = \WC_Helper_Product::create_simple_product();
- $cart->add_to_cart( $product->get_id(), 1 );
-
- WC()->customer->set_shipping_country( 'US' );
- WC()->customer->set_shipping_state( 'CA' );
- WC()->customer->set_shipping_postcode( '90210' );
- WC()->customer->set_shipping_city( 'Los Angeles' );
- WC()->customer->set_shipping_address_1( '123 Main St' );
- WC()->customer->save();
-
- // Set chosen shipping method.
- WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, array( 'flat_rate' ) );
-
- // First, set IN_PROGRESS flag.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, true );
- $status = AgenticCheckoutUtils::calculate_status( $checkout_session );
- $this->assertEquals( CheckoutSessionStatus::IN_PROGRESS, $status );
-
- // Clear the IN_PROGRESS flag.
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_PAYMENT_IN_PROGRESS, false );
-
- // Recalculate status - should be READY_FOR_PAYMENT now.
- $status = AgenticCheckoutUtils::calculate_status( $checkout_session );
-
- // Assert that status is READY_FOR_PAYMENT after clearing IN_PROGRESS.
- $this->assertNotEquals( CheckoutSessionStatus::IN_PROGRESS, $status );
- }
-
- /**
- * Test validate_jetpack_request returns error when Jetpack authentication is not present.
- */
- public function test_validate_jetpack_request_returns_error_without_authentication() {
- // Ensure no Jetpack authentication is set.
- $this->mock_jetpack_auth_failure();
-
- // Test validation.
- $result = AgenticCheckoutUtils::validate_jetpack_request();
-
- // Assert authorization fails with 401 error.
- $this->assertWPError( $result );
- $this->assertEquals( 'rest_forbidden', $result->get_error_code() );
- $this->assertEquals( 401, $result->get_error_data()['status'] );
- $this->assertStringContainsString( 'Jetpack blog token', $result->get_error_message() );
- }
-
- /**
- * Test validate_jetpack_request returns true with valid blog token authentication.
- */
- public function test_validate_jetpack_request_returns_true_with_valid_blog_token() {
- // Mock successful Jetpack blog token authentication.
- $this->mock_jetpack_blog_token_auth();
-
- // Test validation.
- $result = AgenticCheckoutUtils::validate_jetpack_request();
-
- // Assert authorization succeeds.
- $this->assertTrue( $result );
- }
-
- /**
- * Test validate_jetpack_request error message is user-friendly.
- */
- public function test_validate_jetpack_request_error_message() {
- // Ensure no Jetpack authentication is set.
- $this->mock_jetpack_auth_failure();
-
- // Test validation.
- $result = AgenticCheckoutUtils::validate_jetpack_request();
-
- // Assert the error message is helpful for debugging.
- $this->assertWPError( $result );
- $this->assertEquals(
- 'This endpoint requires Jetpack blog token authentication.',
- $result->get_error_message()
- );
- }
-
- /**
- * @testdox Should preserve backslashes in buyer data, which arrives JSON-decoded and is never magic-quoted.
- */
- public function test_set_buyer_data_preserves_backslashes() {
- $customer = WC()->customer;
-
- AgenticCheckoutUtils::set_buyer_data(
- array(
- 'first_name' => 'C:\\Users',
- 'last_name' => 'O\\Brien',
- ),
- $customer
- );
-
- $this->assertSame( 'C:\\Users', $customer->get_billing_first_name(), 'Backslashes in first name should be preserved.' );
- $this->assertSame( 'O\\Brien', $customer->get_billing_last_name(), 'Backslashes in last name should be preserved.' );
- }
-
- /**
- * @testdox Should preserve backslashes in fulfillment address fields.
- */
- public function test_set_fulfillment_address_preserves_backslashes() {
- $customer = WC()->customer;
-
- AgenticCheckoutUtils::set_fulfillment_address(
- array(
- 'name' => 'O\\Brien Family',
- 'line_one' => 'Apt 4\\B',
- 'city' => 'Düsseldorf\\Nord',
- 'country' => 'US',
- 'postal_code' => '90210',
- ),
- $customer
- );
-
- $this->assertSame( 'O\\Brien', $customer->get_shipping_first_name(), 'Backslashes in the parsed first name should be preserved.' );
- $this->assertSame( 'Apt 4\\B', $customer->get_shipping_address_1(), 'Backslashes in address line should be preserved.' );
- $this->assertSame( 'Düsseldorf\\Nord', $customer->get_shipping_city(), 'Backslashes in city should be preserved.' );
- }
-
- /**
- * @testdox Should preserve backslashes in billing address fields.
- */
- public function test_set_billing_address_preserves_backslashes() {
- $customer = WC()->customer;
-
- AgenticCheckoutUtils::set_billing_address(
- array(
- 'name' => 'O\\Brien Family',
- 'line_one' => 'Apt 4\\B',
- ),
- $customer
- );
-
- $this->assertSame( 'O\\Brien', $customer->get_billing_first_name(), 'Backslashes in the parsed first name should be preserved.' );
- $this->assertSame( 'Apt 4\\B', $customer->get_billing_address_1(), 'Backslashes in address line should be preserved.' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticControllerTest.php
deleted file mode 100644
index 94d5ac2c149..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticControllerTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Admin\Agentic;
-
-use Automattic\WooCommerce\Internal\Admin\Agentic\AgenticController;
-use Automattic\WooCommerce\Internal\Admin\Agentic\AgenticWebhookManager;
-
-/**
- * Tests for AgenticController class.
- */
-class AgenticControllerTest extends \WC_Unit_Test_Case {
- /**
- * Test that controller initializes webhook manager.
- */
- public function test_register_initializes_webhook_manager() {
- // Enable the agentic checkout feature.
- update_option( 'woocommerce_feature_agentic_checkout_enabled', 'yes' );
-
- // Ensure WC_INSTALLING is not set during the test.
- if ( defined( 'WC_INSTALLING' ) ) {
- \Automattic\Jetpack\Constants::set_constant( 'WC_INSTALLING', false );
- }
-
- // Resolve controller from container to ensure proper DI.
- $controller = wc_get_container()->get( AgenticController::class );
- $controller->register();
-
- // Call on_init directly to initialize the webhook manager.
- $controller->on_init();
-
- /**
- * Verify webhook topics are registered (indicates manager was initialized).
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::register_webhook_topic_names()
- */
- $topics = apply_filters( 'woocommerce_webhook_topics', array() );
- $this->assertArrayHasKey( AgenticWebhookManager::WEBHOOK_TOPIC, $topics );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticTestHelpers.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticTestHelpers.php
deleted file mode 100644
index b6d9b7b304a..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticTestHelpers.php
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Admin\Agentic;
-
-use Automattic\Jetpack\Connection\Manager as JetpackConnectionManager;
-use Automattic\Jetpack\Connection\Rest_Authentication as JetpackRestAuthentication;
-use Automattic\WooCommerce\Internal\Admin\Agentic\AgenticWebhookManager;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey;
-use WC_Order;
-use WC_Webhook;
-
-/**
- * Shared test helpers for Agentic tests.
- */
-trait AgenticTestHelpers {
-
- /**
- * Mock for Jetpack Connection Manager.
- *
- * @var JetpackConnectionManager|\PHPUnit\Framework\MockObject\MockObject
- */
- protected $jetpack_manager_mock;
-
- /**
- * Clear the Jetpack REST_Authentication singleton instance.
- *
- * This allows tests to reset the authentication state between test cases.
- */
- protected function clear_jetpack_auth_singleton(): void {
- if ( ! class_exists( JetpackRestAuthentication::class ) ) {
- return;
- }
-
- $reflection_class = new \ReflectionClass( JetpackRestAuthentication::class );
- $instance_property = $reflection_class->getProperty( 'instance' );
- $instance_property->setAccessible( true );
- $instance_property->setValue( null, null );
- }
-
- /**
- * Set up Jetpack authentication to simulate a valid blog token request.
- *
- * This mocks the Jetpack Connection Manager's verify_xml_rpc_signature method
- * to return a successful blog token verification result.
- */
- protected function mock_jetpack_blog_token_auth(): void {
- if ( ! class_exists( JetpackRestAuthentication::class ) ) {
- $this->markTestSkipped( 'Jetpack Connection package not available.' );
- return;
- }
-
- // Clear any existing singleton.
- $this->clear_jetpack_auth_singleton();
-
- // Initialize the REST Authentication singleton.
- $rest_auth = JetpackRestAuthentication::init();
-
- // Create a mock for the Connection Manager.
- $this->jetpack_manager_mock = $this->getMockBuilder( JetpackConnectionManager::class )
- ->disableOriginalConstructor()
- ->onlyMethods( array( 'verify_xml_rpc_signature', 'reset_saved_auth_state' ) )
- ->getMock();
-
- // Configure the mock to return successful blog token verification.
- $this->jetpack_manager_mock->expects( $this->any() )
- ->method( 'verify_xml_rpc_signature' )
- ->willReturn(
- array(
- 'type' => 'blog',
- 'token_key' => 'test_blog_token',
- 'user_id' => 0,
- )
- );
-
- $this->jetpack_manager_mock->expects( $this->any() )
- ->method( 'reset_saved_auth_state' )
- ->willReturn( null );
-
- // Inject the mock into the REST Authentication instance using reflection.
- $reflection_class = new \ReflectionClass( JetpackRestAuthentication::class );
- $manager_property = $reflection_class->getProperty( 'connection_manager' );
- $manager_property->setAccessible( true );
- $manager_property->setValue( $rest_auth, $this->jetpack_manager_mock );
-
- // Set up the $_GET parameters and $_SERVER to simulate a Jetpack-signed request.
- $_GET['_for'] = 'jetpack';
- $_GET['token'] = 'test_token';
- $_GET['signature'] = 'test_signature';
- $_SERVER['REQUEST_METHOD'] = 'POST';
-
- // Trigger the authentication.
- $rest_auth->wp_rest_authenticate( '' );
- }
-
- /**
- * Set up Jetpack authentication to simulate a failed/missing authentication.
- *
- * This ensures that is_signed_with_blog_token() returns false.
- */
- protected function mock_jetpack_auth_failure(): void {
- if ( ! class_exists( JetpackRestAuthentication::class ) ) {
- return;
- }
-
- // Clear any existing singleton - this ensures no authentication is set.
- $this->clear_jetpack_auth_singleton();
-
- // Clear Jetpack-related $_GET parameters.
- unset( $_GET['_for'], $_GET['token'], $_GET['signature'] );
- }
-
- /**
- * Reset Jetpack authentication state after tests.
- *
- * Call this in tearDown() to ensure clean state between tests.
- */
- protected function reset_jetpack_auth_state(): void {
- if ( class_exists( JetpackRestAuthentication::class ) ) {
- $this->clear_jetpack_auth_singleton();
- }
-
- // Clear Jetpack-related $_GET parameters.
- unset( $_GET['_for'], $_GET['token'], $_GET['signature'] );
-
- // Reset request method if it was set.
- unset( $_SERVER['REQUEST_METHOD'] );
- }
- /**
- * Create an order with Agentic session ID.
- *
- * @param string $session_id Session ID to use. Defaults to 'test_session_123'.
- * @param string $status Order status. Defaults to 'pending'.
- * @return WC_Order Created order.
- */
- protected function create_agentic_order( $session_id = 'test_session_123', $status = 'pending' ) {
- $order = \WC_Helper_Order::create_order();
- $order->update_meta_data( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID, $session_id );
- $order->set_status( $status );
- $order->save();
- return $order;
- }
-
- /**
- * Add webhook sent meta to order.
- *
- * @param WC_Order $order Order object.
- */
- protected function add_webhook_sent_meta( $order ) {
- $order->update_meta_data( '_acp_order_created_sent', 'sent' );
- $order->save();
- }
-
- /**
- * Create an Agentic webhook.
- *
- * @param string $topic Topic for webhook. Defaults to 'action.woocommerce_agentic_order_created'.
- * @return WC_Webhook Created webhook.
- */
- protected function create_agentic_webhook( $topic = AgenticWebhookManager::WEBHOOK_TOPIC ) {
- $webhook = new WC_Webhook();
- $webhook->set_topic( $topic );
- $webhook->set_delivery_url( 'https://test.com' );
- $webhook->set_secret( 'test_secret' );
- $webhook->save();
- return $webhook;
- }
-
- /**
- * Assert that payload has ACP structure.
- *
- * @param array $payload Payload to check.
- * @param string $expected_type Expected type ('order_create' or 'order_update').
- */
- protected function assert_agentic_payload_structure( $payload, $expected_type ) {
- $this->assertEquals( $expected_type, $payload['type'] );
- $this->assertArrayHasKey( 'data', $payload );
- $this->assertEquals( 'order', $payload['data']['type'] );
- $this->assertArrayHasKey( 'checkout_session_id', $payload['data'] );
- $this->assertArrayHasKey( 'status', $payload['data'] );
- $this->assertArrayHasKey( 'refunds', $payload['data'] );
- }
-
- /**
- * Track action firing with a counter.
- *
- * @param string $action Action name to track.
- * @return int Counter reference.
- */
- protected function track_action( $action ) {
- $count = 0;
- add_action(
- $action,
- function () use ( &$count ) {
- $count++;
- }
- );
- return $count;
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookManagerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookManagerTest.php
deleted file mode 100644
index 1ea002b5abc..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookManagerTest.php
+++ /dev/null
@@ -1,557 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Admin\Agentic;
-
-use Automattic\WooCommerce\Internal\Admin\Agentic\AgenticWebhookManager;
-use Automattic\WooCommerce\StoreApi\Routes\V1\Agentic\Enums\OrderMetaKey;
-
-/**
- * Tests for AgenticWebhookManager class.
- */
-class AgenticWebhookManagerTest extends \WC_Unit_Test_Case {
- use AgenticTestHelpers;
-
- /**
- * Webhook manager instance.
- *
- * @var AgenticWebhookManager
- */
- private $webhook_manager;
-
- /**
- * Set up before each test.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->webhook_manager = wc_get_container()->get( AgenticWebhookManager::class );
-
- // During tests, the controller never triggered hooks to be registered. Do it manually.
- $this->webhook_manager->register();
- }
-
- /**
- * Tear down after each test.
- */
- public function tearDown(): void {
- // Remove any existing hooks to prevent duplicates.
- remove_all_filters( 'woocommerce_webhook_topics' );
- remove_all_actions( 'woocommerce_new_order' );
- remove_all_actions( 'woocommerce_order_status_changed' );
- remove_all_actions( 'woocommerce_order_refunded' );
-
- parent::tearDown();
- }
-
- /**
- * Test that custom webhook topics are registered.
- */
- public function test_custom_topics_registered() {
- /**
- * Filters the list of webhook topic hooks.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::register_webhook_topic_names()
- */
- $topics = apply_filters( 'woocommerce_webhook_topics', array() );
-
- $this->assertArrayHasKey( AgenticWebhookManager::WEBHOOK_TOPIC, $topics );
- $this->assertEquals( 'Agentic Commerce Protocol: Order created or updated', $topics[ AgenticWebhookManager::WEBHOOK_TOPIC ] );
- }
-
- /**
- * Test action firing based on session ID presence.
- *
- * @dataProvider action_firing_provider
- *
- * @param bool $has_session_id Whether order has session ID.
- * @param bool $should_fire Whether action should fire.
- */
- public function test_action_firing_based_on_session_id( $has_session_id, $should_fire ) {
- // Create order.
- $order = \WC_Helper_Order::create_order();
- if ( $has_session_id ) {
- $order->update_meta_data( OrderMetaKey::AGENTIC_CHECKOUT_SESSION_ID, 'test_session_123' );
- }
- $order->save();
-
- // Set up action listener.
- $action_count = 0;
- add_action(
- AgenticWebhookManager::WEBHOOK_ACTION,
- function () use ( &$action_count ) {
- $action_count++;
- }
- );
-
- /**
- * Manually trigger the new order action to test the hook.
- *
- * @since 10.3.0
- */
- do_action( 'woocommerce_new_order', $order->get_id(), $order );
-
- $this->assertEquals( $should_fire ? 1 : 0, $action_count );
- }
-
- /**
- * Provider for action firing tests.
- */
- public function action_firing_provider() {
- return array(
- 'with session ID' => array( true, true ),
- 'without session ID' => array( false, false ),
- );
- }
-
- /**
- * Test that order status changes trigger update action.
- */
- public function test_order_status_change_triggers_update() {
- $order = $this->create_agentic_order( 'test_session', 'processing' );
-
- $action_count = 0;
- /**
- * Fires when an Agentic order is updated.
- *
- * @see AgenticWebhookManager::handle_order_status_changed()
- */
- add_action(
- AgenticWebhookManager::WEBHOOK_ACTION,
- function () use ( &$action_count ) {
- $action_count++;
- }
- );
-
- $order->set_status( 'completed' );
- $order->save();
-
- $this->assertEquals( 1, $action_count );
- }
-
- /**
- * Test refund events trigger update action.
- *
- * @dataProvider refund_test_provider
- *
- * @param array $refund_amounts Refund amounts to create.
- * @param int $expected_count Expected action count.
- */
- public function test_refund_triggers_update( $refund_amounts, $expected_count ) {
- $order = $this->create_agentic_order();
-
- $action_count = 0;
- add_action(
- AgenticWebhookManager::WEBHOOK_ACTION,
- function () use ( &$action_count ) {
- $action_count++;
- }
- );
-
- foreach ( $refund_amounts as $amount ) {
- wc_create_refund(
- array(
- 'order_id' => $order->get_id(),
- 'amount' => $amount,
- 'reason' => 'Test refund',
- )
- );
- }
-
- $this->assertEquals( $expected_count, $action_count );
- }
-
- /**
- * Provider for refund tests.
- */
- public function refund_test_provider() {
- return array(
- 'single refund' => array( array( 10.00 ), 1 ),
- 'multiple refunds' => array( array( 10.00, 5.00, 15.00 ), 3 ),
- );
- }
-
- /**
- * Test webhook payload contains all refunds.
- */
- public function test_webhook_payload_contains_all_refunds() {
- $webhook = $this->create_agentic_webhook();
- $order = $this->create_agentic_order();
- $this->add_webhook_sent_meta( $order );
-
- // Create multiple refunds.
- $refund_amounts = array( 10.00, 5.00, 15.00 );
- foreach ( $refund_amounts as $amount ) {
- wc_create_refund(
- array(
- 'order_id' => $order->get_id(),
- 'amount' => $amount,
- )
- );
- }
-
- /**
- * Filters the webhook payload.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::customize_webhook_payload()
- */
- $payload = apply_filters(
- 'woocommerce_webhook_payload',
- array(),
- 'order',
- $order->get_id(),
- $webhook->get_id()
- );
-
- $this->assertEquals( 'order_update', $payload['type'] );
- $this->assertCount( 3, $payload['data']['refunds'] );
-
- $refund_amounts_in_payload = array_column( $payload['data']['refunds'], 'amount' );
- // Amounts should be in cents (integer).
- $this->assertContains( 1000, $refund_amounts_in_payload );
- $this->assertContains( 500, $refund_amounts_in_payload );
- $this->assertContains( 1500, $refund_amounts_in_payload );
-
- $webhook->delete( true );
- }
-
- /**
- * Test webhook payload customization for ACP format.
- */
- public function test_webhook_payload_customization() {
- $webhook = $this->create_agentic_webhook();
- $order = $this->create_agentic_order( 'test_session_456' );
-
- /**
- * Filters the webhook payload.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::customize_webhook_payload()
- */
- $payload = apply_filters(
- 'woocommerce_webhook_payload',
- array( 'original' => 'data' ),
- 'order',
- $order->get_id(),
- $webhook->get_id()
- );
-
- $this->assert_agentic_payload_structure( $payload, 'order_create' );
- $this->assertEquals( 'test_session_456', $payload['data']['checkout_session_id'] );
-
- $webhook->delete( true );
- }
-
- /**
- * Test webhook HTTP args customization for ACP compliance.
- */
- public function test_webhook_http_args_customization() {
- $webhook = $this->create_agentic_webhook();
- $webhook->set_secret( 'test_secret' );
- $webhook->save();
-
- $payload = wp_json_encode( array( 'test' => 'data' ) );
- $original_args = array(
- 'headers' => array(),
- 'body' => $payload,
- );
-
- /**
- * Filters the webhook HTTP args.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::customize_webhook_http_args()
- */
- $modified_args = apply_filters(
- 'woocommerce_webhook_http_args',
- $original_args,
- null,
- $webhook->get_id()
- );
-
- // Verify Merchant-Signature was added with correct computed value.
- $this->assertArrayHasKey( 'Merchant-Signature', $modified_args['headers'] );
-
- // Compute expected signature same way WooCommerce does.
- $expected_signature = base64_encode( hash_hmac( 'sha256', $payload, 'test_secret', true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
- $this->assertEquals( $expected_signature, $modified_args['headers']['Merchant-Signature'] );
-
- $webhook->delete( true );
- }
-
- /**
- * Test that signature is computed correctly for different payloads.
- */
- public function test_merchant_signature_computation() {
- $webhook = $this->create_agentic_webhook();
- $webhook->set_secret( 'my_webhook_secret_123' );
- $webhook->save();
-
- // Test with various payload types.
- $test_cases = array(
- array(
- 'payload' => wp_json_encode( array( 'order_id' => 123 ) ),
- 'description' => 'Simple JSON payload',
- ),
- array(
- 'payload' => wp_json_encode( array( 'unicode' => '€£¥' ) ),
- 'description' => 'Unicode characters',
- ),
- array(
- 'payload' => '{"nested":{"data":{"value":true}}}',
- 'description' => 'Nested JSON',
- ),
- );
-
- foreach ( $test_cases as $test ) {
- $args = array(
- 'headers' => array(),
- 'body' => $test['payload'],
- );
-
- $modified_args = apply_filters(
- 'woocommerce_webhook_http_args',
- $args,
- null,
- $webhook->get_id()
- );
-
- // Verify signature matches expected HMAC-SHA256.
- $expected = base64_encode( hash_hmac( 'sha256', $test['payload'], 'my_webhook_secret_123', true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
- $this->assertEquals(
- $expected,
- $modified_args['headers']['Merchant-Signature'],
- 'Failed for: ' . $test['description']
- );
- }
-
- $webhook->delete( true );
- }
-
- /**
- * Test that non-Agentic webhooks are not affected.
- */
- public function test_non_agentic_webhooks_unaffected() {
- // Create a regular WooCommerce webhook.
- $webhook = new \WC_Webhook();
- $webhook->set_topic( 'order.created' ); // Regular WC topic.
- $webhook->set_delivery_url( 'https://example.com/webhook' );
- $webhook->save();
-
- $args = array(
- 'headers' => array(),
- 'body' => '{"test":"data"}',
- );
-
- $modified_args = apply_filters(
- 'woocommerce_webhook_http_args',
- $args,
- null,
- $webhook->get_id()
- );
-
- // Should not have Merchant-Signature for non-Agentic webhooks.
- $this->assertArrayNotHasKey( 'Merchant-Signature', $modified_args['headers'] );
- // Should not have empty X-WC-Webhook-Signature.
- $this->assertArrayNotHasKey( 'X-WC-Webhook-Signature', $modified_args['headers'] );
-
- $webhook->delete( true );
- }
-
- /**
- * Test that first event is marked as delivered on successful webhook delivery.
- */
- public function test_mark_first_event_delivered_success() {
- $webhook = $this->create_agentic_webhook();
- $order = $this->create_agentic_order( 'test_session_789' );
-
- // Verify the order doesn't have the meta key set initially.
- $this->assertEmpty( $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- // Simulate successful webhook delivery.
- $http_args = array(
- 'headers' => array(),
- 'body' => wp_json_encode( array( 'test' => 'payload' ) ),
- );
-
- // Mock successful HTTP response.
- $response = array(
- 'response' => array( 'code' => 200 ),
- );
-
- /**
- * Fires when a webhook is delivered.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::mark_first_event_delivered()
- */
- do_action(
- 'woocommerce_webhook_delivery',
- $http_args,
- $response,
- 0.5, // duration.
- $order->get_id(), // arg (order_id).
- $webhook->get_id()
- );
-
- // Verify the meta key was set to 'sent'.
- $order = wc_get_order( $order->get_id() ); // Refresh order from database.
- $this->assertEquals( 'sent', $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- $webhook->delete( true );
- }
-
- /**
- * Test that first event is not marked as delivered on failed webhook delivery.
- */
- public function test_mark_first_event_delivered_failure() {
- $webhook = $this->create_agentic_webhook();
- $order = $this->create_agentic_order( 'test_session_456' );
-
- // Verify the order doesn't have the meta key set initially.
- $this->assertEmpty( $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- // Simulate failed webhook delivery (HTTP error).
- $http_args = array(
- 'headers' => array(),
- 'body' => wp_json_encode( array( 'test' => 'payload' ) ),
- );
-
- // Mock failed HTTP response.
- $response = array(
- 'response' => array( 'code' => 500 ),
- );
-
- /**
- * Fires when a webhook is delivered.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::mark_first_event_delivered()
- */
- do_action(
- 'woocommerce_webhook_delivery',
- $http_args,
- $response,
- 0.5, // duration.
- $order->get_id(), // arg (order_id).
- $webhook->get_id()
- );
-
- // Verify the meta key was NOT set.
- $order = wc_get_order( $order->get_id() ); // Refresh order from database.
- $this->assertEmpty( $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- $webhook->delete( true );
- }
-
- /**
- * Test that first event marking is skipped for non-Agentic webhooks.
- */
- public function test_mark_first_event_delivered_non_agentic_webhook() {
- // Create a regular WooCommerce webhook (not Agentic).
- $webhook = new \WC_Webhook();
- $webhook->set_topic( 'order.created' );
- $webhook->set_delivery_url( 'https://example.com/webhook' );
- $webhook->save();
-
- $order = $this->create_agentic_order( 'test_session_123' );
-
- // Verify the order doesn't have the meta key set initially.
- $this->assertEmpty( $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- // Simulate successful webhook delivery.
- $http_args = array(
- 'headers' => array(),
- 'body' => wp_json_encode( array( 'test' => 'payload' ) ),
- );
-
- $response = array(
- 'response' => array( 'code' => 200 ),
- );
-
- /**
- * Fires when a webhook is delivered.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::mark_first_event_delivered()
- */
- do_action(
- 'woocommerce_webhook_delivery',
- $http_args,
- $response,
- 0.5, // duration.
- $order->get_id(), // arg (order_id).
- $webhook->get_id()
- );
-
- // Verify the meta key was NOT set for non-Agentic webhook.
- $order = wc_get_order( $order->get_id() ); // Refresh order from database.
- $this->assertEmpty( $order->get_meta( AgenticWebhookManager::FIRST_EVENT_DELIVERED_META_KEY ) );
-
- $webhook->delete( true );
- }
-
- /**
- * Test that first event marking is skipped when order doesn't exist.
- */
- public function test_mark_first_event_delivered_nonexistent_order() {
- $webhook = $this->create_agentic_webhook();
- $order = $this->create_agentic_order( 'test_session_999' );
- $order_id = $order->get_id();
-
- // Delete the order to simulate non-existent order.
- $order->delete( true );
-
- // Simulate successful webhook delivery.
- $http_args = array(
- 'headers' => array(),
- 'body' => wp_json_encode( array( 'test' => 'payload' ) ),
- );
-
- $response = array(
- 'response' => array( 'code' => 200 ),
- );
-
- /**
- * Fires when a webhook is delivered.
- *
- * @since 10.3.0
- * @see AgenticWebhookManager::mark_first_event_delivered()
- */
- do_action(
- 'woocommerce_webhook_delivery',
- $http_args,
- $response,
- 0.5, // duration.
- $order_id, // arg (order_id) - order no longer exists.
- $webhook->get_id()
- );
-
- // This should not cause any errors and should complete successfully.
- $this->assertTrue( true ); // If we get here, no exception was thrown.
-
- $webhook->delete( true );
- }
-
- /**
- * Clean up existing Agentic webhooks for testing.
- */
- private function cleanup_existing_agentic_webhooks() {
- $data_store = \WC_Data_Store::load( 'webhook' );
- $webhooks = $data_store->search_webhooks(
- array(
- 'search' => 'ACP',
- 'status' => 'all',
- )
- );
-
- foreach ( $webhooks as $webhook_id ) {
- $webhook = wc_get_webhook( $webhook_id );
- if ( $webhook ) {
- $webhook->delete( true );
- }
- }
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilderTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilderTest.php
deleted file mode 100644
index 037b9482b33..00000000000
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Agentic/AgenticWebhookPayloadBuilderTest.php
+++ /dev/null
@@ -1,225 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Internal\Admin\Agentic;
-
-use Automattic\WooCommerce\Internal\Admin\Agentic\AgenticWebhookPayloadBuilder;
-
-/**
- * Tests for AgenticWebhookPayloadBuilder class.
- */
-class AgenticWebhookPayloadBuilderTest extends \WC_Unit_Test_Case {
- use AgenticTestHelpers;
-
- /**
- * Payload builder instance.
- *
- * @var AgenticWebhookPayloadBuilder
- */
- protected $payload_builder;
-
- /**
- * Set up before each test.
- */
- public function setUp(): void {
- parent::setUp();
- $this->payload_builder = new AgenticWebhookPayloadBuilder();
- $this->payload_builder->init();
- }
-
- /**
- * Clean up after each test.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_agentic_webhook_order_status_map' );
- remove_all_filters( 'woocommerce_agentic_webhook_refund_type' );
- parent::tearDown();
- }
-
- /**
- * Test building payloads for different event types.
- *
- * @dataProvider event_type_provider
- *
- * @param string $event Event type.
- * @param string $status WooCommerce order status.
- * @param string $expected_acp_status Expected ACP status.
- */
- public function test_build_payload_for_event_type( $event, $status, $expected_acp_status ) {
- $order = $this->create_agentic_order( 'test_session_123', $status );
- $payload = $this->payload_builder->build_payload( $event, $order );
-
- $this->assert_agentic_payload_structure( $payload, $event );
- $this->assertEquals( 'test_session_123', $payload['data']['checkout_session_id'] );
- $this->assertEquals( $expected_acp_status, $payload['data']['status'] );
- $this->assertEmpty( $payload['data']['refunds'] );
- }
-
- /**
- * Provider for event type tests.
- */
- public function event_type_provider() {
- return array(
- 'order create' => array( 'order_create', 'processing', 'confirmed' ),
- 'order update' => array( 'order_update', 'completed', 'fulfilled' ),
- );
- }
-
- /**
- * Test status mapping from WooCommerce to ACP.
- *
- * @dataProvider status_mapping_provider
- *
- * @param string $wc_status WooCommerce order status.
- * @param string $expected_acp_status Expected ACP status.
- */
- public function test_status_mapping( $wc_status, $expected_acp_status ) {
- $order = $this->create_agentic_order( 'test_session', $wc_status );
- $payload = $this->payload_builder->build_payload( 'order_update', $order );
-
- $this->assertEquals( $expected_acp_status, $payload['data']['status'] );
- }
-
- /**
- * Provider for status mapping tests.
- */
- public function status_mapping_provider() {
- return array(
- array( 'pending', 'created' ),
- array( 'processing', 'confirmed' ),
- array( 'on-hold', 'manual_review' ),
- array( 'completed', 'fulfilled' ),
- array( 'cancelled', 'canceled' ),
- array( 'refunded', 'fulfilled' ),
- array( 'failed', 'canceled' ),
- );
- }
-
- /**
- * Test refunds are included in payload and default to original_payment.
- */
- public function test_build_payload_with_refunds() {
- $order = $this->create_agentic_order();
-
- // Create multiple refunds - all should default to original_payment.
- wc_create_refund(
- array(
- 'order_id' => $order->get_id(),
- 'amount' => 10.00,
- 'reason' => 'Product defect',
- )
- );
-
- wc_create_refund(
- array(
- 'order_id' => $order->get_id(),
- 'amount' => 5.00,
- 'reason' => 'Store credit issued', // Even this defaults to original_payment now.
- )
- );
-
- $payload = $this->payload_builder->build_payload( 'order_update', $order );
-
- $this->assertCount( 2, $payload['data']['refunds'] );
-
- // Both refunds should default to original_payment.
- // Amounts should be in cents (integer).
- // Use order-independent assertions since refund order is not guaranteed.
- $amounts = array_column( $payload['data']['refunds'], 'amount' );
- $types = array_column( $payload['data']['refunds'], 'type' );
-
- $this->assertContains( 1000, $amounts );
- $this->assertContains( 500, $amounts );
- $this->assertEquals( array( 'original_payment', 'original_payment' ), $types );
- }
-
- /**
- * Test status mapping filter.
- *
- * @dataProvider status_filter_provider
- *
- * @param callable $filter_callback Filter callback function.
- * @param string $wc_status WooCommerce order status.
- * @param string $expected_status Expected ACP status.
- */
- public function test_status_mapping_filter( $filter_callback, $wc_status, $expected_status ) {
- add_filter( 'woocommerce_agentic_webhook_order_status_map', $filter_callback, 10, 2 );
-
- $order = $this->create_agentic_order( 'test_session', $wc_status );
- $payload = $this->payload_builder->build_payload( 'order_update', $order );
-
- $this->assertEquals( $expected_status, $payload['data']['status'] );
-
- remove_all_filters( 'woocommerce_agentic_webhook_order_status_map' );
- }
-
- /**
- * Provider for status filter tests.
- */
- public function status_filter_provider() {
- return array(
- 'override to confirmed' => array(
- function ( $map ) {
- $map['pending'] = 'confirmed';
- return $map;
- },
- 'pending',
- 'confirmed',
- ),
- 'map to shipped' => array(
- function ( $map ) {
- $map['processing'] = 'shipped';
- return $map;
- },
- 'processing',
- 'shipped',
- ),
- 'invalid status fallback' => array(
- function ( $map ) {
- $map['pending'] = 'invalid_status';
- return $map;
- },
- 'pending',
- 'created', // Should fallback to 'created'.
- ),
- );
- }
-
- /**
- * Test refund type filter allows customization.
- */
- public function test_refund_type_filter() {
- $order = $this->create_agentic_order();
-
- // Create a refund.
- wc_create_refund(
- array(
- 'order_id' => $order->get_id(),
- 'amount' => 15.00,
- 'reason' => 'Store credit issued',
- )
- );
-
- // Hook into the filter to change refund type based on reason.
- add_filter(
- 'woocommerce_agentic_webhook_refund_type',
- function ( $type, $refund_obj ) {
- if ( stripos( $refund_obj->get_reason(), 'store credit' ) !== false ) {
- return 'store_credit';
- }
- return $type;
- },
- 10,
- 2
- );
-
- $payload = $this->payload_builder->build_payload( 'order_update', $order );
-
- // Check that refund type was changed to store_credit via filter.
- $this->assertNotEmpty( $payload['data']['refunds'] );
- $this->assertEquals( 'store_credit', $payload['data']['refunds'][0]['type'] );
-
- // Clean up filter.
- remove_all_filters( 'woocommerce_agentic_webhook_refund_type' );
- }
-}
diff --git a/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListsControllerTests.php b/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListsControllerTests.php
index c294e37955f..117daebdafe 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListsControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ShopperLists/ShopperListsControllerTests.php
@@ -178,7 +178,7 @@ class ShopperListsControllerTests extends WC_Unit_Test_Case {
return array(
'wishlist toggles flush' => array( 'product_wishlist', true ),
'sfl change ignored' => array( 'cart_save_for_later', false ),
- 'unrelated feature ignored' => array( 'agentic_checkout', false ),
+ 'unrelated feature ignored' => array( 'fulfillments', false ),
);
}