Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -193,8 +193,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 );
Comment thread
mmaymo marked this conversation as resolved.
Outdated

$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, $cart ): bool {
Comment thread
mmaymo marked this conversation as resolved.
Outdated
// 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
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 @@ -56,6 +56,23 @@ public function new_cart( PayPalCart $cart, string $cart_id, string $token ): Ne
return new NewCartResponse( $cart, $cart_id, $token, $applied_coupons, $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,76 @@
<?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 {

/**
* 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, $applied_coupons, $cart_id, $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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,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 Down
Loading