Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions modules/ppcp-store-sync/src/Endpoint/AgenticRestEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ protected function create_local_cart( PayPalCart $cart, string $ec_token ): stri
return $this->session_handler->create_cart_session( $cart, $ec_token );
}

protected function store_local_cart( string $cart_id, PayPalCart $cart ): bool {
return $this->session_handler->update_cart_session( $cart_id, $cart );
protected function store_local_cart( string $cart_id, PayPalCart $cart, ?string $ec_token = null ): bool {
return $this->session_handler->update_cart_session( $cart_id, $cart, $ec_token );
}

protected function flush_local_cart( string $cart_id ): bool {
Expand Down
68 changes: 64 additions & 4 deletions modules/ppcp-store-sync/src/Endpoint/ReplaceCartEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace WooCommerce\PayPalCommerce\StoreSync\Endpoint;

use WooCommerce\PayPalCommerce\StoreSync\Schema\PayPalCart;
use WP_REST_Request;
use WP_REST_Response;

Expand Down Expand Up @@ -56,13 +57,17 @@ public function register_routes(): void {
/**
* Replace an existing cart with new data.
*
* Per PayPal specs, the PUT endpoint may create a new PayPal order when:
* - The cart doesn't have an ec_token yet (POST validation failed)
* - The existing ec_token has expired
*
* @param WP_REST_Request $request The REST request.
* @return WP_REST_Response The REST response.
*/
public function replace_cart( WP_REST_Request $request ): WP_REST_Response {
$cart_id = $request->get_param( 'cart_id' );

// Verify cart exists.
// Verify cart exists and get current session data.
$session = $this->get_stored_cart( $cart_id );

if ( $session instanceof AgenticError ) {
Expand All @@ -75,8 +80,26 @@ public function replace_cart( WP_REST_Request $request ): WP_REST_Response {
return $this->error( $new_cart );
}

// Replace the cart session (preserving ec_token).
$update_result = $this->store_local_cart( $cart_id, $new_cart );
// Determine if we need to create a new PayPal order.
$existing_token = $session['ec_token'] ?? '';
$new_token = null;

if ( $this->needs_new_token( $existing_token, $new_cart ) ) {
// Create new PayPal order - may return empty string if cart is invalid.
$new_token = $this->order_manager->create_order( $new_cart ) ?: null;

$this->logger->info(
'[REST] PUT created new PayPal order',
array(
'cart_id' => $cart_id,
'new_token' => $new_token ?: '(none - order creation failed)',
'reason' => empty( $existing_token ) ? 'no_existing_token' : 'token_expired',
)
);
}

// Update the cart session, passing new token if one was created.
$update_result = $this->store_local_cart( $cart_id, $new_cart, $new_token );

if ( ! $update_result ) {
return $this->error_not_found(
Expand All @@ -88,11 +111,48 @@ public function replace_cart( WP_REST_Request $request ): WP_REST_Response {
);
}

$response = $this->response_factory->from_cart( $new_cart, $cart_id );
// Build response - include token only if a new one was created.
if ( $new_token ) {
$response = $this->response_factory->cart_with_token( $new_cart, $cart_id, $new_token );
} else {
$response = $this->response_factory->from_cart( $new_cart, $cart_id );
}

return $this->cart_details( $response, 200 );
}

/**
* Determine if a new PayPal order/token needs to be created.
*
* A new token is needed when:
* 1. No existing token (previous POST failed to create one)
* 2. Existing token has expired (future: implement expiry check)
*
* @param string $existing_token The current ec_token from session.
* @param PayPalCart $cart The new cart data.
* @return bool True if a new token should be created.
*/
private function needs_new_token( string $existing_token, PayPalCart $cart ): bool {
// Case 1: No existing token.
if ( empty( $existing_token ) ) {
// Only attempt to create if the cart is valid (no validation issues).
return ! $cart->issues();
}

// Case 2: Token expired - could be implemented by checking PayPal order status.
// For now, we preserve valid tokens and don't recreate them.
// Future enhancement: call PayPal API to check if order is still valid.

return false;
}

/**
* Create a not found error response.
*
* @param string $message Error message.
* @param array $details Error details.
* @return WP_REST_Response The error response.
*/
private function error_not_found( string $message, array $details ): WP_REST_Response {
return $this->error( new NotFoundError( $message, array( $details ) ) );
}
Expand Down
8 changes: 4 additions & 4 deletions modules/ppcp-store-sync/src/Response/CartResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class CartResponse {
'REQUIRES_ADDITIONAL_INFORMATION',
);

private PayPalCart $cart;
protected PayPalCart $cart;

private string $default_currency = '';

Expand All @@ -51,7 +51,7 @@ class CartResponse {
* Used to track cart lifecycle.
* Possible values: CREATED, INCOMPLETE, READY, COMPLETED
*/
private string $status = 'INCOMPLETE';
protected string $status = 'INCOMPLETE';

/**
* Used to determine the next step.
Expand All @@ -62,7 +62,7 @@ class CartResponse {
/**
* The payment method token, only set for newly created carts.
*/
private string $token = '';
protected string $token = '';

/**
* The WooCommerce order created during checkout, only set for completed carts.
Expand All @@ -73,7 +73,7 @@ class CartResponse {
* @param PayPalCart $cart The PayPal cart.
* @param string $cart_id The cart ID.
*/
private function __construct( PayPalCart $cart, string $cart_id = '' ) {
protected function __construct( PayPalCart $cart, string $cart_id = '' ) {
$this->cart = $cart;
$this->cart_id = $cart_id;

Expand Down
17 changes: 17 additions & 0 deletions modules/ppcp-store-sync/src/Response/ResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ public function new_cart( PayPalCart $cart, string $cart_id, string $token ): Ca
->shipping_options( $this->shipping_options_builder->build( $wc_cart ) );
}

/**
* Create a cart response with token (for PUT when a new token was created).
*
* Used for PUT /merchant-cart/{id} when the endpoint creates a new PayPal order
* because the cart didn't have a token or the existing token was expired.
*
* @param PayPalCart $cart The cart object.
* @param string $cart_id The cart ID.
* @param string $token The payment token.
* @return UpdatedCartWithTokenResponse The response object.
*/
public function cart_with_token( PayPalCart $cart, string $cart_id, string $token ): UpdatedCartWithTokenResponse {
$wc_cart = $this->build_wc_cart_or_null( $cart );
$applied_coupons = $this->build_applied_coupons( $cart );
return new UpdatedCartWithTokenResponse( $cart, $cart_id, $token, $applied_coupons, $wc_cart );
}

/**
* Create a paid cart response.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
/**
* PayPal Cart Response (cart updated with new token).
*
* Used when PUT /merchant-cart/{id} creates a new PayPal order because:
* - The cart didn't have a token (POST validation failed)
* - The existing token was expired
*
* @package WooCommerce\PayPalCommerce\StoreSync\Response
*/

declare( strict_types = 1 );

namespace WooCommerce\PayPalCommerce\StoreSync\Response;

use WC_Cart;

use WooCommerce\PayPalCommerce\StoreSync\Schema\PayPalCart;

class UpdatedCartWithTokenResponse extends CartResponse {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a filename typo — UpdatedCartWithTokenRespones.php instead of UpdatedCartWithTokenRespone.php.


/**
* Constructor.
*
* @param PayPalCart $cart The PayPal cart.
* @param string $cart_id The cart ID.
* @param string $token The EC token.
* @param array $applied_coupons Applied coupons data.
* @param WC_Cart|null $wc_cart The WooCommerce cart.
*/
public function __construct(
PayPalCart $cart,
string $cart_id,
string $token,
array $applied_coupons = array(),
?WC_Cart $wc_cart = null
) {
parent::__construct( $cart, $cart_id );
$this->applied_coupons( $applied_coupons );
$this->wc_cart( $wc_cart );
$this->token = $token;

// Updated carts with valid tokens are READY for checkout.
if ( ! $this->cart->issues() && ! empty( $token ) ) {
$this->status = 'READY';
}
}

/**
* Convert to array for API response.
*
* Includes the payment_method.token only when a new token was created.
* Per PayPal spec: "The API response should only mention new token,
* i.e. when no token was generated, the payment_method.token property
* should not exist in the response."
*
* @return array The response array.
*/
public function to_array(): array {
$data = parent::to_array();

// Only include payment_method when a token exists.
if ( ! empty( $this->token ) ) {
$data['payment_method'] = array(
'type' => 'paypal', // hard-coded.
'token' => $this->token,
);

// Add sandbox approval URL for testing.
$data['_testing'] = array(
'sandbox_approval_url' => 'https://www.sandbox.paypal.com/checkoutnow?token=' . $this->token,
'instructions' => 'For sandbox testing: Open this URL in browser, login as buyer, and approve payment before checkout.',
);
}

return $data;
}
}
4 changes: 2 additions & 2 deletions modules/ppcp-store-sync/src/Session/AgenticSessionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function load_cart_session( string $session_id ): ?array {
* @param PayPalCart $cart The updated cart.
* @return bool True on success.
*/
public function update_cart_session( string $session_id, PayPalCart $cart ): bool {
public function update_cart_session( string $session_id, PayPalCart $cart, ?string $ec_token = null ): bool {
// Load the session first.
$existing = $this->load_cart_session( $session_id );
if ( ! $existing ) {
Expand All @@ -127,7 +127,7 @@ public function update_cart_session( string $session_id, PayPalCart $cart ): boo

$data = array(
'cart' => $cart->to_array(),
'ec_token' => $existing['ec_token'],
'ec_token' => $ec_token ?? $existing['ec_token'],
'created' => $existing['created'],
'modified' => time(),
);
Expand Down
Loading