class-wc-rest-payments-orders-controller.php
15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
<?php
/**
* Class WC_REST_Payments_Orders_Controller
*
* @package WooCommerce\Payments\Admin
*/
defined( 'ABSPATH' ) || exit;
use WCPay\Logger;
use WCPay\Constants\Order_Status;
use WCPay\Constants\Payment_Intent_Status;
use WCPay\Constants\Payment_Method;
/**
* REST controller for order processing.
*/
class WC_REST_Payments_Orders_Controller extends WC_Payments_REST_Controller {
/**
* Endpoint path.
*
* @var string
*/
protected $rest_base = 'payments/orders';
/**
* Instance of WC_Payment_Gateway_WCPay
*
* @var WC_Payment_Gateway_WCPay
*/
private $gateway;
/**
* WC_Payments_Customer_Service instance for working with customer information
*
* @var WC_Payments_Customer_Service
*/
private $customer_service;
/**
* WC_Payments_Order_Service instance for updating order statuses.
*
* @var WC_Payments_Order_Service
*/
private $order_service;
/**
* WC_Payments_REST_Controller constructor.
*
* @param WC_Payments_API_Client $api_client WooCommerce Payments API client.
* @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments payment gateway.
* @param WC_Payments_Customer_Service $customer_service Customer class instance.
* @param WC_Payments_Order_Service $order_service Order Service class instance.
*/
public function __construct( WC_Payments_API_Client $api_client, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Customer_Service $customer_service, WC_Payments_Order_Service $order_service ) {
parent::__construct( $api_client );
$this->gateway = $gateway;
$this->customer_service = $customer_service;
$this->order_service = $order_service;
}
/**
* Configure REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<order_id>\w+)/capture_terminal_payment',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'capture_terminal_payment' ],
'permission_callback' => [ $this, 'check_permission' ],
'args' => [
'payment_intent_id' => [
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<order_id>\w+)/capture_authorization',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'capture_authorization' ],
'permission_callback' => [ $this, 'check_permission' ],
'args' => [
'payment_intent_id' => [
'required' => true,
],
],
]
);
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<order_id>\w+)/create_terminal_intent',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_terminal_intent' ],
'permission_callback' => [ $this, 'check_permission' ],
]
);
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<order_id>\d+)/create_customer',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_customer' ],
'permission_callback' => [ $this, 'check_permission' ],
]
);
}
/**
* Given an intent ID and an order ID, add the intent ID to the order and capture it.
* Use-cases: Mobile apps using it for `card_present` and `interac_present` payment types.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function capture_terminal_payment( WP_REST_Request $request ) {
try {
$intent_id = $request['payment_intent_id'];
$order_id = $request['order_id'];
// Do not process non-existing orders.
$order = wc_get_order( $order_id );
if ( false === $order ) {
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
}
// Do not process orders with refund(s).
if ( 0 < $order->get_total_refunded() ) {
return new WP_Error(
'wcpay_refunded_order_uncapturable',
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
[ 'status' => 400 ]
);
}
// Do not process intents that can't be captured.
$intent = $this->api_client->get_intent( $intent_id );
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
if ( $intent_meta_order_id !== $order->get_id() ) {
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
}
if ( ! in_array( $intent->get_status(), WC_Payment_Gateway_WCPay::SUCCESSFUL_INTENT_STATUS, true ) ) {
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
}
// Update the order: set the payment method and attach intent attributes.
$order->set_payment_method( WC_Payment_Gateway_WCPay::GATEWAY_ID );
$order->set_payment_method_title( __( 'WooCommerce In-Person Payments', 'woocommerce-payments' ) );
$intent_id = $intent->get_id();
$intent_status = $intent->get_status();
$charge = $intent->get_charge();
$charge_id = $charge ? $charge->get_id() : null;
$this->gateway->attach_intent_info_to_order(
$order,
$intent_id,
$intent_status,
$intent->get_payment_method_id(),
$intent->get_customer_id(),
$charge_id,
$intent->get_currency()
);
$this->gateway->update_order_status_from_intent(
$order,
$intent_id,
$intent_status,
$charge_id
);
// Certain payments (eg. Interac) are captured on the client-side (mobile app).
// The client may send us the captured intent to link it to its WC order.
// Doing so via this endpoint is more reliable than depending on the payment_intent.succeeded event.
$is_intent_captured = Payment_Intent_Status::SUCCEEDED === $intent->get_status();
$result_for_captured_intent = [
'status' => Payment_Intent_Status::SUCCEEDED,
'id' => $intent->get_id(),
];
$result = $is_intent_captured ? $result_for_captured_intent : $this->gateway->capture_charge( $order, false );
if ( Payment_Intent_Status::SUCCEEDED !== $result['status'] ) {
$http_code = $result['http_code'] ?? 502;
return new WP_Error(
'wcpay_capture_error',
sprintf(
// translators: %s: the error message.
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
),
[ 'status' => $http_code ]
);
}
// Store receipt generation URL for mobile applications in order meta-data.
$order->add_meta_data( 'receipt_url', get_rest_url( null, sprintf( '%s/payments/readers/receipts/%s', $this->namespace, $intent->get_id() ) ) );
// Actualize order status.
$this->order_service->mark_terminal_payment_completed( $order, $intent_id, $result['status'] );
return rest_ensure_response(
[
'status' => $result['status'],
'id' => $result['id'],
]
);
} catch ( \Throwable $e ) {
Logger::error( 'Failed to capture a terminal payment via REST API: ' . $e );
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
}
}
/**
* Captures an authorization.
* Use-cases: Merchants manually capturing a payment when they enable "capture later" option.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function capture_authorization( WP_REST_Request $request ) {
try {
$intent_id = $request['payment_intent_id'];
$order_id = $request['order_id'];
// Do not process non-existing orders.
$order = wc_get_order( $order_id );
if ( false === $order ) {
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
}
// Do not process orders with refund(s).
if ( 0 < $order->get_total_refunded() ) {
return new WP_Error(
'wcpay_refunded_order_uncapturable',
__( 'Payment cannot be captured for partially or fully refunded orders.', 'woocommerce-payments' ),
[ 'status' => 400 ]
);
}
// Do not process intents that can't be captured.
$intent = $this->api_client->get_intent( $intent_id );
$intent_metadata = is_array( $intent->get_metadata() ) ? $intent->get_metadata() : [];
$intent_meta_order_id_raw = $intent_metadata['order_id'] ?? '';
$intent_meta_order_id = is_numeric( $intent_meta_order_id_raw ) ? intval( $intent_meta_order_id_raw ) : 0;
if ( $intent_meta_order_id !== $order->get_id() ) {
Logger::error( 'Payment capture rejected due to failed validation: order id on intent is incorrect or missing.' );
return new WP_Error( 'wcpay_intent_order_mismatch', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
}
if ( ! in_array( $intent->get_status(), WC_Payment_Gateway_WCPay::SUCCESSFUL_INTENT_STATUS, true ) ) {
return new WP_Error( 'wcpay_payment_uncapturable', __( 'The payment cannot be captured', 'woocommerce-payments' ), [ 'status' => 409 ] );
}
$result = $this->gateway->capture_charge( $order, false );
if ( Payment_Intent_Status::SUCCEEDED !== $result['status'] ) {
return new WP_Error(
'wcpay_capture_error',
sprintf(
// translators: %s: the error message.
__( 'Payment capture failed to complete with the following message: %s', 'woocommerce-payments' ),
$result['message'] ?? __( 'Unknown error', 'woocommerce-payments' )
),
[ 'status' => $result['http_code'] ?? 502 ]
);
}
// Actualize order status.
$charge = $intent->get_charge();
$this->order_service->mark_payment_capture_completed( $order, $intent_id, $result['status'], $charge->get_id() );
return rest_ensure_response(
[
'status' => $result['status'],
'id' => $result['id'],
]
);
} catch ( \Throwable $e ) {
Logger::error( 'Failed to capture an authorization via REST API: ' . $e );
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
}
}
/**
* Returns customer id from order. Create or update customer if needed.
* Use-cases: It was used by older versions of our Mobile apps in their workflows.
*
* @deprecated 3.9.0
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_customer( $request ) {
wc_deprecated_function( __FUNCTION__, '3.9.0' );
try {
$order_id = $request['order_id'];
// Do not process non-existing orders.
$order = wc_get_order( $order_id );
if ( false === $order || ! ( $order instanceof WC_Order ) ) {
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
}
$disallowed_order_statuses = apply_filters(
'wcpay_create_customer_disallowed_order_statuses',
[
Order_Status::COMPLETED,
Order_Status::CANCELLED,
Order_Status::REFUNDED,
Order_Status::FAILED,
]
);
if ( $order->has_status( $disallowed_order_statuses ) ) {
return new WP_Error( 'wcpay_invalid_order_status', __( 'Invalid order status', 'woocommerce-payments' ), [ 'status' => 400 ] );
}
$order_user = $order->get_user();
$customer_id = $order->get_meta( '_stripe_customer_id' );
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
$is_guest_customer = false === $order_user;
// If the order is created for a registered customer, try extracting it's Stripe customer ID.
if ( ! $customer_id && ! $is_guest_customer ) {
$customer_id = $this->customer_service->get_customer_id_by_user_id( $order_user->ID );
}
$order_user = $is_guest_customer ? new WP_User() : $order_user;
$customer_id = $customer_id
? $this->customer_service->update_customer_for_user( $customer_id, $order_user, $customer_data )
: $this->customer_service->create_customer_for_user( $order_user, $customer_data );
$order->update_meta_data( '_stripe_customer_id', $customer_id );
$order->save();
return rest_ensure_response(
[
'id' => $customer_id,
]
);
} catch ( \Throwable $e ) {
Logger::error( 'Failed to create / update customer from order via REST API: ' . $e );
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
}
}
/**
* Create a new in-person payment intent for the given order ID without confirming it.
* Use-cases: Mobile apps using it for `card_present` payment types. (`interac_present` is handled by the apps via Stripe SDK).
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_terminal_intent( $request ) {
// Do not process non-existing orders.
$order = wc_get_order( $request['order_id'] );
if ( false === $order ) {
return new WP_Error( 'wcpay_missing_order', __( 'Order not found', 'woocommerce-payments' ), [ 'status' => 404 ] );
}
try {
$result = $this->gateway->create_intent(
$order,
$this->get_terminal_intent_payment_method( $request ),
$this->get_terminal_intent_capture_method( $request ),
$request->get_param( 'metadata' ) ?? [],
$request->get_param( 'customer_id' )
);
return rest_ensure_response( $result );
} catch ( \Throwable $e ) {
Logger::error( 'Failed to create an intention via REST API: ' . $e );
return new WP_Error( 'wcpay_server_error', __( 'Unexpected server error', 'woocommerce-payments' ), [ 'status' => 500 ] );
}
}
/**
* Return terminal intent payment method array based on payment methods request.
*
* @param WP_REST_Request $request Request object.
* @param array $default_value - default value.
*
* @return array|null
* @throws \Exception
*/
public function get_terminal_intent_payment_method( $request, array $default_value = [ Payment_Method::CARD_PRESENT ] ) :array {
$payment_methods = $request->get_param( 'payment_methods' );
if ( null === $payment_methods ) {
return $default_value;
}
if ( ! is_array( $payment_methods ) ) {
throw new \Exception( 'Invalid param \'payment_methods\'!' );
}
foreach ( $payment_methods as $value ) {
if ( ! in_array( $value, Payment_Method::IPP_ALLOWED_PAYMENT_METHODS, true ) ) {
throw new \Exception( 'One or more payment methods are not supported!' );
}
}
return $payment_methods;
}
/**
* Return terminal intent capture method based on capture method request.
*
* @param WP_REST_Request $request Request object.
* @param string $default_value default value.
*
* @return string|null
* @throws \Exception
*/
public function get_terminal_intent_capture_method( $request, string $default_value = 'manual' ) : string {
$capture_method = $request->get_param( 'capture_method' );
if ( null === $capture_method ) {
return $default_value;
}
if ( ! in_array( $capture_method, [ 'manual', 'automatic' ], true ) ) {
throw new \Exception( 'Invalid param \'capture_method\'!' );
}
return $capture_method;
}
}