machineService = $machineService; } /** * Process a new transaction (B600). */ public function processTransaction(array $data): Order { $flowId = $data['flow_id'] ?? null; if ($flowId) { $existingOrder = Order::withoutGlobalScopes() ->where('flow_id', $flowId) ->first(); if ($existingOrder) { return $existingOrder; } } return DB::transaction(function () use ($data) { $machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail(); // Create Order $order = Order::create([ 'company_id' => $machine->company_id, 'flow_id' => $data['flow_id'] ?? null, 'order_no' => $data['order_no'] ?? $this->generateOrderNo(), 'machine_id' => $machine->id, 'member_id' => $data['member_id'] ?? null, 'total_amount' => $data['total_amount'], 'discount_amount' => $data['discount_amount'] ?? 0, 'pay_amount' => $data['pay_amount'], 'change_amount' => $data['change_amount'] ?? 0, 'points_used' => $data['points_used'] ?? 0, 'original_amount' => $data['original_amount'] ?? $data['total_amount'], 'payment_type' => (int) ($data['payment_type'] ?? 0), 'payment_status' => $data['payment_status'] ?? 1, 'payment_request' => $data['payment_request'] ?? null, 'payment_response' => $data['payment_response'] ?? null, 'member_barcode' => $data['member_barcode'] ?? null, 'code_id' => $data['code_id'] ?? null, 'welcome_gift_id' => $data['welcome_gift_id'] ?? null, 'invoice_info' => $data['invoice_info'] ?? null, 'machine_time' => $data['machine_time'] ?? now(), 'payment_at' => now(), 'status' => 'completed', 'metadata' => $data['metadata'] ?? null, ]); // Create Order Items if (!empty($data['items'])) { foreach ($data['items'] as $item) { $productName = $item['product_name'] ?? null; // 如果沒傳名稱,嘗試從資料庫抓取 if (empty($productName)) { $product = \App\Models\Product\Product::find($item['product_id']); $productName = $product?->name ?? 'Unknown'; } $order->items()->create([ 'product_id' => $item['product_id'], 'product_name' => $productName, 'barcode' => $item['barcode'] ?? null, 'price' => $item['price'], 'quantity' => $item['quantity'], 'subtotal' => $item['price'] * $item['quantity'], ]); } } return $order; }); } /** * Generate a unique order number. */ protected function generateOrderNo(): string { return 'ORD-' . now()->format('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3))); } /** * Record Invoice (B601). */ public function recordInvoice(array $data): Invoice { return DB::transaction(function () use ($data) { $machine = Machine::where('serial_no', $data['serial_no'])->firstOrFail(); $order = null; if (!empty($data['flow_id'])) { $order = Order::where('flow_id', $data['flow_id'])->first(); } // 處理額外的發票資訊 (business_tax_id, carrier_type) $metadata = $data['metadata'] ?? []; if (isset($data['business_tax_id'])) $metadata['business_tax_id'] = $data['business_tax_id']; if (isset($data['carrier_type'])) $metadata['carrier_type'] = $data['carrier_type']; return Invoice::create([ 'company_id' => $machine->company_id, 'order_id' => $order?->id ?? ($data['order_id'] ?? null), 'machine_id' => $machine->id, 'flow_id' => $data['flow_id'] ?? null, 'invoice_no' => $data['invoice_no'] ?? null, 'amount' => $data['amount'] ?? 0, 'carrier_id' => $data['carrier_id'] ?? null, 'invoice_date' => $data['invoice_date'] ?? null, 'machine_time' => $data['machine_time'] ?? null, 'random_number' => $data['random_number'] ?? ($data['random_no'] ?? null), 'love_code' => $data['love_code'] ?? null, 'rtn_code' => $data['rtn_code'] ?? null, 'rtn_msg' => $data['rtn_msg'] ?? null, 'metadata' => $metadata, ]); }); } /** * Record dispense result (B602). */ public function recordDispense(array $data): DispenseRecord { return DB::transaction(function () use ($data) { // Use withoutGlobalScopes for background job safety $machine = Machine::withoutGlobalScopes()->where('serial_no', $data['serial_no'])->firstOrFail(); $order = null; if (!empty($data['flow_id'])) { $order = Order::withoutGlobalScopes()->where('flow_id', $data['flow_id'])->first(); } // Find slot with robust matching (handle "1" vs "01") $slotNo = $data['slot_no'] ?? null; $slot = null; if ($slotNo) { $slot = $machine->slots()->where('slot_no', (string) $slotNo)->first(); if (!$slot) { $normalized = ltrim((string) $slotNo, '0'); if ($normalized === '') $normalized = '0'; $slot = $machine->slots()->get()->first(function ($s) use ($normalized) { $sn = ltrim($s->slot_no, '0'); if ($sn === '') $sn = '0'; return $sn === $normalized; }); } } $record = DispenseRecord::create([ 'company_id' => $machine->company_id, 'order_id' => $order?->id ?? ($data['order_id'] ?? null), 'flow_id' => $data['flow_id'] ?? null, 'machine_id' => $machine->id, 'slot_no' => $slotNo ?? 'unknown', 'product_id' => ($data['product_id'] ?? 0) > 0 ? $data['product_id'] : ($slot ? $slot->product_id : null), 'amount' => $data['amount'] ?? 0, 'remaining_stock' => $data['remaining_stock'] ?? null, 'dispense_status' => $data['dispense_status'] ?? 0, 'member_barcode' => $data['member_barcode'] ?? ($order?->member_barcode ?? null), 'machine_time' => $data['machine_time'] ?? now(), 'points_used' => $data['points_used'] ?? 0, ]); // 執行實體庫存扣除 (真實交易化) if ((int) ($data['dispense_status'] ?? 0) === 1) { if ($slot) { $slot->decrement('stock'); $type = \App\Models\Machine\MachineStockMovement::TYPE_SALE; $this->machineService->recordStockMovement( $slot, -1, $type, $order ?? $record, $order ? "Sale Order: #{$order->order_no}" : "Dispense Record: #{$record->id}", ['order_id' => $order?->id, 'dispense_id' => $record->id] ); } else { // Log warning if slot matching fails to help diagnostic \Log::warning("Dispense record created but slot not found for decrement", [ 'machine_id' => $machine->id, 'slot_no' => $slotNo ]); } } return $record; }); } /** * Finalize a complete transaction (Unified B600/B601/B602). */ public function finalizeTransaction(array $data): Order { // 優先從 Root 讀取,再從 order 內讀取 $flowId = $data['flow_id'] ?? ($data['order']['flow_id'] ?? null); // 冪等性檢查:如果 flow_id 已經存在,直接回傳既有訂單 if ($flowId) { $existingOrder = Order::withoutGlobalScopes()->where('flow_id', $flowId)->first(); if ($existingOrder) { Log::info("Transaction already finalized, returning existing order", ['flow_id' => $flowId]); 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']; $orderData['serial_no'] = $serialNo; // 確保提取 payment_type 與 flow_id (可能在 root 或 order 內) $orderData['flow_id'] = $flowId; if (!isset($orderData['payment_type']) && isset($data['payment_type'])) { $orderData['payment_type'] = $data['payment_type']; } $order = $this->processTransaction($orderData); // 2. Record Invoice (B601) - Optional if (!empty($data['invoice'])) { $invoiceData = $data['invoice']; $invoiceData['serial_no'] = $serialNo; $invoiceData['flow_id'] = $order->flow_id; $invoiceData['order_id'] = $order->id; $this->recordInvoice($invoiceData); } // 3. Record Dispense Results (B602) - Optional/Multiple if (!empty($data['dispense'])) { $dispenseList = isset($data['dispense'][0]) ? $data['dispense'] : [$data['dispense']]; foreach ($dispenseList as $dispenseItem) { $dispenseItem['serial_no'] = $serialNo; $dispenseItem['flow_id'] = $order->flow_id; $dispenseItem['order_id'] = $order->id; // 自動繼承:如果出貨紀錄沒帶 Barcode,就用訂單的 if (empty($dispenseItem['member_barcode']) && !empty($order->member_barcode)) { $dispenseItem['member_barcode'] = $order->member_barcode; } $this->recordDispense($dispenseItem); } } // 4. 閉環勾稽 (Closed Loop Check) $codeId = $data['order']['code_id'] ?? ($data['code_id'] ?? null); $paymentType = (int) $order->payment_type; if ($codeId) { switch ($paymentType) { case 41: // 員工卡 (Staff Card) // 建立員工卡核銷日誌 StaffCardLog::create([ 'company_id' => $order->company_id, 'machine_id' => $order->machine_id, 'staff_card_id' => $codeId, 'payment_type' => 41, 'code_id' => $codeId, 'order_id' => $order->id, 'action' => 'consume', 'remark' => "MQTT 交易完成自動核銷,訂單: #{$order->id}" ]); break; case 6: // 取貨碼 (Pickup Code) $pickupCode = PickupCode::where('machine_id', $order->machine_id) ->where('id', $codeId) ->where('status', 'active') ->first(); if ($pickupCode) { $newCount = $pickupCode->usage_count + 1; $isUsedUp = $newCount >= $pickupCode->usage_limit; $pickupCode->update([ 'order_id' => $order->id, 'usage_count' => $newCount, 'status' => $isUsedUp ? 'used' : 'active', 'used_at' => $isUsedUp ? now() : $pickupCode->used_at ]); // 建立核銷日誌 (閉環最後一哩路) PickupCodeLog::create([ 'company_id' => $order->company_id, 'machine_id' => $order->machine_id, 'pickup_code_id' => $pickupCode->id, 'order_id' => $order->id, 'action' => $isUsedUp ? 'used' : 'consume', 'remark' => "MQTT 交易完成自動核銷 (" . ($newCount) . "/" . $pickupCode->usage_limit . "),訂單: #{$order->id}", 'raw_data' => [ 'slot' => $pickupCode->slot_no, 'code' => $pickupCode->code, 'count' => $newCount, 'limit' => $pickupCode->usage_limit ] ]); } break; case 5: // 通行碼 (Pass Code) // 建立通行碼核銷日誌 PassCodeLog::create([ 'company_id' => $order->company_id, 'machine_id' => $order->machine_id, 'pass_code_id' => $codeId, 'order_id' => $order->id, 'action' => 'completed', 'remark' => "MQTT 交易完成自動紀錄取貨,訂單: #{$order->id}" ]); break; } } // 處理來店禮 (Welcome Gift) 自動核銷 $welcomeGiftId = $data['order']['welcome_gift_id'] ?? ($data['welcome_gift_id'] ?? null); if ($welcomeGiftId) { $welcomeGift = WelcomeGift::where('machine_id', $order->machine_id) ->where('id', $welcomeGiftId) ->where('status', 'active') ->first(); if ($welcomeGift) { $newCount = $welcomeGift->usage_count + 1; $isUsedUp = ($welcomeGift->usage_type === 'once' && $newCount >= ($welcomeGift->usage_limit ?? 1)); $welcomeGift->update([ 'usage_count' => $newCount, 'status' => $isUsedUp ? 'used' : 'active', ]); WelcomeGiftLog::create([ 'company_id' => $order->company_id, 'machine_id' => $order->machine_id, 'welcome_gift_id' => $welcomeGift->id, 'order_id' => $order->id, 'action' => 'consume', 'remark' => "MQTT 交易完成自動核銷,訂單: #{$order->id}", 'raw_data' => [ 'code' => $welcomeGift->code, 'count' => $newCount, 'type' => $welcomeGift->usage_type ] ]); } } return $order; }); } }