[FEAT] 機台刷卡機 (Nexsys 終端機) 健康狀態燈號與失敗日誌

1. 刷卡機共用判定:信用卡(1)、電子票證(2)、手機支付(10) 走同一台實體刷卡機,以交易結果維護燈號(刷卡機無斷線心跳)。
2. ProcessTransactionFinalized 新增 syncCardTerminalStatus:成功交易自動消除該機台未解決的 card_terminal 警告;失敗交易解析 payment_response 的 "| code=XXXX" 回應碼,寫一筆 type='card_terminal' 的 warning 日誌(warning 級不觸發 Discord)。
3. MachineService 新增 CARD_TERMINAL_CODE_MAP(NCCC/聯卡中心回應碼對照,值為英文翻譯鍵);MachineLog 依 card_code 重組多語可讀失敗原因。
4. Machine 模型新增 card_terminal_status 與 latest_card_terminal_log_time 取值器;MachineController 列表以 withCount + 相關子查詢預載彙總,避免 N+1。
5. 機台列表頁新增「刷卡機」狀態燈號(綠/琥珀/紅 + 最近異常時間)與「刷卡機日誌」分頁。
6. 文件更新:config/api-docs.php 與 api-technical-specs SKILL.md 補充 payment_type 說明(2/10 與 1 共用同一台刷卡機、補列 42 取物單與 100 遠端出貨)。
7. 既有 migration change_products_spec_to_text 加 sqlite 守衛,讓測試環境可跑(VARCHAR/TEXT 在 sqlite 等價且不支援 MODIFY)。
8. 新增 CardTerminalStatusTest 功能測試(18 斷言通過,涵蓋成功消警、失敗寫日誌、掃碼排除、多語訊息、N+1 預載路徑)。
9. 新增 25 組三語系 key(刷卡機狀態與回應碼),zh_TW/en/ja 對齊並 ksort 排序。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-18 13:09:05 +08:00
parent 840052dd5a
commit df3c72e927
13 changed files with 447 additions and 12 deletions

View File

@ -414,7 +414,7 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| 代碼 | 支付方式 | 備註 / 說明 | | 代碼 | 支付方式 | 備註 / 說明 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| `1` | 信用卡 | 一般實體信用卡刷卡 | | `1` | 信用卡 | 一般實體信用卡刷卡 |
| `2` | 電子票證 | 包含悠遊卡、一卡通| | `2` | 電子票證 | 包含悠遊卡、一卡通、iCash 等(與信用卡、手機支付共用同一台實體刷卡機)|
| `3` | 掃碼支付 | 一般掃碼支付 | | `3` | 掃碼支付 | 一般掃碼支付 |
| `4` | 投幣機支付 | 實體紙鈔機 | | `4` | 投幣機支付 | 實體紙鈔機 |
| `5` | 通行碼 | 現場維護人員測試使用 | | `5` | 通行碼 | 現場維護人員測試使用 |
@ -431,8 +431,10 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與
| `34` | TapPay - 全盈+PAY | | | `34` | TapPay - 全盈+PAY | |
| `40` | 會員驗證取貨商品 | | | `40` | 會員驗證取貨商品 | |
| `41` | 員工卡 | RFID / NFC 刷卡 | | `41` | 員工卡 | RFID / NFC 刷卡 |
| `42` | 取物單 | |
| `50` ~ `54` | 線下付款 + TapPay | `50`=線下+30, `51`=線下+31... `54`=線下+34 | | `50` ~ `54` | 線下付款 + TapPay | `50`=線下+30, `51`=線下+31... `54`=線下+34 |
| `70` | LINE Pay官方直連 | LINE Pay 官方直連,非 TapPay 底下的 `30` | | `70` | LINE Pay官方直連 | LINE Pay 官方直連,非 TapPay 底下的 `30` |
| `60` | 點數 / 優惠卷全額折抵 | 點數全額折抵出貨 或 優惠卷全額折抵出貨 | | `60` | 點數 / 優惠卷全額折抵 | 點數全額折抵出貨 或 優惠卷全額折抵出貨 |
| `61` ~ `69` | 會員 + X | `61`=會員+1, `62`=會員+2... `69`=會員+9 | | `61` ~ `69` | 會員 + X | `61`=會員+1, `62`=會員+2... `69`=會員+9 |
| `90` ~ `94` | 會員 + TapPay | `90`=會員+30, `91`=會員+31... `94`=會員+34 | | `90` ~ `94` | 會員 + TapPay | `90`=會員+30, `91`=會員+31... `94`=會員+34 |
| `100` | 遠端出貨 | 後台遠端觸發出貨 |

View File

@ -34,6 +34,21 @@ class MachineController extends AdminController
$query->where('company_id', $companyId); $query->where('company_id', $companyId);
} }
// 刷卡機狀態:以彙總/相關子查詢預載,避免列表逐列 N+1
$query->select('machines.*')
->withCount([
'logs as card_terminal_error_count' => fn($q) => $q->where('type', 'card_terminal')->where('level', 'error')->where('is_resolved', false),
'logs as card_terminal_warning_count' => fn($q) => $q->where('type', 'card_terminal')->where('level', 'warning')->where('is_resolved', false),
])
->addSelect(['latest_card_terminal_log_at' => \App\Models\Machine\MachineLog::select('created_at')
->whereColumn('machine_id', 'machines.id')
->where('type', 'card_terminal')
->whereIn('level', ['error', 'warning'])
->where('is_resolved', false)
->latest()
->limit(1)
]);
// 預加載統計資料 // 預加載統計資料
$machines = $query->orderBy("last_heartbeat_at", "desc") $machines = $query->orderBy("last_heartbeat_at", "desc")
->orderBy("id", "desc") ->orderBy("id", "desc")

View File

@ -11,11 +11,16 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Jobs\Machine\ProcessStateLog; use App\Jobs\Machine\ProcessStateLog;
use App\Models\Transaction\Order;
use App\Services\Machine\MachineService;
class ProcessTransactionFinalized implements ShouldQueue class ProcessTransactionFinalized implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** 走實體刷卡機(Nexsys)的支付類型1 信用卡、2 電子票證、10 手機感應支付,共用同一台機器。 */
private const CARD_TERMINAL_PAYMENT_TYPES = [1, 2, 10];
protected $serialNo; protected $serialNo;
protected $payload; protected $payload;
@ -60,6 +65,9 @@ class ProcessTransactionFinalized implements ShouldQueue
'transaction' 'transaction'
); );
// 刷卡機健康狀態同步:成功消警 / 失敗寫 card_terminal 日誌
$this->syncCardTerminalStatus($machine, $order);
Log::info("MQTT Transaction [finalize] processed successfully", [ Log::info("MQTT Transaction [finalize] processed successfully", [
'serial_no' => $this->serialNo, 'serial_no' => $this->serialNo,
'order_no' => $order->order_no 'order_no' => $order->order_no
@ -89,4 +97,45 @@ class ProcessTransactionFinalized implements ShouldQueue
throw $e; throw $e;
} }
} }
/**
* 刷卡機 (信用卡/電子票證/手機支付payment_type 1/2/10) 狀態同步。
* 刷卡機沒有斷線心跳,所以靠交易結果維護燈號:
* - 成功交易 自動消除該機台先前未解決的刷卡機警告,恢復正常。
* - 失敗交易(payment_response NFC 回應碼) 寫一筆 type='card_terminal' warning 日誌
* (warning 級依 MachineLog 既有規則不會觸發 Discord)
*/
private function syncCardTerminalStatus(Machine $machine, Order $order): void
{
if (!in_array((int) $order->payment_type, self::CARD_TERMINAL_PAYMENT_TYPES, true)) {
return;
}
// 刷卡成功 → 消警,恢復刷卡機燈號
if ((int) $order->payment_status === Order::PAYMENT_STATUS_SUCCESS) {
$machine->logs()
->where('type', 'card_terminal')
->where('is_resolved', false)
->update(['is_resolved' => true]);
return;
}
// 刷卡失敗 → 解析 App 端格式 "[電文] | code=XXXX" 取回應碼
if (!preg_match('/\|\s*code=([A-Za-z0-9_]+)/', (string) $order->payment_response, $m)) {
return; // 無刷卡機回應碼(例如顧客未感應就放棄),不視為硬體訊號問題
}
$code = $m[1];
if ($code === '0000') {
return; // 0000 為成功碼,理論上不會走到失敗分支,保險過濾
}
ProcessStateLog::dispatch(
$machine->id,
$machine->company_id,
'Card payment failed',
'warning',
['card_code' => $code, 'order_no' => $order->order_no],
'card_terminal'
);
}
} }

View File

@ -221,6 +221,52 @@ class Machine extends Model
->first()?->created_at; ->first()?->created_at;
} }
/**
* 刷卡機健康狀態 (基於 type = 'card_terminal' 未解決日誌)
* 信用卡/電子票證/手機支付共用同一台實體刷卡機,刷卡失敗會寫此類日誌,
* 下一次刷卡成功時由 finalize 自動消警恢復正常。
* 列表頁優先使用 MachineController 預載的彙總欄位 (避免 N+1),其餘情境退回即時查詢。
*/
public function getCardTerminalStatusAttribute(): string
{
if (array_key_exists('card_terminal_error_count', $this->attributes)
|| array_key_exists('card_terminal_warning_count', $this->attributes)) {
if (($this->attributes['card_terminal_error_count'] ?? 0) > 0) {
return 'error';
}
if (($this->attributes['card_terminal_warning_count'] ?? 0) > 0) {
return 'warning';
}
return 'normal';
}
if ($this->logs()->where('type', 'card_terminal')->where('level', 'error')->where('is_resolved', false)->exists()) {
return 'error';
}
if ($this->logs()->where('type', 'card_terminal')->where('level', 'warning')->where('is_resolved', false)->exists()) {
return 'warning';
}
return 'normal';
}
/**
* 取得最新一筆未解決的刷卡機日誌時間 (列表頁優先用預載 subquery 欄位,避免 N+1)
*/
public function getLatestCardTerminalLogTimeAttribute()
{
if (array_key_exists('latest_card_terminal_log_at', $this->attributes)) {
$value = $this->attributes['latest_card_terminal_log_at'];
return $value ? \Illuminate\Support\Carbon::parse($value) : null;
}
return $this->logs()
->where('type', 'card_terminal')
->whereIn('level', ['error', 'warning'])
->where('is_resolved', false)
->latest()
->first()?->created_at;
}
/** /**
* Scope: 判定在線 (讀取 status 欄位) * Scope: 判定在線 (讀取 status 欄位)
*/ */

View File

@ -99,6 +99,13 @@ class MachineLog extends Model
{ {
$context = $this->context; $context = $this->context;
// 刷卡機回應碼:用目前代碼表重組失敗原因,支援多語系顯示。
if (isset($context['card_code'])) {
$code = $context['card_code'];
$reason = \App\Services\Machine\MachineService::CARD_TERMINAL_CODE_MAP[$code] ?? 'Transaction failed';
return __('Card payment failed') . '' . __($reason) . " (Code: {$code})";
}
// B013 封裝:優先用目前代碼表重組,讓舊日誌也能吃到最新文案。 // B013 封裝:優先用目前代碼表重組,讓舊日誌也能吃到最新文案。
if (isset($context['translated_label'])) { if (isset($context['translated_label'])) {
$code = $context['raw_code'] ?? $context['error_code'] ?? '0000'; $code = $context['raw_code'] ?? $context['error_code'] ?? '0000';

View File

@ -149,6 +149,37 @@ class MachineService
*/ */
public const DISPENSE_SUCCESS_CODES = ['0402', '0424']; public const DISPENSE_SUCCESS_CODES = ['0402', '0424'];
/**
* 刷卡機 (Nexsys 終端機 / NCCC 標準) 交易回應碼對照表。
* 來源:聯合信用卡處理中心終端機回應碼規格。
* 用於把 order.payment_response 內的 "| code=XXXX" 解析成可讀(可多語)的失敗原因,
* 寫入 type='card_terminal' 機台日誌。信用卡(1)/電子票證(2)/手機支付(10) 共用同一台實體刷卡機。
* 值為英文翻譯鍵,實際顯示由 lang/*.json 多語系翻譯。
*/
public const CARD_TERMINAL_CODE_MAP = [
'0000' => 'Transaction approved',
'0001' => 'Transaction failed (issuer/card declined)',
'0002' => 'Please contact bank / e-wallet provider',
'0003' => 'Transaction timeout',
'0004' => 'Operation error',
'0005' => 'Communication failure',
'0006' => 'User terminated transaction',
'0009' => 'Non-participating institution card',
'0010' => 'Message format error',
'0011' => 'Not a co-branded card',
'0012' => 'Stored-value amount over limit',
'0013' => 'Card locked',
'0014' => 'Cannot match original stored-value transaction',
'0015' => 'Card auto-load not enabled',
'0016' => 'Insufficient stored-value / e-wallet balance',
'0017' => 'Multiple cards detected',
'0018' => 'Please re-tap card / re-scan / retry',
'0019' => 'Auto-load succeeded but purchase failed',
'1001' => 'Credit card and stored-value settlement both failed',
'1002' => 'Credit card settled, stored-value settlement failed',
'1003' => 'Credit card settlement failed, stored-value settled',
];
/** /**
* Update machine heartbeat and status. * Update machine heartbeat and status.
* *

View File

@ -857,7 +857,7 @@ return [
'order.original_amount' => ['type' => 'numeric', 'description' => '(選填) 原始標價金額', 'required' => false], 'order.original_amount' => ['type' => 'numeric', 'description' => '(選填) 原始標價金額', 'required' => false],
'order.discount_amount' => ['type' => 'numeric', 'description' => '(選填) 折扣金額,預設 0', 'required' => false], 'order.discount_amount' => ['type' => 'numeric', 'description' => '(選填) 折扣金額,預設 0', 'required' => false],
'order.points_used' => ['type' => 'integer', '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:零錢, 10:手機支付(行動支付/NFC手機), 41:員工卡, 42:取物單。\n21~25:線下付款+X (21:線下+1, 22:線下+2, 23:線下+3, 24:線下+4, 25:線下+9)。\n30:LINE Pay, 31:街口, 32:悠遊付, 33:Pi, 34:全盈+\n70:LINE Pay(官方直連,非 TapPay 底下的 30)。\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)。\n100:遠端出貨。"], 'order.payment_type' => ['type' => 'integer', 'description' => "(必填) 支付類型代碼:\n1:信用卡, 2:電子票證(悠遊卡/一卡通/iCash 等), 3:掃碼支付, 4:紙鈔機, 5:通行碼, 6:取貨碼, 7:來店禮, 8:問卷, 9:零錢(硬幣), 10:手機支付(行動支付/NFC 手機感應), 41:員工卡, 42:取物單。\n※ 1(信用卡)、2(電子票證)、10(手機支付) 走同一台實體刷卡機(Nexsys 終端機)。\n21~25:線下付款+基本類型 (21:線下+1 信用卡, 22:線下+2 電子票證, 23:線下+3 掃碼, 24:線下+4 紙鈔, 25:線下+9 零錢)。\n30:LINE Pay(TapPay), 31:街口, 32:悠遊付, 33:Pi 拍錢包, 34:全盈+PAY\n70:LINE Pay(官方直連,非 TapPay 底下的 30)。\n40:會員驗證取貨商品。\n50~54:線下付款+TapPay (50:線下+30, 51:線下+31, 52:線下+32, 53:線下+33, 54:線下+34)。\n60:點數/優惠券全額折抵。61~69:會員+基本類型 (61:會員+1...69:會員+9)。\n90~94:會員+TapPay (90:會員+30...94:會員+34)。\n100:遠端出貨。"],
'order.payment_status' => ['type' => 'integer', 'description' => '(必填) 支付狀態 (1:成功)'], 'order.payment_status' => ['type' => 'integer', 'description' => '(必填) 支付狀態 (1:成功)'],
'order.code_id' => ['type' => 'string', 'description' => '(選填) 閉環勾稽用的代碼 ID (payment_type 為 5, 6, 41 時必填)', 'required' => false], 'order.code_id' => ['type' => 'string', 'description' => '(選填) 閉環勾稽用的代碼 ID (payment_type 為 5, 6, 41 時必填)', 'required' => false],
'order.payment_request' => ['type' => 'string', 'description' => '(選填) 金流請求原始字串', 'required' => false], 'order.payment_request' => ['type' => 'string', 'description' => '(選填) 金流請求原始字串', 'required' => false],

View File

@ -12,11 +12,16 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
// sqlite 為動態型別VARCHAR/TEXT 等價且不支援 MODIFY 語法,僅在 MySQL 執行。
if (DB::getDriverName() !== 'sqlite') {
DB::statement('ALTER TABLE products MODIFY spec TEXT NULL COMMENT \'詳情\''); DB::statement('ALTER TABLE products MODIFY spec TEXT NULL COMMENT \'詳情\'');
} }
}
public function down(): void public function down(): void
{ {
if (DB::getDriverName() !== 'sqlite') {
DB::statement('ALTER TABLE products MODIFY spec VARCHAR(255) NULL COMMENT \'規格\''); DB::statement('ALTER TABLE products MODIFY spec VARCHAR(255) NULL COMMENT \'規格\'');
} }
}
}; };

View File

@ -214,6 +214,7 @@
"Authorized Status": "Authorized Status", "Authorized Status": "Authorized Status",
"Auto Refresh": "Auto Refresh", "Auto Refresh": "Auto Refresh",
"Auto Replenishment": "Auto Replenishment", "Auto Replenishment": "Auto Replenishment",
"Auto-load succeeded but purchase failed": "Auto-load succeeded but purchase failed",
"Automatically calculate replenishment needs based on machine capacity": "Automatically calculate replenishment needs based on machine capacity", "Automatically calculate replenishment needs based on machine capacity": "Automatically calculate replenishment needs based on machine capacity",
"Availability": "Availability", "Availability": "Availability",
"Available Machines": "Available Machines", "Available Machines": "Available Machines",
@ -274,6 +275,7 @@
"Cannot delete model that is currently in use by machines.": "Cannot delete model that is currently in use by machines.", "Cannot delete model that is currently in use by machines.": "Cannot delete model that is currently in use by machines.",
"Cannot delete role with active users.": "Cannot delete role with active users.", "Cannot delete role with active users.": "Cannot delete role with active users.",
"Cannot delete warehouse with existing stock": "Cannot delete warehouse with existing stock", "Cannot delete warehouse with existing stock": "Cannot delete warehouse with existing stock",
"Cannot match original stored-value transaction": "Cannot match original stored-value transaction",
"Card Machine System": "Card Machine System", "Card Machine System": "Card Machine System",
"Card Payment": "Card Payment", "Card Payment": "Card Payment",
"Card Reader": "Card Reader", "Card Reader": "Card Reader",
@ -284,8 +286,13 @@
"Card Side Background": "Card Side Background", "Card Side Background": "Card Side Background",
"Card System": "Card System", "Card System": "Card System",
"Card Terminal": "Card Terminal", "Card Terminal": "Card Terminal",
"Card Terminal Logs": "Card Terminal Logs",
"Card Terminal Status": "Card Terminal Status",
"Card Terminal System": "Card Terminal System", "Card Terminal System": "Card Terminal System",
"Card UID": "Card UID", "Card UID": "Card UID",
"Card auto-load not enabled": "Card auto-load not enabled",
"Card locked": "Card locked",
"Card payment failed": "Card payment failed",
"Cash Module": "Cash Module", "Cash Module": "Cash Module",
"Cash Module Feature": "Cash Module Feature", "Cash Module Feature": "Cash Module Feature",
"Cash Module Function": "Cash Module Function", "Cash Module Function": "Cash Module Function",
@ -342,6 +349,7 @@
"Command has been queued successfully.": "Command has been queued successfully.", "Command has been queued successfully.": "Command has been queued successfully.",
"Command not found": "Command not found", "Command not found": "Command not found",
"Command queued successfully.": "Command queued successfully.", "Command queued successfully.": "Command queued successfully.",
"Communication failure": "Communication failure",
"Company": "Company", "Company": "Company",
"Company Code": "Company Code", "Company Code": "Company Code",
"Company Discord notification settings updated successfully.": "Company Discord notification settings updated successfully.", "Company Discord notification settings updated successfully.": "Company Discord notification settings updated successfully.",
@ -440,6 +448,9 @@
"Credit Card": "Credit Card", "Credit Card": "Credit Card",
"Credit Card / Contactless": "Credit Card / Contactless", "Credit Card / Contactless": "Credit Card / Contactless",
"Credit Card Payment": "Credit Card Payment", "Credit Card Payment": "Credit Card Payment",
"Credit card and stored-value settlement both failed": "Credit card and stored-value settlement both failed",
"Credit card settled, stored-value settlement failed": "Credit card settled, stored-value settlement failed",
"Credit card settlement failed, stored-value settled": "Credit card settlement failed, stored-value settled",
"Critical": "Critical", "Critical": "Critical",
"Critical failures requiring immediate hands-on attention.": "Critical failures requiring immediate hands-on attention.", "Critical failures requiring immediate hands-on attention.": "Critical failures requiring immediate hands-on attention.",
"Cross-Company / Headquarter Global Stats": "Cross-Company / Headquarter Global Stats", "Cross-Company / Headquarter Global Stats": "Cross-Company / Headquarter Global Stats",
@ -941,6 +952,7 @@
"Installation": "Installation", "Installation": "Installation",
"Installation Location": "Installation Location", "Installation Location": "Installation Location",
"Insufficient stock for transfer": "Insufficient stock for transfer", "Insufficient stock for transfer": "Insufficient stock for transfer",
"Insufficient stored-value / e-wallet balance": "Insufficient stored-value / e-wallet balance",
"Invalid personnel selection": "Invalid personnel selection", "Invalid personnel selection": "Invalid personnel selection",
"Invalid status transition": "Invalid status transition", "Invalid status transition": "Invalid status transition",
"Inventory": "Inventory", "Inventory": "Inventory",
@ -1202,6 +1214,7 @@
"Message": "Message", "Message": "Message",
"Message Content": "Message Content", "Message Content": "Message Content",
"Message Display": "Message Display", "Message Display": "Message Display",
"Message format error": "Message format error",
"Microwave delivery door close error": "Microwave delivery door close error", "Microwave delivery door close error": "Microwave delivery door close error",
"Microwave delivery door close error (0211)": "Microwave delivery door close error (0211)", "Microwave delivery door close error (0211)": "Microwave delivery door close error (0211)",
"Microwave delivery door closing": "Microwave delivery door closing", "Microwave delivery door closing": "Microwave delivery door closing",
@ -1245,6 +1258,7 @@
"Movement History": "Movement History", "Movement History": "Movement History",
"Movement Logs": "Movement Logs", "Movement Logs": "Movement Logs",
"Multilingual Names": "Multilingual Names", "Multilingual Names": "Multilingual Names",
"Multiple cards detected": "Multiple cards detected",
"Must be strictly greater than previous version": "Must be strictly greater than previous version", "Must be strictly greater than previous version": "Must be strictly greater than previous version",
"N/A": "N/A", "N/A": "N/A",
"Name": "Name", "Name": "Name",
@ -1344,11 +1358,13 @@
"No versions found": "No versions found", "No versions found": "No versions found",
"No warehouses found": "No warehouses found", "No warehouses found": "No warehouses found",
"No welcome gifts found": "No welcome gifts found", "No welcome gifts found": "No welcome gifts found",
"Non-participating institution card": "Non-participating institution card",
"None": "None", "None": "None",
"Normal": "Normal", "Normal": "Normal",
"Not Configured": "Not Configured", "Not Configured": "Not Configured",
"Not Used": "Not Used", "Not Used": "Not Used",
"Not Used Description": "Not Used Description", "Not Used Description": "Not Used Description",
"Not a co-branded card": "Not a co-branded card",
"Note": "Note", "Note": "Note",
"Note (optional)": "Note (optional)", "Note (optional)": "Note (optional)",
"Notes": "Notes", "Notes": "Notes",
@ -1403,6 +1419,7 @@
"Operation Logs": "Operation Logs", "Operation Logs": "Operation Logs",
"Operation Note": "Operation Note", "Operation Note": "Operation Note",
"Operation Records": "Operation Records", "Operation Records": "Operation Records",
"Operation error": "Operation error",
"Operation failed": "Operation failed", "Operation failed": "Operation failed",
"Operational Parameters": "Operational Parameters", "Operational Parameters": "Operational Parameters",
"Operations": "Operations", "Operations": "Operations",
@ -1548,6 +1565,7 @@
"Please check the following errors:": "Please check the following errors:", "Please check the following errors:": "Please check the following errors:",
"Please check the form for errors.": "Please check the form for errors.", "Please check the form for errors.": "Please check the form for errors.",
"Please confirm the details below": "Please confirm the details below", "Please confirm the details below": "Please confirm the details below",
"Please contact bank / e-wallet provider": "Please contact bank / e-wallet provider",
"Please enter Webhook URL first.": "Please enter Webhook URL first.", "Please enter Webhook URL first.": "Please enter Webhook URL first.",
"Please enter a valid ECPay e-invoice notification email.": "Please enter a valid ECPay e-invoice notification email.", "Please enter a valid ECPay e-invoice notification email.": "Please enter a valid ECPay e-invoice notification email.",
"Please enter address first": "Please enter address first", "Please enter address first": "Please enter address first",
@ -1556,6 +1574,7 @@
"Please enter version name": "Please enter version name", "Please enter version name": "Please enter version name",
"Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.", "Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.",
"Please provide a download URL": "Please provide a download URL", "Please provide a download URL": "Please provide a download URL",
"Please re-tap card / re-scan / retry": "Please re-tap card / re-scan / retry",
"Please remove bento from microwave": "Please remove bento from microwave", "Please remove bento from microwave": "Please remove bento from microwave",
"Please select a company": "Please select a company", "Please select a company": "Please select a company",
"Please select a company.": "Please select a company.", "Please select a company.": "Please select a company.",
@ -2017,6 +2036,7 @@
"Store ID": "Store ID", "Store ID": "Store ID",
"Store Management": "Store Management", "Store Management": "Store Management",
"StoreID": "StoreID", "StoreID": "StoreID",
"Stored-value amount over limit": "Stored-value amount over limit",
"Sub / Card / Scan": "Sub / Card / Scan", "Sub / Card / Scan": "Sub / Card / Scan",
"Sub Account Management": "Sub Account Management", "Sub Account Management": "Sub Account Management",
"Sub Account Roles": "Sub Account Roles", "Sub Account Roles": "Sub Account Roles",
@ -2168,7 +2188,11 @@
"Transaction Info": "Transaction Info", "Transaction Info": "Transaction Info",
"Transaction Orders": "Transaction Orders", "Transaction Orders": "Transaction Orders",
"Transaction Time": "Transaction Time", "Transaction Time": "Transaction Time",
"Transaction approved": "Transaction approved",
"Transaction failed": "Transaction failed",
"Transaction failed (issuer/card declined)": "Transaction failed (issuer/card declined)",
"Transaction processed: :id": "Transaction processed: :id", "Transaction processed: :id": "Transaction processed: :id",
"Transaction timeout": "Transaction timeout",
"Transfer Audit": "Transfer Audit", "Transfer Audit": "Transfer Audit",
"Transfer Details": "Transfer Details", "Transfer Details": "Transfer Details",
"Transfer In": "Transfer In", "Transfer In": "Transfer In",
@ -2258,6 +2282,7 @@
"User": "User", "User": "User",
"User Info": "User Info", "User Info": "User Info",
"User logged in: :name": "User logged in: :name", "User logged in: :name": "User logged in: :name",
"User terminated transaction": "User terminated transaction",
"Username": "Username", "Username": "Username",
"Users": "Users", "Users": "Users",
"Utilization Rate": "Utilization Rate", "Utilization Rate": "Utilization Rate",

View File

@ -214,6 +214,7 @@
"Authorized Status": "承認ステータス", "Authorized Status": "承認ステータス",
"Auto Refresh": "自動更新", "Auto Refresh": "自動更新",
"Auto Replenishment": "一括補充", "Auto Replenishment": "一括補充",
"Auto-load succeeded but purchase failed": "オートチャージ成功、但し購買取引失敗",
"Automatically calculate replenishment needs based on machine capacity": "機器容量に基づいて補充必要量を自動計算", "Automatically calculate replenishment needs based on machine capacity": "機器容量に基づいて補充必要量を自動計算",
"Availability": "可用性 (Availability)", "Availability": "可用性 (Availability)",
"Available Machines": "割り当て可能な機器", "Available Machines": "割り当て可能な機器",
@ -274,6 +275,7 @@
"Cannot delete model that is currently in use by machines.": "機器で使用中のモデルは削除できません。", "Cannot delete model that is currently in use by machines.": "機器で使用中のモデルは削除できません。",
"Cannot delete role with active users.": "ユーザーが紐付けられているロールは削除できません。", "Cannot delete role with active users.": "ユーザーが紐付けられているロールは削除できません。",
"Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません", "Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません",
"Cannot match original stored-value transaction": "元の電子マネー取引と照合できません",
"Card Machine System": "カードリーダーシステム", "Card Machine System": "カードリーダーシステム",
"Card Payment": "カード決済", "Card Payment": "カード決済",
"Card Reader": "カードリーダー", "Card Reader": "カードリーダー",
@ -284,8 +286,13 @@
"Card Side Background": "カード側背景画像", "Card Side Background": "カード側背景画像",
"Card System": "カードシステム", "Card System": "カードシステム",
"Card Terminal": "カード端末", "Card Terminal": "カード端末",
"Card Terminal Logs": "カード端末ログ",
"Card Terminal Status": "カード端末ステータス",
"Card Terminal System": "カードリーダーシステム", "Card Terminal System": "カードリーダーシステム",
"Card UID": "カードUID", "Card UID": "カードUID",
"Card auto-load not enabled": "カードのオートチャージが未設定です",
"Card locked": "カードがロックされています",
"Card payment failed": "カード決済失敗",
"Cash Module": "現金モジュール", "Cash Module": "現金モジュール",
"Cash Module Feature": "現金モジュール機能", "Cash Module Feature": "現金モジュール機能",
"Cash Module Function": "現金モジュール機能", "Cash Module Function": "現金モジュール機能",
@ -342,6 +349,7 @@
"Command has been queued successfully.": "コマンドがキューに追加されました。", "Command has been queued successfully.": "コマンドがキューに追加されました。",
"Command not found": "コマンドが見つかりません", "Command not found": "コマンドが見つかりません",
"Command queued successfully.": "コマンドが正常にキューされました。", "Command queued successfully.": "コマンドが正常にキューされました。",
"Communication failure": "通信エラー",
"Company": "顧客企業", "Company": "顧客企業",
"Company Code": "会社コード", "Company Code": "会社コード",
"Company Discord notification settings updated successfully.": "会社の Discord 通知設定が正常に更新されました。", "Company Discord notification settings updated successfully.": "会社の Discord 通知設定が正常に更新されました。",
@ -440,6 +448,9 @@
"Credit Card": "クレジットカード", "Credit Card": "クレジットカード",
"Credit Card / Contactless": "クレジットカード/非接触決済対応", "Credit Card / Contactless": "クレジットカード/非接触決済対応",
"Credit Card Payment": "クレジットカード決済", "Credit Card Payment": "クレジットカード決済",
"Credit card and stored-value settlement both failed": "クレジットカード・電子マネーともに精算失敗",
"Credit card settled, stored-value settlement failed": "クレジットカード精算成功、電子マネー精算失敗",
"Credit card settlement failed, stored-value settled": "クレジットカード精算失敗、電子マネー精算成功",
"Critical": "深刻", "Critical": "深刻",
"Critical failures requiring immediate hands-on attention.": "即座に対処が必要な重大な障害。", "Critical failures requiring immediate hands-on attention.": "即座に対処が必要な重大な障害。",
"Cross-Company / Headquarter Global Stats": "複数会社間 / 本部グローバル統計", "Cross-Company / Headquarter Global Stats": "複数会社間 / 本部グローバル統計",
@ -941,6 +952,7 @@
"Installation": "設置", "Installation": "設置",
"Installation Location": "設置場所", "Installation Location": "設置場所",
"Insufficient stock for transfer": "在庫不足のため移動できません", "Insufficient stock for transfer": "在庫不足のため移動できません",
"Insufficient stored-value / e-wallet balance": "電子マネー/電子ウォレット残高不足",
"Invalid personnel selection": "無効な担当者選択", "Invalid personnel selection": "無効な担当者選択",
"Invalid status transition": "無効なステータス遷移", "Invalid status transition": "無効なステータス遷移",
"Inventory": "在庫概要", "Inventory": "在庫概要",
@ -1202,6 +1214,7 @@
"Message": "メッセージ", "Message": "メッセージ",
"Message Content": "メッセージ内容", "Message Content": "メッセージ内容",
"Message Display": "メッセージ表示", "Message Display": "メッセージ表示",
"Message format error": "電文/フォーマットエラー",
"Microwave delivery door close error": "電子レンジ搬入口扉閉鎖エラー", "Microwave delivery door close error": "電子レンジ搬入口扉閉鎖エラー",
"Microwave delivery door close error (0211)": "電子レンジ搬入口扉閉エラー (0211)", "Microwave delivery door close error (0211)": "電子レンジ搬入口扉閉エラー (0211)",
"Microwave delivery door closing": "電子レンジ搬入口扉閉鎖中", "Microwave delivery door closing": "電子レンジ搬入口扉閉鎖中",
@ -1245,6 +1258,7 @@
"Movement History": "変動履歴", "Movement History": "変動履歴",
"Movement Logs": "変動ログ", "Movement Logs": "変動ログ",
"Multilingual Names": "多言語名", "Multilingual Names": "多言語名",
"Multiple cards detected": "複数カードの読み取り失敗",
"Must be strictly greater than previous version": "Must be strictly greater than previous version", "Must be strictly greater than previous version": "Must be strictly greater than previous version",
"N/A": "なし", "N/A": "なし",
"Name": "名前", "Name": "名前",
@ -1344,11 +1358,13 @@
"No versions found": "No versions found", "No versions found": "No versions found",
"No warehouses found": "倉庫が見つかりません", "No warehouses found": "倉庫が見つかりません",
"No welcome gifts found": "ウェルカムギフトが見つかりません", "No welcome gifts found": "ウェルカムギフトが見つかりません",
"Non-participating institution card": "非参加機関のカード",
"None": "なし", "None": "なし",
"Normal": "正常", "Normal": "正常",
"Not Configured": "未設定", "Not Configured": "未設定",
"Not Used": "使用しない", "Not Used": "使用しない",
"Not Used Description": "サードパーティ決済を使用しない", "Not Used Description": "サードパーティ決済を使用しない",
"Not a co-branded card": "提携カードではありません",
"Note": "備考", "Note": "備考",
"Note (optional)": "備考 (任意)", "Note (optional)": "備考 (任意)",
"Notes": "備考", "Notes": "備考",
@ -1403,6 +1419,7 @@
"Operation Logs": "Operation Logs", "Operation Logs": "Operation Logs",
"Operation Note": "操作メモ", "Operation Note": "操作メモ",
"Operation Records": "操作記録", "Operation Records": "操作記録",
"Operation error": "操作エラー",
"Operation failed": "操作失敗", "Operation failed": "操作失敗",
"Operational Parameters": "運用パラメータ", "Operational Parameters": "運用パラメータ",
"Operations": "運用設定", "Operations": "運用設定",
@ -1548,6 +1565,7 @@
"Please check the following errors:": "以下のエラーを確認してください:", "Please check the following errors:": "以下のエラーを確認してください:",
"Please check the form for errors.": "フォームのエラーを確認してください。", "Please check the form for errors.": "フォームのエラーを確認してください。",
"Please confirm the details below": "以下の詳細を確認してください", "Please confirm the details below": "以下の詳細を確認してください",
"Please contact bank / e-wallet provider": "銀行/電子マネー事業者へお問い合わせください",
"Please enter Webhook URL first.": "最初にWebhook URLを入力してください。", "Please enter Webhook URL first.": "最初にWebhook URLを入力してください。",
"Please enter a valid ECPay e-invoice notification email.": "有効な ECPay 電子インボイス通知用メールアドレスを入力してください。", "Please enter a valid ECPay e-invoice notification email.": "有効な ECPay 電子インボイス通知用メールアドレスを入力してください。",
"Please enter address first": "先に住所を入力してください", "Please enter address first": "先に住所を入力してください",
@ -1556,6 +1574,7 @@
"Please enter version name": "Please enter version name", "Please enter version name": "Please enter version name",
"Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "商品の画像やスタイル付きコンポーネントを正しく印刷するために、ブラウザの印刷設定で『背景のグラフィック』を有効にしてください。", "Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "商品の画像やスタイル付きコンポーネントを正しく印刷するために、ブラウザの印刷設定で『背景のグラフィック』を有効にしてください。",
"Please provide a download URL": "Please provide a download URL", "Please provide a download URL": "Please provide a download URL",
"Please re-tap card / re-scan / retry": "カードを再タッチ/再スキャン/再試行してください",
"Please remove bento from microwave": "電子レンジ内の弁当を取り出してください", "Please remove bento from microwave": "電子レンジ内の弁当を取り出してください",
"Please select a company": "会社を選択してください", "Please select a company": "会社を選択してください",
"Please select a company.": "会社を選択してください。", "Please select a company.": "会社を選択してください。",
@ -2017,6 +2036,7 @@
"Store ID": "店舗ID (Store ID)", "Store ID": "店舗ID (Store ID)",
"Store Management": "店舗管理", "Store Management": "店舗管理",
"StoreID": "店舗ID", "StoreID": "店舗ID",
"Stored-value amount over limit": "電子マネー金額が上限超過",
"Sub / Card / Scan": "下位機 / カードリーダー / スキャナー", "Sub / Card / Scan": "下位機 / カードリーダー / スキャナー",
"Sub Account Management": "サブアカウント管理", "Sub Account Management": "サブアカウント管理",
"Sub Account Roles": "サブアカウントロール", "Sub Account Roles": "サブアカウントロール",
@ -2168,7 +2188,11 @@
"Transaction Info": "取引情報", "Transaction Info": "取引情報",
"Transaction Orders": "取引注文", "Transaction Orders": "取引注文",
"Transaction Time": "取引時間", "Transaction Time": "取引時間",
"Transaction approved": "取引成功",
"Transaction failed": "取引失敗",
"Transaction failed (issuer/card declined)": "取引失敗(ホストまたはカード拒否)",
"Transaction processed: :id": "取引処理完了: :id", "Transaction processed: :id": "取引処理完了: :id",
"Transaction timeout": "取引タイムアウト",
"Transfer Audit": "移動伝票監査", "Transfer Audit": "移動伝票監査",
"Transfer Details": "移動伝票詳細", "Transfer Details": "移動伝票詳細",
"Transfer In": "転入", "Transfer In": "転入",
@ -2258,6 +2282,7 @@
"User": "一般ユーザー", "User": "一般ユーザー",
"User Info": "ユーザー情報", "User Info": "ユーザー情報",
"User logged in: :name": "ユーザーがログインしました: :name", "User logged in: :name": "ユーザーがログインしました: :name",
"User terminated transaction": "ユーザーが取引を中止しました",
"Username": "ユーザー名", "Username": "ユーザー名",
"Users": "アカウント数", "Users": "アカウント数",
"Utilization Rate": "機器稼働率", "Utilization Rate": "機器稼働率",

View File

@ -214,6 +214,7 @@
"Authorized Status": "已授權", "Authorized Status": "已授權",
"Auto Refresh": "自動更新", "Auto Refresh": "自動更新",
"Auto Replenishment": "自動補貨單", "Auto Replenishment": "自動補貨單",
"Auto-load succeeded but purchase failed": "自動加值成功,但購貨交易失敗",
"Automatically calculate replenishment needs based on machine capacity": "根據機台容量自動計算補貨需求", "Automatically calculate replenishment needs based on machine capacity": "根據機台容量自動計算補貨需求",
"Availability": "可用性 (Availability)", "Availability": "可用性 (Availability)",
"Available Machines": "可供分配的機台", "Available Machines": "可供分配的機台",
@ -274,6 +275,7 @@
"Cannot delete model that is currently in use by machines.": "無法刪除目前正在被機台使用的型號。", "Cannot delete model that is currently in use by machines.": "無法刪除目前正在被機台使用的型號。",
"Cannot delete role with active users.": "無法刪除已有綁定帳號的角色。", "Cannot delete role with active users.": "無法刪除已有綁定帳號的角色。",
"Cannot delete warehouse with existing stock": "無法刪除仍有庫存的倉庫", "Cannot delete warehouse with existing stock": "無法刪除仍有庫存的倉庫",
"Cannot match original stored-value transaction": "無法比對原始電票交易",
"Card Machine System": "刷卡機系統", "Card Machine System": "刷卡機系統",
"Card Payment": "卡片支付", "Card Payment": "卡片支付",
"Card Reader": "刷卡機", "Card Reader": "刷卡機",
@ -284,8 +286,13 @@
"Card Side Background": "左側卡片背景圖", "Card Side Background": "左側卡片背景圖",
"Card System": "刷卡機系統", "Card System": "刷卡機系統",
"Card Terminal": "刷卡機端", "Card Terminal": "刷卡機端",
"Card Terminal Logs": "刷卡機日誌",
"Card Terminal Status": "刷卡機狀態",
"Card Terminal System": "刷卡機系統", "Card Terminal System": "刷卡機系統",
"Card UID": "卡片 UID", "Card UID": "卡片 UID",
"Card auto-load not enabled": "票卡未開啟自動加值",
"Card locked": "票卡已鎖",
"Card payment failed": "刷卡失敗",
"Cash Module": "現金模組", "Cash Module": "現金模組",
"Cash Module Feature": "現金模組功能", "Cash Module Feature": "現金模組功能",
"Cash Module Function": "現金模組功能", "Cash Module Function": "現金模組功能",
@ -342,6 +349,7 @@
"Command has been queued successfully.": "指令已成功排入佇列。", "Command has been queued successfully.": "指令已成功排入佇列。",
"Command not found": "找不到指令", "Command not found": "找不到指令",
"Command queued successfully.": "指令已加入佇列。", "Command queued successfully.": "指令已加入佇列。",
"Communication failure": "通訊失敗",
"Company": "客戶公司", "Company": "客戶公司",
"Company Code": "公司代碼", "Company Code": "公司代碼",
"Company Discord notification settings updated successfully.": "公司 Discord 通知設定已成功更新。", "Company Discord notification settings updated successfully.": "公司 Discord 通知設定已成功更新。",
@ -440,6 +448,9 @@
"Credit Card": "信用卡", "Credit Card": "信用卡",
"Credit Card / Contactless": "支援信用卡 / 感應支付", "Credit Card / Contactless": "支援信用卡 / 感應支付",
"Credit Card Payment": "信用卡支付", "Credit Card Payment": "信用卡支付",
"Credit card and stored-value settlement both failed": "信用卡及電票皆結帳失敗",
"Credit card settled, stored-value settlement failed": "信用卡結帳成功,電票結帳失敗",
"Credit card settlement failed, stored-value settled": "信用卡結帳失敗,電票結帳成功",
"Critical": "嚴重", "Critical": "嚴重",
"Critical failures requiring immediate hands-on attention.": "需要立即處理的關鍵故障。", "Critical failures requiring immediate hands-on attention.": "需要立即處理的關鍵故障。",
"Cross-Company / Headquarter Global Stats": "跨公司 / 總部全域統計", "Cross-Company / Headquarter Global Stats": "跨公司 / 總部全域統計",
@ -941,6 +952,7 @@
"Installation": "裝機", "Installation": "裝機",
"Installation Location": "安裝位置", "Installation Location": "安裝位置",
"Insufficient stock for transfer": "庫存不足,無法調撥", "Insufficient stock for transfer": "庫存不足,無法調撥",
"Insufficient stored-value / e-wallet balance": "電票/電子錢包餘額不足",
"Invalid personnel selection": "無效的人員選擇", "Invalid personnel selection": "無效的人員選擇",
"Invalid status transition": "無效的狀態轉換", "Invalid status transition": "無效的狀態轉換",
"Inventory": "庫存概覽", "Inventory": "庫存概覽",
@ -1202,6 +1214,7 @@
"Message": "訊息", "Message": "訊息",
"Message Content": "日誌內容", "Message Content": "日誌內容",
"Message Display": "訊息顯示", "Message Display": "訊息顯示",
"Message format error": "電文/格式錯誤",
"Microwave delivery door close error": "微波爐進貨口門關閉錯誤", "Microwave delivery door close error": "微波爐進貨口門關閉錯誤",
"Microwave delivery door close error (0211)": "微波爐進貨口門關閉錯誤", "Microwave delivery door close error (0211)": "微波爐進貨口門關閉錯誤",
"Microwave delivery door closing": "正在關閉微波爐進貨口門", "Microwave delivery door closing": "正在關閉微波爐進貨口門",
@ -1245,6 +1258,7 @@
"Movement History": "異動紀錄", "Movement History": "異動紀錄",
"Movement Logs": "異動紀錄", "Movement Logs": "異動紀錄",
"Multilingual Names": "多語系名稱", "Multilingual Names": "多語系名稱",
"Multiple cards detected": "多張票卡讀取失敗",
"Must be strictly greater than previous version": "必須嚴格大於上一個版本代碼", "Must be strictly greater than previous version": "必須嚴格大於上一個版本代碼",
"N/A": "不適用", "N/A": "不適用",
"Name": "名稱", "Name": "名稱",
@ -1344,11 +1358,13 @@
"No versions found": "未找到任何版本", "No versions found": "未找到任何版本",
"No warehouses found": "未找到倉庫", "No warehouses found": "未找到倉庫",
"No welcome gifts found": "找不到來店禮資料", "No welcome gifts found": "找不到來店禮資料",
"Non-participating institution card": "非參加機構卡片",
"None": "無", "None": "無",
"Normal": "正常", "Normal": "正常",
"Not Configured": "尚未設定", "Not Configured": "尚未設定",
"Not Used": "不使用", "Not Used": "不使用",
"Not Used Description": "不使用第三方支付介接", "Not Used Description": "不使用第三方支付介接",
"Not a co-branded card": "非聯名卡",
"Note": "備註", "Note": "備註",
"Note (optional)": "備註 (選填)", "Note (optional)": "備註 (選填)",
"Notes": "備註", "Notes": "備註",
@ -1403,6 +1419,7 @@
"Operation Logs": "操作紀錄", "Operation Logs": "操作紀錄",
"Operation Note": "操作備註", "Operation Note": "操作備註",
"Operation Records": "操作紀錄", "Operation Records": "操作紀錄",
"Operation error": "操作錯誤",
"Operation failed": "操作失敗", "Operation failed": "操作失敗",
"Operational Parameters": "運作參數", "Operational Parameters": "運作參數",
"Operations": "運作設定", "Operations": "運作設定",
@ -1548,6 +1565,7 @@
"Please check the following errors:": "請檢查以下錯誤:", "Please check the following errors:": "請檢查以下錯誤:",
"Please check the form for errors.": "請檢查欄位內容是否正確。", "Please check the form for errors.": "請檢查欄位內容是否正確。",
"Please confirm the details below": "請確認以下詳細資訊", "Please confirm the details below": "請確認以下詳細資訊",
"Please contact bank / e-wallet provider": "請連絡銀行/電子錢包業者",
"Please enter Webhook URL first.": "請先輸入 Webhook 網址。", "Please enter Webhook URL first.": "請先輸入 Webhook 網址。",
"Please enter a valid ECPay e-invoice notification email.": "請輸入有效的綠界電子發票通知 Email。", "Please enter a valid ECPay e-invoice notification email.": "請輸入有效的綠界電子發票通知 Email。",
"Please enter address first": "請先輸入地址", "Please enter address first": "請先輸入地址",
@ -1556,6 +1574,7 @@
"Please enter version name": "請輸入版本名稱", "Please enter version name": "請輸入版本名稱",
"Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "請確保在瀏覽器列印設定中啟用「背景圖形」,以便正確列印商品圖片和樣式元件。", "Please make sure to enable \"Background graphics\" in your browser print settings to print product images and styled components correctly.": "請確保在瀏覽器列印設定中啟用「背景圖形」,以便正確列印商品圖片和樣式元件。",
"Please provide a download URL": "請提供下載連結", "Please provide a download URL": "請提供下載連結",
"Please re-tap card / re-scan / retry": "請重新感應卡片/重新掃碼/重試交易",
"Please remove bento from microwave": "請取出微波爐內盒飯", "Please remove bento from microwave": "請取出微波爐內盒飯",
"Please select a company": "請選擇公司", "Please select a company": "請選擇公司",
"Please select a company.": "請選擇公司", "Please select a company.": "請選擇公司",
@ -2017,6 +2036,7 @@
"Store ID": "商店代號", "Store ID": "商店代號",
"Store Management": "店家管理", "Store Management": "店家管理",
"StoreID": "商店代號 (StoreID)", "StoreID": "商店代號 (StoreID)",
"Stored-value amount over limit": "電票金額超過上限",
"Sub / Card / Scan": "下位機 / 刷卡機 / 掃碼機", "Sub / Card / Scan": "下位機 / 刷卡機 / 掃碼機",
"Sub Account Management": "子帳號管理", "Sub Account Management": "子帳號管理",
"Sub Account Roles": "子帳號角色", "Sub Account Roles": "子帳號角色",
@ -2168,7 +2188,11 @@
"Transaction Info": "交易資訊", "Transaction Info": "交易資訊",
"Transaction Orders": "交易訂單", "Transaction Orders": "交易訂單",
"Transaction Time": "交易時間", "Transaction Time": "交易時間",
"Transaction approved": "交易成功",
"Transaction failed": "交易失敗",
"Transaction failed (issuer/card declined)": "交易失敗(主機或卡片拒絕)",
"Transaction processed: :id": "交易處理完成::id", "Transaction processed: :id": "交易處理完成::id",
"Transaction timeout": "交易逾時",
"Transfer Audit": "調撥單", "Transfer Audit": "調撥單",
"Transfer Details": "調撥單詳情", "Transfer Details": "調撥單詳情",
"Transfer In": "轉入", "Transfer In": "轉入",
@ -2258,6 +2282,7 @@
"User": "一般用戶", "User": "一般用戶",
"User Info": "用戶資訊", "User Info": "用戶資訊",
"User logged in: :name": "使用者登入::name", "User logged in: :name": "使用者登入::name",
"User terminated transaction": "使用者終止交易",
"Username": "使用者帳號", "Username": "使用者帳號",
"Users": "帳號數", "Users": "帳號數",
"Utilization Rate": "機台稼動率", "Utilization Rate": "機台稼動率",

View File

@ -641,8 +641,9 @@
<td class="px-6 py-6 text-center"> <td class="px-6 py-6 text-center">
@php @php
$hStatus = $machine->hardware_status; $hStatus = $machine->hardware_status;
$ctStatus = $machine->card_terminal_status;
@endphp @endphp
<div class="flex items-center justify-center"> <div class="flex items-center justify-center gap-3">
{{-- 下位機 --}} {{-- 下位機 --}}
<div class="flex items-center gap-1.5 tooltip" title="{{ __('Sub-machine Status') }}"> <div class="flex items-center gap-1.5 tooltip" title="{{ __('Sub-machine Status') }}">
<span class="w-1.5 h-1.5 rounded-full {{ <span class="w-1.5 h-1.5 rounded-full {{
@ -659,6 +660,22 @@
@endif @endif
</span> </span>
</div> </div>
{{-- 刷卡機 (信用卡/電子票證/手機支付共用同一台) --}}
<div class="flex items-center gap-1.5 tooltip" title="{{ __('Card Terminal Status') }}">
<span class="w-1.5 h-1.5 rounded-full {{
$ctStatus === 'error' ? 'bg-rose-500 animate-pulse' :
($ctStatus === 'warning' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500/50')
}}"></span>
<span class="text-xs font-black {{
$ctStatus === 'error' ? 'text-rose-500' :
($ctStatus === 'warning' ? 'text-amber-500' : 'text-slate-400')
}} uppercase tracking-widest">
{{ __('Card Terminal') }}
@if($machine->latest_card_terminal_log_time && $ctStatus !== 'normal')
<span class="text-[9px] lowercase opacity-70 block -mt-0.5 ml-3 font-bold">{{ $machine->latest_card_terminal_log_time->diffForHumans() }}</span>
@endif
</span>
</div>
</div> </div>
</td> </td>
<td <td
@ -1011,6 +1028,11 @@
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300"> class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
{{ __('Sub-machine Status Request') }} {{ __('Sub-machine Status Request') }}
</button> </button>
<button @click="activeTab = 'card_terminal'"
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'card_terminal', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'card_terminal'}"
class="whitespace-nowrap py-4 px-1 border-b-2 font-bold text-[13px] sm:text-sm transition duration-300">
{{ __('Card Terminal Logs') }}
</button>
<button @click="activeTab = 'ambient_temp'" <button @click="activeTab = 'ambient_temp'"
x-show="currentMachineAmbientTempEnabled" x-show="currentMachineAmbientTempEnabled"
:class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'ambient_temp', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'ambient_temp'}" :class="{'border-cyan-500 text-cyan-600 dark:text-cyan-400': activeTab === 'ambient_temp', 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300 dark:hover:text-slate-300': activeTab !== 'ambient_temp'}"

View File

@ -0,0 +1,183 @@
<?php
namespace Tests\Feature;
use App\Jobs\Transaction\ProcessTransactionFinalized;
use App\Models\Machine\Machine;
use App\Models\Machine\MachineLog;
use App\Services\Machine\MqttService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 刷卡機 (Nexsys 終端機) 交易訊號 後台機台日誌/燈號 (Plan A) 測試。
* 涵蓋 payment_type {1 信用卡, 2 電子票證, 10 手機支付} 共用同一台實體刷卡機。
*/
class CardTerminalStatusTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Job handle 內注入 MqttService 發 ack測試環境以 mock 取代避免真的連 MQTT
$this->mock(MqttService::class, function ($m) {
$m->shouldReceive('pushCommand')->andReturnTrue();
});
}
/** 組裝一筆 finalize payload */
private function finalizePayload(string $serialNo, string $flowId, int $paymentType, int $paymentStatus, ?string $paymentResponse, string $status): array
{
return [
'action' => 'finalize',
'flow_id' => $flowId,
'payment_type' => $paymentType,
'serial_no' => $serialNo,
'order' => [
'flow_id' => $flowId,
'order_no' => $flowId,
'total_amount' => 59,
'pay_amount' => 59,
'payment_type' => $paymentType,
'payment_status' => $paymentStatus,
'payment_response' => $paymentResponse,
'status' => $status,
'items' => [],
],
];
}
public function test_failed_credit_card_writes_card_terminal_warning_log(): void
{
$machine = Machine::factory()->create(['last_heartbeat_at' => now()]);
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, 'FLOW-CC-FAIL', 1, 0, 'RAWMSG... | code=0003', 'failed')
);
$log = MachineLog::where('machine_id', $machine->id)->where('type', 'card_terminal')->first();
$this->assertNotNull($log, '應寫入一筆 card_terminal 日誌');
$this->assertEquals('warning', $log->level, 'card_terminal 應為 warning 級 (不觸發 Discord)');
$this->assertEquals('0003', $log->context['card_code']);
// 燈號 (fallback 即時查詢路徑)
$this->assertEquals('warning', $machine->fresh()->card_terminal_status);
}
public function test_failed_eticket_and_mobile_pay_also_write_log(): void
{
foreach ([2 => 'FLOW-ET', 10 => 'FLOW-MO'] as $type => $flow) {
$machine = Machine::factory()->create(['last_heartbeat_at' => now()]);
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, $flow, $type, 0, ' | code=0005', 'failed')
);
$this->assertEquals('warning', $machine->fresh()->card_terminal_status, "payment_type={$type} 應納入刷卡機燈號");
}
}
public function test_scan_payment_failure_is_excluded(): void
{
$machine = Machine::factory()->create(['last_heartbeat_at' => now()]);
// payment_type=3 掃碼支付 (走網路,非實體刷卡機) → 不應寫 card_terminal 日誌
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, 'FLOW-SCAN', 3, 0, ' | code=scan_failed', 'failed')
);
$this->assertEquals(0, MachineLog::where('machine_id', $machine->id)->where('type', 'card_terminal')->count());
$this->assertEquals('normal', $machine->fresh()->card_terminal_status);
}
public function test_successful_transaction_auto_resolves_prior_warnings(): void
{
$machine = Machine::factory()->create(['last_heartbeat_at' => now()]);
// 先製造一筆未解決的刷卡機警告
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, 'FLOW-FAIL', 1, 0, ' | code=0003', 'failed')
);
$this->assertEquals('warning', $machine->fresh()->card_terminal_status);
// 下一筆刷卡成功 → 應自動消警,燈號恢復 normal
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, 'FLOW-OK', 1, 1, 'RAWMSG-SUCCESS', 'completed')
);
$this->assertEquals('normal', $machine->fresh()->card_terminal_status, '刷卡成功後應自動消警');
$unresolved = MachineLog::where('machine_id', $machine->id)
->where('type', 'card_terminal')->where('is_resolved', false)->count();
$this->assertEquals(0, $unresolved);
}
public function test_failure_without_response_code_does_not_log(): void
{
$machine = Machine::factory()->create(['last_heartbeat_at' => now()]);
// 失敗但沒有刷卡機回應碼 (例如顧客未感應就放棄) → 不視為硬體訊號問題
ProcessTransactionFinalized::dispatchSync(
$machine->serial_no,
$this->finalizePayload($machine->serial_no, 'FLOW-NOCODE', 1, 0, 'JUSTRAWNOCODE', 'abandoned')
);
$this->assertEquals(0, MachineLog::where('machine_id', $machine->id)->where('type', 'card_terminal')->count());
}
public function test_translated_message_renders_official_chinese(): void
{
app()->setLocale('zh_TW');
$machine = Machine::factory()->create();
$log = MachineLog::create([
'machine_id' => $machine->id,
'type' => 'card_terminal',
'level' => 'warning',
'message' => 'Card payment failed',
'context' => ['card_code' => '0003', 'order_no' => 'X'],
]);
$this->assertStringContainsString('刷卡失敗', $log->translated_message);
$this->assertStringContainsString('交易逾時', $log->translated_message);
$this->assertStringContainsString('0003', $log->translated_message);
}
public function test_preloaded_aggregate_path_avoids_n_plus_one(): void
{
$machine = Machine::factory()->create();
MachineLog::create([
'machine_id' => $machine->id,
'type' => 'card_terminal',
'level' => 'warning',
'message' => 'Card payment failed',
'context' => ['card_code' => '0003'],
]);
// 模擬 MachineController index 的預載 (withCount + 相關子查詢)
$loaded = Machine::query()
->select('machines.*')
->withCount([
'logs as card_terminal_error_count' => fn($q) => $q->where('type', 'card_terminal')->where('level', 'error')->where('is_resolved', false),
'logs as card_terminal_warning_count' => fn($q) => $q->where('type', 'card_terminal')->where('level', 'warning')->where('is_resolved', false),
])
->addSelect(['latest_card_terminal_log_at' => MachineLog::select('created_at')
->whereColumn('machine_id', 'machines.id')
->where('type', 'card_terminal')
->whereIn('level', ['error', 'warning'])
->where('is_resolved', false)
->latest()
->limit(1)
])
->find($machine->id);
$this->assertArrayHasKey('card_terminal_warning_count', $loaded->getAttributes());
$this->assertEquals('warning', $loaded->card_terminal_status);
$this->assertNotNull($loaded->latest_card_terminal_log_time);
}
}