Skip to content

Commit 9b0dad7

Browse files
committed
fix: 🐛 Validate and retry PayPal order response missing id
PayPal's v2/checkout/orders endpoint intermittently returns HTTP 200/201 with a response body missing the required id field, causing orders to fail with "Order does not contain an id" at OrderFactory.php. Extract request execution into execute_order_request() which validates the decoded response before passing it to the order factory. When the response is incomplete (null json_decode or missing id), log the raw response body and retry once with a new idempotency key. This matches the manual retry that always succeeds for affected customers.
1 parent 658e627 commit 9b0dad7

2 files changed

Lines changed: 98 additions & 22 deletions

File tree

modules/ppcp-wc-gateway/src/Endpoint/CaptureCardPayment.php

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,57 @@ static function ( PurchaseUnit $item ): array {
164164
'body' => wp_json_encode( $data ),
165165
);
166166

167-
$response = $this->request( $url, $args );
168-
if ( $response instanceof WP_Error ) {
169-
throw new RuntimeException( $response->get_error_message() );
170-
}
167+
$json = $this->execute_order_request( $url, $args );
168+
169+
return $this->order_factory->from_paypal_response( $json );
170+
}
171171

172-
$json = json_decode( $response['body'] );
173-
$status_code = (int) wp_remote_retrieve_response_code( $response );
174-
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
175-
$error = new PayPalApiException( $json, $status_code );
176-
$this->logger->warning( $error->getMessage() );
177-
throw $error;
172+
/**
173+
* Executes the order creation request, retrying once on an incomplete response.
174+
*
175+
* PayPal's v2/checkout/orders endpoint intermittently returns a success status
176+
* with a response body missing the required id field. A single retry with a new
177+
* idempotency key recovers from this transient issue.
178+
*
179+
* @param string $url The request URL.
180+
* @param array $args The request arguments.
181+
* @return \stdClass Decoded response with a valid id.
182+
*
183+
* @throws RuntimeException When the response is incomplete after retry or PayPal returns a non-success status.
184+
*/
185+
private function execute_order_request( string $url, array $args ): \stdClass {
186+
for ( $attempt = 0; $attempt < 2; $attempt++ ) {
187+
if ( $attempt > 0 ) {
188+
$this->logger->info( 'Retrying PayPal order creation after incomplete response.' );
189+
$args['headers']['PayPal-Request-Id'] = uniqid( 'ppcp-', true );
190+
}
191+
192+
$response = $this->request( $url, $args );
193+
if ( $response instanceof WP_Error ) {
194+
throw new RuntimeException( $response->get_error_message() );
195+
}
196+
197+
$json = json_decode( $response['body'] );
198+
$status_code = (int) wp_remote_retrieve_response_code( $response );
199+
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
200+
$error = new PayPalApiException( $json, $status_code );
201+
$this->logger->warning( $error->getMessage() );
202+
throw $error;
203+
}
204+
205+
if ( $json instanceof \stdClass && isset( $json->id ) ) {
206+
return $json;
207+
}
208+
209+
$this->logger->warning(
210+
'PayPal order response missing id.',
211+
array(
212+
'status_code' => $status_code,
213+
'response_body' => $response['body'],
214+
)
215+
);
178216
}
179217

180-
return $this->order_factory->from_paypal_response( $json );
218+
throw new RuntimeException( 'PayPal order response missing id after retry.' );
181219
}
182220
}

modules/ppcp-wc-gateway/src/Endpoint/CapturePayPalPayment.php

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,57 @@ static function ( PurchaseUnit $item ): array {
104104
'body' => wp_json_encode( $data ),
105105
);
106106

107-
$response = $this->request( $url, $args );
108-
if ( $response instanceof WP_Error ) {
109-
throw new RuntimeException( $response->get_error_message() );
110-
}
107+
$json = $this->execute_order_request( $url, $args );
111108

112-
$json = json_decode( $response['body'] );
113-
$status_code = (int) wp_remote_retrieve_response_code( $response );
114-
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
115-
$error = new PayPalApiException( $json, $status_code );
116-
$this->logger->warning( $error->getMessage() );
117-
throw $error;
109+
return $this->order_factory->from_paypal_response( $json );
110+
}
111+
112+
/**
113+
* Executes the order creation request, retrying once on an incomplete response.
114+
*
115+
* PayPal's v2/checkout/orders endpoint intermittently returns a success status
116+
* with a response body missing the required id field. A single retry with a new
117+
* idempotency key recovers from this transient issue.
118+
*
119+
* @param string $url The request URL.
120+
* @param array $args The request arguments.
121+
* @return \stdClass Decoded response with a valid id.
122+
*
123+
* @throws RuntimeException When the response is incomplete after retry or PayPal returns a non-success status.
124+
*/
125+
private function execute_order_request( string $url, array $args ): \stdClass {
126+
for ( $attempt = 0; $attempt < 2; $attempt++ ) {
127+
if ( $attempt > 0 ) {
128+
$this->logger->info( 'Retrying PayPal order creation after incomplete response.' );
129+
$args['headers']['PayPal-Request-Id'] = uniqid( 'ppcp-', true );
130+
}
131+
132+
$response = $this->request( $url, $args );
133+
if ( $response instanceof WP_Error ) {
134+
throw new RuntimeException( $response->get_error_message() );
135+
}
136+
137+
$json = json_decode( $response['body'] );
138+
$status_code = (int) wp_remote_retrieve_response_code( $response );
139+
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
140+
$error = new PayPalApiException( $json, $status_code );
141+
$this->logger->warning( $error->getMessage() );
142+
throw $error;
143+
}
144+
145+
if ( $json instanceof \stdClass && isset( $json->id ) ) {
146+
return $json;
147+
}
148+
149+
$this->logger->warning(
150+
'PayPal order response missing id.',
151+
array(
152+
'status_code' => $status_code,
153+
'response_body' => $response['body'],
154+
)
155+
);
118156
}
119157

120-
return $this->order_factory->from_paypal_response( $json );
158+
throw new RuntimeException( 'PayPal order response missing id after retry.' );
121159
}
122160
}

0 commit comments

Comments
 (0)