From d52a215916373279b8407346535533af8115a33f Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 10 Mar 2026 11:15:55 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E5=84=AA=E5=8C=96=E5=BA=AB=E5=AD=98?= =?UTF-8?q?=E5=88=86=E6=9E=90=E9=82=8F=E8=BC=AF=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=8A=B7=E5=94=AE=20Reference=20Type=20=E8=BF=BD=E8=B9=A4?= =?UTF-8?q?=E4=B8=A6=E4=BF=AE=E6=AD=A3=20InventoryService=20=E9=96=89?= =?UTF-8?q?=E5=8C=85=E8=AE=8A=E6=95=B8=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Integration/Actions/SyncOrderAction.php | 5 +++- .../Actions/SyncVendingOrderAction.php | 5 +++- .../Contracts/InventoryServiceInterface.php | 2 +- .../Inventory/Services/InventoryService.php | 8 ++--- .../Inventory/Services/TurnoverService.php | 30 +++++++++++++++++++ .../Controllers/SalesImportController.php | 4 ++- tests/Feature/Integration/PosApiTest.php | 11 +++++++ 7 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/Modules/Integration/Actions/SyncOrderAction.php b/app/Modules/Integration/Actions/SyncOrderAction.php index a9b61c1..107e5fc 100644 --- a/app/Modules/Integration/Actions/SyncOrderAction.php +++ b/app/Modules/Integration/Actions/SyncOrderAction.php @@ -133,7 +133,10 @@ class SyncOrderAction $warehouseId, $qty, "POS Order: " . $order->external_order_id, - true + true, + null, + \App\Modules\Integration\Models\SalesOrder::class, + $order->id ); } diff --git a/app/Modules/Integration/Actions/SyncVendingOrderAction.php b/app/Modules/Integration/Actions/SyncVendingOrderAction.php index 7ec2061..06e153b 100644 --- a/app/Modules/Integration/Actions/SyncVendingOrderAction.php +++ b/app/Modules/Integration/Actions/SyncVendingOrderAction.php @@ -130,7 +130,10 @@ class SyncVendingOrderAction $warehouseId, $qty, "Vending Order: " . $order->external_order_id, - true + true, + null, + \App\Modules\Integration\Models\SalesOrder::class, + $order->id ); } diff --git a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php index 9f20b39..8e3af22 100644 --- a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php +++ b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php @@ -23,7 +23,7 @@ interface InventoryServiceInterface * @param string|null $slot * @return void */ - public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null): void; + public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void; /** * Get all active warehouses. diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index 90f0e6b..2993fb6 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -87,9 +87,9 @@ class InventoryService implements InventoryServiceInterface return $stock >= $quantity; } - public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null): void + public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void { - DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot) { + DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot, $referenceType, $referenceId) { $query = Inventory::where('product_id', $productId) ->where('warehouse_id', $warehouseId) ->where('quantity', '>', 0); @@ -108,7 +108,7 @@ class InventoryService implements InventoryServiceInterface if ($remainingToDecrease <= 0) break; $decreaseAmount = min($inventory->quantity, $remainingToDecrease); - $this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason); + $this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason, $referenceType, $referenceId); $remainingToDecrease -= $decreaseAmount; } @@ -139,7 +139,7 @@ class InventoryService implements InventoryServiceInterface ]); } - $this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason); + $this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason, $referenceType, $referenceId); } else { throw new \Exception("庫存不足,無法扣除所有請求的數量。"); } diff --git a/app/Modules/Inventory/Services/TurnoverService.php b/app/Modules/Inventory/Services/TurnoverService.php index 88f3637..c715a39 100644 --- a/app/Modules/Inventory/Services/TurnoverService.php +++ b/app/Modules/Inventory/Services/TurnoverService.php @@ -69,6 +69,12 @@ class TurnoverService ->select('inventories.product_id', DB::raw('ABS(SUM(inventory_transactions.quantity)) as sales_qty_30d')) ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->where('inventory_transactions.type', '出庫') // Adjust type as needed based on actual data + ->where(function ($q) { + $q->whereIn('inventory_transactions.reference_type', [ + \App\Modules\Integration\Models\SalesOrder::class, + \App\Modules\Sales\Models\SalesImportBatch::class, + ])->orWhereNull('inventory_transactions.reference_type'); + }) ->where('inventory_transactions.actual_time', '>=', $thirtyDaysAgo) ->groupBy('inventories.product_id'); @@ -87,6 +93,12 @@ class TurnoverService ->select('inventories.product_id', DB::raw('MAX(actual_time) as last_sale_date')) ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->where('inventory_transactions.type', '出庫') + ->where(function ($q) { + $q->whereIn('inventory_transactions.reference_type', [ + \App\Modules\Integration\Models\SalesOrder::class, + \App\Modules\Sales\Models\SalesImportBatch::class, + ])->orWhereNull('inventory_transactions.reference_type'); + }) ->groupBy('inventories.product_id'); if ($warehouseId) { @@ -199,6 +211,12 @@ class TurnoverService // Get IDs of products sold in last 90 days $soldProductIds = InventoryTransaction::query() ->where('type', '出庫') + ->where(function ($q) { + $q->whereIn('reference_type', [ + \App\Modules\Integration\Models\SalesOrder::class, + \App\Modules\Sales\Models\SalesImportBatch::class, + ])->orWhereNull('reference_type'); + }) ->where('actual_time', '>=', $ninetyDaysAgo) ->distinct() ->pluck('inventory_id') // Wait, transaction links to inventory, inventory links to product. @@ -214,6 +232,12 @@ class TurnoverService $soldProductIdsQuery = DB::table('inventory_transactions') ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->where('inventory_transactions.type', '出庫') + ->where(function ($q) { + $q->whereIn('inventory_transactions.reference_type', [ + \App\Modules\Integration\Models\SalesOrder::class, + \App\Modules\Sales\Models\SalesImportBatch::class, + ])->orWhereNull('inventory_transactions.reference_type'); + }) ->where('inventory_transactions.actual_time', '>=', $ninetyDaysAgo) ->select('inventories.product_id') ->distinct(); @@ -236,6 +260,12 @@ class TurnoverService ->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id') ->join('products', 'inventories.product_id', '=', 'products.id') ->where('inventory_transactions.type', '出庫') + ->where(function ($q) { + $q->whereIn('inventory_transactions.reference_type', [ + \App\Modules\Integration\Models\SalesOrder::class, + \App\Modules\Sales\Models\SalesImportBatch::class, + ])->orWhereNull('inventory_transactions.reference_type'); + }) ->where('inventory_transactions.actual_time', '>=', Carbon::now()->subDays($analysisDays)) ->when($warehouseId, fn($q) => $q->where('inventories.warehouse_id', $warehouseId)) ->when($categoryId, fn($q) => $q->where('products.category_id', $categoryId)) diff --git a/app/Modules/Sales/Controllers/SalesImportController.php b/app/Modules/Sales/Controllers/SalesImportController.php index 1ba11e3..f66cd3c 100644 --- a/app/Modules/Sales/Controllers/SalesImportController.php +++ b/app/Modules/Sales/Controllers/SalesImportController.php @@ -157,7 +157,9 @@ class SalesImportController extends Controller $deduction['quantity'], $reason, true, // Force deduction - $deduction['slot'] // Location/Slot + $deduction['slot'], // Location/Slot + \App\Modules\Sales\Models\SalesImportBatch::class, + $import->id ); } diff --git a/tests/Feature/Integration/PosApiTest.php b/tests/Feature/Integration/PosApiTest.php index 4369d29..bbac412 100644 --- a/tests/Feature/Integration/PosApiTest.php +++ b/tests/Feature/Integration/PosApiTest.php @@ -175,6 +175,9 @@ class PosApiTest extends TestCase 'Accept' => 'application/json', ])->postJson('/api/v1/integration/orders', $payload); + $response->assertStatus(201) + ->assertJsonPath('message', 'Order synced and stock deducted successfully'); + $response->assertStatus(201) ->assertJsonPath('message', 'Order synced and stock deducted successfully'); @@ -196,6 +199,14 @@ class PosApiTest extends TestCase 'warehouse_id' => $warehouseId, 'quantity' => 95, ]); + + $order = \App\Modules\Integration\Models\SalesOrder::where('external_order_id', 'ORD-001')->first(); + $this->assertDatabaseHas('inventory_transactions', [ + 'reference_type' => \App\Modules\Integration\Models\SalesOrder::class, + 'reference_id' => $order->id, + 'quantity' => -5, + 'type' => '出庫', + ]); tenancy()->end(); }