1. 整合交易流程:將 B600 (交易紀錄)、B601 (發票紀錄) 與 B602 (出貨紀錄) 整合為單一原子化的 MQTT 'finalize' 操作,確保資料一致性。 2. 實作非同步 Job:新增 ProcessTransactionFinalized Job 負責解析複合式 Payload 並寫入訂單、發票與出貨日誌。 3. 重構交易服務:更新 TransactionService 以支援同時處理訂單與發票資料,並修正產品 ID 驗證邏輯以避免外鍵衝突。 4. 更新技術文件:同步更新 api-docs.php 與 .agents/skills 中的 MQTT 規格,定義詳細的 finalize payload 結構與範例。 5. 優化文件 UI:改善 api-docs.blade.php 的描述渲染,支援換行符號以提升閱讀體驗。 6. 更新監聽指令:在 ListenMqttQueue 中加入 finalize 動作的處理分支並對接至新實作的 Job。
184 lines
7.3 KiB
PHP
184 lines
7.3 KiB
PHP
<?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;
|
|
|
|
class TransactionService
|
|
{
|
|
/**
|
|
* Process a new transaction (B600).
|
|
*/
|
|
public function processTransaction(array $data): Order
|
|
{
|
|
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' => $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,
|
|
'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,
|
|
'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'] ?? 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
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
$serialNo = $data['serial_no'];
|
|
|
|
// 1. Process Order (B600)
|
|
$orderData = $data['order'];
|
|
$orderData['serial_no'] = $serialNo;
|
|
$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;
|
|
$this->recordDispense($dispenseItem);
|
|
}
|
|
}
|
|
|
|
return $order;
|
|
});
|
|
}
|
|
}
|