[FEAT] 整合 MQTT 交易完成 (Finalize) 流程並更新 API 技術規格文件
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。
This commit is contained in:
parent
8feca33f13
commit
e779880f9b
@ -238,7 +238,6 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
**Response (Success 200):**
|
**Response (Success 200):**
|
||||||
- `pickup_code_id`: 代碼內部 ID,供第二階段 PUT 使用。
|
- `pickup_code_id`: 代碼內部 ID,供第二階段 PUT 使用。
|
||||||
- `slot_no`: 建議出貨貨道。
|
- `slot_no`: 建議出貨貨道。
|
||||||
- `product_name`: 商品名稱。
|
|
||||||
|
|
||||||
#### B. 消耗階段 (PUT)
|
#### B. 消耗階段 (PUT)
|
||||||
當機台確認出貨成功後發送,雲端此時才正式執行 `decrement` 庫存與標記代碼已使用。
|
當機台確認出貨成功後發送,雲端此時才正式執行 `decrement` 庫存與標記代碼已使用。
|
||||||
@ -313,4 +312,36 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
|
|||||||
| B600 | 交易建立 | `machine/{serial_no}/transaction` | `action: create` |
|
| B600 | 交易建立 | `machine/{serial_no}/transaction` | `action: create` |
|
||||||
| B601 | 發票紀錄 | `machine/{serial_no}/transaction` | `action: invoice` |
|
| B601 | 發票紀錄 | `machine/{serial_no}/transaction` | `action: invoice` |
|
||||||
| B602 | 出貨結果 | `machine/{serial_no}/transaction` | `action: dispense` |
|
| B602 | 出貨結果 | `machine/{serial_no}/transaction` | `action: dispense` |
|
||||||
|
| B600+ | 交易完成(統整) | `machine/{serial_no}/transaction` | `action: finalize` |
|
||||||
| B055 (PUT) | 遠端出貨回報 | `machine/{serial_no}/command/ack` | GET 查詢路由保留 |
|
| B055 (PUT) | 遠端出貨回報 | `machine/{serial_no}/command/ack` | GET 查詢路由保留 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄 (Appendix)
|
||||||
|
|
||||||
|
### Appendix A: B600 支付類型對照表 (`payment_type`)
|
||||||
|
|
||||||
|
此對照表依據機台 B600 規格定義,用於解析 `order.payment_type`:
|
||||||
|
|
||||||
|
| 代碼 | 支付方式 | 備註 / 說明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `1` | 信用卡 | 一般實體信用卡刷卡 |
|
||||||
|
| `2` | 電子票證 | 包含悠遊卡、一卡通等 |
|
||||||
|
| `3` | 掃碼支付 | 一般掃碼支付 |
|
||||||
|
| `4` | 投幣機支付 | 實體紙鈔機 |
|
||||||
|
| `5` | 通關密碼 | |
|
||||||
|
| `6` | 取貨碼 | |
|
||||||
|
| `7` | 來店禮 | |
|
||||||
|
| `8` | 問卷 | |
|
||||||
|
| `9` | 零錢 | 硬幣投幣機 |
|
||||||
|
| `21` ~ `25` | 線下付款 + X | `21`=線下+1, `22`=線下+2... `25`=線下+9 |
|
||||||
|
| `30` | TapPay - LINE Pay | |
|
||||||
|
| `31` | TapPay - 街口支付 | |
|
||||||
|
| `32` | TapPay - 悠遊付 | |
|
||||||
|
| `33` | TapPay - Pi 拍錢包 | |
|
||||||
|
| `34` | TapPay - 全盈+PAY | |
|
||||||
|
| `40` | 會員驗證取貨商品 | |
|
||||||
|
| `50` ~ `54` | 線下付款 + TapPay | `50`=線下+30, `51`=線下+31... `54`=線下+34 |
|
||||||
|
| `60` | 點數 / 優惠卷全額折抵 | 點數全額折抵出貨 或 優惠卷全額折抵出貨 |
|
||||||
|
| `61` ~ `69` | 會員 + X | `61`=會員+1, `62`=會員+2... `69`=會員+9 |
|
||||||
|
| `90` ~ `94` | 會員 + TapPay | `90`=會員+30, `91`=會員+31... `94`=會員+34 |
|
||||||
|
|||||||
@ -167,7 +167,6 @@ Mqtt3Client client = MqttClient.builder()
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"product_id": "5",
|
"product_id": "5",
|
||||||
"product_name": "Coca Cola",
|
|
||||||
"price": 25.0,
|
"price": 25.0,
|
||||||
"quantity": 1
|
"quantity": 1
|
||||||
}
|
}
|
||||||
@ -202,6 +201,63 @@ Mqtt3Client client = MqttClient.builder()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### B600+ 交易完成統整 (`action: finalize`)
|
||||||
|
這是目前推薦的標準流程,將交易、發票與出貨結果一次性上報,確保資料完整性。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "finalize",
|
||||||
|
"order": {
|
||||||
|
"order_no": "T202508080001",
|
||||||
|
"flow_id": "CLIENT_UID_123",
|
||||||
|
"total_amount": 100,
|
||||||
|
"pay_amount": 100,
|
||||||
|
"original_amount": 100,
|
||||||
|
"discount_amount": 0,
|
||||||
|
"change_amount": 0,
|
||||||
|
"payment_type": 1,
|
||||||
|
"payment_status": 1,
|
||||||
|
"payment_request": "{}",
|
||||||
|
"payment_response": "{}",
|
||||||
|
"member_barcode": "MBR999",
|
||||||
|
"invoice_info": "12345678-0912345678",
|
||||||
|
"machine_time": "2025-08-08 10:00:00",
|
||||||
|
"points_used": 0,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"product_id": 1,
|
||||||
|
"price": 100,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"invoice": {
|
||||||
|
"invoice_no": "AB12345678",
|
||||||
|
"invoice_date": "2025-08-08",
|
||||||
|
"amount": 100,
|
||||||
|
"random_number": "1234",
|
||||||
|
"love_code": "",
|
||||||
|
"carrier_id": "/ABC1234",
|
||||||
|
"carrier_type": "3J0002",
|
||||||
|
"business_tax_id": "12345678",
|
||||||
|
"rtn_code": "1",
|
||||||
|
"rtn_msg": "成功"
|
||||||
|
},
|
||||||
|
"dispense": [
|
||||||
|
{
|
||||||
|
"slot_no": "1",
|
||||||
|
"product_id": 1,
|
||||||
|
"amount": 100,
|
||||||
|
"dispense_status": 1,
|
||||||
|
"remaining_stock": 9,
|
||||||
|
"member_barcode": "MBR999",
|
||||||
|
"machine_time": "2025-08-08 10:01:00",
|
||||||
|
"points_used": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> - `serial_no` 不需放在 Payload 中,Go Gateway 自動從 Topic 路徑提取並注入。
|
> - `serial_no` 不需放在 Payload 中,Go Gateway 自動從 Topic 路徑提取並注入。
|
||||||
> - 若 Payload 不含 `action` 欄位,系統預設視為 `create`(向後相容既有 B600 格式)。
|
> - 若 Payload 不含 `action` 欄位,系統預設視為 `create`(向後相容既有 B600 格式)。
|
||||||
|
|||||||
@ -71,6 +71,14 @@ class ListenMqttQueue extends Command
|
|||||||
$serialNo = $data['serial_no'] ?? '';
|
$serialNo = $data['serial_no'] ?? '';
|
||||||
$payload = $data['payload'] ?? [];
|
$payload = $data['payload'] ?? [];
|
||||||
|
|
||||||
|
// 如果 payload 是 JSON 字串,則解析它
|
||||||
|
if (is_string($payload)) {
|
||||||
|
$decoded = json_decode($payload, true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$payload = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($serialNo)) {
|
if (empty($serialNo)) {
|
||||||
Log::warning("MQTT Listen: Missing serialNo in message", [
|
Log::warning("MQTT Listen: Missing serialNo in message", [
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
@ -113,6 +121,11 @@ class ListenMqttQueue extends Command
|
|||||||
*/
|
*/
|
||||||
protected function dispatchTransactionByAction(string $serialNo, array $payload): void
|
protected function dispatchTransactionByAction(string $serialNo, array $payload): void
|
||||||
{
|
{
|
||||||
|
Log::debug("MQTT Transaction incoming payload", [
|
||||||
|
'serial_no' => $serialNo,
|
||||||
|
'payload' => $payload
|
||||||
|
]);
|
||||||
|
|
||||||
$action = $payload['action'] ?? 'create';
|
$action = $payload['action'] ?? 'create';
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
@ -130,6 +143,12 @@ class ListenMqttQueue extends Command
|
|||||||
Log::info("MQTT Transaction [dispense] dispatched", ['serial_no' => $serialNo]);
|
Log::info("MQTT Transaction [dispense] dispatched", ['serial_no' => $serialNo]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'finalize':
|
||||||
|
// 新增:B600/B601/B602 統整上報
|
||||||
|
\App\Jobs\Transaction\ProcessTransactionFinalized::dispatch($serialNo, $payload);
|
||||||
|
Log::info("MQTT Transaction [finalize] dispatched", ['serial_no' => $serialNo]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'create':
|
case 'create':
|
||||||
default:
|
default:
|
||||||
// B600: 交易建立(既有邏輯,向後相容無 action 欄位的舊格式)
|
// B600: 交易建立(既有邏輯,向後相容無 action 欄位的舊格式)
|
||||||
|
|||||||
74
app/Jobs/Transaction/ProcessTransactionFinalized.php
Normal file
74
app/Jobs/Transaction/ProcessTransactionFinalized.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Transaction;
|
||||||
|
|
||||||
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Services\Transaction\TransactionService;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use App\Jobs\Machine\ProcessStateLog;
|
||||||
|
|
||||||
|
class ProcessTransactionFinalized implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $serialNo;
|
||||||
|
protected $payload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct(string $serialNo, array $payload)
|
||||||
|
{
|
||||||
|
$this->serialNo = $serialNo;
|
||||||
|
$this->payload = $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(TransactionService $transactionService): void
|
||||||
|
{
|
||||||
|
$machine = Machine::withoutGlobalScopes()->where('serial_no', $this->serialNo)->first();
|
||||||
|
|
||||||
|
if (!$machine) {
|
||||||
|
Log::warning("MQTT Transaction Finalized: Machine not found", ['serial_no' => $this->serialNo]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $this->payload;
|
||||||
|
$data['serial_no'] = $this->serialNo;
|
||||||
|
|
||||||
|
$order = $transactionService->finalizeTransaction($data);
|
||||||
|
|
||||||
|
ProcessStateLog::dispatch(
|
||||||
|
$machine->id,
|
||||||
|
$machine->company_id,
|
||||||
|
"Transaction finalized: :id",
|
||||||
|
'info',
|
||||||
|
[
|
||||||
|
'id' => $order->order_no,
|
||||||
|
'flow_id' => $order->flow_id
|
||||||
|
],
|
||||||
|
'transaction'
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::info("MQTT Transaction [finalize] processed successfully", [
|
||||||
|
'serial_no' => $this->serialNo,
|
||||||
|
'order_no' => $order->order_no
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Failed to process MQTT transaction finalized for machine {$this->serialNo}: " . $e->getMessage(), [
|
||||||
|
'payload' => $this->payload,
|
||||||
|
'exception' => $e
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,12 +19,20 @@ class Order extends Model
|
|||||||
'order_no',
|
'order_no',
|
||||||
'machine_id',
|
'machine_id',
|
||||||
'member_id',
|
'member_id',
|
||||||
|
'payment_type',
|
||||||
'total_amount',
|
'total_amount',
|
||||||
'discount_amount',
|
'discount_amount',
|
||||||
'pay_amount',
|
'pay_amount',
|
||||||
'payment_type',
|
'change_amount',
|
||||||
|
'points_used',
|
||||||
|
'original_amount',
|
||||||
'payment_status',
|
'payment_status',
|
||||||
|
'payment_request',
|
||||||
|
'payment_response',
|
||||||
'payment_at',
|
'payment_at',
|
||||||
|
'member_barcode',
|
||||||
|
'invoice_info',
|
||||||
|
'machine_time',
|
||||||
'status',
|
'status',
|
||||||
'metadata',
|
'metadata',
|
||||||
];
|
];
|
||||||
@ -33,7 +41,10 @@ class Order extends Model
|
|||||||
'total_amount' => 'decimal:2',
|
'total_amount' => 'decimal:2',
|
||||||
'discount_amount' => 'decimal:2',
|
'discount_amount' => 'decimal:2',
|
||||||
'pay_amount' => 'decimal:2',
|
'pay_amount' => 'decimal:2',
|
||||||
|
'change_amount' => 'decimal:2',
|
||||||
|
'original_amount' => 'decimal:2',
|
||||||
'payment_at' => 'datetime',
|
'payment_at' => 'datetime',
|
||||||
|
'machine_time' => 'datetime',
|
||||||
'metadata' => 'array',
|
'metadata' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,16 @@ class TransactionService
|
|||||||
'total_amount' => $data['total_amount'],
|
'total_amount' => $data['total_amount'],
|
||||||
'discount_amount' => $data['discount_amount'] ?? 0,
|
'discount_amount' => $data['discount_amount'] ?? 0,
|
||||||
'pay_amount' => $data['pay_amount'],
|
'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_type' => $data['payment_type'] ?? 0,
|
||||||
'payment_status' => $data['payment_status'] ?? 1,
|
'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(),
|
'payment_at' => now(),
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
'metadata' => $data['metadata'] ?? null,
|
'metadata' => $data['metadata'] ?? null,
|
||||||
@ -39,9 +47,17 @@ class TransactionService
|
|||||||
// Create Order Items
|
// Create Order Items
|
||||||
if (!empty($data['items'])) {
|
if (!empty($data['items'])) {
|
||||||
foreach ($data['items'] as $item) {
|
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([
|
$order->items()->create([
|
||||||
'product_id' => $item['product_id'],
|
'product_id' => $item['product_id'],
|
||||||
'product_name' => $item['product_name'] ?? 'Unknown',
|
'product_name' => $productName,
|
||||||
'barcode' => $item['barcode'] ?? null,
|
'barcode' => $item['barcode'] ?? null,
|
||||||
'price' => $item['price'],
|
'price' => $item['price'],
|
||||||
'quantity' => $item['quantity'],
|
'quantity' => $item['quantity'],
|
||||||
@ -75,6 +91,11 @@ class TransactionService
|
|||||||
$order = Order::where('flow_id', $data['flow_id'])->first();
|
$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([
|
return Invoice::create([
|
||||||
'company_id' => $machine->company_id,
|
'company_id' => $machine->company_id,
|
||||||
'order_id' => $order?->id ?? ($data['order_id'] ?? null),
|
'order_id' => $order?->id ?? ($data['order_id'] ?? null),
|
||||||
@ -84,9 +105,11 @@ class TransactionService
|
|||||||
'amount' => $data['amount'] ?? 0,
|
'amount' => $data['amount'] ?? 0,
|
||||||
'carrier_id' => $data['carrier_id'] ?? null,
|
'carrier_id' => $data['carrier_id'] ?? null,
|
||||||
'invoice_date' => $data['invoice_date'] ?? null,
|
'invoice_date' => $data['invoice_date'] ?? null,
|
||||||
'random_number' => $data['random_no'] ?? null,
|
'random_number' => $data['random_number'] ?? ($data['random_no'] ?? null),
|
||||||
'love_code' => $data['love_code'] ?? null,
|
'love_code' => $data['love_code'] ?? null,
|
||||||
'metadata' => $data['metadata'] ?? null,
|
'rtn_code' => $data['rtn_code'] ?? null,
|
||||||
|
'rtn_msg' => $data['rtn_msg'] ?? null,
|
||||||
|
'metadata' => $metadata,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -114,8 +137,47 @@ class TransactionService
|
|||||||
'amount' => $data['amount'] ?? 0,
|
'amount' => $data['amount'] ?? 0,
|
||||||
'remaining_stock' => $data['remaining_stock'] ?? null,
|
'remaining_stock' => $data['remaining_stock'] ?? null,
|
||||||
'dispense_status' => $data['dispense_status'] ?? 0,
|
'dispense_status' => $data['dispense_status'] ?? 0,
|
||||||
|
'member_barcode' => $data['member_barcode'] ?? null,
|
||||||
'machine_time' => $data['machine_time'] ?? now(),
|
'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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -474,26 +474,122 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => '交易生命週期上報 (Transaction)',
|
'name' => '交易階段性上報 (Transaction Steps)',
|
||||||
'slug' => 'mqtt-transaction',
|
'slug' => 'mqtt-transaction-steps',
|
||||||
'action' => 'PUB',
|
'action' => 'PUB',
|
||||||
'topic' => 'machine/{serial_no}/transaction',
|
'topic' => 'machine/{serial_no}/transaction',
|
||||||
'qos' => 1,
|
'qos' => 1,
|
||||||
'description' => '整合交易生命週期的三個階段:交易建立 (create)、發票開立 (invoice) 以及出貨結果 (dispense)。',
|
'description' => '用於傳統的分段式上報:交易建立 (create)、發票紀錄 (invoice) 或單次出貨結果 (dispense)。',
|
||||||
'payload_parameters' => [
|
'payload_parameters' => [
|
||||||
'action' => ['type' => 'string', 'description' => '生命週期動作 (create, invoice, dispense)'],
|
'action' => ['type' => 'string', 'description' => '動作:create, invoice, dispense'],
|
||||||
'payment_type' => ['type' => 'string', 'description' => '支付方式 (action 為 create 時必填)'],
|
'flow_id' => ['type' => 'string', 'description' => '唯一流水號'],
|
||||||
'amount' => ['type' => 'number', 'description' => '交易金額 (create/invoice 時使用)'],
|
|
||||||
'slot_no' => ['type' => 'string', 'description' => '出貨貨道號碼'],
|
|
||||||
'invoice_no' => ['type' => 'string', 'description' => '發票號碼 (action 為 invoice 時必填)'],
|
|
||||||
'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,如 success, failed)'],
|
|
||||||
],
|
],
|
||||||
'payload_example' => [
|
'payload_example' => [
|
||||||
'action' => 'create',
|
'action' => 'create',
|
||||||
'payment_type' => 'credit_card',
|
'flow_id' => 'FLOW123',
|
||||||
'amount' => 25.0,
|
'total_amount' => 25.0,
|
||||||
'slot_no' => '3',
|
'payment_type' => 1,
|
||||||
'product_id' => '5'
|
'items' => [['product_id' => 5, 'price' => 25.0, 'quantity' => 1]]
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => '交易完成統整上報 (Transaction Finalized - 推薦)',
|
||||||
|
'slug' => 'mqtt-transaction-finalize',
|
||||||
|
'action' => 'PUB',
|
||||||
|
'topic' => 'machine/{serial_no}/transaction',
|
||||||
|
'qos' => 1,
|
||||||
|
'description' => '【推薦使用】將交易、發票與出貨結果統整為單一動作上報,確保資料庫寫入的原子性與一致性。',
|
||||||
|
'payload_parameters' => [
|
||||||
|
'action' => ['type' => 'string', 'description' => '(必填) 固定為 "finalize"'],
|
||||||
|
'order' => ['type' => 'object', 'description' => '(必填) 訂單主檔資訊'],
|
||||||
|
'order.order_no' => ['type' => 'string', 'description' => '(必填) 訂單編號'],
|
||||||
|
'order.flow_id' => ['type' => 'string', 'description' => '(必填) 機台端交易流水號'],
|
||||||
|
'order.total_amount' => ['type' => 'numeric', 'description' => '(必填) 應付總金額'],
|
||||||
|
'order.pay_amount' => ['type' => 'numeric', 'description' => '(必填) 實際支付金額'],
|
||||||
|
'order.change_amount' => ['type' => 'numeric', 'description' => '(選填) 找零金額,預設 0', 'required' => false],
|
||||||
|
'order.original_amount' => ['type' => 'numeric', 'description' => '(選填) 原始標價金額', 'required' => false],
|
||||||
|
'order.discount_amount' => ['type' => 'numeric', 'description' => '(選填) 折扣金額,預設 0', 'required' => false],
|
||||||
|
'order.points_used' => ['type' => 'integer', 'description' => '(選填) 使用點數,預設 0', 'required' => false],
|
||||||
|
'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:悠遊卡/一卡通, 3:掃碼支付, 4:紙鈔機, 5:通關密碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢。\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:TapPay-Line, 31:TapPay-街口, 32:TapPay-悠遊付, 33:TapPay-Pi, 34:TapPay-全盈+。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠卷全額折抵。61~69:會員+X (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。"],
|
||||||
|
'order.payment_status' => ['type' => 'integer', 'description' => '(必填) 支付狀態 (1:成功)'],
|
||||||
|
'order.payment_request' => ['type' => 'string', 'description' => '(選填) 金流請求原始字串', 'required' => false],
|
||||||
|
'order.payment_response' => ['type' => 'string', 'description' => '(選填) 金流回應原始字串', 'required' => false],
|
||||||
|
'order.member_barcode' => ['type' => 'string', 'description' => '(選填) 會員條碼', 'required' => false],
|
||||||
|
'order.invoice_info' => ['type' => 'string', 'description' => '(選填) 預設發票歸戶字串 (如 統編-電話)', 'required' => false],
|
||||||
|
'order.machine_time' => ['type' => 'datetime', 'description' => '(必填) 機台交易時間 (Y-m-d H:i:s)'],
|
||||||
|
'order.items' => ['type' => 'array', 'description' => '(必填) 訂單明細陣列'],
|
||||||
|
'order.items[].product_id' => ['type' => 'integer', 'description' => '(必填) 商品 ID'],
|
||||||
|
'order.items[].price' => ['type' => 'numeric', 'description' => '(必填) 商品單價'],
|
||||||
|
'order.items[].quantity' => ['type' => 'integer', 'description' => '(必填) 數量'],
|
||||||
|
|
||||||
|
'invoice' => ['type' => 'object', 'description' => '(選填) 發票開立資訊 (若無發票可省略)', 'required' => false],
|
||||||
|
'invoice.invoice_no' => ['type' => 'string', 'description' => '(必填,若有invoice) 發票號碼', 'required' => false],
|
||||||
|
'invoice.invoice_date' => ['type' => 'string', 'description' => '(必填,若有invoice) 發票日期 (Y-m-d)', 'required' => false],
|
||||||
|
'invoice.amount' => ['type' => 'numeric', 'description' => '(必填,若有invoice) 發票金額', 'required' => false],
|
||||||
|
'invoice.random_number' => ['type' => 'string', 'description' => '(必填,若有invoice) 發票隨機碼 (4位)', 'required' => false],
|
||||||
|
'invoice.love_code' => ['type' => 'string', 'description' => '(選填) 捐贈碼', 'required' => false],
|
||||||
|
'invoice.carrier_id' => ['type' => 'string', 'description' => '(選填) 載具編號', 'required' => false],
|
||||||
|
'invoice.carrier_type' => ['type' => 'string', 'description' => '(選填) 載具類型 (如 3J0002)', 'required' => false],
|
||||||
|
'invoice.business_tax_id' => ['type' => 'string', 'description' => '(選填) 買受人統編', 'required' => false],
|
||||||
|
'invoice.rtn_code' => ['type' => 'string', 'description' => '(選填) 電子發票回傳代碼', 'required' => false],
|
||||||
|
'invoice.rtn_msg' => ['type' => 'string', 'description' => '(選填) 電子發票回傳訊息', 'required' => false],
|
||||||
|
|
||||||
|
'dispense' => ['type' => 'array', 'description' => '(必填) 出貨結果清單'],
|
||||||
|
'dispense[].slot_no' => ['type' => 'string', 'description' => '(必填) 貨道編號'],
|
||||||
|
'dispense[].product_id' => ['type' => 'integer', 'description' => '(必填) 商品 ID'],
|
||||||
|
'dispense[].amount' => ['type' => 'numeric', 'description' => '(必填) 實際扣款額 (單項)'],
|
||||||
|
'dispense[].points_used' => ['type' => 'integer', 'description' => '(選填) 該商品花費點數,預設 0', 'required' => false],
|
||||||
|
'dispense[].dispense_status' => ['type' => 'integer', 'description' => '(必填) 出貨狀態 (1:成功, 0:失敗)'],
|
||||||
|
'dispense[].remaining_stock' => ['type' => 'integer', 'description' => '(必填) 出貨後剩餘庫存'],
|
||||||
|
'dispense[].member_barcode' => ['type' => 'string', 'description' => '(選填) 會員條碼', 'required' => false],
|
||||||
|
'dispense[].machine_time' => ['type' => 'datetime', 'description' => '(選填) 機台出貨時間 (Y-m-d H:i:s)', 'required' => false],
|
||||||
|
],
|
||||||
|
'payload_example' => [
|
||||||
|
'action' => 'finalize',
|
||||||
|
'order' => [
|
||||||
|
'order_no' => 'T202508080001',
|
||||||
|
'flow_id' => 'FLOW123',
|
||||||
|
'total_amount' => 100.0,
|
||||||
|
'original_amount' => 100.0,
|
||||||
|
'discount_amount' => 0.0,
|
||||||
|
'pay_amount' => 100.0,
|
||||||
|
'change_amount' => 0.0,
|
||||||
|
'points_used' => 0,
|
||||||
|
'payment_type' => 1,
|
||||||
|
'payment_status' => 1,
|
||||||
|
'payment_request' => '{"req": "data"}',
|
||||||
|
'payment_response' => '{"res": "ok"}',
|
||||||
|
'member_barcode' => 'MBR999',
|
||||||
|
'invoice_info' => '12345678-0912345678',
|
||||||
|
'machine_time' => '2025-08-08 10:00:00',
|
||||||
|
'items' => [
|
||||||
|
['product_id' => 5, 'price' => 100.0, 'quantity' => 1]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'invoice' => [
|
||||||
|
'invoice_no' => 'AB12345678',
|
||||||
|
'invoice_date' => '2025-08-08',
|
||||||
|
'amount' => 100.0,
|
||||||
|
'random_number' => '1234',
|
||||||
|
'love_code' => '',
|
||||||
|
'carrier_id' => '/ABC1234',
|
||||||
|
'carrier_type' => '3J0002',
|
||||||
|
'business_tax_id' => '12345678',
|
||||||
|
'rtn_code' => '1',
|
||||||
|
'rtn_msg' => '成功'
|
||||||
|
],
|
||||||
|
'dispense' => [
|
||||||
|
[
|
||||||
|
'slot_no' => '3',
|
||||||
|
'product_id' => 5,
|
||||||
|
'amount' => 100.0,
|
||||||
|
'points_used' => 0,
|
||||||
|
'dispense_status' => 1,
|
||||||
|
'remaining_stock' => 9,
|
||||||
|
'member_barcode' => 'MBR999',
|
||||||
|
'machine_time' => '2025-08-08 10:01:00'
|
||||||
|
]
|
||||||
|
]
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@ -216,7 +216,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $api['name'] }}</h3>
|
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $api['name'] }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-slate-600 mb-8 text-lg font-medium">{{ $api['description'] }}</p>
|
<p class="text-slate-600 mb-8 text-lg font-medium">{!! nl2br(e($api['description'])) !!}</p>
|
||||||
|
|
||||||
<!-- Headers & URL -->
|
<!-- Headers & URL -->
|
||||||
<div class="mb-6 overflow-hidden rounded-2xl border border-slate-200">
|
<div class="mb-6 overflow-hidden rounded-2xl border border-slate-200">
|
||||||
@ -270,7 +270,7 @@
|
|||||||
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
|
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-slate-600 leading-relaxed font-semibold">
|
<td class="text-slate-600 leading-relaxed font-semibold">
|
||||||
{{ $param['description'] }}
|
{!! nl2br(e($param['description'])) !!}
|
||||||
@if(isset($param['example']))
|
@if(isset($param['example']))
|
||||||
<div class="mt-2 flex items-center gap-2">
|
<div class="mt-2 flex items-center gap-2">
|
||||||
<span class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
|
<span class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
|
||||||
@ -356,7 +356,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $topic['name'] }}</h3>
|
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $topic['name'] }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-slate-600 mb-8 text-lg font-medium">{{ $topic['description'] }}</p>
|
<p class="text-slate-600 mb-8 text-lg font-medium">{!! nl2br(e($topic['description'])) !!}</p>
|
||||||
|
|
||||||
<!-- Topic Block -->
|
<!-- Topic Block -->
|
||||||
<div class="mb-8 overflow-hidden rounded-2xl border border-slate-200">
|
<div class="mb-8 overflow-hidden rounded-2xl border border-slate-200">
|
||||||
@ -398,7 +398,7 @@
|
|||||||
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
|
<span class="px-2 py-1 rounded bg-slate-100 text-xs font-black text-slate-600 uppercase tracking-wider">{{ $param['type'] }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-slate-600 leading-relaxed font-semibold">
|
<td class="text-slate-600 leading-relaxed font-semibold">
|
||||||
{{ $param['description'] }}
|
{!! nl2br(e($param['description'])) !!}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user