Ragdoll 宜睿禮券(Edenred Voucher)完整知識庫。涵蓋票券查詢、授權核銷、 金額換算規則、product_code 解析、錯誤代碼處理、付款紀錄組裝,以及 Electron 主進程 與 Next.js 渲染端的程式碼位置與資料流。 以下情況必須參考此文件再動手: - 修改或擴充宜睿禮券付款流程(查詢、核銷、折抵) - 處理宜睿 API 回傳的錯誤代碼或異常狀態 - 修改 product_code 解析邏輯或 remaining_value 金額換算 - 修改宜睿禮券與其他付款方式(現金/信用卡)並行付款的紀錄組裝邏輯 - 修改 createSale 中 voucherTotal / totalWithoutVoucher 的計算 - 修改 Summary 頁面中宜睿禮券的顯示邏輯(已付合計、找零) - 撰寫宜睿禮券相關的單元測試或整合測試 - 理解 IPC 資料流(渲染端 store → Electron 主進程 → 宜睿 API)
npx claudepluginhub peacepan/peace-agent-marketplaceThis skill uses the workspace's default tool permissions.
宜睿禮券採用 **「查詢再核銷」** 的兩階段流程,每次授權核銷都是不可逆的真實金融交易。
Applies Acme Corporation brand guidelines including colors, fonts, layouts, and messaging to generated PowerPoint, Excel, and PDF documents.
Builds DCF models with sensitivity analysis, Monte Carlo simulations, and scenario planning for investment valuation and risk assessment.
Calculates profitability (ROE, margins), liquidity (current ratio), leverage, efficiency, and valuation (P/E, EV/EBITDA) ratios from financial statements in CSV, JSON, text, or Excel for investment analysis.
Share bugs, ideas, or general feedback.
宜睿禮券採用 「查詢再核銷」 的兩階段流程,每次授權核銷都是不可逆的真實金融交易。
宜睿禮券為 獨立折抵機制,不佔用 payment.selectedMethod,可與現金/信用卡/台新 ONE 碼並行使用。
[渲染端 Next.js] [Electron 主進程] [宜睿 API]
useEdenredPayment store
queryVoucher(voucherNumber)
→ ragdollAPI.devices.edenred.getVoucher(...)
→ edenredDevice.getVoucher()
→ readEdenredCredentials()
→ fetch(GET /api/vouchers/ETX_001-{ref})
→ parseProductCode()
→ 換算 remaining_value
← EdenredGetVoucherIpcResponse
authorizeVoucher(acceptancePointRef, value)
→ ragdollAPI.devices.edenred.authorization(...)
→ edenredDevice.authorization()
→ readEdenredCredentials()
→ fetch(POST /api/transactions)
← EdenredAuthorizationIpcResponse
(authorization_id, authorized_amount)
[累計 authorizedVouchers / totalAuthorizedAmount]
[收銀員可繼續加入下一張或結帳]
| 功能 | 檔案路徑 |
|---|---|
| 票券查詢 / 授權核銷裝置介面 | electron/main/third-party/edenred/index.ts |
| API endpoints、Base URL | electron/main/third-party/edenred/const.ts |
| IPC 型別定義 | electron/main/types/ipc-devices.ts(EdenredVoucher、EdenredGetVoucherIpcRequest、EdenredAuthorizationIpcRequest 等) |
| IPC handler 註冊 | electron/main/settings/ipc-main-setup.ts |
| IPC preload 橋接 | electron/main/settings/ipc-renderer-preload.ts |
| 功能 | 檔案路徑 |
|---|---|
| product_code 解析(parseProductCode) | shared/utils/edenred.ts |
| 錯誤代碼對照表(EdenredResponseCode) | shared/utils/edenred.ts |
| 收銀員可識別錯誤碼子集(EdenredSaleIdentifiedResponseCode) | shared/utils/edenred.ts |
| ProductCodeInfo 型別 | shared/utils/edenred.ts |
| 功能 | 檔案路徑 |
|---|---|
| 付款 Store(queryVoucher、authorizeVoucher actions) | next/lib/stores/checkout/payment/edenred/use-edenred-payment.ts |
| Store 型別(EdenredPaymentData、EdenredPaymentActions) | next/lib/stores/checkout/payment/edenred/type.ts |
| 付款頁面(Summary page,宜睿付款顯示與找零計算) | next/app/summary/page.tsx |
| 票券核銷對話框 UI 元件(EdenredVoucherPanel) | next/app/summary/components/payment-block/edenred-voucher-panel.tsx |
| 付款方式選擇區塊(含宜睿按鈕與折抵摘要) | next/app/summary/components/payment-block/index.tsx |
| 結帳按鈕(canCheckout 判斷含宜睿折抵) | next/app/summary/components/checkout-button.tsx |
| 銷售紀錄組裝(createSale 中的付款紀錄與 voucherTotal) | next/lib/stores/checkout/sale/use-sale.ts |
| 付款方式顯示名稱(EDENRED: '宜睿禮券') | next/lib/stores/checkout/sale/const.ts |
| 功能 | 檔案路徑 |
|---|---|
| E2E 測試(票券核銷流程) | test/e2e/1-checkout-flow/edenred-voucher.spec.ts |
| Electron 單元測試(edenredDevice) | test/electron/unit/third-party/edenred.test.ts |
| Store 整合測試(useEdenredPayment) | test/next/integration/stores/payment/edenred.test.ts |
| 工具函式測試(parseProductCode) | test/next/integration/utils/edenred.test.ts |
next/app/summary/page.tsx 的 handleQueryEdenredVoucher:
const handleQueryEdenredVoucher = async (voucherNumber: string): Promise<void> => {
await edenredActions.queryVoucher(voucherNumber);
};
useEdenredPayment store 的 queryVoucher action:
authorizedVouchers 中(防止重複核銷)status: 'QUERYING' 作為防重入鎖type EdenredGetVoucherIpcRequest = {
voucherNumber: string; // 票券編號
};
electron/main/third-party/edenred/index.ts 的 edenredDevice.getVoucher():
Step 1:從 pos_store 資料表讀取門市的 edenredClientId 與 edenredClientSecret
Step 2:發送 GET 請求至 /api/vouchers/ETX_001-{voucherNumber},Header 包含:
X-Client-id:門市 clientIdX-Client-secret:門市 clientSecretX-Correlation-Id:隨機 UUIDStep 3:驗證回應 meta.status === 'succeeded',失敗時從 meta.messages[0].code 取得錯誤代碼
Step 4:解析 product_code 並換算 remaining_value(詳見下方金額換算規則)
type EdenredGetVoucherIpcResponse = {
success: boolean;
data?: EdenredVoucher;
error?: string;
};
type EdenredVoucher = {
product_code: string; // 產品代碼(8 碼以上)
product_label: string; // 產品名稱
remaining_value: number; // 已換算的剩餘價值
expiration_date: string; // 到期日
ref: string; // 票券編號
};
收銀員在 EdenredVoucherPanel 的 VoucherInfoScene 確認折抵金額後觸發:
const handleAuthorizeEdenredVoucher = async (value: number): Promise<void> => {
await edenredActions.authorizeVoucher(`EDENRED_${Date.now()}`, value);
};
useEdenredPayment store 的 authorizeVoucher action:
status: 'AUTHORIZING' 防重入acceptancePointRef 加上 authorizedVouchers.length 後綴確保唯一authorizedVouchers,累加 totalAuthorizedAmountqueriedVoucher 讓收銀員可繼續加入下一張type EdenredAuthorizationIpcRequest = {
acceptancePointRef: string; // 交易碼(唯一值)
voucherRef: string; // 票券編號
value: number; // 折抵金額
};
edenredDevice.authorization():
Step 1:從 pos_store 讀取門市憑證
Step 2:發送 POST 請求至 /api/transactions?return_vouchers_info=true
{
"acceptance_point_ref": "EDENRED_1711012345_0",
"capture_mode": "auto",
"vouchers": [{
"value": 500,
"product_class": "ETX_001",
"ref": "ABC123456"
}]
}
Step 3:驗證回應 meta.status === 'succeeded'
Step 4:回傳 authorization_id 與 authorized_amount
type EdenredAuthorizationIpcResponse = {
success: boolean;
data?: EdenredAuthorizationData;
error?: string;
};
type EdenredAuthorizationData = {
authorization_id: string; // 宜睿授權交易碼
authorized_amount: number; // 實際核銷金額
};
8 碼以上字串,前 2 碼為分類資訊,第 3-8 碼為面額:
| 位置 | 代碼 | 意義 |
|---|---|---|
| 第 1 碼 | S | 單次使用 |
| 第 1 碼 | M | 多次使用 |
| 第 2 碼 | V | 面額券 |
| 第 2 碼 | P | 商品券 |
| 第 3-8 碼 | 6 位數字 | 面額值(000000 或 999999 視為 null) |
parseProductCode('SV010000') // => { category: '單次使用', type: '面額券', value: 10000 }
parseProductCode('MP000000') // => { category: '多次使用', type: '商品券', value: null }
parseProductCode('SV999999') // => { category: '單次使用', type: '面額券', value: null }
API 回傳的 remaining_value 原始值需依據券種換算為新台幣金額:
| 條件 | 換算公式 | 範例 |
|---|---|---|
| 多次使用(M) | rawValue / 100 | API 回傳 5000 → 實際 50 元 |
| 單次使用(S)+ 面額券(V)+ value 不為 null | rawValue × value | API 回傳 1,value=500 → 實際 500 元 |
| 其他(商品券等) | 直接使用 | API 回傳 1 → 實際 1 |
在 EdenredVoucherPanel 的 UI 中:
1 ~ Math.min(remainingValue, remainingTotal) 之間調整宜睿禮券 不佔用 payment.selectedMethod,而是獨立累計折抵:
edenredState.totalAuthorizedAmount:累計已核銷金額totalWithoutVoucher = total - edenredVoucherTotal:扣除禮券後的實際待付金額totalWithoutVouchernext/lib/stores/checkout/payment/use-payment.ts 的 canCheckout():
// 無 selectedMethod 時,若宜睿核銷金額已足夠 → 可結帳
if (!payment.selectedMethod) {
if (edenredData) return canCheckoutEdenred(edenredData, subtotal);
return false;
}
// 現金付款:收款金額 >= 應付金額 - 宜睿折抵
if (payment.selectedMethod === 'CASH') {
const edenredDiscount = edenredData?.totalAuthorizedAmount ?? 0;
return payment.cashReceived >= subtotal - edenredDiscount;
}
Summary 頁面的找零金額需扣除宜睿折抵:
const edenredTotal: number = edenredState.totalAuthorizedAmount;
const changeAmount = getChangeAmount(payment, Math.max(0, total - edenredTotal));
// getChangeAmount: cashReceived - subtotal(subtotal 已扣除宜睿折抵)
當宜睿禮券與其他付款方式並用時,顯示結構為:
付款方式
⊙ 現金 $1,000
⊙ 宜睿票券 $20
已付合計 $1,020
找零 $787
next/lib/stores/checkout/sale/use-sale.ts 的 createSale action 中,
宜睿禮券紀錄與主要付款方式紀錄分開組裝後合併:
// 宜睿紀錄:從 authorizedVouchers 逐張組裝
const edenredRecords = edenred.authorizedVouchers.map((voucher) => ({
type: 'EDENRED' as const,
amount: voucher.authorizedAmount,
edenredVoucherNumber: voucher.voucherRef,
edenredAuthorizationId: voucher.authorizationId,
}));
// 主要付款方式紀錄:金額為 totalWithoutVoucher(扣除宜睿折抵後)
const primaryPaymentRecords = (() => {
if (payment.selectedMethod === 'CASH' || payment.selectedMethod === 'CREDIT') {
return [{ type: payment.selectedMethod, amount: totalWithoutVoucher, ... }];
}
// ...其他付款方式
})();
// 合併
const paymentRecords = [...edenredRecords, ...primaryPaymentRecords];
const dbRecord: OfflineSaleLocalRecord = {
body: {
total, // 應付總額(不含宜睿折抵)
voucherTotal, // 宜睿禮券折抵總額(有使用時才寫入)
totalWithoutVoucher, // 扣除禮券後的實付金額(有使用時才寫入)
// ...其他欄位
},
lines: {
payments: paymentRecords, // 宜睿紀錄 + 主要付款紀錄
},
};
convertSaleToInvoiceItems() 會在品項列表末尾加入宜睿禮券負項:
// 若 voucherTotal > 0,將禮券折抵作為負項加入品項明細
// 因為地端程式會檢查 items 總計是否等於 total
...((sale.body.voucherTotal || 0) > 0
? [{ name: '宜睿禮券', amount: 1, price: -(total - totalWithoutVoucher) }]
: []),
useEdenredPayment store 的 status 欄位驅動 UI 顯示與防重入:
IDLE
↓ queryVoucher()
QUERYING ──────(查詢成功)──→ IDLE(queriedVoucher 已設定)
│ ↓ authorizeVoucher()
│ AUTHORIZING
│ │
│ ┌────────────────────┼────────────────────┐
│ ↓ ↓ ↓
│ SUCCESS ERROR ERROR
│ (voucher 移入 (授權失敗) (查詢失敗)
│ authorizedVouchers)
│ ↓
│ IDLE(可繼續查詢下一張)
↓
ERROR(查詢失敗)
↓ 重新查詢
IDLE
canCheckout(data, subtotal) 回傳 true 的條件:
authorizedVouchers.length > 0(至少有一張已核銷)status 不在 QUERYING 或 AUTHORIZINGtotalAuthorizedAmount >= subtotal| 環境 | Base URL |
|---|---|
| production / edge | https://voucher.ap.edenred.io/v1 |
| 其他(staging/dev) | https://xp-voucher-stg-sg-v1.sg-s1.cloudhub.io |
API endpoints:
GET {base}/api/vouchers/ETX_001-{voucherNumber}POST {base}/api/transactions?return_vouchers_info=true| 操作 | Channel | 定義位置 |
|---|---|---|
| 查詢票券 | devices:edenred:getVoucher | electron/main/settings/ipc-main-setup.ts |
| 授權核銷 | devices:edenred:authorization | electron/main/settings/ipc-main-setup.ts |
| 代碼 | 說明 | 處理方式 |
|---|---|---|
ERV0001 | 序號不存在 | 請顧客確認票券編號 |
ERV0004 | 序號已過期 | 無法使用,請顧客更換 |
ERV0006 | 序號已作廢 | 無法使用,請顧客聯繫宜睿 |
ERV0010 | 數量不足 | 餘額不足,調整折抵金額或更換票券 |
ERV0033 | 序號已經使用 | 單次使用券已核銷過 |
ERV0077 | 預先交易完成 | 票券已被使用 |
完整錯誤代碼對照表定義於 shared/utils/edenred.ts 的 EdenredResponseCode,
涵蓋 ERV0000 至 ERV9999 共 80+ 筆錯誤碼。非收銀員可識別的錯誤應由系統管理員介入。
宜睿 API 憑證(edenredClientId、edenredClientSecret)儲存於各門市的 pos_store 資料表中,
由 readEdenredCredentials() 依當前門市名稱動態讀取。
若憑證缺失,系統會提前拋出 {storeName} 門市缺少宜睿 API 憑證,請聯絡系統發展部 錯誤,
避免延遲至 API 呼叫時才失敗。