PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 1.2.0 All 47 releases
fluent-cart / app / Modules / StockManagement / StockManagement.php

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

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