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 / Orders.php

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

701 lines 23.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS Orders Class
4 * Extends WooCommerce Orders.
5 *
6 * @author Paul Kilmurray <paul@kilbot.com>
7 *
8 * @see http://wcpos.com
9 * @package WCPOS\WooCommercePOS
10 */
11
12 namespace WCPOS\WooCommercePOS;
13
14 use WC_Abstract_Order;
15 use WC_Coupon;
16 use WC_Order;
17 use WC_Order_Item;
18 use WC_Order_Item_Product;
19 use WC_Order_Item_Shipping;
20 use WC_Discounts;
21 use WC_Product;
22 use WC_Product_Simple;
23 use WC_Tax;
24
25 /**
26 * Orders Class
27 * - runs for all life cycles.
28 */
29 class Orders {
30 /**
31 * Map of temporary product IDs to category IDs for coupon validation.
32 *
33 * Static because multiple Orders instances may register hooks on the same
34 * filter (plugin init + test setUp). All instances must recognize temp IDs
35 * assigned by any other instance to avoid invalid DB reads.
36 *
37 * @var array<int, int[]>
38 */
39 private static $temp_product_categories = array();
40
41 /**
42 * Counter for generating unique temporary product IDs.
43 *
44 * Initialized with a random offset per request to avoid collisions when
45 * a persistent object cache backend (Redis/Memcached) is active and
46 * concurrent requests prime the same cache groups.
47 *
48 * @var int
49 */
50 private static $temp_id_counter = 0;
51
52 /**
53 * Constructor.
54 */
55 public function __construct() {
56 $this->register_order_status();
57 add_filter( 'wc_order_statuses', array( $this, 'wc_order_statuses' ), 10, 1 );
58 add_filter( 'woocommerce_order_needs_payment', array( $this, 'order_needs_payment' ), 10, 3 );
59 add_filter( 'woocommerce_valid_order_statuses_for_payment', array( $this, 'valid_order_statuses_for_payment' ), 10, 2 );
60 add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', array( $this, 'valid_order_statuses_for_payment_complete' ), 10, 2 );
61 add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'payment_complete_order_status' ), 10, 3 );
62 add_filter( 'woocommerce_bacs_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 );
63 add_filter( 'woocommerce_cheque_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 );
64 add_filter( 'woocommerce_cod_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 );
65 add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) );
66 add_filter( 'woocommerce_order_item_product', array( $this, 'order_item_product' ), 10, 2 );
67 add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 );
68 add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) );
69 add_action( 'woocommerce_order_item_shipping_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) );
70 add_filter( 'woocommerce_coupon_get_items_to_validate', array( $this, 'coupon_get_items_to_validate' ), 10, 2 );
71 add_filter( 'woocommerce_coupon_is_valid_for_product', array( $this, 'coupon_is_valid_for_product' ), 10, 4 );
72 add_action( 'woocommerce_order_after_calculate_totals', array( __CLASS__, 'cleanup_temp_caches' ), 999 );
73 }
74
75 /**
76 * Add custom POS order statuses.
77 *
78 * @param array $order_statuses Existing order statuses.
79 *
80 * @return array
81 */
82 public function wc_order_statuses( array $order_statuses ): array {
83 $order_statuses['wc-pos-open'] = /* translators: POS order status label. */ _x( 'POS - Open', 'Order status', 'woocommerce-pos' );
84 $order_statuses['wc-pos-partial'] = _x( 'POS - Partial Payment', 'Order status', 'woocommerce-pos' );
85
86 return $order_statuses;
87 }
88
89 /**
90 * WooCommerce order-pay form won't allow processing of orders with total = 0.
91 *
92 * NOTE: $needs_payment is meant to be a boolean, but I have seen it as null.
93 *
94 * @param bool $needs_payment Whether payment is needed.
95 * @param WC_Abstract_Order $order The order object.
96 * @param array $valid_order_statuses Valid order statuses for payment.
97 *
98 * @return bool
99 */
100 public function order_needs_payment( $needs_payment, WC_Abstract_Order $order, array $valid_order_statuses ) {
101 // If the order total is zero and status is a POS status, then allow payment to be taken, ie: Gift Card.
102 if ( 0 == $order->get_total() && \in_array( $order->get_status(), array( 'pos-open', 'pos-partial' ), true ) ) {
103 return true;
104 }
105
106 return $needs_payment;
107 }
108
109 /**
110 * Note: the wc- prefix is not used here because it is added by WooCommerce.
111 *
112 * @param array $order_statuses Valid order statuses.
113 * @param WC_Abstract_Order $order The order object.
114 *
115 * @return array
116 */
117 public function valid_order_statuses_for_payment( array $order_statuses, WC_Abstract_Order $order ): array {
118 $order_statuses[] = 'pos-open';
119 $order_statuses[] = 'pos-partial';
120
121 return $order_statuses;
122 }
123
124 /**
125 * Valid order statuses for payment complete.
126 *
127 * @param array $order_statuses Valid order statuses.
128 * @param WC_Abstract_Order $order The order object.
129 *
130 * @return array
131 */
132 public function valid_order_statuses_for_payment_complete( array $order_statuses, WC_Abstract_Order $order ): array {
133 $order_statuses[] = 'pos-open';
134 $order_statuses[] = 'pos-partial';
135
136 return $order_statuses;
137 }
138
139 /**
140 * Payment complete order status.
141 *
142 * @param string $status Order status.
143 * @param int $id Order ID.
144 * @param WC_Abstract_Order $order The order object.
145 *
146 * @return string
147 */
148 public function payment_complete_order_status( string $status, int $id, WC_Abstract_Order $order ): string {
149 if ( woocommerce_pos_request() ) {
150 $gateway_status = $this->get_gateway_order_status( $order->get_payment_method() );
151
152 // This filter expects statuses without the 'wc-' prefix.
153 $normalized_status = 0 === strpos( $gateway_status, 'wc-' )
154 ? substr( $gateway_status, 3 )
155 : $gateway_status;
156
157 if ( '' === $normalized_status ) {
158 return $status;
159 }
160
161 $valid_statuses = array_map(
162 function ( string $order_status ): string {
163 return 0 === strpos( $order_status, 'wc-' )
164 ? substr( $order_status, 3 )
165 : $order_status;
166 },
167 array_keys( wc_get_order_statuses() )
168 );
169
170 return \in_array( $normalized_status, $valid_statuses, true )
171 ? $normalized_status
172 : $status;
173 }
174
175 return $status;
176 }
177
178 /**
179 * Process payment order status for offline gateways (BACS, cheque, COD).
180 *
181 * @param string $status Order status from gateway.
182 * @param WC_Abstract_Order $order The order object.
183 *
184 * @return string
185 */
186 public function offline_process_payment_order_status( string $status, WC_Abstract_Order $order ): string {
187 if ( ! woocommerce_pos_request() ) {
188 return $status;
189 }
190
191 if ( ! $order->get_id() ) {
192 return $status;
193 }
194
195 if ( ! woocommerce_pos_is_pos_order( $order ) ) {
196 return $status;
197 }
198
199 $gateway_order_status = $this->get_gateway_order_status( $order->get_payment_method() );
200
201 $normalized_status = 0 === strpos( $gateway_order_status, 'wc-' )
202 ? substr( $gateway_order_status, 3 )
203 : $gateway_order_status;
204
205 if ( '' === $normalized_status ) {
206 return $status;
207 }
208
209 $valid_statuses = array_map(
210 function ( string $order_status ): string {
211 return 0 === strpos( $order_status, 'wc-' )
212 ? substr( $order_status, 3 )
213 : $order_status;
214 },
215 array_keys( wc_get_order_statuses() )
216 );
217
218 return \in_array( $normalized_status, $valid_statuses, true )
219 ? $normalized_status
220 : $status;
221 }
222
223 /**
224 * Resolve the configured POS order status for a given payment gateway.
225 *
226 * Looks up the per-gateway order_status from payment_gateways settings.
227 * Falls back to 'wc-completed' if no setting is found.
228 *
229 * @param string $gateway_id The payment gateway ID.
230 *
231 * @return string The configured order status (may include wc- prefix).
232 */
233 private function get_gateway_order_status( string $gateway_id ): string {
234 $gateway_settings = woocommerce_pos_get_settings( 'payment_gateways' );
235
236 if (
237 is_array( $gateway_settings )
238 && isset( $gateway_settings['gateways'][ $gateway_id ]['order_status'] )
239 && is_string( $gateway_settings['gateways'][ $gateway_id ]['order_status'] )
240 && '' !== $gateway_settings['gateways'][ $gateway_id ]['order_status']
241 ) {
242 return $gateway_settings['gateways'][ $gateway_id ]['order_status'];
243 }
244
245 return 'wc-completed';
246 }
247
248 /**
249 * Hides uuid from appearing on Order Edit page.
250 *
251 * @param array $meta_keys Hidden meta keys.
252 *
253 * @return array
254 */
255 public function hidden_order_itemmeta( array $meta_keys ): array {
256 return array_merge( $meta_keys, array( '_woocommerce_pos_uuid', '_woocommerce_pos_tax_status', '_woocommerce_pos_data' ) );
257 }
258
259 /**
260 * Filter the product object for an order item.
261 *
262 * @param bool|WC_Product $product The product object or false if not found.
263 * @param WC_Order_Item_Product $item The order item object.
264 *
265 * @return bool|WC_Product
266 */
267 public function order_item_product( $product, $item ) {
268 $pos_data_json = $item->get_meta( '_woocommerce_pos_data', true );
269
270 // For misc products (product_id=0), create a synthetic WC_Product_Simple.
271 // Requires _woocommerce_pos_data to distinguish POS items from other plugins.
272 if ( 0 === $item->get_product_id() ) {
273 if ( ! $pos_data_json ) {
274 return $product;
275 }
276
277 $product = new WC_Product_Simple();
278 $product->set_name( $item->get_name() );
279 $sku = $item->get_meta( '_sku', true );
280 if ( $sku ) {
281 $this->set_synthetic_product_sku( $product, $sku );
282 }
283
284 // Misc products are synthetic and never persisted to DB, so we can
285 // safely apply POS price context directly.
286 $pos_data = json_decode( $pos_data_json, true );
287 if ( JSON_ERROR_NONE === json_last_error() && \is_array( $pos_data ) ) {
288 if ( isset( $pos_data['price'] ) ) {
289 $product->set_price( $pos_data['price'] );
290 }
291 if ( isset( $pos_data['regular_price'] ) ) {
292 $product->set_regular_price( $pos_data['regular_price'] );
293 }
294 if ( isset( $pos_data['tax_status'] ) ) {
295 $product->set_tax_status( $pos_data['tax_status'] );
296 }
297 if ( ! empty( $pos_data['virtual'] ) ) {
298 $product->set_virtual( true );
299 }
300 if ( ! empty( $pos_data['downloadable'] ) ) {
301 $product->set_downloadable( true );
302 }
303 if ( ! empty( $pos_data['categories'] ) && is_array( $pos_data['categories'] ) ) {
304 $category_ids = array_filter( array_map( 'intval', array_column( $pos_data['categories'], 'id' ) ) );
305 $product->set_category_ids( $category_ids );
306 }
307 if ( $this->is_pos_discounted_item_on_sale( $item, $product ) && isset( $pos_data['price'] ) ) {
308 $product->set_sale_price( $pos_data['price'] );
309 }
310 }
311
312 return $product;
313 }
314
315 // For real products, only apply POS overrides when pos_data exists.
316 if ( ! $product || empty( $pos_data_json ) ) {
317 return $product;
318 }
319
320 $pos_data = json_decode( $pos_data_json, true );
321 if ( JSON_ERROR_NONE !== json_last_error() || ! \is_array( $pos_data ) ) {
322 return $product;
323 }
324
325 // Use an isolated product instance for coupon-specific context.
326 if ( $product->get_id() ) {
327 $product = wc_get_product_object( $product->get_type(), $product->get_id() );
328 }
329
330 if ( isset( $pos_data['tax_status'] ) ) {
331 $product->set_tax_status( $pos_data['tax_status'] );
332 }
333
334 return $product;
335 }
336
337 /**
338 * Provide coupon-validation products with POS context (sale state, tax status).
339 *
340 * This runs only inside WC_Discounts. We return per-line-item product objects
341 * so coupon rules (exclude_sale_items, tax-aware discount amounts) use POS data
342 * without mutating products used by stock update routines.
343 *
344 * @param array $items Discount items (stdClass objects).
345 * @param WC_Discounts $discounts Discounts context.
346 *
347 * @return array
348 */
349 public function coupon_get_items_to_validate( array $items, WC_Discounts $discounts ): array {
350 $object = $discounts->get_object();
351 if ( ! $object instanceof WC_Order || ! woocommerce_pos_is_pos_order( $object ) ) {
352 return $items;
353 }
354
355 foreach ( $items as $index => $discount_item ) {
356 if ( ! isset( $discount_item->object ) || ! $discount_item->object instanceof WC_Order_Item_Product ) {
357 continue;
358 }
359
360 $original_product = isset( $discount_item->product ) && $discount_item->product instanceof WC_Product
361 ? $discount_item->product
362 : null;
363 $coupon_product = $this->build_coupon_product_context( $discount_item->object, $original_product );
364
365 if ( $coupon_product instanceof WC_Product ) {
366 $items[ $index ]->product = $coupon_product;
367 }
368 }
369
370 return $items;
371 }
372
373 /**
374 * Build a product object used only for coupon validation/calculation.
375 *
376 * @param WC_Order_Item_Product $item Order item.
377 * @param WC_Product|null $product Current product object.
378 *
379 * @return WC_Product|null
380 */
381 private function build_coupon_product_context( WC_Order_Item_Product $item, ?WC_Product $product = null ): ?WC_Product {
382 $pos_data = $this->get_pos_item_data( $item );
383 if ( null === $pos_data ) {
384 return $product;
385 }
386
387 $is_temp_id = $product && isset( self::$temp_product_categories[ $product->get_id() ] );
388
389 if ( $product && $product->get_id() && ! $is_temp_id ) {
390 // Get a fresh product instance to apply POS overrides.
391 $product = wc_get_product_object( $product->get_type(), $product->get_id() );
392 } elseif ( 0 === $item->get_product_id() ) {
393 $product = new WC_Product_Simple();
394 $product->set_name( $item->get_name() );
395 $sku = $item->get_meta( '_sku', true );
396 if ( $sku ) {
397 $this->set_synthetic_product_sku( $product, $sku );
398 }
399 }
400
401 if ( ! $product ) {
402 return null;
403 }
404
405 if ( isset( $pos_data['price'] ) ) {
406 $product->set_price( $pos_data['price'] );
407 }
408 if ( isset( $pos_data['regular_price'] ) ) {
409 $product->set_regular_price( $pos_data['regular_price'] );
410 }
411 if ( isset( $pos_data['tax_status'] ) ) {
412 $product->set_tax_status( $pos_data['tax_status'] );
413 }
414 if ( ! empty( $pos_data['virtual'] ) ) {
415 $product->set_virtual( true );
416 }
417 if ( ! empty( $pos_data['downloadable'] ) ) {
418 $product->set_downloadable( true );
419 }
420 if ( ! empty( $pos_data['categories'] ) && is_array( $pos_data['categories'] ) ) {
421 $category_ids = array_filter( array_map( 'intval', array_column( $pos_data['categories'], 'id' ) ) );
422 $product->set_category_ids( $category_ids );
423
424 // Assign a temporary non-zero ID and prime WP caches so that
425 // WC's get_the_terms() (called via wc_get_product_cat_ids) finds
426 // our categories. get_the_terms() requires get_post() to succeed
427 // and checks the object term cache before querying the DB.
428 if ( 0 === $product->get_id() && ! empty( $category_ids ) ) {
429 if ( 0 === self::$temp_id_counter ) {
430 // Use a random offset so concurrent requests don't collide
431 // when a persistent object cache is active.
432 self::$temp_id_counter = PHP_INT_MAX - wp_rand( 0, 999999 );
433 }
434 $temp_id = self::$temp_id_counter--;
435 $product->set_id( $temp_id );
436 self::$temp_product_categories[ $temp_id ] = $category_ids;
437
438 // Prime post cache so get_post(temp_id) succeeds.
439 $fake_post = new \stdClass();
440 $fake_post->ID = $temp_id;
441 $fake_post->post_type = 'product';
442 $fake_post->post_status = 'publish';
443 $fake_post->filter = 'raw';
444 $fake_post->post_parent = 0;
445 $fake_post->post_title = '';
446 $fake_post->post_content = '';
447 $fake_post->post_excerpt = '';
448 $fake_post->post_date = '';
449 $fake_post->post_date_gmt = '';
450 wp_cache_set( $temp_id, $fake_post, 'posts' );
451
452 // Prime term cache so get_object_term_cache() returns our IDs.
453 // get_the_terms() checks this cache before querying the DB,
454 // which is how validate_coupon_product_categories sees our categories.
455 wp_cache_set( $temp_id, $category_ids, 'product_cat_relationships' );
456 }
457 }
458 if ( $this->is_pos_discounted_item_on_sale( $item, $product ) && isset( $pos_data['price'] ) ) {
459 $product->set_sale_price( $pos_data['price'] );
460 }
461
462 return $product;
463 }
464
465 /**
466 * Override coupon category validation for misc products.
467 *
468 * WooCommerce uses wc_get_product_cat_ids( $product->get_id() ) to check
469 * product_categories and excluded_product_categories coupon restrictions.
470 * Synthetic misc products use temporary non-zero IDs so WC's DB lookup
471 * doesn't short-circuit. This filter re-evaluates using per-item categories.
472 *
473 * @param bool $valid Whether the coupon is valid for the product.
474 * @param WC_Product $product Product being validated.
475 * @param WC_Coupon $coupon Coupon being applied.
476 * @param mixed $values Values (order item or cart item data).
477 *
478 * @return bool
479 */
480 public function coupon_is_valid_for_product( bool $valid, $product, $coupon, $values ): bool {
481 if ( ! $product instanceof WC_Product ) {
482 return $valid;
483 }
484
485 // Only handle products with temp IDs assigned by build_coupon_product_context.
486 $product_id = $product->get_id();
487 if ( ! isset( self::$temp_product_categories[ $product_id ] ) ) {
488 return $valid;
489 }
490
491 $product_cats = $product->get_category_ids();
492 if ( empty( $product_cats ) ) {
493 return $valid;
494 }
495
496 // Include parent categories for hierarchy matching (parity with wc_get_product_cat_ids).
497 foreach ( $product_cats as $cat ) {
498 $product_cats = array_merge( $product_cats, get_ancestors( $cat, 'product_cat' ) );
499 }
500 $product_cats = array_unique( $product_cats );
501
502 // Re-evaluate product_categories restriction.
503 $coupon_cats = $coupon->get_product_categories();
504 if ( ! empty( $coupon_cats ) ) {
505 $valid = $valid && count( array_intersect( $product_cats, $coupon_cats ) ) > 0;
506 }
507
508 // Re-evaluate excluded_product_categories restriction.
509 $excluded_cats = $coupon->get_excluded_product_categories();
510 if ( ! empty( $excluded_cats ) && count( array_intersect( $product_cats, $excluded_cats ) ) > 0 ) {
511 $valid = false;
512 }
513
514 return $valid;
515 }
516
517 /**
518 * Remove temporary cache entries created by build_coupon_product_context().
519 *
520 * Hooked to woocommerce_order_after_calculate_totals (after all coupon
521 * validation is complete) so that persistent object cache backends
522 * (Redis/Memcached) don't accumulate stale entries across requests.
523 */
524 public static function cleanup_temp_caches(): void {
525 foreach ( array_keys( self::$temp_product_categories ) as $temp_id ) {
526 wp_cache_delete( $temp_id, 'posts' );
527 wp_cache_delete( $temp_id, 'product_cat_relationships' );
528 }
529 self::$temp_product_categories = array();
530 self::$temp_id_counter = 0;
531 }
532
533 /**
534 * Determine whether an order item should be treated as "on sale" in coupon checks.
535 *
536 * @param WC_Order_Item_Product $item Order item.
537 * @param WC_Product|null $product Product context (optional).
538 *
539 * @return bool
540 */
541 private function is_pos_discounted_item_on_sale( WC_Order_Item_Product $item, ?WC_Product $product = null ): bool {
542 $pos_data = $this->get_pos_item_data( $item );
543 if ( null === $pos_data || ! isset( $pos_data['price'], $pos_data['regular_price'] ) ) {
544 return false;
545 }
546
547 $is_on_sale = (float) $pos_data['price'] < (float) $pos_data['regular_price'];
548 $product = $product ? $product : $item->get_product();
549
550 return (bool) apply_filters(
551 'woocommerce_pos_item_is_on_sale',
552 $is_on_sale,
553 $product,
554 $item,
555 $pos_data
556 );
557 }
558
559 /**
560 * Decode _woocommerce_pos_data from an order item.
561 *
562 * @param WC_Order_Item_Product $item Order item.
563 *
564 * @return array<string, mixed>|null
565 */
566 private function get_pos_item_data( WC_Order_Item_Product $item ): ?array {
567 $pos_data_json = $item->get_meta( '_woocommerce_pos_data', true );
568 if ( empty( $pos_data_json ) ) {
569 return null;
570 }
571
572 $pos_data = json_decode( $pos_data_json, true );
573 if ( JSON_ERROR_NONE !== json_last_error() || ! \is_array( $pos_data ) ) {
574 return null;
575 }
576
577 return $pos_data;
578 }
579
580 /**
581 * Get tax location for this order.
582 *
583 * @param array $args Override the location.
584 * @param WC_Abstract_Order $order The order object.
585 *
586 * @return array
587 */
588 public function get_tax_location( $args, WC_Abstract_Order $order ) {
589 if ( ! woocommerce_pos_is_pos_order( $order ) ) {
590 return $args;
591 }
592
593 $tax_based_on = $order->get_meta( '_woocommerce_pos_tax_based_on' );
594
595 if ( $order instanceof WC_Order ) {
596 if ( 'billing' == $tax_based_on ) {
597 $args['country'] = $order->get_billing_country();
598 $args['state'] = $order->get_billing_state();
599 $args['postcode'] = $order->get_billing_postcode();
600 $args['city'] = $order->get_billing_city();
601 } elseif ( 'shipping' == $tax_based_on ) {
602 $args['country'] = $order->get_shipping_country();
603 $args['state'] = $order->get_shipping_state();
604 $args['postcode'] = $order->get_shipping_postcode();
605 $args['city'] = $order->get_shipping_city();
606 } else {
607 $args['country'] = WC()->countries->get_base_country();
608 $args['state'] = WC()->countries->get_base_state();
609 $args['postcode'] = WC()->countries->get_base_postcode();
610 $args['city'] = WC()->countries->get_base_city();
611 }
612 }
613
614 return $args;
615 }
616
617 /**
618 * Calculate taxes for an order item.
619 *
620 * @param WC_Order_Item|WC_Order_Item_Shipping $item Order item object.
621 *
622 * @return void
623 */
624 public function order_item_after_calculate_taxes( $item ): void {
625 $meta_data = $item->get_meta_data();
626
627 foreach ( $meta_data as $meta ) {
628 if ( '_woocommerce_pos_data' === $meta->key ) {
629 $pos_data = json_decode( $meta->value, true );
630
631 if ( JSON_ERROR_NONE === json_last_error() ) {
632 if ( isset( $pos_data['tax_status'] ) && 'none' == $pos_data['tax_status'] ) {
633 $item->set_taxes( false );
634 }
635 } else {
636 Logger::log( 'JSON parse error: ' . json_last_error_msg() );
637 }
638
639 break;
640 }
641 }
642 }
643
644 /**
645 * Register the POS order statuses.
646 */
647 private function register_order_status(): void {
648 // Order status for open orders.
649 register_post_status(
650 'wc-pos-open',
651 array(
652 'label' => /* translators: POS order status label. */ _x( 'POS - Open', 'Order status', 'woocommerce-pos' ),
653 'public' => true,
654 'exclude_from_search' => false,
655 'show_in_admin_all_list' => true,
656 'show_in_admin_status_list' => true,
657 // translators: %s is the number of orders with POS - Open status.
658 'label_count' => _n_noop(
659 'POS - Open <span class="count">(%s)</span>',
660 'POS - Open <span class="count">(%s)</span>',
661 'woocommerce-pos'
662 ),
663 )
664 );
665
666 // Order status for partial payment orders.
667 register_post_status(
668 'wc-pos-partial',
669 array(
670 'label' => _x( 'POS - Partial Payment', 'Order status', 'woocommerce-pos' ),
671 'public' => true,
672 'exclude_from_search' => false,
673 'show_in_admin_all_list' => true,
674 'show_in_admin_status_list' => true,
675 // translators: %s is the number of orders with POS - Partial Payment status.
676 'label_count' => _n_noop(
677 'POS - Partial Payment <span class="count">(%s)</span>',
678 'POS - Partial Payment <span class="count">(%s)</span>',
679 'woocommerce-pos'
680 ),
681 )
682 );
683 }
684
685 /**
686 * Set SKU on a synthetic product, bypassing WooCommerce's uniqueness check.
687 *
688 * Synthetic products (product_id=0) are never saved to the database, so
689 * SKU collisions with real products are irrelevant. Temporarily disabling
690 * object_read causes set_sku() to skip the wc_product_has_unique_sku() call.
691 *
692 * @param WC_Product_Simple $product Synthetic product instance.
693 * @param string $sku SKU value from order-item meta.
694 */
695 private function set_synthetic_product_sku( WC_Product_Simple $product, string $sku ): void {
696 $product->set_object_read( false );
697 $product->set_sku( $sku );
698 $product->set_object_read( true );
699 }
700 }
701