|
|
@@ -0,0 +1,109 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Longyi\Core\Listeners;
|
|
|
+
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Longyi\Core\Models\ProductVariant;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Keeps `product_variants.quantity` in sync with order lifecycle events.
|
|
|
+ *
|
|
|
+ * Bagisto's OrderItemRepository::manageInventory only knows about the standard
|
|
|
+ * `product_inventories` / `product_ordered_inventories` tables; flexible_variant
|
|
|
+ * carries inventory on its own table, so we adjust it here.
|
|
|
+ *
|
|
|
+ * Strategy: decrement at order save, restore on cancel. Refunds are not handled
|
|
|
+ * yet — extend with `sales.refund.save.after` if needed.
|
|
|
+ */
|
|
|
+class VariantInventoryListener
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * Decrement variant stock for every order item that carries a `variant_id`.
|
|
|
+ */
|
|
|
+ public function decrementOnOrderSave($order): void
|
|
|
+ {
|
|
|
+ if (! $order || ! method_exists($order, 'getRelationValue')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ DB::transaction(function () use ($order) {
|
|
|
+ foreach ($order->items as $item) {
|
|
|
+ $variantId = $this->extractVariantId($item);
|
|
|
+ $qty = (int) ($item->qty_ordered ?? 0);
|
|
|
+
|
|
|
+ if (! $variantId || $qty <= 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ ProductVariant::whereKey($variantId)->decrement('quantity', $qty);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::error('VariantInventoryListener::decrementOnOrderSave failed', [
|
|
|
+ 'order_id' => $order->id ?? null,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Restore variant stock when an order (or a portion of it) is cancelled.
|
|
|
+ */
|
|
|
+ public function restoreOnOrderCancel($order): void
|
|
|
+ {
|
|
|
+ if (! $order) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ DB::transaction(function () use ($order) {
|
|
|
+ foreach ($order->items as $item) {
|
|
|
+ $variantId = $this->extractVariantId($item);
|
|
|
+
|
|
|
+ if (! $variantId) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $remaining = (int) ($item->qty_ordered ?? 0)
|
|
|
+ - (int) ($item->qty_canceled ?? 0)
|
|
|
+ - (int) ($item->qty_refunded ?? 0);
|
|
|
+
|
|
|
+ if ($remaining > 0) {
|
|
|
+ ProductVariant::whereKey($variantId)->increment('quantity', $remaining);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::error('VariantInventoryListener::restoreOnOrderCancel failed', [
|
|
|
+ 'order_id' => $order->id ?? null,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Pull `variant_id` out of an order item's `additional` payload.
|
|
|
+ *
|
|
|
+ * `additional` is normally a JSON column cast to array, but be defensive in case
|
|
|
+ * it arrives as a JSON string.
|
|
|
+ */
|
|
|
+ protected function extractVariantId($item): ?int
|
|
|
+ {
|
|
|
+ $additional = $item->additional ?? null;
|
|
|
+
|
|
|
+ if (is_string($additional)) {
|
|
|
+ $decoded = json_decode($additional, true);
|
|
|
+ $additional = is_array($decoded) ? $decoded : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! is_array($additional)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $variantId = $additional['variant_id'] ?? null;
|
|
|
+
|
|
|
+ return $variantId ? (int) $variantId : null;
|
|
|
+ }
|
|
|
+}
|