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 / Services / Stock_Validator.php

Stock_Validator.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.0, at includes/Services/Stock_Validator.php

219 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * POS stock validation.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 use WC_Order;
11 use WC_Order_Item_Product;
12 use WC_Product;
13 use WP_Error;
14 use WP_REST_Request;
15
16 /**
17 * Rejects paid POS orders whose quantities exceed available stock.
18 */
19 class Stock_Validator {
20 /**
21 * Register the REST order validation hook.
22 */
23 public function __construct() {
24 add_filter( 'woocommerce_rest_pre_insert_shop_order_object', array( $this, 'validate_stock' ), 10, 3 );
25 }
26
27 /**
28 * Validate stock before a POS REST order is written.
29 *
30 * @param WC_Order|WP_Error $order Prepared order object.
31 * @param WP_REST_Request $request REST request.
32 * @param bool $creating Whether the order is being created.
33 *
34 * @return WC_Order|WP_Error
35 */
36 public function validate_stock( $order, WP_REST_Request $request, bool $creating ) {
37 if ( is_wp_error( $order ) || ! $order instanceof WC_Order ) {
38 return $order;
39 }
40
41 if ( ! \wcpos_request() || ! Settings::instance()->prevent_overselling_enabled() ) {
42 return $order;
43 }
44
45 if ( ! $this->should_validate_status( $order, $request, $creating ) ) {
46 return $order;
47 }
48
49 $failures = array();
50 $managed_stock = array();
51
52 $line_index = 0;
53 foreach ( $order->get_items( 'line_item' ) as $item ) {
54 if ( ! $item instanceof WC_Order_Item_Product ) {
55 ++$line_index;
56 continue;
57 }
58
59 $product_id = (int) $item->get_product_id();
60 $quantity = (float) $item->get_quantity();
61 if ( 0 === $product_id || $quantity <= 0 ) {
62 ++$line_index;
63 continue;
64 }
65
66 $product = $item->get_product();
67 if ( ! $product instanceof WC_Product ) {
68 ++$line_index;
69 continue;
70 }
71
72 $line = $this->line_data( $item, $product, $quantity );
73 $stock_owner = $this->stock_owner( $product );
74
75 if ( $stock_owner instanceof WC_Product ) {
76 $owner_id = $stock_owner->get_id();
77 if ( ! isset( $managed_stock[ $owner_id ] ) ) {
78 $managed_stock[ $owner_id ] = array(
79 'owner' => $stock_owner,
80 'requested' => 0.0,
81 'lines' => array(),
82 );
83 }
84
85 $managed_stock[ $owner_id ]['requested'] += $quantity;
86 $managed_stock[ $owner_id ]['lines'][ $line_index ] = $line;
87 ++$line_index;
88 continue;
89 }
90
91 if ( 'outofstock' === $product->get_stock_status() ) {
92 $failures[ $line_index ] = array_merge(
93 $line,
94 array(
95 'available' => null,
96 'reason' => 'out_of_stock_status',
97 'backorders' => $product->get_backorders(),
98 )
99 );
100 }
101
102 ++$line_index;
103 }
104
105 foreach ( $managed_stock as $group ) {
106 $owner = $group['owner'];
107 $available = (float) $owner->get_stock_quantity();
108 $backorders = $owner->get_backorders();
109
110 if ( 'no' !== $backorders || $group['requested'] <= $available ) {
111 continue;
112 }
113
114 foreach ( $group['lines'] as $index => $line ) {
115 $failures[ $index ] = array_merge(
116 $line,
117 array(
118 'available' => $available,
119 'reason' => 'insufficient_stock',
120 'backorders' => $backorders,
121 )
122 );
123 }
124 }
125
126 if ( empty( $failures ) ) {
127 return $order;
128 }
129 \ksort( $failures );
130 $failures = \array_values( $failures );
131
132 return new WP_Error(
133 'wcpos_insufficient_stock',
134 \sprintf(
135 /* translators: %d: Number of order line items without enough stock. */
136 __( 'Cannot complete order: %d item(s) exceed available stock.', 'woocommerce-pos' ),
137 \count( $failures )
138 ),
139 array(
140 'status' => 400,
141 'items' => $failures,
142 )
143 );
144 }
145
146 /**
147 * Whether the prepared order is leaving draft-land for a sale status.
148 *
149 * Draft/cart syncs must always succeed; validation fires only on the
150 * checkout transition. The gate is an exempt-list rather than
151 * wc_get_is_paid_statuses() because a gateway's configured order_status
152 * may be any status, including custom ones.
153 *
154 * @param WC_Order $order Prepared order object.
155 * @param WP_REST_Request $request REST request.
156 * @param bool $creating Whether the order is being created.
157 */
158 private function should_validate_status( WC_Order $order, WP_REST_Request $request, bool $creating ): bool {
159 $exempt_statuses = apply_filters(
160 'woocommerce_pos_stock_validation_exempt_statuses',
161 array( 'pos-open', 'pos-partial', 'pending', 'auto-draft', 'checkout-draft', 'draft', 'cancelled', 'refunded', 'failed', 'trash' )
162 );
163 $target_status = $request->has_param( 'status' ) ? (string) $request->get_param( 'status' ) : $order->get_status();
164 $target_status = 0 === strpos( $target_status, 'wc-' ) ? substr( $target_status, 3 ) : $target_status;
165 $set_paid = $request->has_param( 'set_paid' ) && rest_sanitize_boolean( $request->get_param( 'set_paid' ) );
166 if ( ! $set_paid && \in_array( $target_status, $exempt_statuses, true ) ) {
167 return false;
168 }
169
170 if ( $creating || 0 === $order->get_id() ) {
171 return true;
172 }
173
174 $stored_order = wc_get_order( $order->get_id() );
175
176 return ! $stored_order instanceof WC_Order
177 || \in_array( $stored_order->get_status(), $exempt_statuses, true );
178 }
179
180 /**
181 * Resolve the product whose quantity owns stock for a line.
182 *
183 * @param WC_Product $product Line item product or variation.
184 */
185 private function stock_owner( WC_Product $product ): ?WC_Product {
186 if ( $product->is_type( 'variation' ) ) {
187 if ( true === $product->get_manage_stock( 'edit' ) && $product->managing_stock() ) {
188 return $product;
189 }
190
191 $parent = wc_get_product( $product->get_parent_id() );
192
193 return $parent instanceof WC_Product && $parent->managing_stock() ? $parent : null;
194 }
195
196 return $product->managing_stock() ? $product : null;
197 }
198
199 /**
200 * Build the line-specific part of an error item.
201 *
202 * @param WC_Order_Item_Product $item Order line item.
203 * @param WC_Product $product Line item product or variation.
204 * @param float $quantity Requested quantity.
205 *
206 * @return array<string,int|float|string>
207 */
208 private function line_data( WC_Order_Item_Product $item, WC_Product $product, float $quantity ): array {
209 $name = $item->get_name();
210
211 return array(
212 'product_id' => (int) $item->get_product_id(),
213 'variation_id' => (int) $item->get_variation_id(),
214 'name' => $name ? $name : $product->get_name(),
215 'requested' => $quantity,
216 );
217 }
218 }
219