PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / API / Checkout_Controller.php

Checkout_Controller.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.15, at includes/API/Checkout_Controller.php

351 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * POS checkout controller.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API;
9
10 \defined( 'ABSPATH' ) || die;
11
12 use WC_Payment_Gateway;
13 use WC_REST_Controller;
14 use WCPOS\WooCommercePOS\Payments\Checkout_State_Repository;
15 use WCPOS\WooCommercePOS\Payments\Gateway_Contract;
16 use WCPOS\WooCommercePOS\Payments\Idempotency_Repository;
17 use WP_Error;
18 use WP_REST_Request;
19 use WP_REST_Response;
20 use WP_REST_Server;
21
22 /**
23 * POS checkout controller.
24 */
25 class Checkout_Controller extends WC_REST_Controller {
26 /**
27 * REST namespace.
28 *
29 * @var string
30 */
31 protected $namespace = 'wcpos/v1';
32
33 /**
34 * REST base.
35 *
36 * @var string
37 */
38 protected $rest_base = 'orders';
39
40 /**
41 * Checkout state repository.
42 *
43 * @var Checkout_State_Repository
44 */
45 private $state_repository;
46
47 /**
48 * Idempotency repository.
49 *
50 * @var Idempotency_Repository
51 */
52 private $idempotency_repository;
53
54 /**
55 * Shared gateway contract helper.
56 *
57 * @var Gateway_Contract
58 */
59 private $gateway_contract;
60
61 /**
62 * Constructor.
63 */
64 public function __construct() {
65 $this->state_repository = new Checkout_State_Repository();
66 $this->idempotency_repository = new Idempotency_Repository();
67 $this->gateway_contract = new Gateway_Contract();
68 }
69
70 /**
71 * Register routes.
72 */
73 public function register_routes(): void {
74 register_rest_route(
75 $this->namespace,
76 '/' . $this->rest_base . '/(?P<id>[\d]+)/checkout',
77 array(
78 array(
79 'methods' => WP_REST_Server::CREATABLE,
80 'callback' => array( $this, 'create_item' ),
81 'permission_callback' => array( $this, 'create_item_permissions_check' ),
82 ),
83 array(
84 'methods' => WP_REST_Server::READABLE,
85 'callback' => array( $this, 'get_item' ),
86 'permission_callback' => array( $this, 'get_item_permissions_check' ),
87 ),
88 )
89 );
90 }
91
92 /**
93 * Read permissions check.
94 *
95 * @param WP_REST_Request $_request Request object.
96 */
97 public function get_item_permissions_check( $_request ) {
98 return current_user_can( 'publish_shop_orders' )
99 ? true
100 : new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot view checkout state.', 'woocommerce-pos' ), array( 'status' => rest_authorization_required_code() ) );
101 }
102
103 /**
104 * Create permissions check.
105 *
106 * @param WP_REST_Request $_request Request object.
107 */
108 public function create_item_permissions_check( $_request ) {
109 return current_user_can( 'publish_shop_orders' )
110 ? true
111 : new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot process checkout.', 'woocommerce-pos' ), array( 'status' => rest_authorization_required_code() ) );
112 }
113
114 /**
115 * Create a checkout state mutation.
116 *
117 * @param WP_REST_Request $request Request object.
118 */
119 public function create_item( $request ) {
120 $order = $this->get_order( (int) $request['id'] );
121 if ( is_wp_error( $order ) ) {
122 return $order;
123 }
124
125 $params = $request->get_json_params();
126 if ( empty( $params ) ) {
127 $params = $request->get_body_params();
128 }
129 if ( empty( $params ) ) {
130 $params = $request->get_params();
131 }
132
133 $idempotency_key = (string) $request->get_header( 'X-WCPOS-Idempotency-Key' );
134 if ( empty( $idempotency_key ) ) {
135 return new WP_Error(
136 'wcpos_missing_idempotency_key',
137 /* translators: REST API schema field label or error message. */
138 __( 'Missing X-WCPOS-Idempotency-Key header.', 'woocommerce-pos' ),
139 array( 'status' => 400 )
140 );
141 }
142
143 $gateway_id = isset( $params['gateway_id'] ) ? (string) $params['gateway_id'] : '';
144 $gateway = $this->get_gateway( $gateway_id );
145 if ( ! $gateway ) {
146 return new WP_Error(
147 'wcpos_payment_gateway_not_found',
148 /* translators: REST API schema field label or error message. */
149 __( 'Payment gateway not found.', 'woocommerce-pos' ),
150 array( 'status' => 404 )
151 );
152 }
153
154 if ( ! $this->gateway_contract->is_pos_enabled( $gateway ) || ! $this->gateway_contract->supports_checkout( $gateway, $request ) ) {
155 return new WP_Error(
156 'wcpos_payment_gateway_not_available',
157 __( 'Payment gateway is not available for POS checkout.', 'woocommerce-pos' ),
158 array( 'status' => 400 )
159 );
160 }
161
162 $idempotency_scope = $this->get_idempotency_scope( $order->get_id() );
163 $request_hash = md5(
164 wp_json_encode(
165 $this->normalize_for_hash(
166 array(
167 'order_id' => $order->get_id(),
168 'params' => $params,
169 )
170 )
171 )
172 );
173 $claim = $this->idempotency_repository->claim( $idempotency_scope, $idempotency_key, $request_hash );
174
175 if ( is_wp_error( $claim ) ) {
176 return $claim;
177 }
178
179 if ( is_array( $claim ) ) {
180 return new WP_REST_Response( $claim['body'], $claim['status_code'] );
181 }
182
183 try {
184 $action = isset( $params['action'] ) ? (string) $params['action'] : 'start';
185 $payment_data = isset( $params['payment_data'] ) && is_array( $params['payment_data'] ) ? $params['payment_data'] : array();
186 $state = $this->dispatch_checkout_action( $gateway_id, $order->get_id(), $action, $payment_data, $order, $request );
187
188 if ( is_wp_error( $state ) ) {
189 return $state;
190 }
191
192 $state = $this->normalize_state( $order->get_id(), $gateway_id, $state );
193 $this->state_repository->upsert( $order->get_id(), $state );
194
195 if ( 'completed' === $state['status'] ) {
196 $order->update_meta_data( '_pos_checkout_gateway_id', $gateway_id );
197 $order->update_meta_data( '_pos_checkout_idempotency_key', $idempotency_key );
198 $order->save_meta_data();
199 }
200
201 $this->idempotency_repository->store( $idempotency_scope, $idempotency_key, $request_hash, 200, $state );
202
203 return rest_ensure_response( $state );
204 } finally {
205 $this->idempotency_repository->release( $idempotency_scope, $idempotency_key );
206 }
207 }
208
209 /**
210 * Return the last known checkout state.
211 *
212 * @param WP_REST_Request $request Request object.
213 */
214 public function get_item( $request ) {
215 $order = $this->get_order( (int) $request['id'] );
216 if ( is_wp_error( $order ) ) {
217 return $order;
218 }
219
220 $state = $this->state_repository->get( $order->get_id() );
221 if ( empty( $state ) ) {
222 $gateway_id = $order->get_meta( '_pos_checkout_gateway_id', true );
223
224 $state = array(
225 'checkout_id' => null,
226 'order_id' => $order->get_id(),
227 'gateway_id' => $gateway_id ? $gateway_id : '',
228 'status' => 'pending',
229 'provider_data' => array(),
230 'terminal' => false,
231 );
232 }
233
234 return rest_ensure_response( $state );
235 }
236
237 /**
238 * Get an order by ID.
239 *
240 * @param int $order_id Order ID.
241 */
242 private function get_order( int $order_id ) {
243 $order = wc_get_order( $order_id );
244
245 if ( ! $order ) {
246 return new WP_Error(
247 'wcpos_order_not_found',
248 /* translators: REST API schema field label or error message. */
249 __( 'Order not found.', 'woocommerce-pos' ),
250 array( 'status' => 404 )
251 );
252 }
253
254 return $order;
255 }
256
257 /**
258 * Get a payment gateway by ID.
259 *
260 * @param string $gateway_id Gateway ID.
261 */
262 private function get_gateway( string $gateway_id ): ?WC_Payment_Gateway {
263 WC()->payment_gateways();
264 $gateways = WC()->payment_gateways->payment_gateways();
265
266 return $gateways[ $gateway_id ] ?? null;
267 }
268
269 /**
270 * Dispatch checkout processing to the resolved gateway only.
271 *
272 * @param string $gateway_id Gateway ID.
273 * @param int $order_id Order ID.
274 * @param string $action Checkout action.
275 * @param array $payment_data Payment data.
276 * @param \WC_Order $order Order object.
277 * @param WP_REST_Request $request Request object.
278 *
279 * @return array|WP_Error
280 */
281 private function dispatch_checkout_action( string $gateway_id, int $order_id, string $action, array $payment_data, $order, WP_REST_Request $request ) {
282 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Public POS checkout contract filter.
283 return apply_filters(
284 "wcpos_process_checkout_action_{$gateway_id}",
285 array(
286 'checkout_id' => wp_generate_uuid4(),
287 'order_id' => $order_id,
288 'gateway_id' => $gateway_id,
289 'status' => 'processing',
290 'provider_data' => array(),
291 'terminal' => false,
292 ),
293 $action,
294 $payment_data,
295 $order,
296 $request
297 );
298 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
299 }
300
301 /**
302 * Build order-scoped idempotency namespace.
303 *
304 * @param int $order_id Order ID.
305 */
306 private function get_idempotency_scope( int $order_id ): string {
307 return 'checkout:' . $order_id;
308 }
309
310 /**
311 * Normalize checkout state payload.
312 *
313 * @param int $order_id Order ID.
314 * @param string $gateway_id Gateway ID.
315 * @param array $state Raw state.
316 */
317 private function normalize_state( int $order_id, string $gateway_id, array $state ): array {
318 $status = (string) ( $state['status'] ?? 'processing' );
319
320 return array(
321 'checkout_id' => $state['checkout_id'] ?? null,
322 'order_id' => $order_id,
323 'gateway_id' => $state['gateway_id'] ?? $gateway_id,
324 'status' => $status,
325 'provider_data' => isset( $state['provider_data'] ) && is_array( $state['provider_data'] ) ? $state['provider_data'] : array(),
326 'terminal' => ( isset( $state['terminal'] ) ? (bool) $state['terminal'] : false ) || $this->gateway_contract->is_terminal_status( $status ),
327 );
328 }
329
330 /**
331 * Normalize request data for idempotency hashing.
332 *
333 * @param mixed $value Raw value.
334 *
335 * @return mixed
336 */
337 private function normalize_for_hash( $value ) {
338 if ( ! is_array( $value ) ) {
339 return $value;
340 }
341
342 ksort( $value );
343
344 foreach ( $value as $key => $nested ) {
345 $value[ $key ] = $this->normalize_for_hash( $nested );
346 }
347
348 return $value;
349 }
350 }
351