star-cloud/app/Services/Transaction/TransactionService.php
sky121113 fa6406b9ad [FEAT] 優化取貨碼管理介面與功能
1. 新增取貨碼使用次數上限功能(限制 1-20 次)。
2. 優化取貨碼管理介面,採用極簡奢華風計數器並調整寬度與間距。
3. 整合 Flatpickr 24小時制日期時間選擇器,並實作模式切換時的時間自動帶入邏輯。
4. 取貨碼清單新增「剩餘使用次數 / 總次數」顯示。
5. 完善相關中文化翻譯(如「小時」、「自定義日期」等)。
6. 新增資料庫欄位 migration 並更新相關模型與 Service 邏輯。
7. 更新 API 技術規格文檔以符合最新取貨碼邏輯。
2026-05-06 16:33:15 +08:00

296 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Transaction;
use App\Models\Transaction\Order;
use App\Models\Transaction\OrderItem;
use App\Models\Transaction\Invoice;
use App\Models\Transaction\DispenseRecord;
use Illuminate\Support\Facades\DB;
use App\Models\Machine\Machine;
use App\Models\StaffCardLog;
use App\Models\Transaction\PickupCode;
use App\Models\Transaction\PickupCodeLog;
use App\Models\Transaction\PassCodeLog;
use Illuminate\Support\Facades\Log;
class TransactionService
{
/**
* 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,
'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) {
$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();
}
return 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' => $data['slot_no'] ?? 'unknown',
'product_id' => $data['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,
]);
});
}
/**
* 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;
}
}
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;
}
}
return $order;
});
}
}