[PROMOTE] feat/expiry-clear-on-sellout → demo:售完清效期測試驗收

1. recordDispense 售完(stock=0)即清空貨道 expiry_date 與 batch_no,避免舊批效期殘留誤鎖新批貨
2. 新增 ExpiryClearOnSelloutMysqlTest 驗收測試
3. 同步併入 main 既有提交:每機台商品定價 + 補貨單/機台列表 RWD 修正(fcd0588、c5a3f99)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	app/Services/Transaction/TransactionService.php
This commit is contained in:
sky121113 2026-06-23 17:24:12 +08:00
commit d1c7940db9
5 changed files with 157 additions and 15 deletions

View File

@ -308,6 +308,17 @@ class TransactionService
if ((int) $slot->stock > 0) { if ((int) $slot->stock > 0) {
$slot->decrement('stock'); $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; $type = \App\Models\Machine\MachineStockMovement::TYPE_SALE;
$this->machineService->recordStockMovement( $this->machineService->recordStockMovement(

View File

@ -765,6 +765,7 @@
"Failed to get invoice print URL": "Failed to get invoice print URL", "Failed to get invoice print URL": "Failed to get invoice print URL",
"Failed to load permissions": "Failed to load permissions", "Failed to load permissions": "Failed to load permissions",
"Failed to load preview": "Failed to load preview", "Failed to load preview": "Failed to load preview",
"Failed to load pricing": "Failed to load pricing",
"Failed to load tab content": "Failed to load tab content", "Failed to load tab content": "Failed to load tab content",
"Failed to resolve issues.": "Failed to resolve issues.", "Failed to resolve issues.": "Failed to resolve issues.",
"Failed to save permissions.": "Failed to save permissions.", "Failed to save permissions.": "Failed to save permissions.",
@ -897,6 +898,7 @@
"Generate Replenishment Order": "Generate Replenishment Order", "Generate Replenishment Order": "Generate Replenishment Order",
"Generate and manage one-time pickup codes for customers": "Generate and manage one-time pickup codes for customers", "Generate and manage one-time pickup codes for customers": "Generate and manage one-time pickup codes for customers",
"Gift Definitions": "Gift Definitions", "Gift Definitions": "Gift Definitions",
"Global": "Global",
"Global roles accessible by all administrators.": "Global roles accessible by all administrators.", "Global roles accessible by all administrators.": "Global roles accessible by all administrators.",
"Go Authorize": "Go Authorize", "Go Authorize": "Go Authorize",
"Go Back": "Go Back", "Go Back": "Go Back",
@ -1020,6 +1022,7 @@
"Latitude": "Latitude", "Latitude": "Latitude",
"Lease": "Lease", "Lease": "Lease",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.", "Leave blank to deploy immediately, or set a future time to schedule a background release.": "Leave blank to deploy immediately, or set a future time to schedule a background release.",
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "Leave blank to use the global product price. Member price will be capped at the machine selling price.",
"Leave empty for permanent code": "Leave empty for permanent code", "Leave empty for permanent code": "Leave empty for permanent code",
"Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code", "Leave empty or 0 for permanent code": "Leave empty or 0 for permanent code",
"Leave empty to inherit company settings": "Leave empty to inherit company settings", "Leave empty to inherit company settings": "Leave empty to inherit company settings",
@ -1105,6 +1108,8 @@
"Machine Model Settings": "Machine Model Settings", "Machine Model Settings": "Machine Model Settings",
"Machine Name": "Machine Name", "Machine Name": "Machine Name",
"Machine Permissions": "Machine Permissions", "Machine Permissions": "Machine Permissions",
"Machine Price": "Machine Price",
"Machine Pricing": "Machine Pricing",
"Machine Reboot": "Machine Reboot", "Machine Reboot": "Machine Reboot",
"Machine Registry": "Machine Registry", "Machine Registry": "Machine Registry",
"Machine Replenishment": "Machine Replenishment", "Machine Replenishment": "Machine Replenishment",
@ -1123,6 +1128,7 @@
"Machine Stock Movements": "Machine Stock Movements", "Machine Stock Movements": "Machine Stock Movements",
"Machine System Settings": "Machine System Settings", "Machine System Settings": "Machine System Settings",
"Machine Utilization": "Machine Utilization", "Machine Utilization": "Machine Utilization",
"Machine company changed": "Machine company changed",
"Machine created successfully.": "Machine created successfully.", "Machine created successfully.": "Machine created successfully.",
"Machine deleted successfully.": "Machine deleted successfully.", "Machine deleted successfully.": "Machine deleted successfully.",
"Machine has reconnected to the server and resumed normal heartbeat.": "Machine has reconnected to the server and resumed normal heartbeat.", "Machine has reconnected to the server and resumed normal heartbeat.": "Machine has reconnected to the server and resumed normal heartbeat.",
@ -1135,6 +1141,8 @@
"Machine normal": "Machine normal", "Machine normal": "Machine normal",
"Machine not found": "Machine not found", "Machine not found": "Machine not found",
"Machine permissions updated successfully.": "Machine permissions updated successfully.", "Machine permissions updated successfully.": "Machine permissions updated successfully.",
"Machine pricing saved and sync command pushed.": "Machine pricing saved and sync command pushed.",
"Machine pricing updated": "Machine pricing updated",
"Machine settings updated successfully.": "Machine settings updated successfully.", "Machine settings updated successfully.": "Machine settings updated successfully.",
"Machine temperature exceeds limit": "Machine temperature exceeds limit, purchase suspended", "Machine temperature exceeds limit": "Machine temperature exceeds limit, purchase suspended",
"Machine to Warehouse": "Machine to Warehouse", "Machine to Warehouse": "Machine to Warehouse",
@ -1338,6 +1346,7 @@
"No maintenance records found": "No maintenance records found", "No maintenance records found": "No maintenance records found",
"No matching logs found": "No matching logs found", "No matching logs found": "No matching logs found",
"No matching machines": "No matching machines", "No matching machines": "No matching machines",
"No matching products": "No matching products",
"No materials available": "No materials available", "No materials available": "No materials available",
"No movement records found": "No movement records found", "No movement records found": "No movement records found",
"No movements found": "No movements found", "No movements found": "No movements found",
@ -1348,9 +1357,11 @@
"No pickup codes found": "No pickup codes found", "No pickup codes found": "No pickup codes found",
"No product data matching search criteria": "No product data matching search criteria", "No product data matching search criteria": "No product data matching search criteria",
"No product record found": "No product record found", "No product record found": "No product record found",
"No products available for this company": "No products available for this company",
"No products found": "No products found", "No products found": "No products found",
"No products found in this warehouse": "No products found in this warehouse", "No products found in this warehouse": "No products found in this warehouse",
"No products found matching your criteria.": "No products found matching your criteria.", "No products found matching your criteria.": "No products found matching your criteria.",
"No products on this machine yet": "No products on this machine yet",
"No records found": "No records found", "No records found": "No records found",
"No related detail found": "No related detail found", "No related detail found": "No related detail found",
"No remark": "No remark", "No remark": "No remark",
@ -1827,6 +1838,7 @@
"Sales Today": "Sales Today", "Sales Today": "Sales Today",
"Sales revenue minus product cost net value": "Sales revenue minus product cost net value", "Sales revenue minus product cost net value": "Sales revenue minus product cost net value",
"Save": "Save", "Save": "Save",
"Save & Sync": "Save & Sync",
"Save Changes": "Save Changes", "Save Changes": "Save Changes",
"Save Config": "Save Config", "Save Config": "Save Config",
"Save Material": "Save Material", "Save Material": "Save Material",
@ -1834,6 +1846,8 @@
"Save Settings": "Save Settings", "Save Settings": "Save Settings",
"Save Version": "Save Version", "Save Version": "Save Version",
"Save error:": "Save error:", "Save error:": "Save error:",
"Save failed": "Save failed",
"Saved": "Saved",
"Saved.": "Saved.", "Saved.": "Saved.",
"Saving...": "Saving...", "Saving...": "Saving...",
"Scale level and access control": "Scale level and access control", "Scale level and access control": "Scale level and access control",
@ -2590,6 +2604,7 @@
"of items": "items", "of items": "items",
"optional": "optional", "optional": "optional",
"orders": "orders", "orders": "orders",
"overrides set": "overrides set",
"pending": "Pending", "pending": "Pending",
"permissions": "Permission Settings", "permissions": "Permission Settings",
"permissions.accounts": "Account Management", "permissions.accounts": "Account Management",

View File

@ -765,6 +765,7 @@
"Failed to get invoice print URL": "領収書印刷URLの取得に失敗しました", "Failed to get invoice print URL": "領収書印刷URLの取得に失敗しました",
"Failed to load permissions": "権限の読み込みに失敗しました", "Failed to load permissions": "権限の読み込みに失敗しました",
"Failed to load preview": "プレビューの読み込みに失敗しました", "Failed to load preview": "プレビューの読み込みに失敗しました",
"Failed to load pricing": "価格設定の読み込みに失敗しました",
"Failed to load tab content": "タブ内容の読み込みに失敗しました", "Failed to load tab content": "タブ内容の読み込みに失敗しました",
"Failed to resolve issues.": "異常の解消に失敗しました。", "Failed to resolve issues.": "異常の解消に失敗しました。",
"Failed to save permissions.": "権限の保存に失敗しました。", "Failed to save permissions.": "権限の保存に失敗しました。",
@ -897,6 +898,7 @@
"Generate Replenishment Order": "補充伝票生成", "Generate Replenishment Order": "補充伝票生成",
"Generate and manage one-time pickup codes for customers": "顧客用のワンタイム受取コードを生成・管理", "Generate and manage one-time pickup codes for customers": "顧客用のワンタイム受取コードを生成・管理",
"Gift Definitions": "ギフト設定", "Gift Definitions": "ギフト設定",
"Global": "グローバル価格",
"Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。", "Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。",
"Go Authorize": "先に授権する", "Go Authorize": "先に授権する",
"Go Back": "戻る", "Go Back": "戻る",
@ -1020,6 +1022,7 @@
"Latitude": "緯度", "Latitude": "緯度",
"Lease": "リース", "Lease": "リース",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。", "Leave blank to deploy immediately, or set a future time to schedule a background release.": "空欄のままにすると即時配信されます。未来の日時を指定すると背景で自動予約配信されます。",
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "空欄の場合はグローバル商品価格を使用します。会員価格はマシン販売価格を上限に自動調整されます。",
"Leave empty for permanent code": "空欄で無期限コードになります", "Leave empty for permanent code": "空欄で無期限コードになります",
"Leave empty or 0 for permanent code": "空欄または0で無期限コードになります", "Leave empty or 0 for permanent code": "空欄または0で無期限コードになります",
"Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します", "Leave empty to inherit company settings": "空白のままにすると会社のグローバル設定を継承します",
@ -1105,6 +1108,8 @@
"Machine Model Settings": "機器モデル設定", "Machine Model Settings": "機器モデル設定",
"Machine Name": "機器名", "Machine Name": "機器名",
"Machine Permissions": "機器権限", "Machine Permissions": "機器権限",
"Machine Price": "マシン価格",
"Machine Pricing": "マシン価格設定",
"Machine Reboot": "機器アプリ再起動", "Machine Reboot": "機器アプリ再起動",
"Machine Registry": "機器リスト", "Machine Registry": "機器リスト",
"Machine Replenishment": "機器補充伝票", "Machine Replenishment": "機器補充伝票",
@ -1123,6 +1128,7 @@
"Machine Stock Movements": "機器在庫変動履歴", "Machine Stock Movements": "機器在庫変動履歴",
"Machine System Settings": "機器システム設定", "Machine System Settings": "機器システム設定",
"Machine Utilization": "機器稼働率", "Machine Utilization": "機器稼働率",
"Machine company changed": "マシンの所属会社が変更されました",
"Machine created successfully.": "機器が正常に作成されました。", "Machine created successfully.": "機器が正常に作成されました。",
"Machine deleted successfully.": "機器が正常に削除されました。", "Machine deleted successfully.": "機器が正常に削除されました。",
"Machine has reconnected to the server and resumed normal heartbeat.": "マシンがサーバーに再接続し、通常のハートビートを再開しました。", "Machine has reconnected to the server and resumed normal heartbeat.": "マシンがサーバーに再接続し、通常のハートビートを再開しました。",
@ -1135,6 +1141,8 @@
"Machine normal": "デバイス正常", "Machine normal": "デバイス正常",
"Machine not found": "機器が見つかりません", "Machine not found": "機器が見つかりません",
"Machine permissions updated successfully.": "機器権限が正常に更新されました。", "Machine permissions updated successfully.": "機器権限が正常に更新されました。",
"Machine pricing saved and sync command pushed.": "マシン価格を保存し、同期コマンドを送信しました。",
"Machine pricing updated": "マシン価格を更新しました",
"Machine settings updated successfully.": "機器設定が正常に更新されました。", "Machine settings updated successfully.": "機器設定が正常に更新されました。",
"Machine temperature exceeds limit": "設定温度超過のため購入一時停止", "Machine temperature exceeds limit": "設定温度超過のため購入一時停止",
"Machine to Warehouse": "機器から倉庫", "Machine to Warehouse": "機器から倉庫",
@ -1338,6 +1346,7 @@
"No maintenance records found": "メンテナンス記録が見つかりません", "No maintenance records found": "メンテナンス記録が見つかりません",
"No matching logs found": "一致するログが見つかりません", "No matching logs found": "一致するログが見つかりません",
"No matching machines": "一致する機器がありません", "No matching machines": "一致する機器がありません",
"No matching products": "一致する商品がありません",
"No materials available": "利用可能な素材がありません", "No materials available": "利用可能な素材がありません",
"No movement records found": "変動記録が見つかりません", "No movement records found": "変動記録が見つかりません",
"No movements found": "変動が見つかりません", "No movements found": "変動が見つかりません",
@ -1348,9 +1357,11 @@
"No pickup codes found": "受取コードが見つかりません", "No pickup codes found": "受取コードが見つかりません",
"No product data matching search criteria": "検索条件に一致する商品データはありません", "No product data matching search criteria": "検索条件に一致する商品データはありません",
"No product record found": "商品記録が見つかりません", "No product record found": "商品記録が見つかりません",
"No products available for this company": "この会社で販売可能な商品がありません",
"No products found": "商品が見つかりません", "No products found": "商品が見つかりません",
"No products found in this warehouse": "この倉庫に商品は見つかりません", "No products found in this warehouse": "この倉庫に商品は見つかりません",
"No products found matching your criteria.": "条件に一致する商品が見つかりません。", "No products found matching your criteria.": "条件に一致する商品が見つかりません。",
"No products on this machine yet": "このマシンにはまだ商品がありません",
"No records found": "記録が見つかりません", "No records found": "記録が見つかりません",
"No related detail found": "関連する詳細が見つかりません", "No related detail found": "関連する詳細が見つかりません",
"No remark": "備考なし", "No remark": "備考なし",
@ -1827,6 +1838,7 @@
"Sales Today": "本日の販売数", "Sales Today": "本日の販売数",
"Sales revenue minus product cost net value": "販売額から商品コストを差し引いた純利益", "Sales revenue minus product cost net value": "販売額から商品コストを差し引いた純利益",
"Save": "保存", "Save": "保存",
"Save & Sync": "保存して同期",
"Save Changes": "変更を保存", "Save Changes": "変更を保存",
"Save Config": "設定を保存", "Save Config": "設定を保存",
"Save Material": "素材を保存", "Save Material": "素材を保存",
@ -1834,6 +1846,8 @@
"Save Settings": "設定を保存", "Save Settings": "設定を保存",
"Save Version": "Save Version", "Save Version": "Save Version",
"Save error:": "保存エラー:", "Save error:": "保存エラー:",
"Save failed": "保存に失敗しました",
"Saved": "保存しました",
"Saved.": "保存しました。", "Saved.": "保存しました。",
"Saving...": "保存中...", "Saving...": "保存中...",
"Scale level and access control": "スケールレベルとアクセス制御", "Scale level and access control": "スケールレベルとアクセス制御",
@ -2590,6 +2604,7 @@
"of items": "件中", "of items": "件中",
"optional": "任意", "optional": "任意",
"orders": "件", "orders": "件",
"overrides set": "件のマシン価格を設定済み",
"pending": "機器受取待機中", "pending": "機器受取待機中",
"permissions": "権限設定", "permissions": "権限設定",
"permissions.accounts": "アカウント管理", "permissions.accounts": "アカウント管理",

View File

@ -765,6 +765,7 @@
"Failed to get invoice print URL": "取得發票列印網址失敗", "Failed to get invoice print URL": "取得發票列印網址失敗",
"Failed to load permissions": "載入權限失敗", "Failed to load permissions": "載入權限失敗",
"Failed to load preview": "載入預覽失敗", "Failed to load preview": "載入預覽失敗",
"Failed to load pricing": "載入定價失敗",
"Failed to load tab content": "載入分頁內容失敗", "Failed to load tab content": "載入分頁內容失敗",
"Failed to resolve issues.": "排除異常失敗。", "Failed to resolve issues.": "排除異常失敗。",
"Failed to save permissions.": "無法儲存權限設定。", "Failed to save permissions.": "無法儲存權限設定。",
@ -897,6 +898,7 @@
"Generate Replenishment Order": "產生補貨單", "Generate Replenishment Order": "產生補貨單",
"Generate and manage one-time pickup codes for customers": "產生並管理客戶的一次性取貨碼", "Generate and manage one-time pickup codes for customers": "產生並管理客戶的一次性取貨碼",
"Gift Definitions": "禮品設定", "Gift Definitions": "禮品設定",
"Global": "全域價",
"Global roles accessible by all administrators.": "適用於所有管理者的全域角色。", "Global roles accessible by all administrators.": "適用於所有管理者的全域角色。",
"Go Authorize": "前往授權", "Go Authorize": "前往授權",
"Go Back": "返回", "Go Back": "返回",
@ -1020,6 +1022,7 @@
"Latitude": "緯度", "Latitude": "緯度",
"Lease": "租賃", "Lease": "租賃",
"Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。", "Leave blank to deploy immediately, or set a future time to schedule a background release.": "留空將立即部署更新,或設定未來時間由背景自動排程發布。",
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "留空則沿用全域商品價。會員價會自動夾為不高於機台售價。",
"Leave empty for permanent code": "留空表示永久有效", "Leave empty for permanent code": "留空表示永久有效",
"Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼", "Leave empty or 0 for permanent code": "留空或輸入 0 則為永久代碼",
"Leave empty to inherit company settings": "留空則繼承公司全域設定", "Leave empty to inherit company settings": "留空則繼承公司全域設定",
@ -1105,6 +1108,8 @@
"Machine Model Settings": "機台型號設定", "Machine Model Settings": "機台型號設定",
"Machine Name": "機台名稱", "Machine Name": "機台名稱",
"Machine Permissions": "機台權限", "Machine Permissions": "機台權限",
"Machine Price": "機台價",
"Machine Pricing": "機台定價",
"Machine Reboot": "機台 APP 重啟", "Machine Reboot": "機台 APP 重啟",
"Machine Registry": "機台清冊", "Machine Registry": "機台清冊",
"Machine Replenishment": "機台補貨單", "Machine Replenishment": "機台補貨單",
@ -1121,23 +1126,9 @@
"Machine Status List": "機台運行狀態列表", "Machine Status List": "機台運行狀態列表",
"Machine Stock": "機台庫存", "Machine Stock": "機台庫存",
"Machine Stock Movements": "機台庫存流水帳", "Machine Stock Movements": "機台庫存流水帳",
"Machine Pricing": "機台定價",
"Machine Price": "機台價",
"Leave blank to use the global product price. Member price will be capped at the machine selling price.": "留空則沿用全域商品價。會員價會自動夾為不高於機台售價。",
"No products on this machine yet": "此機台尚無商品",
"No products available for this company": "此公司尚無可販售商品",
"No matching products": "找不到符合的商品",
"overrides set": "項已設定機台價",
"Global": "全域價",
"Save & Sync": "儲存並同步",
"Failed to load pricing": "載入定價失敗",
"Saved": "已儲存",
"Save failed": "儲存失敗",
"Machine pricing saved and sync command pushed.": "機台定價已儲存並推送同步指令。",
"Machine pricing updated": "機台定價已更新",
"Machine company changed": "機台所屬公司已變更",
"Machine System Settings": "機台系統設定", "Machine System Settings": "機台系統設定",
"Machine Utilization": "機台稼動率", "Machine Utilization": "機台稼動率",
"Machine company changed": "機台所屬公司已變更",
"Machine created successfully.": "機台已成功建立。", "Machine created successfully.": "機台已成功建立。",
"Machine deleted successfully.": "機台已成功刪除。", "Machine deleted successfully.": "機台已成功刪除。",
"Machine has reconnected to the server and resumed normal heartbeat.": "機台已重新連線至伺服器並恢復正常心跳。", "Machine has reconnected to the server and resumed normal heartbeat.": "機台已重新連線至伺服器並恢復正常心跳。",
@ -1150,6 +1141,8 @@
"Machine normal": "機台正常", "Machine normal": "機台正常",
"Machine not found": "找不到機台", "Machine not found": "找不到機台",
"Machine permissions updated successfully.": "機台權限已成功更新。", "Machine permissions updated successfully.": "機台權限已成功更新。",
"Machine pricing saved and sync command pushed.": "機台定價已儲存並推送同步指令。",
"Machine pricing updated": "機台定價已更新",
"Machine settings updated successfully.": "機台設定已成功更新。", "Machine settings updated successfully.": "機台設定已成功更新。",
"Machine temperature exceeds limit": "機器超過設定溫度,暫停購買", "Machine temperature exceeds limit": "機器超過設定溫度,暫停購買",
"Machine to Warehouse": "機台對倉庫", "Machine to Warehouse": "機台對倉庫",
@ -1353,6 +1346,7 @@
"No maintenance records found": "找不到維修紀錄", "No maintenance records found": "找不到維修紀錄",
"No matching logs found": "找不到符合條件的日誌", "No matching logs found": "找不到符合條件的日誌",
"No matching machines": "查無匹配機台", "No matching machines": "查無匹配機台",
"No matching products": "找不到符合的商品",
"No materials available": "沒有可用的素材", "No materials available": "沒有可用的素材",
"No movement records found": "查無異動紀錄", "No movement records found": "查無異動紀錄",
"No movements found": "尚無異動紀錄", "No movements found": "尚無異動紀錄",
@ -1363,9 +1357,11 @@
"No pickup codes found": "找不到取貨碼", "No pickup codes found": "找不到取貨碼",
"No product data matching search criteria": "沒有符合搜尋條件的商品資料", "No product data matching search criteria": "沒有符合搜尋條件的商品資料",
"No product record found": "無商品紀錄", "No product record found": "無商品紀錄",
"No products available for this company": "此公司尚無可販售商品",
"No products found": "未找到商品", "No products found": "未找到商品",
"No products found in this warehouse": "此倉庫目前無商品庫存", "No products found in this warehouse": "此倉庫目前無商品庫存",
"No products found matching your criteria.": "找不到符合條件的商品。", "No products found matching your criteria.": "找不到符合條件的商品。",
"No products on this machine yet": "此機台尚無商品",
"No records found": "未找到相關紀錄", "No records found": "未找到相關紀錄",
"No related detail found": "無關聯詳細資料", "No related detail found": "無關聯詳細資料",
"No remark": "尚無備註", "No remark": "尚無備註",
@ -1842,6 +1838,7 @@
"Sales Today": "今日銷售", "Sales Today": "今日銷售",
"Sales revenue minus product cost net value": "銷售額扣除商品成本淨值", "Sales revenue minus product cost net value": "銷售額扣除商品成本淨值",
"Save": "儲存", "Save": "儲存",
"Save & Sync": "儲存並同步",
"Save Changes": "儲存變更", "Save Changes": "儲存變更",
"Save Config": "儲存配置", "Save Config": "儲存配置",
"Save Material": "儲存素材", "Save Material": "儲存素材",
@ -1849,6 +1846,8 @@
"Save Settings": "儲存設定", "Save Settings": "儲存設定",
"Save Version": "儲存版本", "Save Version": "儲存版本",
"Save error:": "儲存錯誤:", "Save error:": "儲存錯誤:",
"Save failed": "儲存失敗",
"Saved": "已儲存",
"Saved.": "已儲存", "Saved.": "已儲存",
"Saving...": "儲存中...", "Saving...": "儲存中...",
"Scale level and access control": "層級與存取控制", "Scale level and access control": "層級與存取控制",
@ -2605,6 +2604,7 @@
"of items": "筆項目", "of items": "筆項目",
"optional": "選填", "optional": "選填",
"orders": "筆", "orders": "筆",
"overrides set": "項已設定機台價",
"pending": "等待機台領取", "pending": "等待機台領取",
"permissions": "權限設定", "permissions": "權限設定",
"permissions.accounts": "帳號管理", "permissions.accounts": "帳號管理",

View File

@ -0,0 +1,101 @@
<?php
namespace Tests\Feature\Transaction;
use App\Models\Machine\Machine;
use App\Models\System\Company;
use App\Services\Transaction\TransactionService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* 售完(庫存歸 0)自動清空效期/批號 —— MySQLrecordDispense 內部寫
* machine_stock_movements type enum MySQL-only DDLsqlite :memory: 無法驗)。執行:
*
* ./vendor/bin/sail test -c phpunit.mysql.xml --filter ExpiryClearOnSelloutMysqlTest
*
* 在預設 sqlite 套件中自動 skip,不影響主套件綠燈。
*
* 動機:機台端「賣完清效期」已做,但 B009 補貨上報不回傳 expiry後台若殘留舊效期
* 日後管理員只補 stock 未改效期 update_inventory 把過期日下發 新批貨被誤鎖。
* 故後台扣庫存歸 0 時亦須清效期,與機台端對稱。
*/
class ExpiryClearOnSelloutMysqlTest extends TestCase
{
use RefreshDatabase;
private Company $company;
private TransactionService $service;
protected function setUp(): void
{
parent::setUp();
if (DB::connection()->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, '未售完應保留批號');
}
}