Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
dd86194
✨ Ensure response always is a serialized PPCart
stracker-phil Sep 22, 2025
4b7b113
♻️ Implement possible success responses
stracker-phil Sep 22, 2025
de6749d
✨ Prepare REST error responses
stracker-phil Sep 22, 2025
21a9bb4
♻️ Use child classes for different cart stages
stracker-phil Sep 23, 2025
8c7de14
🚚 Rename the not found error
stracker-phil Sep 23, 2025
a830d07
✨ Add new error case for invalid request
stracker-phil Sep 23, 2025
02123a5
📝 Document error classes
stracker-phil Sep 23, 2025
617b5f5
✨ New base class for all schema classes
stracker-phil Sep 23, 2025
7bd424a
🚚 Move AgenticErrors to own top-level folder
stracker-phil Sep 23, 2025
0dd639d
✨ New top-level folder for validation issues
stracker-phil Sep 23, 2025
ce5f26e
♻️ Fully refactor the schema base class
stracker-phil Sep 24, 2025
5277317
✨ Allow propagating the “add_issue” callback
stracker-phil Sep 24, 2025
546b9a6
🚚 Move response schemas to own folder
stracker-phil Sep 24, 2025
7238ecb
♻️ Refactor the response classes
stracker-phil Sep 24, 2025
d757b5a
✨ Response factory for use by the REST endpoints
stracker-phil Sep 24, 2025
8938613
✨ Implement first ValidationIssue
stracker-phil Sep 24, 2025
996bf43
♻️ Shorten some class names
stracker-phil Sep 24, 2025
15c874b
🚧 Begin to implement schema classes
stracker-phil Sep 24, 2025
f76a308
🔀 Merge branch 'PCP-4891’
stracker-phil Oct 1, 2025
0aa6940
🦺 Replace exception with default message
stracker-phil Oct 6, 2025
6732c3a
🔧 Do not enforce Psalm checks for test code
stracker-phil Oct 6, 2025
9b8f853
✅ Move static code analysis to unit test
stracker-phil Oct 6, 2025
a6c5eb2
📝 Document test cases
stracker-phil Oct 6, 2025
7d779d1
🦺 Add new agentic validation issues
stracker-phil Oct 6, 2025
9e9be54
🚚 Simplfy validation issue test
stracker-phil Oct 6, 2025
b3014d8
🔥 Remove obsolete error suppression
stracker-phil Oct 6, 2025
b779957
✅ Add first unit test for the Money schema
stracker-phil Oct 6, 2025
dc59e0e
🩹 Fix failing test cases
stracker-phil Oct 6, 2025
4d5eb7a
🔥 Simplify test cases
stracker-phil Oct 6, 2025
998824c
🧪 Verify existence of Address class
stracker-phil Oct 6, 2025
259cf1f
✨ Add the Address schema stub
stracker-phil Oct 6, 2025
658bc86
🧪 Expect country code
stracker-phil Oct 8, 2025
dda6e36
✅ Pass the tests
stracker-phil Oct 8, 2025
ed31939
🧪 Expect country code based on input
stracker-phil Oct 8, 2025
51f265a
✅ Pass tests
stracker-phil Oct 8, 2025
d536e61
🧪 Expect to trim the country code
stracker-phil Oct 8, 2025
fce5325
✅ Pass test
stracker-phil Oct 8, 2025
fe6eed5
🧪 Expect length validation
stracker-phil Oct 8, 2025
eca41e1
✅ Pass tests
stracker-phil Oct 8, 2025
7318078
♻️ Refactor passing tests
stracker-phil Oct 8, 2025
d8d8951
🧪 Expect country_code to be mandatory
stracker-phil Oct 8, 2025
f37c02b
✅ Pass tests
stracker-phil Oct 8, 2025
e0c4678
🧪 Exepct address_line_1 to return a value
stracker-phil Oct 8, 2025
95ebafa
✅ Pass the tests
stracker-phil Oct 8, 2025
c48ad0d
🧪 Expect address_line_1 to be optional
stracker-phil Oct 8, 2025
60cb33f
✅ Pass tests
stracker-phil Oct 8, 2025
c0ac0cf
🧪 Expect address_line_1 max length validation
stracker-phil Oct 8, 2025
6d06387
✅ Pass tests
stracker-phil Oct 8, 2025
39bd3dc
🧪 Expect address_line_2 storage
stracker-phil Oct 8, 2025
14c9959
✅ Pass tests
stracker-phil Oct 8, 2025
8c28cd6
🧪 Expect address_line_2 to return null when not provided
stracker-phil Oct 8, 2025
5e7af09
✅ Pass tests
stracker-phil Oct 8, 2025
c45ee77
🧪 Expect address_line_2 max length validation
stracker-phil Oct 8, 2025
c5eb966
🧪 Expect admin_area_2 storage
stracker-phil Oct 8, 2025
6603781
✅ Pass tests
stracker-phil Oct 8, 2025
73e86cf
🧪 Expect admin_area_2 to return null when not provided
stracker-phil Oct 8, 2025
995ec9b
✅ Pass tests
stracker-phil Oct 8, 2025
1ac02bc
🧪 Expect admin_area_2 max length validation (120 chars)
stracker-phil Oct 8, 2025
34de13e
✅ Pass all tests
stracker-phil Oct 8, 2025
24541ab
🧪 Expect admin_area_1 storage
stracker-phil Oct 8, 2025
47cb51e
✅ Pass tests
stracker-phil Oct 8, 2025
85f5d3d
🧪 Expect admin_area_1 to return null when not provided
stracker-phil Oct 8, 2025
f944c8c
✅ Pass all tests
stracker-phil Oct 8, 2025
905491f
🧪 Expect admin_area_1 to accept 121 characters (max 300)
stracker-phil Oct 8, 2025
ef7b865
✅ Pass tests
stracker-phil Oct 8, 2025
2b025a5
🧪 Expect address_line_1 to accept exactly 300 characters
stracker-phil Oct 8, 2025
036b718
🧪 Expect postal_code storage
stracker-phil Oct 8, 2025
c6d6c5d
✅ Pass tests
stracker-phil Oct 8, 2025
9c06490
🧪 Expect postal_code to return null when not provided
stracker-phil Oct 8, 2025
ca7223f
✅ Pass tests
stracker-phil Oct 8, 2025
2524c7b
🧪 Expect postal_code max length validation (60 chars)
stracker-phil Oct 8, 2025
3308276
🦺 Enforce max size for postal code
stracker-phil Oct 8, 2025
b242a59
♻️ Refactor optional field tests to use data providers
stracker-phil Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/ppcp-agentic-commerce/docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Testing

Unit tests for this module are located in the `tests/PHPUnit/AgenticCommerce` directory.

To run them, use the command:

```sh
ddev exec phpunit --filter AgenticCommerce
```
9 changes: 8 additions & 1 deletion modules/ppcp-agentic-commerce/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@

namespace WooCommerce\PayPalCommerce\AgenticCommerce;

return array();
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\AgenticCommerce\Response\ResponseFactory;

return array(
'agentic.response.factory' => static function ( ContainerInterface $container ): ResponseFactory {
return new ResponseFactory();
},
);
68 changes: 68 additions & 0 deletions modules/ppcp-agentic-commerce/src/Errors/AgenticError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* Base class for all agentic commerce REST errors.
*
* @package WooCommerce\PayPalCommerce\AgenticCommerce\Errors
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Errors;

use RuntimeException;

abstract class AgenticError {
/**
* The error name is defined by the PayPal API, usually in upper-case, e.g. "INVALID_REQUEST".
* Child classes must define this constant.
*/
protected const ERROR_NAME = '';

/**
* The HTTP status code for the error, usually 400 or 500.
* Child classes must define this constant.
*/
protected const STATUS_CODE = 0;

private string $message;
private ?array $details;

/**
* Defines the error contents.
*
* @param string $message Descriptive text of the error.
* @param array|null $details Optional. Additional details about the error.
* @throws RuntimeException When the error specs are incomplete.
*/
public function __construct( string $message, ?array $details = null ) {
if ( empty( static::ERROR_NAME ) ) {
throw new RuntimeException( 'Child classes must override ERROR_NAME constant' );
}
if ( ! is_numeric( static::STATUS_CODE ) || static::STATUS_CODE < 400 ) {
throw new RuntimeException( 'Child classes must define a valid STATUS_CODE constant' );
}
if ( empty( $message ) ) {
throw new RuntimeException( 'Error message cannot be empty' );
}

$this->message = $message;
$this->details = $details;
}

public function get_status_code(): int {
return static::STATUS_CODE;
}

public function to_array(): array {
$data = array(
'name' => static::ERROR_NAME,
'message' => $this->message,
);

if ( $this->details ) {
$data['details'] = $this->details;
}

return $data;
}
}
31 changes: 31 additions & 0 deletions modules/ppcp-agentic-commerce/src/Errors/CartNotFoundError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
// phpcs:disable Squiz.PHP.CommentedOutCode.Found

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Errors;

/**
* When to use:
* - Invalid cart ID provided
*/
class CartNotFoundError extends AgenticError {
protected const ERROR_NAME = 'CART_NOT_FOUND';
protected const STATUS_CODE = 404;
}

/*
Sample:
{
"name": "CART_NOT_FOUND",
"message": "Cart with ID 'CART-MISSING-123' does not exist",
"debug_id": "ERROR-404-12345",
"details": [
{
"field": "cartId",
"issue": "NOT_FOUND",
"description": "Cart with ID 'CART-MISSING-123' does not exist for merchant 'MERCHANT_789'. Verify cart ID or create a new cart."
}
]
}
*/
32 changes: 32 additions & 0 deletions modules/ppcp-agentic-commerce/src/Errors/InvalidRequestError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
// phpcs:disable Squiz.PHP.CommentedOutCode.Found

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Errors;

/**
* When to use:
* - Empty request body
* - Malformed request
* - Missing mandatory fields
*/
class InvalidRequestError extends AgenticError {
protected const ERROR_NAME = 'INVALID_REQUEST';
protected const STATUS_CODE = 400;
}

/*
Sample:
{
"name": "INVALID_REQUEST",
"message": "Required field 'items' is missing",
"details": [
{
"field": "items",
"issue": "MISSING_REQUIRED_FIELD",
"description": "The items field is required and cannot be empty."
}
]
}
*/
63 changes: 63 additions & 0 deletions modules/ppcp-agentic-commerce/src/Response/CartResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* PayPal Cart Response.
*
* @package WooCommerce\PayPalCommerce\AgenticCommerce\Response
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Response;

use WooCommerce\PayPalCommerce\AgenticCommerce\Schema\PayPalCart;

class CartResponse {
protected ?PayPalCart $cart = null;

/**
* The cart ID used by the API to reference to an existing cart.
*
* @var string The cart ID is usually part of the REST endpoint path.
*/
protected string $cart_id = '';

/**
* Used to track cart lifecycle.
* Possible values: CREATED, INCOMPLETE, READY, COMPLETED
*
* @var string Business workflow state.
*/
protected string $status = 'INCOMPLETE';

/**
* Used to determine the next step.
* Possible values: VALID, INVALID, REQUIRES_ADDITIONAL_INFORMATION
*
* @var string Data validation state.
*/
protected string $validation_status = 'INVALID';

/**
* The payment method token, used to verify checkout.
*/
protected string $token = '';

public function __construct( PayPalCart $cart ) {
$this->cart = $cart;
// todo - set the other props of this class, once the flow becomes more clear.
}

public function to_array(): array {
// Always set via the constructor, but the IDE does not believe it.
assert( $this->cart instanceof PayPalCart );

$data = array(
'id' => $this->cart_id,
'status' => $this->status,
'validation_status' => $this->validation_status,
'validation_issues' => $this->cart->validate(),
);

return array_merge( $data, $this->cart->to_array() );
}
}
31 changes: 31 additions & 0 deletions modules/ppcp-agentic-commerce/src/Response/NewCartResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* PayPal Cart Response (new cart created).
*
* @package WooCommerce\PayPalCommerce\AgenticCommerce\Response\
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Response;

use WooCommerce\PayPalCommerce\AgenticCommerce\Schema\PayPalCart;

class NewCartResponse extends CartResponse {
public function __construct( PayPalCart $cart, string $token ) {
parent::__construct( $cart );
$this->token = $token;
}

public function to_array(): array {
$data = parent::to_array();

$data['payment_method'] = array(
'type' => 'paypal',
'token' => $this->token,
'approval_url' => 'not-implemented',
);

return $data;
}
}
38 changes: 38 additions & 0 deletions modules/ppcp-agentic-commerce/src/Response/PaidCartResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* PayPal Cart Response (cart checkout confirmed).
*
* @package WooCommerce\PayPalCommerce\AgenticCommerce\Response
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Response;

use WC_Order;
use WooCommerce\PayPalCommerce\AgenticCommerce\Schema\PayPalCart;

class PaidCartResponse extends CartResponse {
/**
* @var WC_Order|null The WooCommerce order which was created during checkout.
*/
protected ?WC_Order $wc_order = null;

public function __construct( PayPalCart $cart, WC_Order $wc_order ) {
parent::__construct( $cart );
$this->wc_order = $wc_order;
}

public function to_array(): array {
$data = parent::to_array();

if ( $this->wc_order ) {
$data['payment_confirmation'] = array(
'merchant_order_number' => $this->wc_order->get_id(),
'order_review_page' => $this->wc_order->get_checkout_order_received_url(),
);
}

return $data;
}
}
27 changes: 27 additions & 0 deletions modules/ppcp-agentic-commerce/src/Response/ResponseFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Factory service for the REST response objects.
*
* @package WooCommerce\PayPalCommerce\AgenticCommerce\Response
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\AgenticCommerce\Response;

use WC_Order;
use WooCommerce\PayPalCommerce\AgenticCommerce\Schema\PayPalCart;

class ResponseFactory {
public function new_cart( PayPalCart $cart ): NewCartResponse {
return new NewCartResponse( $cart, 'not-implemented' );
}

public function from_order( WC_Order $order, PayPalCart $cart ): PaidCartResponse {
return new PaidCartResponse( $cart, $order );
}

public function from_cart( PayPalCart $cart ): CartResponse {
return new CartResponse( $cart );
}
}
Loading
Loading