diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index f96e78f..4354430 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -305,6 +305,17 @@ class TransactionService if ($slot) { $slot->decrement('stock'); + // 售完(庫存歸 0)即清空效期與批號,避免舊批貨效期殘留。 + // 場景:日後管理員只補 stock 未改 expiry_date 時,update_inventory 才不會 + // 把過期日一起下發、把新批貨誤鎖成「暫不販售」。與機台端「賣完清效期」對稱。 + // 純加法:僅在仍有殘留值時才寫,不影響既有扣庫存行為。 + if ($slot->stock <= 0 && ($slot->expiry_date !== null || $slot->batch_no !== null)) { + $slot->update([ + 'expiry_date' => null, + 'batch_no' => null, + ]); + } + $type = \App\Models\Machine\MachineStockMovement::TYPE_SALE; $this->machineService->recordStockMovement( diff --git a/tests/Feature/Transaction/ExpiryClearOnSelloutMysqlTest.php b/tests/Feature/Transaction/ExpiryClearOnSelloutMysqlTest.php new file mode 100644 index 0000000..1bfe5dd --- /dev/null +++ b/tests/Feature/Transaction/ExpiryClearOnSelloutMysqlTest.php @@ -0,0 +1,101 @@ +getDriverName() !== 'mysql') { + $this->markTestSkipped('需 MySQL:machine_stock_movements.type enum 為 MySQL-only DDL。'); + } + + $this->company = Company::create(['name' => 'Test Company']); + $this->service = app(TransactionService::class); + } + + private function dispense(string $serial, string $slotNo): array + { + return [ + 'serial_no' => $serial, + 'slot_no' => $slotNo, + 'product_id' => 101, + 'amount' => 50, + 'dispense_status' => 1, + ]; + } + + /** 庫存賣到 0 → 效期與批號被清空。 */ + public function test_clears_expiry_and_batch_when_stock_hits_zero(): void + { + $serial = '1234567890'; + $machine = Machine::factory()->create([ + 'company_id' => $this->company->id, + 'serial_no' => $serial, + ]); + $slot = $machine->slots()->create([ + 'slot_no' => '1', + 'stock' => 1, + 'max_stock' => 10, + 'expiry_date' => '2030-12-31', + 'batch_no' => 'B-001', + ]); + + $this->service->recordDispense($this->dispense($serial, '1')); + + $fresh = $slot->fresh(); + $this->assertEquals(0, $fresh->stock, '庫存應扣為 0'); + $this->assertNull($fresh->expiry_date, '售完應清空效期'); + $this->assertNull($fresh->batch_no, '售完應清空批號'); + } + + /** 庫存尚未歸 0 → 效期與批號保留(同批貨還在販售)。 */ + public function test_keeps_expiry_when_stock_remains(): void + { + $serial = '1234567891'; + $machine = Machine::factory()->create([ + 'company_id' => $this->company->id, + 'serial_no' => $serial, + ]); + $slot = $machine->slots()->create([ + 'slot_no' => '2', + 'stock' => 5, + 'max_stock' => 10, + 'expiry_date' => '2030-12-31', + 'batch_no' => 'B-001', + ]); + + $this->service->recordDispense($this->dispense($serial, '2')); + + $fresh = $slot->fresh(); + $this->assertEquals(4, $fresh->stock, '庫存應扣為 4'); + $this->assertEquals('2030-12-31', $fresh->expiry_date->format('Y-m-d'), '未售完應保留效期'); + $this->assertEquals('B-001', $fresh->batch_no, '未售完應保留批號'); + } +}