[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:
sky121113 2026-05-04 10:18:30 +08:00
parent 8feca33f13
commit e779880f9b
8 changed files with 372 additions and 23 deletions

View File

@ -238,7 +238,6 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
**Response (Success 200):**
- `pickup_code_id`: 代碼內部 ID供第二階段 PUT 使用。
- `slot_no`: 建議出貨貨道。
- `product_name`: 商品名稱。
#### B. 消耗階段 (PUT)
當機台確認出貨成功後發送,雲端此時才正式執行 `decrement` 庫存與標記代碼已使用。
@ -313,4 +312,36 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| B600 | 交易建立 | `machine/{serial_no}/transaction` | `action: create` |
| B601 | 發票紀錄 | `machine/{serial_no}/transaction` | `action: invoice` |
| B602 | 出貨結果 | `machine/{serial_no}/transaction` | `action: dispense` |
| B600+ | 交易完成(統整) | `machine/{serial_no}/transaction` | `action: finalize` |
| 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 |

View File

@ -167,7 +167,6 @@ Mqtt3Client client = MqttClient.builder()
"items": [
{
"product_id": "5",
"product_name": "Coca Cola",
"price": 25.0,
"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]
> - `serial_no` 不需放在 Payload 中Go Gateway 自動從 Topic 路徑提取並注入。
> - 若 Payload 不含 `action` 欄位,系統預設視為 `create`(向後相容既有 B600 格式)。

View File

@ -71,6 +71,14 @@ class ListenMqttQueue extends Command
$serialNo = $data['serial_no'] ?? '';
$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)) {
Log::warning("MQTT Listen: Missing serialNo in message", [
'type' => $type,
@ -113,6 +121,11 @@ class ListenMqttQueue extends Command
*/
protected function dispatchTransactionByAction(string $serialNo, array $payload): void
{
Log::debug("MQTT Transaction incoming payload", [
'serial_no' => $serialNo,
'payload' => $payload
]);
$action = $payload['action'] ?? 'create';
switch ($action) {
@ -130,6 +143,12 @@ class ListenMqttQueue extends Command
Log::info("MQTT Transaction [dispense] dispatched", ['serial_no' => $serialNo]);
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':
default:
// B600: 交易建立(既有邏輯,向後相容無 action 欄位的舊格式)

View 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;
}
}
}

View File

@ -19,12 +19,20 @@ class Order extends Model
'order_no',
'machine_id',
'member_id',
'payment_type',
'total_amount',
'discount_amount',
'pay_amount',
'payment_type',
'change_amount',
'points_used',
'original_amount',
'payment_status',
'payment_request',
'payment_response',
'payment_at',
'member_barcode',
'invoice_info',
'machine_time',
'status',
'metadata',
];
@ -33,7 +41,10 @@ class Order extends Model
'total_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'pay_amount' => 'decimal:2',
'change_amount' => 'decimal:2',
'original_amount' => 'decimal:2',
'payment_at' => 'datetime',
'machine_time' => 'datetime',
'metadata' => 'array',
];

View File

@ -29,8 +29,16 @@ class TransactionService
'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,
@ -39,9 +47,17 @@ class TransactionService
// 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' => $item['product_name'] ?? 'Unknown',
'product_name' => $productName,
'barcode' => $item['barcode'] ?? null,
'price' => $item['price'],
'quantity' => $item['quantity'],
@ -75,6 +91,11 @@ class TransactionService
$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),
@ -84,9 +105,11 @@ class TransactionService
'amount' => $data['amount'] ?? 0,
'carrier_id' => $data['carrier_id'] ?? 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,
'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,
'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;
});
}
}

View File

@ -474,26 +474,122 @@ return [
],
],
[
'name' => '交易生命週期上報 (Transaction)',
'slug' => 'mqtt-transaction',
'name' => '交易階段性上報 (Transaction Steps)',
'slug' => 'mqtt-transaction-steps',
'action' => 'PUB',
'topic' => 'machine/{serial_no}/transaction',
'qos' => 1,
'description' => '整合交易生命週期的三個階段:交易建立 (create)、發票開立 (invoice) 以及出貨結果 (dispense)。',
'description' => '用於傳統的分段式上報:交易建立 (create)、發票紀錄 (invoice) 或單次出貨結果 (dispense)。',
'payload_parameters' => [
'action' => ['type' => 'string', 'description' => '生命週期動作 (create, invoice, dispense)'],
'payment_type' => ['type' => 'string', 'description' => '支付方式 (action 為 create 時必填)'],
'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)'],
'action' => ['type' => 'string', 'description' => '動作create, invoice, dispense'],
'flow_id' => ['type' => 'string', 'description' => '唯一流水號'],
],
'payload_example' => [
'action' => 'create',
'payment_type' => 'credit_card',
'amount' => 25.0,
'slot_no' => '3',
'product_id' => '5'
'flow_id' => 'FLOW123',
'total_amount' => 25.0,
'payment_type' => 1,
'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'
]
]
],
],
[

View File

@ -216,7 +216,7 @@
</span>
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $api['name'] }}</h3>
</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 -->
<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>
</td>
<td class="text-slate-600 leading-relaxed font-semibold">
{{ $param['description'] }}
{!! nl2br(e($param['description'])) !!}
@if(isset($param['example']))
<div class="mt-2 flex items-center gap-2">
<span class="text-[11px] font-black text-slate-400 uppercase tracking-tight">範例:</span>
@ -356,7 +356,7 @@
</span>
<h3 class="font-display text-3xl font-black text-slate-900 tracking-tight">{{ $topic['name'] }}</h3>
</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 -->
<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>
</td>
<td class="text-slate-600 leading-relaxed font-semibold">
{{ $param['description'] }}
{!! nl2br(e($param['description'])) !!}
</td>
</tr>
@endforeach