PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.19
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.19
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / StockManagement / StockManagement.php

StockManagement.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.19, at app/Modules/StockManagement/StockManagement.php

1,092 lines 39.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\StockManagement;
4
5
6 use FluentCart\Api\ModuleSettings;
7 use FluentCart\App\Helpers\Helper;
8 use FluentCart\App\Helpers\Status;
9 use FluentCart\App\Models\OrderItem;
10 use FluentCart\App\Models\OrderMeta;
11 use FluentCart\App\Models\ProductDetail;
12 use FluentCart\App\Models\ProductVariation;
13 use FluentCart\Framework\Support\Arr;
14
15 class StockManagement
16 {
17
18 public function register($app)
19 {
20 $app->addFilter('fluent_cart/module_setting/fields', function ($fields, $args) {
21 $fields['stock_management'] = [
22 'title' => __('Stock Management', 'fluent-cart'),
23 'description' => __('Manage stock of your products easier than ever!', 'fluent-cart'),
24 'type' => 'component',
25 'component' => 'ModuleSettings',
26 ];
27 return $fields;
28 }, 10, 2);
29
30
31 $app->addFilter('fluent_cart/module_setting/default_values', function ($values, $args) {
32 if (empty($values['stock_management']['active'])) {
33 $values['stock_management']['active'] = 'no';
34 }
35 return $values;
36 }, 10, 2);
37
38 add_filter('fluent_cart/shop_query', [$this, 'filterShopQuery'], 10, 2);
39
40
41 if (!ModuleSettings::isActive('stock_management')) {
42 return;
43 }
44
45 add_filter('fluent_cart/variation/can_purchase_bundle', function ($result, $data) {
46 $variation = $data['variation'];
47 $quantity = (int)$data['quantity'];
48
49 if ($variation->product && method_exists($variation->product, 'isBundleProduct') && $variation->product->isBundleProduct()) {
50 $children = $variation->bundleChildren()->get();
51 if ($children && $children->count()) {
52 foreach ($children as $child) {
53 $childDetail = $child->product_detail;
54 if (!$childDetail) {
55 return new \WP_Error('unpublished', __('This product is not available for purchase', 'fluent-cart'));
56 }
57 if (($childDetail->manage_stock && $child->manage_stock) && $quantity > $child->available) {
58 return new \WP_Error('insufficient_stock', __('Sorry, this product is currently out of stock.', 'fluent-cart'));
59 }
60 }
61 }
62 }
63
64 return true;
65 }, 10, 2);
66
67 // Manage stock on order created
68 $app->addAction('fluent_cart/order_created', [$this, 'manageStockOnOrderCreated']);
69 $app->addAction('fluent_cart/shipping_status_changed', [$this, 'manageStockOnShippingStatusChanged']);
70 $app->addAction('fluent_cart/order_status_changed', [$this, 'manageStockOnOrderStatusChanged']);
71 $app->addAction('fluent_cart/order_refunded', [$this, 'manageStockOnOrderRefunded']);
72 $app->addAction('fluent_cart/order_paid', [$this, 'manageStockOnOrderPaid']);
73 $app->addAction('fluent_cart/order_updated', [$this, 'manageStockOnOrderUpdated']);
74
75 }
76
77 public function filterShopQuery($query, $params)
78 {
79 $query = $query->when(Arr::get($params, 'selected_status'), function ($query) use ($params) {
80 $status = Arr::get($params, 'status');
81 $allowOutOfStock = Arr::get($params, 'allow_out_of_stock', false);
82
83 return $query->where(function ($query) use ($status, $allowOutOfStock) {
84 $query->search($status);
85
86 if (ModuleSettings::isActive('stock_management') && !$allowOutOfStock) {
87 $query->whereHas('detail', function ($query) {
88 return $query->search([
89 "stock_availability" => [
90 "column" => "stock_availability",
91 "value" => Helper::IN_STOCK
92 ]
93 ]);
94 });
95 }
96 });
97 });
98
99 return $query;
100 }
101
102 public function manageStockOnOrderCreated($event)
103 {
104 $order = Arr::get($event, 'order');
105 $prevOrder = Arr::get($event, 'prev_order');
106 $orderItems = $order->order_items;
107
108 if ($prevOrder) {
109 return;
110 }
111
112 if (!$order || !$orderItems) {
113 return;
114 }
115
116 $orderItems = $orderItems->filter(function ($item) {
117 return !in_array($item->payment_type, ['signup_fee', 'fee']);
118 });
119
120 $pluckVariationIds = $orderItems->pluck('object_id')->toArray();
121 $orderItems = $orderItems->keyBy('object_id')->toArray();
122
123 if (empty($pluckVariationIds)) {
124 return;
125 }
126
127 $variations = ProductVariation::query()
128 ->select('id', 'post_id', 'available', 'committed', 'on_hold', 'manage_stock', 'other_info')
129 ->with('product_detail')
130 ->whereIn('id', $pluckVariationIds)
131 ->where('manage_stock', 1)
132 ->get()
133 ->keyBy('id');
134
135 if ($variations->isEmpty()) {
136 return;
137 }
138
139 // Get existing meta for this order
140 $existingMeta = OrderMeta::where('order_id', $order->id)
141 ->where('meta_key', 'stock_movement')
142 ->value('meta_value');
143
144 $stockMovement = $existingMeta ? $existingMeta : [];
145
146 $updatedVariants = [];
147 $affectedProductIds = [];
148
149 foreach ($variations as $item) {
150 $orderItemId = Arr::get($orderItems, $item->id . '.id');
151 $quantity = (int)Arr::get($orderItems, $item->id . '.quantity', 0);
152
153 if ($quantity <= 0) {
154 continue;
155 }
156
157 $newAvailable = $item->available - $quantity;
158 $updatedData = [
159 'id' => $item->id,
160 'on_hold' => ['+', $quantity],
161 'available' => $newAvailable <= 0 ? 0 : $newAvailable,
162 'stock_status' => $newAvailable <= 0 ? 'out-of-stock' : 'in-stock',
163 ];
164
165 $updatedVariants[] = $updatedData;
166 $affectedProductIds[] = $item->post_id;
167
168 // Add to stock_movement array
169 $stockMovement[$orderItemId] = [
170 'on_hold' => $quantity
171 ];
172
173 if ($item->product_detail && Arr::get($item->product_detail->other_info, 'is_bundle_product') === 'yes') {
174 $childVariations = $item->bundleChildren()->get();
175 if ($childVariations && $childVariations->count()) {
176 foreach ($childVariations as $child) {
177 if ((int)$child->manage_stock !== 1) {
178 continue;
179 }
180 $childAvailable = (int)$child->available - $quantity;
181 $updatedVariants[] = [
182 'id' => $child->id,
183 'on_hold' => ['+', $quantity],
184 'available' => $childAvailable <= 0 ? 0 : $childAvailable,
185 'stock_status' => $childAvailable <= 0 ? 'out-of-stock' : 'in-stock',
186 ];
187 $affectedProductIds[] = $child->post_id;
188 }
189 }
190 }
191 }
192
193 if (!empty($updatedVariants)) {
194 ProductVariation::query()->batchUpdate($updatedVariants);
195 }
196
197 if (!empty($affectedProductIds)) {
198 $affectedProductIds = array_unique($affectedProductIds);
199
200 $updatedProducts = [];
201 foreach ($affectedProductIds as $productId) {
202 // check all variations of this product
203 $hasInStock = ProductVariation::query()
204 ->where('post_id', $productId)
205 ->where('stock_status', 'in-stock')
206 ->exists();
207
208 // get product details by $productId and get only id from it
209 $detail = ProductDetail::query()->where('post_id', $productId)->select('id')->first();
210
211 if (!$detail->id) {
212 continue;
213 }
214 $updatedProducts[] = [
215 'id' => $detail->id,
216 'stock_availability' => $hasInStock ? 'in-stock' : 'out-of-stock'
217 ];
218 }
219
220 if (!empty($updatedProducts)) {
221 ProductDetail::query()->batchUpdate($updatedProducts);
222 }
223 }
224
225 // Save merged data back to order_meta
226 OrderMeta::updateOrCreate(
227 [
228 'order_id' => $order->id,
229 'meta_key' => 'stock_movement',
230 ],
231 [
232 'meta_value' => json_encode($stockMovement),
233 ]
234 );
235 }
236
237 public function manageStockOnShippingStatusChanged($event)
238 {
239 $order = Arr::get($event, 'order');
240 $orderItems = $order->order_items;
241 $status = Arr::get($event, 'new_status');
242 $oldStatus = Arr::get($event, 'old_status');
243
244 if (!$order || !$orderItems) {
245 return;
246 }
247
248 $orderItems = $orderItems->filter(function ($item) {
249 return !in_array($item->payment_type, ['signup_fee', 'fee']);
250 });
251
252 $pluckVariationIds = $orderItems->pluck('object_id')->toArray();
253 $orderItems = $orderItems->keyBy('object_id')->toArray();
254
255 if (empty($pluckVariationIds)) {
256 return;
257 }
258
259 $variations = ProductVariation::query()
260 ->select('id', 'post_id', 'available', 'committed', 'on_hold', 'manage_stock', 'fulfillment_type', 'other_info')
261 ->with('product_detail')
262 ->whereIn('id', $pluckVariationIds)
263 ->where('manage_stock', 1)
264 ->where('fulfillment_type', 'physical')
265 ->get()
266 ->keyBy('id');
267
268 if ($variations->isEmpty()) {
269 return;
270 }
271
272 // Get existing meta for this order
273 $existingMeta = OrderMeta::where('order_id', $order->id)
274 ->where('meta_key', 'stock_movement')
275 ->value('meta_value');
276
277 $stockMovement = $existingMeta ? $existingMeta : [];
278
279 $updatedVariants = [];
280
281 // Handle stock updates
282 $isDelivered = ($status === 'delivered');
283 $wasDelivered = ($oldStatus === 'delivered');
284
285 foreach ($variations as $variation) {
286 $orderItemId = Arr::get($orderItems, $variation->id . '.id');
287 $quantity = (int)Arr::get($orderItems, $variation->id . '.quantity', 0);
288
289 if ($quantity <= 0) {
290 continue;
291 }
292
293 if ($variation->product_detail && Arr::get($variation->product_detail->other_info, 'is_bundle_product') === 'yes') {
294 $childVariations = $variation->bundleChildren()->get();
295 if ($childVariations && $childVariations->count()) {
296 foreach ($childVariations as $child) {
297 if ((int)$child->manage_stock !== 1) {
298 continue;
299 }
300 if ($isDelivered) {
301 $updatedVariants[] = [
302 'id' => $child->id,
303 'committed' => ['+', $quantity],
304 'on_hold' => ['-', $quantity],
305 ];
306 } elseif ($wasDelivered && !$isDelivered) {
307 $updatedVariants[] = [
308 'id' => $child->id,
309 'committed' => ['-', $quantity],
310 'on_hold' => ['+', $quantity],
311 ];
312 }
313 }
314 }
315 }
316
317 $newDecreaseStock = ['-', $quantity];
318 if ($isDelivered) {
319 // Order delivered: move stock from on_hold → committed
320 $updatedVariants[] = [
321 'id' => $variation->id,
322 'committed' => ['+', $quantity],
323 'on_hold' => $newDecreaseStock,
324 ];
325
326 // Add to stock_movement array
327 $stockMovement[$orderItemId] = [
328 'committed' => $quantity,
329 'on_hold' => 0,
330 ];
331 } elseif ($wasDelivered && !$isDelivered) {
332 // Status changed away from delivered: revert changes
333 $updatedVariants[] = [
334 'id' => $variation->id,
335 'committed' => $newDecreaseStock,
336 'on_hold' => ['+', $quantity],
337 ];
338
339 // Add to stock_movement array
340 $stockMovement[$orderItemId] = [
341 'committed' => 0,
342 'on_hold' => $quantity,
343 ];
344 }
345 }
346
347 if (!empty($updatedVariants)) {
348 ProductVariation::query()->batchUpdate($updatedVariants);
349 }
350
351 // Save merged data back to order_meta
352 OrderMeta::updateOrCreate(
353 [
354 'order_id' => $order->id,
355 'meta_key' => 'stock_movement',
356 ],
357 [
358 'meta_value' => json_encode($stockMovement),
359 ]
360 );
361 }
362
363 public function manageStockOnOrderStatusChanged($event)
364 {
365 $newStatus = Arr::get($event, 'new_status');
366 $order = Arr::get($event, 'order');
367 $orderItems = $order->order_items;
368 if ($newStatus !== 'canceled') {
369 return;
370 }
371
372 if (!$order || !$orderItems) {
373 return;
374 }
375
376
377 $orderItems = $orderItems->filter(function ($item) {
378 return !in_array($item->payment_type, ['signup_fee', 'fee']);
379 });
380
381 $pluckVariationIds = $orderItems->pluck('object_id')->toArray();
382 $orderItems = $orderItems->keyBy('object_id')->toArray();
383 if (empty($pluckVariationIds)) {
384 return;
385 }
386
387 // Get existing meta for this order
388 $existingMeta = OrderMeta::where('order_id', $order->id)
389 ->where('meta_key', 'stock_movement')
390 ->value('meta_value');
391
392 $stockMovement = $existingMeta ? $existingMeta : [];
393
394 $variations = ProductVariation::query()
395 ->select('id', 'post_id', 'available', 'committed', 'on_hold', 'manage_stock', 'fulfillment_type', 'other_info')
396 ->with('product_detail')
397 ->whereIn('id', $pluckVariationIds)
398 ->where('manage_stock', 1)
399 ->get()
400 ->keyBy('id');
401
402 if ($variations->isEmpty()) {
403 return;
404 }
405 $updatedVariants = [];
406 $affectedProductIds = [];
407
408 // get shipping_status from $order
409 $shippingStatus = $order->shipping_status;
410
411 foreach ($variations as $item) {
412 $quantity = (int)Arr::get($orderItems, $item->id . '.quantity', 0); // refund request qty
413 $orderItemId = Arr::get($orderItems, $item->id . '.id');
414
415 if ($quantity <= 0) {
416 continue;
417 }
418
419 $oldOnHold = (int)Arr::get($stockMovement, $orderItemId . '.on_hold', 0);
420 $oldCommitted = (int)Arr::get($stockMovement, $orderItemId . '.committed', 0);
421
422 $removedOnHold = 0;
423 $removedCommitted = 0;
424 $remainingRefund = $quantity;
425
426 // Priority: delivered/digital → committed first, else → on_hold first
427 if ($shippingStatus === 'delivered' || ($item->fulfillment_type === 'digital' && $order->payment_status === 'paid')) {
428 $orderOfRemoval = ['committed', 'on_hold'];
429 } else {
430 $orderOfRemoval = ['on_hold', 'committed'];
431 }
432
433 if ($item->product_detail && Arr::get($item->product_detail->other_info, 'is_bundle_product') === 'yes') {
434 $childVariations = $item->bundleChildren()->get();
435 if ($childVariations && $childVariations->count()) {
436 foreach ($childVariations as $child) {
437 if ((int)$child->manage_stock !== 1) {
438 continue;
439 }
440 $childOldOnHold = (int)$child->on_hold;
441 $childOldCommitted = (int)$child->committed;
442 $childRemovedOnHold = 0;
443 $childRemovedCommitted = 0;
444 $childRemaining = $quantity;
445 foreach ($orderOfRemoval as $place) {
446 if ($childRemaining <= 0) {
447 break;
448 }
449 if ($place === 'on_hold' && $childOldOnHold > 0) {
450 $remove = min($childOldOnHold, $childRemaining);
451 $childRemovedOnHold = $remove;
452 $childRemaining -= $remove;
453 }
454 if ($place === 'committed' && $childOldCommitted > 0) {
455 $remove = min($childOldCommitted, $childRemaining);
456 $childRemovedCommitted = $remove;
457 $childRemaining -= $remove;
458 }
459 }
460 $childAvailable = (int)$child->available + $quantity;
461 $childUpdate = [
462 'id' => $child->id,
463 'available' => $childAvailable <= 0 ? 0 : $childAvailable,
464 'stock_status' => $childAvailable <= 0 ? 'out-of-stock' : 'in-stock',
465 ];
466 if ($childRemovedOnHold > 0) {
467 $childUpdate['on_hold'] = ['-', $childRemovedOnHold];
468 }
469 if ($childRemovedCommitted > 0) {
470 $childUpdate['committed'] = ['-', $childRemovedCommitted];
471 }
472 $updatedVariants[] = $childUpdate;
473 $affectedProductIds[] = $child->post_id;
474 }
475 }
476 }
477
478 // Deduct according to priority
479 foreach ($orderOfRemoval as $place) {
480 if ($remainingRefund <= 0) {
481 break;
482 }
483
484 if ($place === 'on_hold' && $oldOnHold > 0) {
485 $remove = min($oldOnHold, $remainingRefund);
486 $removedOnHold = $remove;
487 $remainingRefund -= $remove;
488
489 $stockMovement[$orderItemId]['on_hold'] = $oldOnHold - $remove;
490 if ($stockMovement[$orderItemId]['on_hold'] <= 0) {
491 // unset($stockMovement[$orderItemId]['on_hold']);
492 }
493 }
494
495 if ($place === 'committed' && $oldCommitted > 0) {
496 $remove = min($oldCommitted, $remainingRefund);
497 $removedCommitted = $remove;
498 $remainingRefund -= $remove;
499
500 $stockMovement[$orderItemId]['committed'] = $oldCommitted - $remove;
501 if ($stockMovement[$orderItemId]['committed'] <= 0) {
502 // unset($stockMovement[$orderItemId]['committed']);
503 }
504 }
505 }
506
507 // Update stock back
508 $newAvailable = $item->available + $quantity;
509 $update = [
510 'id' => $item->id,
511 'available' => $newAvailable <= 0 ? 0 : $newAvailable,
512 'stock_status' => $newAvailable <= 0 ? 'out-of-stock' : 'in-stock',
513 ];
514
515 if ($removedOnHold > 0) {
516 $update['on_hold'] = ['-', $removedOnHold];
517 }
518 if ($removedCommitted > 0) {
519 $update['committed'] = ['-', $removedCommitted];
520 }
521
522 $updatedVariants[] = $update;
523 $affectedProductIds[] = $item->post_id;
524 }
525
526 if (empty($updatedVariants)) {
527 return;
528 }
529
530 ProductVariation::query()->batchUpdate($updatedVariants);
531
532 if (empty($affectedProductIds)) {
533 return;
534 }
535
536 $affectedProductIds = array_unique($affectedProductIds);
537
538 $updatedProducts = [];
539 foreach ($affectedProductIds as $productId) {
540 // check all variations of this product
541 $hasInStock = ProductVariation::query()
542 ->where('post_id', $productId)
543 ->where('stock_status', 'in-stock')
544 ->exists();
545
546 // get product details by $productId and get only id from it
547 $detail = ProductDetail::query()->where('post_id', $productId)->select('id')->first();
548
549 $updatedProducts[] = [
550 'id' => $detail->id,
551 'stock_availability' => $hasInStock ? 'in-stock' : 'out-of-stock'
552 ];
553 }
554
555 if (!empty($updatedProducts)) {
556 ProductDetail::query()->batchUpdate($updatedProducts);
557 }
558
559 // Save merged data back to order_meta
560 OrderMeta::updateOrCreate(
561 [
562 'order_id' => $order->id,
563 'meta_key' => 'stock_movement',
564 ],
565 [
566 'meta_value' => json_encode($stockMovement),
567 ]
568 );
569 }
570
571 public function manageStockOnOrderRefunded($data)
572 {
573 $manageStock = Arr::get($data, 'manage_stock', false);
574 if (!$manageStock) {
575 return;
576 }
577
578 $order = Arr::get($data, 'order');
579 $refundedItems = Arr::get($data, 'new_refunded_items');
580
581 $pluckVariationIds = [];
582 $mappedRestockItems = [];
583 foreach ($refundedItems as $orderItem) {
584 $pluckVariationIds[] = $orderItem['variation_id'];
585 $mappedRestockItems[$orderItem['variation_id']] = [
586 'quantity' => $orderItem['restore_quantity'],
587 'id' => $orderItem['id']
588 ];
589 }
590
591 if (empty ($pluckVariationIds)) {
592 return;
593 }
594
595 $variations = ProductVariation::query()
596 ->select('id', 'post_id', 'available', 'committed', 'on_hold', 'manage_stock', 'fulfillment_type', 'other_info')
597 ->with('product_detail')
598 ->whereIn('id', $pluckVariationIds)
599 ->where('manage_stock', 1)
600 ->get()
601 ->keyBy('id');
602
603 if ($variations->isEmpty()) {
604 return;
605 }
606
607 // Get existing meta for this order
608 $existingMeta = OrderMeta::where('order_id', $order->id)
609 ->where('meta_key', 'stock_movement')
610 ->value('meta_value');
611
612 $stockMovement = $existingMeta ? $existingMeta : [];
613
614
615 $updatedVariants = [];
616 $affectedProductIds = [];
617
618 $shippingStatus = $order->shipping_status;
619
620 $restockedItems = [];
621
622 foreach ($variations as $item) {
623 $orderItemId = Arr::get($mappedRestockItems, $item->id . '.id', 0);
624
625 $quantity = (int)Arr::get($mappedRestockItems, $item->id . '.quantity', 0);
626
627 if ($quantity <= 0) {
628 continue;
629 }
630
631
632 $oldOnHold = (int)Arr::get($stockMovement, $orderItemId . '.on_hold', 0);
633 $oldCommitted = (int)Arr::get($stockMovement, $orderItemId . '.committed', 0);
634
635 // Track how much refunded from each place
636 $removedOnHold = 0;
637 $removedCommitted = 0;
638
639 $remainingRefund = $quantity;
640
641 if ($shippingStatus === 'delivered' || ($item->fulfillment_type === 'digital' && $order->payment_status === 'paid')) {
642 $orderOfRemoval = ['committed', 'on_hold'];
643 } else {
644 $orderOfRemoval = ['on_hold', 'committed'];
645 }
646
647 foreach ($orderOfRemoval as $place) {
648 if ($remainingRefund <= 0) {
649 break;
650 }
651
652 if ($place === 'on_hold' && $oldOnHold > 0) {
653 $remove = min($oldOnHold, $remainingRefund);
654 $removedOnHold = $remove;
655 $remainingRefund -= $remove;
656
657 $stockMovement[$orderItemId]['on_hold'] = $oldOnHold - $remove;
658 }
659
660 if ($place === 'committed' && $oldCommitted > 0) {
661 $remove = min($oldCommitted, $remainingRefund);
662 $removedCommitted = $remove;
663 $remainingRefund -= $remove;
664
665 $stockMovement[$orderItemId]['committed'] = $oldCommitted - $remove;
666 }
667 }
668
669 $newAvailable = $item->available + $quantity;
670
671 $restockedItems[$item->id] = $quantity;
672
673 $update = [
674 'id' => $item->id,
675 'available' => $newAvailable <= 0 ? 0 : $newAvailable,
676 'stock_status' => $newAvailable <= 0 ? 'out-of-stock' : 'in-stock',
677 ];
678
679 if ($removedOnHold > 0) {
680 $update['on_hold'] = ['-', $removedOnHold];
681 }
682 if ($removedCommitted > 0) {
683 $update['committed'] = ['-', $removedCommitted];
684 }
685
686 $updatedVariants[] = $update;
687 $affectedProductIds[] = $item->post_id;
688
689 if ($item->product_detail && Arr::get($item->product_detail->other_info, 'is_bundle_product') === 'yes') {
690 $childVariations = $item->bundleChildren()->get();
691 if ($childVariations && $childVariations->count()) {
692 foreach ($childVariations as $child) {
693 if ((int)$child->manage_stock !== 1) {
694 continue;
695 }
696
697 $childOldOnHold = (int)$child->on_hold;
698 $childOldCommitted = (int)$child->committed;
699
700 $childRemovedOnHold = 0;
701 $childRemovedCommitted = 0;
702 $childRemaining = $quantity;
703
704 foreach ($orderOfRemoval as $place) {
705 if ($childRemaining <= 0) {
706 break;
707 }
708 if ($place === 'on_hold' && $childOldOnHold > 0) {
709 $remove = min($childOldOnHold, $childRemaining);
710 $childRemovedOnHold = $remove;
711 $childRemaining -= $remove;
712 }
713 if ($place === 'committed' && $childOldCommitted > 0) {
714 $remove = min($childOldCommitted, $childRemaining);
715 $childRemovedCommitted = $remove;
716 $childRemaining -= $remove;
717 }
718 }
719
720 $childAvailable = (int)$child->available + $quantity;
721
722 $childUpdate = [
723 'id' => $child->id,
724 'available' => $childAvailable <= 0 ? 0 : $childAvailable,
725 'stock_status' => $childAvailable <= 0 ? 'out-of-stock' : 'in-stock',
726 ];
727 if ($childRemovedOnHold > 0) {
728 $childUpdate['on_hold'] = ['-', $childRemovedOnHold];
729 }
730 if ($childRemovedCommitted > 0) {
731 $childUpdate['committed'] = ['-', $childRemovedCommitted];
732 }
733 $updatedVariants[] = $childUpdate;
734 $affectedProductIds[] = $child->post_id;
735 }
736 }
737 }
738 }
739
740 if (empty($updatedVariants)) {
741 return;
742 }
743
744 ProductVariation::query()->batchUpdate($updatedVariants);
745
746
747 // Save merged data back to order_meta
748 OrderMeta::updateOrCreate(
749 [
750 'order_id' => $order->id,
751 'meta_key' => 'stock_movement',
752 ],
753 [
754 'meta_value' => json_encode($stockMovement),
755 ]
756 );
757
758
759 $orderItems = $order->order_items;
760 $deleteableOrderItems = [];
761 $orderItemsUpdateData = [];
762
763 foreach ($orderItems as $item) {
764 $quantity = (int)Arr::get($restockedItems, $item->object_id, 0);
765 $newQuantity = $item->quantity - $quantity;
766 if ($newQuantity <= 0) {
767 $deleteableOrderItems[] = $item->id;
768 } else {
769 $orderItemsUpdateData[] = [
770 'id' => $item->id,
771 'quantity' => $newQuantity
772 ];
773 }
774 }
775
776
777 //delete order items
778 if (!empty($deleteableOrderItems)) {
779 OrderItem::query()->whereIn('id', $deleteableOrderItems)->delete();
780 }
781
782 //update order items quantity
783 if (!empty($orderItemsUpdateData)) {
784 OrderItem::query()->batchUpdate($orderItemsUpdateData);
785 }
786
787 if (empty($affectedProductIds)) {
788 return;
789 }
790
791 $affectedProductIds = array_unique($affectedProductIds);
792
793 $updatedProducts = [];
794 foreach ($affectedProductIds as $productId) {
795 // check all variations of this product
796 $hasInStock = ProductVariation::query()
797 ->where('post_id', $productId)
798 ->where('stock_status', 'in-stock')
799 ->exists();
800
801 // get product details by $productId and get only id from it
802 $detail = ProductDetail::query()->where('post_id', $productId)->select('id')->first();
803
804 $updatedProducts[] = [
805 'id' => $detail->id,
806 'stock_availability' => $hasInStock ? 'in-stock' : 'out-of-stock'
807 ];
808 }
809
810 if (!empty($updatedProducts)) {
811 ProductDetail::query()->batchUpdate($updatedProducts);
812 }
813 }
814
815 public function manageStockOnOrderPaid($event)
816 {
817 $order = Arr::get($event, 'order');
818 $orderItems = $order->order_items;
819
820 if (!$order || !$orderItems) {
821 return;
822 }
823
824 $orderItems = $orderItems->filter(function ($item) {
825 return !in_array($item->payment_type, ['signup_fee', 'fee']);
826 });
827
828 $pluckVariationIds = $orderItems->pluck('object_id')->toArray();
829 $orderItems = $orderItems->keyBy('object_id')->toArray();
830
831 if (empty($pluckVariationIds)) {
832 return;
833 }
834
835 $variations = ProductVariation::query()
836 ->select('id', 'post_id', 'available', 'committed', 'on_hold', 'manage_stock', 'fulfillment_type', 'other_info')
837 ->with('product_detail')
838 ->whereIn('id', $pluckVariationIds)
839 ->where('manage_stock', 1)
840 ->where('fulfillment_type', 'digital')
841 ->get()
842 ->keyBy('id');
843
844 if ($variations->isEmpty()) {
845 return;
846 }
847
848 // Get existing meta for this order
849 $existingMeta = OrderMeta::where('order_id', $order->id)
850 ->where('meta_key', 'stock_movement')
851 ->value('meta_value');
852
853 $stockMovement = $existingMeta ? $existingMeta : [];
854
855 $updatedVariants = [];
856
857 foreach ($variations as $item) {
858 $orderItemId = Arr::get($orderItems, $item->id . '.id');
859
860 // get quantity from $stockMovement
861 $quantity = (int)Arr::get($orderItems, $item->id . '.quantity', 0);
862
863
864 if ($quantity <= 0) {
865 continue;
866 }
867
868 $updatedVariants[] = [
869 'id' => $item->id,
870 'on_hold' => ['-', $quantity],
871 'committed' => ['+', $quantity],
872 ];
873
874 $stockMovement[$orderItemId] = [
875 'committed' => $quantity,
876 'on_hold' => 0,
877 ];
878
879 if ($item->product_detail && Arr::get($item->product_detail->other_info, 'is_bundle_product') === 'yes') {
880 $childVariations = $item->bundleChildren()->get();
881 if ($childVariations && $childVariations->count()) {
882 foreach ($childVariations as $child) {
883 if ((int)$child->manage_stock !== 1) {
884 continue;
885 }
886 $updatedVariants[] = [
887 'id' => $child->id,
888 'on_hold' => ['-', $quantity],
889 'committed' => ['+', $quantity],
890 ];
891 }
892 }
893 }
894 }
895
896 if (empty($updatedVariants)) {
897 return;
898 }
899
900 ProductVariation::query()->batchUpdate($updatedVariants);
901
902 // Save merged data back to order_meta
903 OrderMeta::updateOrCreate(
904 [
905 'order_id' => $order->id,
906 'meta_key' => 'stock_movement',
907 ],
908 [
909 'meta_value' => json_encode($stockMovement),
910 ]
911 );
912 }
913
914 public function manageStockOnOrderUpdated($event)
915 {
916 $order = Arr::get($event, 'order');
917 $orderItems = $order->order_items;
918 $oldOrder = Arr::get($event, 'old_order');
919 $oldOrderItems = $oldOrder->order_items;
920
921 if (!$order || !$orderItems) {
922 return;
923 }
924
925 $orderItems = $orderItems->filter(function ($item) {
926 return !in_array($item->payment_type, ['signup_fee', 'fee']);
927 });
928
929 // Map by object_id for easier comparison
930 $newItems = $orderItems->keyBy('object_id');
931 $oldItems = $oldOrderItems->keyBy('object_id');
932
933 $mergedIds = $newItems->keys()
934 ->merge($oldItems->keys())
935 ->unique();
936
937 // Get existing meta
938 $existingMeta = OrderMeta::where('order_id', $order->id)
939 ->where('meta_key', 'stock_movement')
940 ->value('meta_value');
941
942 $stockMovement = $existingMeta ? $existingMeta : [];
943
944 $updatedVariants = [];
945 $affectedProductIds = [];
946
947 foreach ($mergedIds as $variationId) {
948 $oldQuantity = (int)Arr::get($oldItems, $variationId . '.quantity', 0);
949 $newQuantity = (int)Arr::get($newItems, $variationId . '.quantity', 0);
950 $diff = $newQuantity - $oldQuantity;
951
952 // No change
953 if ($diff === 0) {
954 continue;
955 }
956
957
958 // Get order item ID (prefer new item, fallback to old item)
959 $orderItemId = Arr::get($newItems, $variationId . '.id')
960 ?? Arr::get($oldItems, $variationId . '.id');
961
962 // Fetch variation
963 $item = ProductVariation::query()
964 ->select('id', 'post_id', 'available', 'on_hold', 'committed', 'manage_stock', 'fulfillment_type', 'other_info')
965 ->with('product_detail')
966 ->where('id', $variationId)
967 ->first();
968
969 if (!$item || !$item->manage_stock) {
970 continue;
971 }
972
973 $newAvailable = $item->available - $diff;
974
975 $update = [
976 'id' => $item->id,
977 'available' => $newAvailable <= 0 ? 0 : $newAvailable,
978 'stock_status' => $newAvailable <= 0 ? 'out-of-stock' : 'in-stock',
979 ];
980
981 // Decide whether it affects on_hold or committed
982 if ($order->shipping_status === 'delivered' ||
983 ($item->fulfillment_type === 'digital' && $order->payment_status === 'paid')) {
984 // Committed
985 if ($diff > 0) {
986 $update['committed'] = ['+', $diff];
987 } else {
988 $update['committed'] = ['-', abs($diff)];
989 }
990
991 $stockMovement[$orderItemId]['committed'] =
992 max(0, (int)Arr::get($stockMovement, $orderItemId . '.committed', 0) + $diff);
993 if ($stockMovement[$orderItemId]['committed'] === 0) {
994 }
995 } else {
996 if ($diff > 0) {
997 $update['on_hold'] = ['+', $diff];
998 } else {
999 $update['on_hold'] = ['-', abs($diff)];
1000 }
1001
1002 $stockMovement[$orderItemId]['on_hold'] =
1003 max(0, (int)Arr::get($stockMovement, $orderItemId . '.on_hold', 0) + $diff);
1004 if ($stockMovement[$orderItemId]['on_hold'] === 0) {
1005 }
1006 }
1007
1008 if (empty($stockMovement[$orderItemId])) {
1009 }
1010
1011 $updatedVariants[] = $update;
1012 $affectedProductIds[] = $item->post_id;
1013
1014 if ($item->product_detail && Arr::get($item->product_detail->other_info, 'is_bundle_product') === 'yes') {
1015 $childVariations = $item->bundleChildren()->get();
1016 if ($childVariations && $childVariations->count()) {
1017 foreach ($childVariations as $child) {
1018 if ((int)$child->manage_stock !== 1) {
1019 continue;
1020 }
1021 $childAvailable = (int)$child->available - $diff;
1022 $childUpdate = [
1023 'id' => $child->id,
1024 'available' => $childAvailable <= 0 ? 0 : $childAvailable,
1025 'stock_status' => $childAvailable <= 0 ? 'out-of-stock' : 'in-stock',
1026 ];
1027 if ($order->shipping_status === 'delivered' ||
1028 ($child->fulfillment_type === 'digital' && $order->payment_status === 'paid')) {
1029 if ($diff > 0) {
1030 $childUpdate['committed'] = ['+', $diff];
1031 } else {
1032 $childUpdate['committed'] = ['-', abs($diff)];
1033 }
1034 } else {
1035 if ($diff > 0) {
1036 $childUpdate['on_hold'] = ['+', $diff];
1037 } else {
1038 $childUpdate['on_hold'] = ['-', abs($diff)];
1039 }
1040 }
1041 $updatedVariants[] = $childUpdate;
1042 $affectedProductIds[] = $child->post_id;
1043 }
1044 }
1045 }
1046 }
1047
1048 if (!empty($updatedVariants)) {
1049 ProductVariation::query()->batchUpdate($updatedVariants);
1050 }
1051
1052 if (!empty($affectedProductIds)) {
1053 $affectedProductIds = array_unique($affectedProductIds);
1054 $updatedProducts = [];
1055
1056 foreach ($affectedProductIds as $productId) {
1057 $hasInStock = ProductVariation::query()
1058 ->where('post_id', $productId)
1059 ->where('stock_status', 'in-stock')
1060 ->exists();
1061
1062 $detail = ProductDetail::query()
1063 ->where('post_id', $productId)
1064 ->select('id')
1065 ->first();
1066
1067 if ($detail) {
1068 $updatedProducts[] = [
1069 'id' => $detail->id,
1070 'stock_availability'=> $hasInStock ? 'in-stock' : 'out-of-stock'
1071 ];
1072 }
1073 }
1074
1075 if (!empty($updatedProducts)) {
1076 ProductDetail::query()->batchUpdate($updatedProducts);
1077 }
1078 }
1079
1080 OrderMeta::updateOrCreate(
1081 [
1082 'order_id' => $order->id,
1083 'meta_key' => 'stock_movement',
1084 ],
1085 [
1086 'meta_value' => json_encode($stockMovement),
1087 ]
1088 );
1089 }
1090
1091 }
1092