PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.0
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.0
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 / V1 / Checkout_Controller.php

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

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