From ddac862cee1cdc320867ab9b0c76574c6c9cac90 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Thu, 18 Jun 2026 17:14:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?[FEAT]=20=E4=BA=A4=E6=98=93=E7=B5=90?= =?UTF-8?q?=E7=AE=97=E5=86=AA=E7=AD=89=E6=80=A7=E5=BC=B7=E5=8C=96=E8=88=87?= =?UTF-8?q?=20flow=5Fid=20=E8=B7=A8=E6=A9=9F=E5=8F=B0=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=96=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. finalizeTransaction 將 flow_id 前綴機台序號(serial+rawFlowId)使其全域唯一,解決跨機台單機 flow_id 撞號問題;ACK 仍回傳原始 flow_id,App/outbox 對帳無感。 2. 終態冪等檢查移入 DB transaction 並對既有 order 加 lockForUpdate 排他鎖,防止兩個 worker 同時穿透 pending 而重複出貨/扣庫存/核銷。 3. 取貨碼(PickupCode)與來店禮(WelcomeGift)核銷加排他鎖,防並發 lost-update 使 usage_count 突破 usage_limit。 4. recordInvoice 的 RelateNumber 推導改為直接取用已前綴序號的 flow_id,與線上既有 serial+rawFlowId 逐字等價,不重複加序號。 5. 新增 FinalizeIdempotencyTest、FinalizeStockMysqlTest 與 phpunit.mysql.xml(MySQL-only DDL 測試設定)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Transaction/TransactionService.php | 46 ++++-- phpunit.mysql.xml | 37 +++++ .../Transaction/FinalizeIdempotencyTest.php | 151 ++++++++++++++++++ .../Transaction/FinalizeStockMysqlTest.php | 94 +++++++++++ 4 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 phpunit.mysql.xml create mode 100644 tests/Feature/Transaction/FinalizeIdempotencyTest.php create mode 100644 tests/Feature/Transaction/FinalizeStockMysqlTest.php diff --git a/app/Services/Transaction/TransactionService.php b/app/Services/Transaction/TransactionService.php index b6d1832..f96e78f 100644 --- a/app/Services/Transaction/TransactionService.php +++ b/app/Services/Transaction/TransactionService.php @@ -208,10 +208,12 @@ class TransactionService } } - // RelateNumber 冪等鍵:App 未帶時由後台以 serial + flow_id 推導(綠界上限 30 字) + // RelateNumber 冪等鍵:App 未帶時由後台以 flow_id 推導(綠界上限 30 字、限英數)。 + // flow_id 在 finalizeTransaction 入口已前綴機台序號(serial+rawFlowId), + // 故此處直接取用即可,輸出與線上既有 serial+rawFlowId 逐字等價,不可再重複加 serial。 $relateNumber = $data['relate_number'] ?? null; if (empty($relateNumber) && !empty($data['flow_id'])) { - $relateNumber = substr($machine->serial_no . $data['flow_id'], 0, 30); + $relateNumber = substr($data['flow_id'], 0, 30); } $attributes = [ @@ -333,14 +335,30 @@ class TransactionService public function finalizeTransaction(array $data): Order { // 優先從 Root 讀取,再從 order 內讀取 - $flowId = $data['flow_id'] ?? ($data['order']['flow_id'] ?? null); + $rawFlowId = $data['flow_id'] ?? ($data['order']['flow_id'] ?? null); + if (empty($rawFlowId)) { + throw new \Exception("Flow ID is required for finalization"); + } - // 冪等性檢查:只有「終態(completed/failed/abandoned)」才直接回傳,避免重複出貨/扣庫存。 - // main(晟崴/中國醫)的單永遠是 completed → 等同原本的 early-return,行為不變。 - // 既有單為 pending(本分支先送過 pending)時則「放行」,往下交給 processTransaction - // 做 pending → paid/failed 的狀態轉移(含本次的 dispense/invoice)。 - if ($flowId) { - $existingOrder = Order::withoutGlobalScopes()->where('flow_id', $flowId)->first(); + // 跨機台防撞號:App 端 flow_id 僅單機唯一(YYYYMMDDHHMMSSXXXX),加上機台序號前綴 + // 才全域唯一(orders.flow_id 是全域 unique)。無分隔符——綠界 RelateNumber 禁特殊符號, + // 且 serial+flow_id 與線上既有 relateNumber 推導逐字等價(見 recordInvoice)。 + // ACK 仍由 ProcessTransactionFinalized 回傳『原始』flow_id,App / outbox 對帳無感。 + $flowId = $data['serial_no'] . $rawFlowId; + $data['flow_id'] = $flowId; + $data['order']['flow_id'] = $flowId; + + return DB::transaction(function () use ($data, $flowId) { + $serialNo = $data['serial_no']; + + // 冪等性 + 併發防護:終態檢查移進交易內並對既有 row 加排他鎖, + // 防止兩個 worker 同時穿透 pending 狀態而重複出貨/扣庫存/核銷。 + // main(晟崴/中國醫)的單永遠是 completed → 等同原本的 early-return,行為不變。 + // 既有單為 pending 時放行,往下交給 processTransaction 做 pending → paid/failed 轉移。 + $existingOrder = Order::withoutGlobalScopes() + ->where('flow_id', $flowId) + ->lockForUpdate() + ->first(); if ($existingOrder && in_array($existingOrder->status, Order::TERMINAL_STATUSES, true)) { Log::info("Transaction already finalized (terminal), returning existing order", [ 'flow_id' => $flowId, @@ -348,12 +366,6 @@ class TransactionService ]); return $existingOrder; } - } else { - throw new \Exception("Flow ID is required for finalization"); - } - - return DB::transaction(function () use ($data, $flowId) { - $serialNo = $data['serial_no']; // 1. Process Order (B600) $orderData = $data['order']; @@ -433,9 +445,11 @@ class TransactionService break; case 6: // 取貨碼 (Pickup Code) + // 排他鎖:防並發核銷 lost-update 使 usage_count 突破 usage_limit。 $pickupCode = PickupCode::where('machine_id', $order->machine_id) ->where('id', $codeId) ->where('status', 'active') + ->lockForUpdate() ->first(); if ($pickupCode) { $newCount = $pickupCode->usage_count + 1; @@ -483,9 +497,11 @@ class TransactionService // 處理來店禮 (Welcome Gift) 自動核銷 $welcomeGiftId = $data['order']['welcome_gift_id'] ?? ($data['welcome_gift_id'] ?? null); if ($welcomeGiftId) { + // 排他鎖:防並發核銷 lost-update 使 usage_count 突破 usage_limit。 $welcomeGift = WelcomeGift::where('machine_id', $order->machine_id) ->where('id', $welcomeGiftId) ->where('status', 'active') + ->lockForUpdate() ->first(); if ($welcomeGift) { $newCount = $welcomeGift->usage_count + 1; diff --git a/phpunit.mysql.xml b/phpunit.mysql.xml new file mode 100644 index 0000000..c08c61c --- /dev/null +++ b/phpunit.mysql.xml @@ -0,0 +1,37 @@ + + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + diff --git a/tests/Feature/Transaction/FinalizeIdempotencyTest.php b/tests/Feature/Transaction/FinalizeIdempotencyTest.php new file mode 100644 index 0000000..684610a --- /dev/null +++ b/tests/Feature/Transaction/FinalizeIdempotencyTest.php @@ -0,0 +1,151 @@ +company = Company::create(['name' => 'Test Company']); + $this->service = app(TransactionService::class); + } + + private function makeMachine(string $serialNo, bool $taxInvoice = false): Machine + { + return Machine::factory()->create([ + 'company_id' => $this->company->id, + 'serial_no' => $serialNo, + 'tax_invoice_enabled' => $taxInvoice, + ]); + } + + private function completedPayload(string $serialNo, string $rawFlowId, array $dispense = []): array + { + return [ + 'serial_no' => $serialNo, + 'flow_id' => $rawFlowId, + 'payment_type' => 1, + 'order' => [ + 'flow_id' => $rawFlowId, + 'total_amount' => 50, + 'pay_amount' => 50, + 'payment_type' => 1, + 'status' => 'completed', + 'payment_status' => 1, + ], + 'dispense' => $dispense, + ]; + } + + /** 順序重送同一筆 → 只建一張單、出貨一次、庫存只扣一次。 */ + public function test_resending_same_transaction_is_idempotent(): void + { + $serial = '1234567890'; + $raw = '202606181530450001'; + $this->makeMachine($serial); + + // 不建貨道:recordDispense 在 slot=null 時仍建出貨紀錄,但跳過 decrement/stockMovement + // (stockMovement 的 type enum 是 MySQL-only DDL,sqlite :memory: 無法驗,屬既有環境限制)。 + // 「出貨紀錄只一筆」與「庫存只扣一次」由同一道終態短路保證,此處以前者代證。 + $dispense = [[ + 'slot_no' => '99', + 'product_id' => 101, + 'amount' => 50, + 'dispense_status' => 1, + 'remaining_stock' => 4, + ]]; + + $first = $this->service->finalizeTransaction($this->completedPayload($serial, $raw, $dispense)); + $second = $this->service->finalizeTransaction($this->completedPayload($serial, $raw, $dispense)); + + // 同一張單(終態短路)、出貨紀錄只有一筆(重送不重複扣庫存的同源保證) + $this->assertEquals($first->id, $second->id); + $this->assertEquals(1, Order::where('flow_id', $serial . $raw)->count()); + $this->assertEquals(1, DispenseRecord::where('flow_id', $serial . $raw)->count()); + } + + /** 不同機台、相同原始 flow_id → 命名空間化後為兩張獨立單,不互吞。 */ + public function test_same_raw_flow_id_across_machines_creates_separate_orders(): void + { + $raw = '202606181530450001'; + $machineA = $this->makeMachine('1111111111'); + $machineB = $this->makeMachine('2222222222'); + + $orderA = $this->service->finalizeTransaction($this->completedPayload('1111111111', $raw)); + $orderB = $this->service->finalizeTransaction($this->completedPayload('2222222222', $raw)); + + $this->assertNotEquals($orderA->id, $orderB->id); + $this->assertEquals('1111111111' . $raw, $orderA->flow_id); + $this->assertEquals('2222222222' . $raw, $orderB->flow_id); + } + + /** RelateNumber = serial + rawFlowId(無分隔符、≤30、與線上推導逐字等價,不可雙重 serial)。 */ + public function test_relate_number_equals_serial_plus_raw_flow_id(): void + { + $serial = '1234567890'; + $raw = '202606181530450001'; + $this->makeMachine($serial, taxInvoice: true); + + $payload = $this->completedPayload($serial, $raw); + $payload['invoice'] = ['status' => 'pending', 'amount' => 50]; + + $order = $this->service->finalizeTransaction($payload); + $invoice = Invoice::where('flow_id', $serial . $raw)->first(); + + $this->assertNotNull($invoice); + $this->assertEquals($serial . $raw, $invoice->relate_number); + $this->assertLessThanOrEqual(30, strlen($invoice->relate_number)); + // 不可出現雙重 serial 前綴 + $this->assertStringStartsNotWith($serial . $serial, $invoice->relate_number); + } + + /** pending 先送、completed 後送 → 同一張單狀態轉移(命名空間化前後一致)。 */ + public function test_pending_then_completed_transitions_same_order(): void + { + $serial = '1234567890'; + $raw = '202606181530450001'; + $this->makeMachine($serial); + + $pending = $this->completedPayload($serial, $raw); + $pending['order']['status'] = 'pending'; + $pending['order']['payment_status'] = 0; + $pending['dispense'] = []; + $pendingOrder = $this->service->finalizeTransaction($pending); + $this->assertEquals('pending', $pendingOrder->status); + + // slot_no=99 不存在 → 跳過 stockMovement(MySQL-only),仍建出貨紀錄 + $completed = $this->completedPayload($serial, $raw, [[ + 'slot_no' => '99', 'product_id' => 101, 'amount' => 50, + 'dispense_status' => 1, 'remaining_stock' => 4, + ]]); + $completedOrder = $this->service->finalizeTransaction($completed); + + // 同一張單(命名空間化前後 key 一致)、轉為終態、出貨紀錄只一筆 + $this->assertEquals($pendingOrder->id, $completedOrder->id); + $this->assertEquals('completed', $completedOrder->fresh()->status); + $this->assertEquals(1, Order::where('flow_id', $serial . $raw)->count()); + $this->assertEquals(1, DispenseRecord::where('flow_id', $serial . $raw)->count()); + } +} diff --git a/tests/Feature/Transaction/FinalizeStockMysqlTest.php b/tests/Feature/Transaction/FinalizeStockMysqlTest.php new file mode 100644 index 0000000..f002fcd --- /dev/null +++ b/tests/Feature/Transaction/FinalizeStockMysqlTest.php @@ -0,0 +1,94 @@ +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 completedPayloadWithDispense(string $serialNo, string $rawFlowId, string $slotNo): array + { + return [ + 'serial_no' => $serialNo, + 'flow_id' => $rawFlowId, + 'payment_type' => 1, + 'order' => [ + 'flow_id' => $rawFlowId, + 'total_amount' => 50, + 'pay_amount' => 50, + 'payment_type' => 1, + 'status' => 'completed', + 'payment_status' => 1, + ], + 'dispense' => [[ + 'slot_no' => $slotNo, + 'product_id' => 101, + 'amount' => 50, + 'dispense_status' => 1, + 'remaining_stock' => 4, + ]], + ]; + } + + /** 出貨成功扣庫存一次;重送同一筆 → 庫存不再被扣、出貨/異動紀錄不重複(終態短路 + 命名空間化)。 */ + public function test_dispense_decrements_stock_once_and_resend_is_idempotent(): void + { + $serial = '1234567890'; + $raw = '202606181530450001'; + $machine = Machine::factory()->create([ + 'company_id' => $this->company->id, + 'serial_no' => $serial, + ]); + $slot = $machine->slots()->create(['slot_no' => '1', 'stock' => 5, 'max_stock' => 10]); + + $payload = $this->completedPayloadWithDispense($serial, $raw, '1'); + + // 第一次:扣庫存一次 + $first = $this->service->finalizeTransaction($payload); + $this->assertEquals(4, $slot->fresh()->stock); + $this->assertEquals(1, DispenseRecord::where('flow_id', $serial . $raw)->count()); + $this->assertEquals(1, DB::table('machine_stock_movements') + ->where('slot_no', '1')->where('machine_id', $machine->id)->count()); + + // 重送:庫存維持 4,紀錄不重複 + $second = $this->service->finalizeTransaction($payload); + $this->assertEquals($first->id, $second->id); + $this->assertEquals(4, $slot->fresh()->stock, '重送不可再扣庫存'); + $this->assertEquals(1, DispenseRecord::where('flow_id', $serial . $raw)->count()); + $this->assertEquals(1, DB::table('machine_stock_movements') + ->where('slot_no', '1')->where('machine_id', $machine->id)->count()); + $this->assertEquals(1, Order::where('flow_id', $serial . $raw)->count()); + } +} From b7e8401a9911b5cf8019dfb0641be2066cd932c7 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Thu, 18 Jun 2026 17:14:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?[FIX]=20=E9=8A=B7=E5=94=AE=E7=B4=80?= =?UTF-8?q?=E9=8C=84=E6=9C=AA=E4=BB=98=E6=AC=BE=E5=96=AE=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?:=E5=B7=B2=E6=A3=84=E5=96=AE=E6=94=B9=E6=9C=AA=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E3=80=81=E6=9C=AA=E5=87=BA=E8=B2=A8=E9=A1=AF=E7=A4=BA?= =?UTF-8?q?=20--?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 付款狀態「已棄單(abandoned)」顯示文字改為「未支付」;新增 Unpaid 語系、移除孤兒 Abandoned key,三語系(zh_TW/en/ja)對齊排序。 2. 出貨狀態:非付款成功(未完成 pending/支付失敗 failed/未支付 abandoned)的單從未出貨,一律顯示「--」,避免 delivery_status 欄位 DB 預設值 1 誤顯示為「出貨成功」。 3. 判斷邏輯收斂至 Order::hasDeliveryOutcome(),桌機表格、手機卡片、明細面板與匯出 delivery_status_label accessor 共用同一基準。 4. 補齊匯出訂單狀態對照表原本缺漏的 failed/abandoned 標籤。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Http/Controllers/Admin/SalesController.php | 2 ++ app/Models/Transaction/Order.php | 14 ++++++++++++++ lang/en.json | 2 +- lang/ja.json | 2 +- lang/zh_TW.json | 2 +- .../sales/partials/order-detail-panel.blade.php | 7 ++++++- .../admin/sales/partials/tab-orders.blade.php | 14 ++++++++++++-- 7 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 756d71a..5f41984 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -935,6 +935,8 @@ class SalesController extends Controller 'pending' => __('Pending'), 'paid' => __('Paid'), 'completed' => __('Completed'), + 'failed' => __('Failed'), + 'abandoned' => __('Unpaid'), 'cancelled' => __('Cancelled'), 'refunded' => __('Refunded'), ]; diff --git a/app/Models/Transaction/Order.php b/app/Models/Transaction/Order.php index 2e98628..5f6ccbb 100644 --- a/app/Models/Transaction/Order.php +++ b/app/Models/Transaction/Order.php @@ -229,11 +229,25 @@ class Order extends Model ]; } + /** + * 出貨狀態是否有意義:唯有付款成功(completed/paid)、機台真的動作過的單才有出貨結果。 + * pending(未完成)、failed(支付失敗)、abandoned(未支付)從未出貨,出貨狀態應顯示 '--'。 + * ⚠ delivery_status 欄位 DB 預設為 1(出貨成功),這些單若直接讀值會誤顯示成「出貨成功」。 + */ + public function hasDeliveryOutcome(): bool + { + return in_array($this->status, [self::STATUS_COMPLETED, 'paid'], true); + } + /** * 取得目前出貨狀態標籤 (Accessor) */ public function getDeliveryStatusLabelAttribute(): string { + if (! $this->hasDeliveryOutcome()) { + return '--'; + } + return self::getDeliveryStatusLabels()[$this->delivery_status] ?? __('Unknown'); } } diff --git a/lang/en.json b/lang/en.json index 4fa8aa9..ef124a7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -27,7 +27,6 @@ "APP Version": "APP Version", "APP_ID": "APP_ID", "APP_KEY": "APP_KEY", - "Abandoned": "Abandoned", "Abnormal": "Abnormal", "Account": "Account", "Account :name status has been changed to :status.": "Account :name status has been changed to :status.", @@ -2244,6 +2243,7 @@ "Unlock": "Unlock", "Unlock Now": "Unlock Now", "Unlock Page": "Unlock Page", + "Unpaid": "Unpaid", "Unpublish": "Unpublish", "Update": "Update", "Update App": "Update App", diff --git a/lang/ja.json b/lang/ja.json index 6d150b6..eb86d99 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -27,7 +27,6 @@ "APP Version": "アプリバージョン", "APP_ID": "APP_ID", "APP_KEY": "APP_KEY", - "Abandoned": "放棄済み", "Abnormal": "異常", "Account": "アカウント", "Account :name status has been changed to :status.": "アカウント :name のステータスが :status に変更されました。", @@ -2244,6 +2243,7 @@ "Unlock": "ロック解除", "Unlock Now": "今すぐロック解除", "Unlock Page": "ページロック解除", + "Unpaid": "未払い", "Unpublish": "非公開", "Update": "更新", "Update App": "アプリ更新", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 4c8880d..8f06c72 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -27,7 +27,6 @@ "APP Version": "APP版本號", "APP_ID": "APP_ID", "APP_KEY": "APP_KEY", - "Abandoned": "已棄單", "Abnormal": "異常", "Account": "帳號", "Account :name status has been changed to :status.": "帳號 :name 的狀態已變更為 :status。", @@ -2244,6 +2243,7 @@ "Unlock": "解鎖", "Unlock Now": "立即解鎖", "Unlock Page": "頁面解鎖", + "Unpaid": "未支付", "Unpublish": "下架", "Update": "編輯", "Update App": "更新 App", diff --git a/resources/views/admin/sales/partials/order-detail-panel.blade.php b/resources/views/admin/sales/partials/order-detail-panel.blade.php index 32d5832..cc457f6 100644 --- a/resources/views/admin/sales/partials/order-detail-panel.blade.php +++ b/resources/views/admin/sales/partials/order-detail-panel.blade.php @@ -44,7 +44,7 @@ 'paid' => __('Paid'), 'completed' => __('Completed'), 'failed' => __('Failed'), - 'abandoned' => __('Abandoned'), + 'abandoned' => __('Unpaid'), 'cancelled' => __('Cancelled'), 'refunded' => __('Refunded'), ]; @@ -63,6 +63,10 @@

{{ __('Dispense Status') }}

+ {{-- 非付款成功(未完成/支付失敗/未支付)的單從未出貨,出貨狀態無意義 → 顯示 '--' --}} + @if(! $order->hasDeliveryOutcome()) + -- + @else @php $deliveryStatusMap = [ 0 => ['label' => __('Dispense Failed'), 'color' => 'rose'], @@ -72,6 +76,7 @@ $ds = $deliveryStatusMap[$order->delivery_status] ?? ['label' => __('Unknown'), 'color' => 'slate']; @endphp + @endif
diff --git a/resources/views/admin/sales/partials/tab-orders.blade.php b/resources/views/admin/sales/partials/tab-orders.blade.php index 5c43443..135fd1e 100644 --- a/resources/views/admin/sales/partials/tab-orders.blade.php +++ b/resources/views/admin/sales/partials/tab-orders.blade.php @@ -268,7 +268,7 @@ 'paid' => ['label' => __('Paid'), 'color' => 'emerald'], 'completed' => ['label' => __('Completed'), 'color' => 'emerald'], 'failed' => ['label' => __('Failed'), 'color' => 'rose'], - 'abandoned' => ['label' => __('Abandoned'), 'color' => 'slate'], + 'abandoned' => ['label' => __('Unpaid'), 'color' => 'slate'], 'cancelled' => ['label' => __('Cancelled'), 'color' => 'slate'], 'refunded' => ['label' => __('Refunded'), 'color' => 'rose'], ]; @@ -277,6 +277,10 @@ + {{-- 非付款成功(未完成/支付失敗/未支付)的單從未出貨,出貨狀態無意義 → 顯示 '--' --}} + @if(! $order->hasDeliveryOutcome()) + -- + @else @php $deliveryStatusMap = [ 0 => ['label' => __('Dispense Failed'), 'color' => 'rose'], @@ -286,6 +290,7 @@ $ds = $deliveryStatusMap[$order->delivery_status] ?? ['label' => __('Unknown'), 'color' => 'slate']; @endphp + @endif @php @@ -351,7 +356,7 @@ 'paid' => ['label' => __('Paid'), 'color' => 'emerald'], 'completed' => ['label' => __('Completed'), 'color' => 'emerald'], 'failed' => ['label' => __('Failed'), 'color' => 'rose'], - 'abandoned' => ['label' => __('Abandoned'), 'color' => 'slate'], + 'abandoned' => ['label' => __('Unpaid'), 'color' => 'slate'], 'cancelled' => ['label' => __('Cancelled'), 'color' => 'slate'], 'refunded' => ['label' => __('Refunded'), 'color' => 'rose'], ]; @@ -451,6 +456,10 @@

{{ __('Dispense Status') }}

+ {{-- 非付款成功(未完成/支付失敗/未支付)的單從未出貨,出貨狀態無意義 → 顯示 '--' --}} + @if(! $order->hasDeliveryOutcome()) + -- + @else @php $deliveryStatusMap = [ 0 => ['label' => __('Dispense Failed'), 'color' => 'rose'], @@ -460,6 +469,7 @@ $ds = $deliveryStatusMap[$order->delivery_status] ?? ['label' => __('Unknown'), 'color' => 'slate']; @endphp + @endif