Skip to content

Commit 5f3c528

Browse files
committed
Allow Cart-Session to Update the ec_token
1 parent 582d923 commit 5f3c528

5 files changed

Lines changed: 160 additions & 7 deletions

File tree

modules/ppcp-store-sync/src/Endpoint/AgenticRestEndpoint.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ protected function create_local_cart( PayPalCart $cart, string $ec_token ): stri
193193
return $this->session_handler->create_cart_session( $cart, $ec_token );
194194
}
195195

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

200200
protected function flush_local_cart( string $cart_id ): bool {

modules/ppcp-store-sync/src/Endpoint/ReplaceCartEndpoint.php

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace WooCommerce\PayPalCommerce\StoreSync\Endpoint;
1313

14+
use WooCommerce\PayPalCommerce\StoreSync\Schema\PayPalCart;
1415
use WP_REST_Request;
1516
use WP_REST_Response;
1617

@@ -56,13 +57,17 @@ public function register_routes(): void {
5657
/**
5758
* Replace an existing cart with new data.
5859
*
60+
* Per PayPal specs, the PUT endpoint may create a new PayPal order when:
61+
* - The cart doesn't have an ec_token yet (POST validation failed)
62+
* - The existing ec_token has expired
63+
*
5964
* @param WP_REST_Request $request The REST request.
6065
* @return WP_REST_Response The REST response.
6166
*/
6267
public function replace_cart( WP_REST_Request $request ): WP_REST_Response {
6368
$cart_id = $request->get_param( 'cart_id' );
6469

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

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

78-
// Replace the cart session (preserving ec_token).
79-
$update_result = $this->store_local_cart( $cart_id, $new_cart );
83+
// Determine if we need to create a new PayPal order.
84+
$existing_token = $session['ec_token'] ?? '';
85+
$new_token = null;
86+
87+
if ( $this->needs_new_token( $existing_token, $new_cart ) ) {
88+
// Create new PayPal order - may return empty string if cart is invalid.
89+
$new_token = $this->order_manager->create_order( $new_cart );
90+
91+
$this->logger->info(
92+
'[REST] PUT created new PayPal order',
93+
array(
94+
'cart_id' => $cart_id,
95+
'new_token' => $new_token ?: '(none - order creation failed)',
96+
'reason' => empty( $existing_token ) ? 'no_existing_token' : 'token_expired',
97+
)
98+
);
99+
}
100+
101+
// Update the cart session, passing new token if one was created.
102+
$update_result = $this->store_local_cart( $cart_id, $new_cart, $new_token );
80103

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

91-
$response = $this->response_factory->from_cart( $new_cart, $cart_id );
114+
// Build response - include token only if a new one was created.
115+
if ( $new_token ) {
116+
$response = $this->response_factory->cart_with_token( $new_cart, $cart_id, $new_token );
117+
} else {
118+
$response = $this->response_factory->from_cart( $new_cart, $cart_id );
119+
}
92120

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

124+
/**
125+
* Determine if a new PayPal order/token needs to be created.
126+
*
127+
* A new token is needed when:
128+
* 1. No existing token (previous POST failed to create one)
129+
* 2. Existing token has expired (future: implement expiry check)
130+
*
131+
* @param string $existing_token The current ec_token from session.
132+
* @param PayPalCart $cart The new cart data.
133+
* @return bool True if a new token should be created.
134+
*/
135+
private function needs_new_token( string $existing_token, $cart ): bool {
136+
// Case 1: No existing token.
137+
if ( empty( $existing_token ) ) {
138+
// Only attempt to create if the cart is valid (no validation issues).
139+
return ! $cart->issues();
140+
}
141+
142+
// Case 2: Token expired - could be implemented by checking PayPal order status.
143+
// For now, we preserve valid tokens and don't recreate them.
144+
// Future enhancement: call PayPal API to check if order is still valid.
145+
146+
return false;
147+
}
148+
149+
/**
150+
* Create a not found error response.
151+
*
152+
* @param string $message Error message.
153+
* @param array $details Error details.
154+
* @return WP_REST_Response The error response.
155+
*/
96156
private function error_not_found( string $message, array $details ): WP_REST_Response {
97157
return $this->error( new NotFoundError( $message, array( $details ) ) );
98158
}

modules/ppcp-store-sync/src/Response/ResponseFactory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ public function new_cart( PayPalCart $cart, string $cart_id, string $token ): Ne
5656
return new NewCartResponse( $cart, $cart_id, $token, $applied_coupons, $wc_cart );
5757
}
5858

59+
/**
60+
* Create a cart response with token (for PUT when a new token was created).
61+
*
62+
* Used for PUT /merchant-cart/{id} when the endpoint creates a new PayPal order
63+
* because the cart didn't have a token or the existing token was expired.
64+
*
65+
* @param PayPalCart $cart The cart object.
66+
* @param string $cart_id The cart ID.
67+
* @param string $token The payment token.
68+
* @return UpdatedCartWithTokenResponse The response object.
69+
*/
70+
public function cart_with_token( PayPalCart $cart, string $cart_id, string $token ): UpdatedCartWithTokenResponse {
71+
$wc_cart = $this->build_wc_cart_or_null( $cart );
72+
$applied_coupons = $this->build_applied_coupons( $cart );
73+
return new UpdatedCartWithTokenResponse( $cart, $cart_id, $token, $applied_coupons, $wc_cart );
74+
}
75+
5976
/**
6077
* Create a paid cart response.
6178
*
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
/**
3+
* PayPal Cart Response (cart updated with new token).
4+
*
5+
* Used when PUT /merchant-cart/{id} creates a new PayPal order because:
6+
* - The cart didn't have a token (POST validation failed)
7+
* - The existing token was expired
8+
*
9+
* @package WooCommerce\PayPalCommerce\StoreSync\Response
10+
*/
11+
12+
declare( strict_types = 1 );
13+
14+
namespace WooCommerce\PayPalCommerce\StoreSync\Response;
15+
16+
use WC_Cart;
17+
18+
use WooCommerce\PayPalCommerce\StoreSync\Schema\PayPalCart;
19+
20+
class UpdatedCartWithTokenResponse extends CartResponse {
21+
22+
/**
23+
* Constructor.
24+
*
25+
* @param PayPalCart $cart The PayPal cart.
26+
* @param string $cart_id The cart ID.
27+
* @param string $token The EC token.
28+
* @param array $applied_coupons Applied coupons data.
29+
* @param WC_Cart|null $wc_cart The WooCommerce cart.
30+
*/
31+
public function __construct(
32+
PayPalCart $cart,
33+
string $cart_id,
34+
string $token,
35+
array $applied_coupons = array(),
36+
?WC_Cart $wc_cart = null
37+
) {
38+
parent::__construct( $cart, $applied_coupons, $cart_id, $wc_cart );
39+
$this->token = $token;
40+
41+
// Updated carts with valid tokens are READY for checkout.
42+
if ( ! $this->cart->issues() && ! empty( $token ) ) {
43+
$this->status = 'READY';
44+
}
45+
}
46+
47+
/**
48+
* Convert to array for API response.
49+
*
50+
* Includes the payment_method.token only when a new token was created.
51+
* Per PayPal spec: "The API response should only mention new token,
52+
* i.e. when no token was generated, the payment_method.token property
53+
* should not exist in the response."
54+
*
55+
* @return array The response array.
56+
*/
57+
public function to_array(): array {
58+
$data = parent::to_array();
59+
60+
// Only include payment_method when a token exists.
61+
if ( ! empty( $this->token ) ) {
62+
$data['payment_method'] = array(
63+
'type' => 'paypal', // hard-coded.
64+
'token' => $this->token,
65+
);
66+
67+
// Add sandbox approval URL for testing.
68+
$data['_testing'] = array(
69+
'sandbox_approval_url' => 'https://www.sandbox.paypal.com/checkoutnow?token=' . $this->token,
70+
'instructions' => 'For sandbox testing: Open this URL in browser, login as buyer, and approve payment before checkout.',
71+
);
72+
}
73+
74+
return $data;
75+
}
76+
}

modules/ppcp-store-sync/src/Session/AgenticSessionHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function load_cart_session( string $session_id ): ?array {
117117
* @param PayPalCart $cart The updated cart.
118118
* @return bool True on success.
119119
*/
120-
public function update_cart_session( string $session_id, PayPalCart $cart ): bool {
120+
public function update_cart_session( string $session_id, PayPalCart $cart, ?string $ec_token = null ): bool {
121121
// Load the session first.
122122
$existing = $this->load_cart_session( $session_id );
123123
if ( ! $existing ) {

0 commit comments

Comments
 (0)