taiwan-payment-skill 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,851 +1,361 @@
1
1
  # {{TITLE}}
2
2
 
3
- > 此技能涵蓋台灣金流 API 整合開發,包含綠界科技 (ECPay)、藍新金流 (NewebPay)、統一金流 (PAYUNi) 三家服務商。
3
+ > {{DESCRIPTION}}
4
4
 
5
- ## 快速導覽
5
+ ## Quick Navigation
6
6
 
7
- ### 相關文件
8
- 使用此技能時,請參考專案中的 API 規格文件:
9
- - `references/ECPAY_PAYMENT_REFERENCE.md` - 綠界金流 API 規格
10
- - `references/NEWEBPAY_PAYMENT_REFERENCE.md` - 藍新金流 API 規格
11
- - `references/PAYUNI_PAYMENT_REFERENCE.md` - 統一金流 API 規格
12
- - [EXAMPLES.md](EXAMPLES.md) - 程式碼範例集
7
+ ### Related Documents
8
+ When using this skill, refer to the API reference documents in the project:
9
+ - `references/ECPAY_API_REFERENCE.md` - ECPay API Specification
10
+ - `references/SMILEPAY_API_REFERENCE.md` - SmilePay API Specification
11
+ - `references/AMEGO_API_REFERENCE.md` - Amego API Specification
12
+ - [EXAMPLES.md](EXAMPLES.md) - Code Examples
13
13
 
14
- ### 智能工具
15
- - `scripts/search.py` - BM25 搜索引擎(查詢 API、錯誤碼、欄位映射、付款方式)
16
- - `scripts/recommend.py` - 金流服務商推薦系統
17
- - `scripts/test_payment.py` - 付款測試工具
18
- - `data/` - CSV 數據檔(providers, operations, error-codes, field-mappings, payment-methods, troubleshooting, reasoning)
14
+ ### When to Use This Skill
15
+ - Developing e-invoice issuance functionality
16
+ - Integrating Taiwan E-Invoice provider APIs
17
+ - Implementing B2C or B2B invoices
18
+ - Handling invoice printing, void, and allowance
19
+ - Processing encryption/signatures (AES, MD5)
20
+ - Troubleshooting invoice API integration issues
19
21
 
20
- ### 何時使用此技能
21
- - 開發線上金流付款功能
22
- - 整合台灣金流服務商 API
23
- - 實作信用卡、ATM、超商、電子錢包等付款方式
24
- - 處理訂單查詢、退款、定期定額扣款
25
- - 處理加密簽章(SHA256、AES-256-CBC、AES-256-GCM)
26
- - 解決金流 API 整合問題
22
+ ## Invoice Types
27
23
 
28
- ## 智能搜索與推薦
24
+ ### B2C Invoice (Two-Part)
25
+ - Buyer without tax ID
26
+ - `BuyerIdentifier` = `0000000000`
27
+ - Amount is **tax-inclusive**
28
+ - Can use carrier or donation
29
+ - Example: General consumer purchase
29
30
 
30
- ### 搜索引擎 (search.py)
31
+ ### B2B Invoice (Three-Part)
32
+ - Buyer has 8-digit tax ID
33
+ - `BuyerIdentifier` = actual tax ID (validate format)
34
+ - Amount is **pre-tax**, requires separate tax calculation
35
+ - **Cannot** use carrier or donation
36
+ - Example: Company purchase
31
37
 
32
- 使用 BM25 算法在資料庫中搜索相關資訊:
38
+ ## Provider Comparison
33
39
 
34
- ```bash
35
- # 搜索服務商
36
- python scripts/search.py "ecpay" --domain provider
40
+ | Feature | ECPay | SmilePay | Amego |
41
+ |---------|-------|----------|-------|
42
+ | Test/Prod URL | Different URLs | Different URLs | **Same URL** |
43
+ | Authentication | AES encryption + HashKey/HashIV | Grvc + Verify_key | MD5 signature + App Key |
44
+ | Print Method | POST form submit | GET URL params | API returns PDF URL |
45
+ | B2B Amount Field | SalesAmount (pre-tax) | UnitTAX=N | DetailVat=0 |
46
+ | Transport Format | JSON (AES encrypted) | URL Parameters | JSON (URL Encoded) |
37
47
 
38
- # 搜索錯誤碼
39
- python scripts/search.py "10100058" --domain error
48
+ ## Implementation Steps
40
49
 
41
- # 搜索欄位映射
42
- python scripts/search.py "MerchantTradeNo" --domain field
50
+ ### 1. Service Architecture
43
51
 
44
- # 搜索付款方式
45
- python scripts/search.py "信用卡" --domain payment_method
46
-
47
- # 搜索疑難排解
48
- python scripts/search.py "CheckMacValue 錯誤" --domain troubleshoot
49
-
50
- # 全域搜索
51
- python scripts/search.py "金額計算" --domain all
52
-
53
- # JSON 輸出
54
- python scripts/search.py "ATM" --format json
55
- ```
56
-
57
- **搜索域:**
58
- | 域 | 說明 | CSV 檔案 |
59
- |-----|------|----------|
60
- | `provider` | 服務商比較 | providers.csv |
61
- | `operation` | API 操作端點 | operations.csv |
62
- | `error` | 錯誤碼查詢 | error-codes.csv |
63
- | `field` | 欄位映射 | field-mappings.csv |
64
- | `payment_method` | 付款方式 | payment-methods.csv |
65
- | `troubleshoot` | 疑難排解 | troubleshooting.csv |
66
- | `reasoning` | 推薦決策規則 | reasoning.csv |
67
-
68
- **域自動偵測:**
69
- 搜索引擎會自動偵測查詢內容並選擇最適合的域:
70
- - 錯誤碼格式(如 "10100058")→ error
71
- - 服務商名稱(如 "ECPay")→ provider
72
- - 付款方式(如 "信用卡"、"ATM")→ payment_method
73
- - API 欄位(如 "MerchantID")→ field
74
-
75
- ### 推薦系統 (recommend.py)
76
-
77
- 根據需求自動推薦最適合的金流服務商:
78
-
79
- ```bash
80
- # 高交易量電商
81
- python scripts/recommend.py "高交易量 穩定 電商"
82
- # → 推薦 ECPay (市佔率高,穩定性佳)
83
-
84
- # 多元支付需求
85
- python scripts/recommend.py "多元支付 LINE Pay Apple Pay"
86
- # → 推薦 NewebPay (支援 13 種付款方式)
87
-
88
- # API 設計優先
89
- python scripts/recommend.py "API RESTful JSON"
90
- # → 推薦 PAYUNi (RESTful JSON API)
91
-
92
- # JSON 輸出
93
- python scripts/recommend.py "新創公司 快速整合" --format json
94
-
95
- # 簡單文字輸出
96
- python scripts/recommend.py "會員制 定期扣款" --format simple
97
- ```
98
-
99
- **推薦關鍵字:**
100
- - **ECPay**: 穩定、市佔、高交易量、電商、ATM、超商、定期、訂閱、分期、發票、物流
101
- - **NewebPay**: 多元、支付方式、電子錢包、LINE、行動、記憶、會員、跨境
102
- - **PAYUNi**: API、JSON、RESTful、統一、新創
103
-
104
- **反模式警告:**
105
- 推薦系統會自動提示不建議的場景:
106
- - ECPay: 無技術資源、極簡需求
107
- - NewebPay: 簡單 API、單一支付
108
- - PAYUNi: 大型專案、完整文檔
109
-
110
- ### 付款測試工具 (test_payment.py)
111
-
112
- 快速測試金流服務商連線:
113
-
114
- ```bash
115
- # 測試 ECPay 連線
116
- python scripts/test_payment.py ecpay
117
-
118
- # 測試 NewebPay 連線
119
- python scripts/test_payment.py newebpay
120
-
121
- # 測試 PAYUNi 連線
122
- python scripts/test_payment.py payuni
123
-
124
- # 測試所有服務商
125
- python scripts/test_payment.py all
126
- ```
127
-
128
- ---
129
-
130
- ## 付款方式說明
131
-
132
- ### 信用卡支付
133
- - **一次付清**: 最常用的付款方式,2-3 天到帳
134
- - **信用卡分期**: 3/6/12/18/24/30 期,需最低金額 1000 元
135
- - **信用卡定期**: 週期扣款,適用訂閱制服務
136
- - **信用卡紅利**: 紅利折抵功能
137
- - **銀聯卡**: 需另外申請,支援中國銀聯
138
- - **美國運通卡**: 需另外申請
139
-
140
- ### 電子錢包
141
- - **Apple Pay**: 需申請,適合 iOS 用戶
142
- - **Google Pay**: 需申請,適合 Android 用戶
143
- - **Samsung Pay**: 需申請,三星手機專用
144
- - **LINE Pay**: 需申請,LINE 生態系整合
145
- - **玉山 Wallet**: 玉山銀行電子錢包
146
- - **台灣 Pay**: 官方行動支付,最高 49,999 元
147
-
148
- ### ATM 轉帳
149
- - **網路 ATM**: 即時轉帳,最高 49,999 元,1 天到帳
150
- - **ATM 虛擬帳號**: 產生專屬繳費帳號,1-3 天到帳,最高 49,999 元
151
-
152
- ### 超商支付
153
- - **超商代碼**: 至四大超商繳費,30-20,000 元,1-3 天到帳
154
- - **超商條碼**: 產生繳費條碼,20-40,000 元,1-3 天到帳
155
-
156
- ### 其他支付方式
157
- - **TWQR**: 台灣 Pay QR Code 掃碼支付
158
- - **BNPL 無卡分期**: 先買後付,50-300,000 元
159
- - **AFTEE**: PAYUNi 專屬先享後付
160
- - **iCash**: PAYUNi 專屬愛金卡支付
161
- - **簡單付支付寶/微信**: 跨境支付(中國市場)
162
-
163
- ## 三家服務商特性比較
164
-
165
- | 特性 | 綠界 ECPay | 藍新 NewebPay | 統一 PAYUNi |
166
- |------|-----------|--------------|------------|
167
- | 加密方式 | URL Encode + SHA256 | AES-256-CBC + SHA256 雙層 | AES-256-GCM + SHA256 |
168
- | API 風格 | Form POST | Form POST + AES | RESTful JSON |
169
- | 內容格式 | application/x-www-form-urlencoded | application/x-www-form-urlencoded | application/json |
170
- | 測試/正式 URL | 不同 URL | 不同 URL | 不同 URL |
171
- | 市佔率 | 最高 | 高 | 中等 |
172
- | 支付方式 | 11 種(含 BNPL、TWQR) | 13 種(含 LINE Pay、Apple Pay) | 8 種(含 AFTEE、iCash) |
173
- | 特色功能 | 完整文檔、SDK、同時支援發票物流 | MPG 整合、信用卡記憶、多元電子錢包 | RESTful 設計、統一集團背景 |
174
- | 適用場景 | 高交易量電商、傳統產業、PHP 開發 | 多元支付、會員制、行動 App | 新創公司、API 優先、Node.js 開發 |
175
-
176
- ### ECPay 特性
177
- - **優勢**: 市佔率最高、穩定性最佳、文檔完整、社群資源豐富、測試帳號可用
178
- - **加密**: URL Encode + SHA256(參數排序 + HashKey + HashIV)
179
- - **傳輸**: Form POST,application/x-www-form-urlencoded
180
- - **特色**: 同時支援金流、發票、物流三合一服務
181
-
182
- ### NewebPay 特性
183
- - **優勢**: 支援最多支付方式(13 種)、MPG 整合、信用卡記憶功能、完整電子錢包
184
- - **加密**: AES-256-CBC 加密 TradeInfo,再計算 SHA256 TradeSha
185
- - **傳輸**: Form POST,雙層加密(AES + SHA256)
186
- - **特色**: LINE Pay、Apple Pay、Google Pay 原生支援
187
-
188
- ### PAYUNi 特性
189
- - **優勢**: RESTful JSON API、統一集團背景、AES-GCM 現代加密
190
- - **加密**: AES-256-GCM 加密 + SHA256 簽章
191
- - **傳輸**: RESTful JSON,application/json
192
- - **特色**: AFTEE 先享後付、iCash 愛金卡(獨家)
193
-
194
- ## 開發實作步驟
195
-
196
- ### 1. 服務實作架構
197
-
198
- 創建服務時遵循以下結構:
52
+ Create services following this structure:
199
53
 
200
54
  ```typescript
201
- // lib/services/payment-provider.ts - 介面定義
202
- export interface PaymentService {
203
- createOrder(userId: string, data: PaymentOrderData): Promise<PaymentOrderResponse>
204
- queryOrder(userId: string, merchantTradeNo: string): Promise<PaymentQueryResponse>
205
- refundOrder(userId: string, tradeNo: string, amount: number): Promise<PaymentRefundResponse>
206
- createPeriodic(userId: string, data: PeriodicPaymentData): Promise<PeriodicResponse>
55
+ // lib/services/invoice-provider.ts - Interface definition
56
+ export interface InvoiceService {
57
+ issueInvoice(userId: string, data: InvoiceIssueData): Promise<InvoiceIssueResponse>
58
+ voidInvoice(userId: string, invoiceNumber: string, reason: string): Promise<InvoiceVoidResponse>
59
+ printInvoice(userId: string, invoiceNumber: string): Promise<InvoicePrintResponse>
207
60
  }
208
61
 
209
- // lib/services/{provider}-payment-service.ts - 各服務商實作
210
- export class ECPayPaymentService implements PaymentService {
211
- private generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
212
- // SHA256 簽章實作
62
+ // lib/services/{provider}-invoice-service.ts - Provider implementation
63
+ export class ECPayInvoiceService implements InvoiceService {
64
+ private async encryptData(data: any, hashKey: string, hashIV: string): Promise<string> {
65
+ // AES-128-CBC encryption implementation
213
66
  }
214
67
 
215
- async createOrder(userId: string, data: PaymentOrderData) {
216
- // 1. 取得使用者設定
217
- // 2. 準備 API 資料
218
- // 3. 計算 CheckMacValue
219
- // 4. 生成表單 HTML
220
- // 5. 回傳標準格式
68
+ async issueInvoice(userId: string, data: InvoiceIssueData) {
69
+ // 1. Get user settings
70
+ // 2. Prepare API data
71
+ // 3. Encrypt and sign
72
+ // 4. Send request
73
+ // 5. Decrypt response
74
+ // 6. Return standard format
221
75
  }
222
76
  }
223
77
  ```
224
78
 
225
- ### 2. 加密實作
226
-
227
- **綠界 (ECPay) - SHA256 簽章:**
228
-
229
- ```typescript
230
- import crypto from 'crypto'
231
-
232
- function generateECPayCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string): string {
233
- // 1. 移除 CheckMacValue 本身
234
- const { CheckMacValue, ...cleanParams } = params
235
-
236
- // 2. 依照 key 排序(字母順序)
237
- const sortedKeys = Object.keys(cleanParams).sort()
238
-
239
- // 3. 組合參數字串: key1=value1&key2=value2
240
- const paramString = sortedKeys
241
- .map(key => `${key}=${cleanParams[key]}`)
242
- .join('&')
243
-
244
- // 4. 前後加上 HashKey 和 HashIV
245
- const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
246
-
247
- // 5. URL Encode (lowercase)
248
- const encoded = encodeURIComponent(rawString).toLowerCase()
249
-
250
- // 6. SHA256 雜湊
251
- const hash = crypto.createHash('sha256').update(encoded).digest('hex')
79
+ ### 2. Amount Calculation
252
80
 
253
- // 7. 轉大寫
254
- return hash.toUpperCase()
255
- }
256
- ```
257
-
258
- **藍新 (NewebPay) - AES-256-CBC 雙層加密:**
81
+ **Tax-inclusive total -> Pre-tax amount + Tax:**
259
82
 
260
83
  ```typescript
261
- function encryptNewebPay(data: Record<string, any>, hashKey: string, hashIV: string): {
262
- TradeInfo: string,
263
- TradeSha: string
264
- } {
265
- // 1. 轉換為查詢字串
266
- const queryString = new URLSearchParams(data).toString()
267
-
268
- // 2. AES-256-CBC 加密
269
- const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
270
- cipher.setAutoPadding(true)
271
- let encrypted = cipher.update(queryString, 'utf8', 'hex')
272
- encrypted += cipher.final('hex')
273
-
274
- // 3. 計算 SHA256
275
- const tradeSha = crypto
276
- .createHash('sha256')
277
- .update(`HashKey=${hashKey}&${encrypted}&HashIV=${hashIV}`)
278
- .digest('hex')
279
- .toUpperCase()
280
-
281
- return {
282
- TradeInfo: encrypted,
283
- TradeSha: tradeSha
84
+ function calculateInvoiceAmounts(totalAmount: number, isB2B: boolean) {
85
+ if (isB2B) {
86
+ // B2B: Need to split tax
87
+ const taxAmount = Math.round(totalAmount - (totalAmount / 1.05))
88
+ const salesAmount = totalAmount - taxAmount
89
+ return { salesAmount, taxAmount, totalAmount }
90
+ } else {
91
+ // B2C: Tax-inclusive total
92
+ return { salesAmount: totalAmount, taxAmount: 0, totalAmount }
284
93
  }
285
94
  }
286
95
 
287
- function decryptNewebPay(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
288
- const decipher = crypto.createDecipheriv('aes-256-cbc', hashKey, hashIV)
289
- decipher.setAutoPadding(true)
290
- let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
291
- decrypted += decipher.final('utf8')
292
-
293
- return Object.fromEntries(new URLSearchParams(decrypted))
294
- }
96
+ // Example
97
+ const amounts = calculateInvoiceAmounts(1050, true)
98
+ // { salesAmount: 1000, taxAmount: 50, totalAmount: 1050 }
295
99
  ```
296
100
 
297
- **統一 (PAYUNi) - AES-256-GCM 加密:**
298
-
299
- ```typescript
300
- function encryptPAYUNi(data: Record<string, any>, hashKey: string, hashIV: string): {
301
- EncryptInfo: string,
302
- HashInfo: string
303
- } {
304
- // 1. JSON 字串化
305
- const jsonString = JSON.stringify(data)
101
+ ### 3. Encryption Implementation
306
102
 
307
- // 2. AES-256-GCM 加密
308
- const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
309
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
310
- encrypted += cipher.final('hex')
103
+ **ECPay - AES Encryption:**
311
104
 
312
- // 3. 取得 Auth Tag (16 bytes)
313
- const authTag = cipher.getAuthTag().toString('hex')
105
+ ```typescript
106
+ import crypto from 'crypto'
314
107
 
315
- // 4. 組合加密資料 (encrypted + tag)
316
- const encryptInfo = encrypted + authTag
108
+ function encryptECPay(data: object, hashKey: string, hashIV: string): string {
109
+ // 1. Convert JSON to string and URL encode
110
+ const jsonString = JSON.stringify(data)
111
+ const urlEncoded = encodeURIComponent(jsonString)
317
112
 
318
- // 5. SHA256 簽章
319
- const hashInfo = crypto
320
- .createHash('sha256')
321
- .update(`HashKey=${hashKey}&${encryptInfo}&HashIV=${hashIV}`)
322
- .digest('hex')
323
- .toUpperCase()
113
+ // 2. AES-128-CBC encryption
114
+ const cipher = crypto.createCipheriv('aes-128-cbc', hashKey, hashIV)
115
+ let encrypted = cipher.update(urlEncoded, 'utf8', 'base64')
116
+ encrypted += cipher.final('base64')
324
117
 
325
- return {
326
- EncryptInfo: encryptInfo,
327
- HashInfo: hashInfo
328
- }
118
+ return encrypted
329
119
  }
330
120
 
331
- function decryptPAYUNi(encryptedData: string, hashKey: string, hashIV: string): Record<string, any> {
332
- // 1. 分離加密內容和 Auth Tag (最後 32 個字元 = 16 bytes hex)
333
- const encryptedContent = encryptedData.slice(0, -32)
334
- const authTag = Buffer.from(encryptedData.slice(-32), 'hex')
335
-
336
- // 2. AES-256-GCM 解密
337
- const decipher = crypto.createDecipheriv('aes-256-gcm', hashKey, hashIV)
338
- decipher.setAuthTag(authTag)
339
- let decrypted = decipher.update(encryptedContent, 'hex', 'utf8')
121
+ function decryptECPay(encryptedData: string, hashKey: string, hashIV: string): object {
122
+ const decipher = crypto.createDecipheriv('aes-128-cbc', hashKey, hashIV)
123
+ let decrypted = decipher.update(encryptedData, 'base64', 'utf8')
340
124
  decrypted += decipher.final('utf8')
341
125
 
342
- return JSON.parse(decrypted)
126
+ const urlDecoded = decodeURIComponent(decrypted)
127
+ return JSON.parse(urlDecoded)
343
128
  }
344
129
  ```
345
130
 
346
- ### 3. 訂單建立流程
347
-
348
- **關鍵:各服務商都使用 Form POST 導向付款頁**
131
+ **Amego - MD5 Signature:**
349
132
 
350
133
  ```typescript
351
- // 後端產生付款表單
352
- async function createPaymentOrder(provider: string, orderData: OrderData) {
353
- const service = PaymentServiceFactory.getService(provider)
354
- const result = await service.createOrder(userId, orderData)
355
-
356
- // 儲存訂單資訊
357
- await prisma.order.update({
358
- where: { id: orderId },
359
- data: {
360
- merchantTradeNo: result.merchantTradeNo,
361
- paymentProvider: provider, // **重要**:儲存使用的服務商
362
- paymentMethod: orderData.paymentMethod,
363
- status: 'PENDING'
364
- }
365
- })
366
-
367
- // 回傳表單資料
368
- return {
369
- type: 'form',
370
- action: result.formAction,
371
- method: result.formMethod,
372
- params: result.formParams
373
- }
374
- }
375
-
376
- // 前端提交表單
377
- function submitPaymentForm(formData: PaymentFormData) {
378
- const form = document.createElement('form')
379
- form.method = formData.method
380
- form.action = formData.action
381
- form.target = '_self' // 整頁跳轉
382
-
383
- // 添加隱藏欄位
384
- Object.entries(formData.params).forEach(([key, value]) => {
385
- const input = document.createElement('input')
386
- input.type = 'hidden'
387
- input.name = key
388
- input.value = value
389
- form.appendChild(input)
390
- })
391
-
392
- document.body.appendChild(form)
393
- form.submit()
134
+ function generateAmegoSign(data: object, time: number, appKey: string): string {
135
+ const dataString = JSON.stringify(data)
136
+ const signString = dataString + time + appKey
137
+ return crypto.createHash('md5').update(signString).digest('hex')
394
138
  }
395
139
  ```
396
140
 
397
- ### 4. 付款通知處理
141
+ ### 4. Provider Binding
398
142
 
399
- **ReturnURL 處理(付款完成後):**
143
+ **Key: When issuing invoice, must record the provider used so printing can call the correct one**
400
144
 
401
145
  ```typescript
402
- // app/api/payment/callback/route.ts
403
- export async function POST(request: Request) {
404
- const formData = await request.formData()
405
- const params = Object.fromEntries(formData)
406
-
407
- // 1. 偵測服務商(根據欄位判斷)
408
- const provider = detectProvider(params)
409
- const service = PaymentServiceFactory.getService(provider)
410
-
411
- // 2. 驗證簽章
412
- const isValid = service.verifyCallback(params)
413
- if (!isValid) {
414
- return new Response('0|CheckMacValue Error', { status: 400 })
146
+ // Save provider when issuing
147
+ await prisma.financialRecord.update({
148
+ where: { id: recordId },
149
+ data: {
150
+ invoiceNo: result.invoiceNumber,
151
+ invoiceProvider: actualProvider, // 'ECPAY' | 'SMILEPAY' | 'AMEGO'
152
+ invoiceRandomNum: result.randomNumber, // **Important**: needed for printing
153
+ invoiceDate: new Date(),
415
154
  }
155
+ })
416
156
 
417
- // 3. 更新訂單狀態
418
- const merchantTradeNo = params.MerchantTradeNo || params.MerchantOrderNo
419
- await prisma.order.update({
420
- where: { merchantTradeNo },
421
- data: {
422
- status: params.RtnCode === '1' ? 'PAID' : 'FAILED',
423
- paidAt: new Date(),
424
- tradeNo: params.TradeNo, // **重要**:儲存金流商訂單號(退款時需要)
425
- paymentDetails: params
426
- }
427
- })
428
-
429
- // 4. 回應固定格式
430
- return new Response('1|OK') // ECPay/NewebPay 要求
431
- }
157
+ // Use the issuing provider when printing
158
+ const service = record.invoiceProvider
159
+ ? InvoiceServiceFactory.getService(record.invoiceProvider)
160
+ : await InvoiceServiceFactory.getServiceForUser(userId)
432
161
  ```
433
162
 
434
- ### 5. 查詢訂單
163
+ ### 5. Print Response Handling
435
164
 
436
- ```typescript
437
- async function queryPaymentOrder(merchantTradeNo: string) {
438
- // 1. 查詢訂單取得服務商
439
- const order = await prisma.order.findUnique({
440
- where: { merchantTradeNo }
441
- })
442
-
443
- // 2. 使用訂單記錄的服務商查詢
444
- const service = PaymentServiceFactory.getService(order.paymentProvider)
445
- const result = await service.queryOrder(order.userId, merchantTradeNo)
165
+ Frontend needs to handle based on response type:
446
166
 
447
- return result
167
+ ```typescript
168
+ // Backend response format
169
+ interface InvoicePrintResponse {
170
+ success: boolean
171
+ type?: 'html' | 'redirect' | 'form'
172
+ htmlContent?: string // ECPay
173
+ printUrl?: string // SmilePay/Amego
174
+ formUrl?: string
175
+ formParams?: Record<string, string>
448
176
  }
449
- ```
450
177
 
451
- ### 6. 退款處理
452
-
453
- ```typescript
454
- async function refundPaymentOrder(merchantTradeNo: string, refundAmount: number) {
455
- const order = await prisma.order.findUnique({
456
- where: { merchantTradeNo }
457
- })
458
-
459
- // **重要**:使用開立時的服務商和 TradeNo
460
- const service = PaymentServiceFactory.getService(order.paymentProvider)
461
- const result = await service.refundOrder(
462
- order.userId,
463
- order.tradeNo, // 金流商訂單號
464
- refundAmount
465
- )
466
-
467
- // 更新訂單狀態
468
- await prisma.order.update({
469
- where: { merchantTradeNo },
470
- data: {
471
- status: 'REFUNDED',
472
- refundAmount,
473
- refundedAt: new Date()
474
- }
475
- })
476
-
477
- return result
178
+ // Frontend handling example
179
+ if (result.type === 'html') {
180
+ const win = window.open('', '_blank')
181
+ win.document.write(result.htmlContent)
182
+ } else if (result.type === 'redirect') {
183
+ window.open(result.url, '_blank')
184
+ } else if (result.type === 'form') {
185
+ // Dynamically create form submission
186
+ const form = document.createElement('form')
187
+ form.method = 'POST'
188
+ form.action = result.formUrl
189
+ form.target = '_blank'
190
+ // ... add parameters
191
+ form.submit()
478
192
  }
479
193
  ```
480
194
 
481
- ## 常見問題排除
195
+ ## Common Issues
482
196
 
483
- ### 問題 1: CheckMacValue 驗證失敗
197
+ ### Issue 1: Invoice issuance failed with unclear error
484
198
 
485
- **錯誤訊息:** ECPay 回傳 `10100058`,NewebPay 回傳 `CheckValue Error`
199
+ **Diagnostic steps:**
200
+ 1. Check logger output for complete error in `raw` field
201
+ 2. Confirm environment variables (test/prod) are correct
202
+ 3. Verify required fields are complete
486
203
 
487
- **常見原因:**
488
- 1. 參數排序錯誤(必須按照字母順序)
489
- 2. URL Encode 不正確(ECPay 需要 lowercase)
490
- 3. 編碼問題(UTF-8)
491
- 4. 忘記移除 CheckMacValue 本身
492
-
493
- **解決方案:**
494
- ```typescript
495
- // ✅ 正確
496
- function generateCheckMacValue(params: Record<string, any>, hashKey: string, hashIV: string) {
497
- // 1. 移除 CheckMacValue
498
- const { CheckMacValue, ...cleanParams } = params
204
+ **ECPay common errors:**
205
+ - `10000006`: RelateNumber duplicate -> Order number already used
206
+ - `10000016`: Amount calculation error -> Check B2C/B2B calculation
207
+ - `10000019`: Cannot use carrier with tax ID -> Remove CarrierType
499
208
 
500
- // 2. 排序
501
- const sortedKeys = Object.keys(cleanParams).sort()
209
+ **SmilePay common errors:**
210
+ - `-10066`: AllAmount validation error -> Check if TotalAmount was sent
211
+ - `-10084`: orderid format error -> Limit to 30 characters
212
+ - `-10053`: Carrier number error -> Validate mobile barcode format
502
213
 
503
- // 3. 組合字串
504
- const paramString = sortedKeys.map(k => `${k}=${cleanParams[k]}`).join('&')
214
+ **Amego common errors:**
215
+ - `1002`: OrderId already exists -> Use unique order number
216
+ - `1007`: Amount calculation error -> Check DetailVat setting
217
+ - `1012`: B2B invoice cannot use carrier or donation
505
218
 
506
- // 4. 加上 HashKey/HashIV
507
- const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
219
+ ### Issue 2: Print shows "Invoice not found"
508
220
 
509
- // 5. URL Encode (lowercase for ECPay)
510
- const encoded = encodeURIComponent(rawString).toLowerCase()
221
+ **Solution:**
222
+ Confirm `invoiceProvider` field is saved correctly, use the issuing provider when printing.
511
223
 
512
- // 6. SHA256
513
- return crypto.createHash('sha256').update(encoded).digest('hex').toUpperCase()
514
- }
515
-
516
- // 錯誤:未排序
517
- const paramString = Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&')
224
+ ```typescript
225
+ // Correct: Use provider from invoice record
226
+ const service = record.invoiceProvider
227
+ ? InvoiceServiceFactory.getService(record.invoiceProvider)
228
+ : await InvoiceServiceFactory.getServiceForUser(userId)
518
229
 
519
- // 錯誤:URL Encode 使用 uppercase
520
- const encoded = encodeURIComponent(rawString) // 應該用 toLowerCase()
230
+ // Incorrect: Use user's current default provider
231
+ const service = await InvoiceServiceFactory.getServiceForUser(userId)
521
232
  ```
522
233
 
523
- ### 問題 2: 訂單編號重複
234
+ ### Issue 3: B2B invoice amount error
524
235
 
525
- **錯誤訊息:** ECPay `10100003`,NewebPay/PAYUNi 訂單已存在
236
+ **Amount fields by provider:**
526
237
 
527
- **原因:** 使用相同的 MerchantTradeNo
528
-
529
- **解決方案:**
530
238
  ```typescript
531
- // ✅ 建議:加入時間戳保證唯一性
532
- function generateMerchantTradeNo(prefix: string = 'ORD') {
533
- const timestamp = Date.now()
534
- const random = Math.random().toString(36).substring(2, 8).toUpperCase()
535
- return `${prefix}${timestamp}${random}`.substring(0, 20) // ECPay 限制 20 字元
239
+ // ECPay
240
+ const b2bData = {
241
+ SalesAmount: 1000, // Pre-tax sales
242
+ TaxAmount: 50, // Tax
243
+ TotalAmount: 1050, // Total
244
+ ItemPrice: 100, // Item unit price (pre-tax)
245
+ ItemAmount: 1000, // Item subtotal (pre-tax)
246
+ ItemTax: 50 // Item tax
536
247
  }
537
248
 
538
- // 範例輸出: ORD1706512345A7B2
539
- ```
540
-
541
- ### 問題 3: 金額計算錯誤
542
-
543
- **錯誤訊息:** 回傳金額驗算錯誤
544
-
545
- **常見原因:**
546
- 1. 金額包含小數
547
- 2. 金額為負數或 0
548
- 3. 商品金額總和不等於訂單金額
549
-
550
- **解決方案:**
551
- ```typescript
552
- // ✅ 確保金額為正整數
553
- function validateAmount(amount: number): number {
554
- if (amount <= 0) {
555
- throw new Error('金額必須大於 0')
556
- }
557
- return Math.round(amount) // 移除小數
249
+ // SmilePay
250
+ const b2bData = {
251
+ AllAmount: '1050', // Tax-inclusive total
252
+ SalesAmount: '1000', // Pre-tax sales (optional but recommended)
253
+ TaxAmount: '50', // Tax (optional)
254
+ UnitTAX: 'N', // **Important**: Unit price is pre-tax
255
+ UnitPrice: '100', // Item unit price (pre-tax)
256
+ Amount: '1000' // Item subtotal (pre-tax)
558
257
  }
559
258
 
560
- // ✅ 驗證商品金額總和
561
- function validateItemsAmount(items: Item[], totalAmount: number) {
562
- const itemsSum = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
563
- if (Math.round(itemsSum) !== Math.round(totalAmount)) {
564
- throw new Error(`商品金額總和 ${itemsSum} 不等於訂單金額 ${totalAmount}`)
565
- }
259
+ // Amego
260
+ const b2bData = {
261
+ DetailVat: 0, // **Important**: 0=pre-tax
262
+ SalesAmount: 1000, // Pre-tax sales
263
+ TaxAmount: 50, // Tax
264
+ TotalAmount: 1050, // Total
265
+ ProductItem: [{
266
+ UnitPrice: 100, // Item unit price (pre-tax)
267
+ Amount: 1000 // Item subtotal (pre-tax)
268
+ }]
566
269
  }
567
270
  ```
568
271
 
569
- ### 問題 4: 收不到付款通知
272
+ ### Issue 4: SmilePay print blank
570
273
 
571
- **原因:**
572
- 1. ReturnURL 不是 HTTPS
573
- 2. 防火牆阻擋
574
- 3. 回應格式錯誤
274
+ **Cause:** Using `type: 'form'` when response has `method: 'GET'`
575
275
 
576
- **解決方案:**
276
+ **Solution:**
577
277
  ```typescript
578
- // ✅ 確認 ReturnURL 使用 HTTPS
579
- const returnURL = 'https://yourdomain.com/api/payment/callback' // 必須 HTTPS
580
-
581
- // ✅ 正確回應格式
582
- export async function POST(request: Request) {
583
- // ... 處理邏輯
584
-
585
- // ECPay/NewebPay 需要回應 "1|OK"
586
- return new Response('1|OK', {
587
- status: 200,
588
- headers: { 'Content-Type': 'text/plain' }
589
- })
278
+ // Correct
279
+ if (printData.method === 'GET' && printData.url) {
280
+ return { type: 'redirect', url: printData.url }
590
281
  }
591
282
 
592
- // ❌ 錯誤:JSON 回應
593
- return Response.json({ success: true }) // 不正確
594
- ```
595
-
596
- ### 問題 5: 測試卡無法付款
597
-
598
- **原因:** 使用真實卡號或測試卡格式錯誤
599
-
600
- **解決方案:**
601
- ```
602
- # ECPay 測試卡
603
- 卡號: 4311-9522-2222-2222
604
- 有效期: 任意未來月年
605
- CVV: 任意 3 碼
606
-
607
- # NewebPay 測試卡
608
- 卡號: 4000-2211-1111-1111
609
- 有效期: 任意未來月年
610
- CVV: 任意 3 碼
611
-
612
- # PAYUNi 測試卡
613
- 請至後台查詢官方測試卡號
283
+ // Incorrect
284
+ return { type: 'form', url: printData.url, params: printData.params }
614
285
  ```
615
286
 
616
- ### 問題 6: AES 加密失敗
287
+ ### Issue 5: Timestamp expired
617
288
 
618
- **NewebPay AES-256-CBC 加密錯誤:**
289
+ **ECPay error 10000005:** Timestamp exceeds 10 minutes
619
290
 
291
+ **Solution:**
620
292
  ```typescript
621
- // 確認 Key/IV 長度
622
- const hashKey = 'your32BytesHashKeyHere123456' // 必須 32 bytes
623
- const hashIV = 'your16BytesIV123' // 必須 16 bytes
293
+ // Ensure using current timestamp
294
+ const timestamp = Math.floor(Date.now() / 1000)
624
295
 
625
- // 使用正確的 padding
626
- const cipher = crypto.createCipheriv('aes-256-cbc', hashKey, hashIV)
627
- cipher.setAutoPadding(true) // PKCS7 padding
296
+ // Amego: tolerance is +/- 60 seconds
297
+ const time = Math.floor(Date.now() / 1000)
628
298
  ```
629
299
 
630
- **PAYUNi AES-256-GCM 加密錯誤:**
300
+ ## Test Accounts
631
301
 
632
- ```typescript
633
- // ✅ 記得附加 Auth Tag
634
- const cipher = crypto.createCipheriv('aes-256-gcm', hashKey, hashIV)
635
- let encrypted = cipher.update(jsonString, 'utf8', 'hex')
636
- encrypted += cipher.final('hex')
637
- const authTag = cipher.getAuthTag().toString('hex') // **重要**
638
- const encryptInfo = encrypted + authTag // 總長度 = encrypted + 32 chars (16 bytes hex)
302
+ ### ECPay Test Environment
639
303
  ```
640
-
641
- ### 問題 7: ATM 虛擬帳號未產生
642
-
643
- **原因:**
644
- 1. 未設定 ChoosePayment=ATM
645
- 2. ExpireDate 格式錯誤
646
- 3. ExpireDate 超過範圍(3-60 天)
647
-
648
- **解決方案:**
649
- ```typescript
650
- // ✅ ECPay ATM 設定
651
- const params = {
652
- ChoosePayment: 'ATM',
653
- ExpireDate: 3, // 3-60 天
654
- PaymentInfoURL: 'https://yourdomain.com/api/payment/atm-info', // 接收帳號通知
655
- // ...
656
- }
657
-
658
- // ✅ NewebPay ATM 設定
659
- const params = {
660
- VACC: 1, // 啟用 ATM
661
- ExpireDate: '2024-12-31', // yyyy-MM-dd 格式
662
- // ...
663
- }
304
+ MerchantID: 2000132
305
+ HashKey: ejCk326UnaZWKisg
306
+ HashIV: q9jcZX8Ib9LM8wYk
307
+ URL: https://einvoice-stage.ecpay.com.tw
664
308
  ```
665
309
 
666
- ### 問題 8: 定期定額建立失敗
667
-
668
- **原因:** 週期參數不完整
669
-
670
- **解決方案:**
671
- ```typescript
672
- // ✅ ECPay 定期定額
673
- const periodicParams = {
674
- PeriodAmount: 1000, // 扣款金額
675
- PeriodType: 'M', // D=日, M=月, Y=年
676
- Frequency: 1, // 頻率(每 1 個週期)
677
- ExecTimes: 12, // 執行次數(12 次)
678
- PeriodReturnURL: 'https://yourdomain.com/api/payment/periodic-callback',
679
- // ...
680
- }
681
-
682
- // ✅ NewebPay 定期定額
683
- const periodicParams = {
684
- PeriodAmt: 1000,
685
- PeriodType: 'M',
686
- PeriodPoint: '01', // 每月 1 號扣款
687
- PeriodTimes: 12,
688
- // ...
689
- }
310
+ ### SmilePay Test Environment
690
311
  ```
691
-
692
- ### 問題 9: 跨域問題
693
-
694
- **錯誤:** 前端導向付款頁失敗
695
-
696
- **原因:** 使用 AJAX 或 Fetch,受 CORS 限制
697
-
698
- **解決方案:**
699
- ```typescript
700
- // ❌ 錯誤:使用 AJAX
701
- fetch(paymentUrl, { method: 'POST', body: formData }) // 會被 CORS 阻擋
702
-
703
- // ✅ 正確:使用 Form POST 整頁跳轉
704
- function submitPaymentForm(action: string, params: Record<string, string>) {
705
- const form = document.createElement('form')
706
- form.method = 'POST'
707
- form.action = action
708
- form.target = '_self' // 整頁跳轉
709
-
710
- Object.entries(params).forEach(([key, value]) => {
711
- const input = document.createElement('input')
712
- input.type = 'hidden'
713
- input.name = key
714
- input.value = value
715
- form.appendChild(input)
716
- })
717
-
718
- document.body.appendChild(form)
719
- form.submit() // 直接提交
720
- }
312
+ Grvc: SEI1000034
313
+ Verify_key: 9D73935693EE0237FABA6AB744E48661
314
+ Test Tax ID: 80129529
315
+ URL: https://ssl.smse.com.tw/api_test/SPEinvoice_Storage.asp
721
316
  ```
722
317
 
723
- ### 問題 10: 商店代號錯誤
724
-
725
- **錯誤訊息:** 回傳商店不存在
318
+ ### Amego Test Environment
319
+ ```
320
+ Tax ID: 12345678
321
+ App Key: sHeq7t8G1wiQvhAuIM27
322
+ Admin: https://invoice.amego.tw/
323
+ Test Account: test@amego.tw
324
+ Test Password: 12345678
325
+ ```
726
326
 
727
- **原因:**
728
- 1. MerchantID 錯誤
729
- 2. 測試/正式環境混用
327
+ ## Development Checklist
730
328
 
731
- **解決方案:**
732
- ```typescript
733
- // ✅ 使用環境變數區分
734
- const config = {
735
- merchantID: process.env.NODE_ENV === 'production'
736
- ? process.env.ECPAY_MERCHANT_ID_PROD
737
- : process.env.ECPAY_MERCHANT_ID_TEST,
738
- apiUrl: process.env.NODE_ENV === 'production'
739
- ? 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5'
740
- : 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'
741
- }
742
- ```
329
+ Use this checklist to ensure complete implementation:
743
330
 
744
- ## 測試帳號
331
+ - [ ] Implement `InvoiceService` interface
332
+ - [ ] Handle B2C / B2B amount calculation differences
333
+ - [ ] Implement encryption/signature mechanism (AES or MD5)
334
+ - [ ] Save `invoiceProvider` field
335
+ - [ ] Save `invoiceRandomNum` (needed for printing)
336
+ - [ ] Handle print response types (html/redirect/form)
337
+ - [ ] Implement error handling and logging
338
+ - [ ] Test environment verification
339
+ - [ ] Handle carrier and donation mutual exclusion
340
+ - [ ] Validate tax ID format (8-digit number)
745
341
 
746
- ### 綠界測試環境
747
- ```
748
- MerchantID: 3002607
749
- HashKey: pwFHCqoQZGmho4w6
750
- HashIV: EkRm7iFT261dpevs
751
- 測試 URL: https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5
752
- 測試卡號: 4311-9522-2222-2222
753
- 後台: https://vendor-stage.ecpay.com.tw/
754
- ```
342
+ ## Adding New Provider
755
343
 
756
- ### 藍新測試環境
757
- ```
758
- MerchantID: 請至後台申請
759
- HashKey: 請至後台申請(32 bytes)
760
- HashIV: 請至後台申請(16 bytes)
761
- 測試 URL: https://ccore.newebpay.com/MPG/mpg_gateway
762
- 測試卡號: 4000-2211-1111-1111
763
- 後台: https://cwww.newebpay.com/
764
- ```
344
+ 1. Create `{provider}-invoice-service.ts` in `lib/services/`
345
+ 2. Implement all methods of `InvoiceService` interface
346
+ 3. Register new provider in `InvoiceServiceFactory`
347
+ 4. Add option to `InvoiceProvider` enum in `prisma/schema.prisma`
348
+ 5. Run `prisma migrate` or `prisma db push`
349
+ 6. Update frontend settings page (`app/settings/invoice/page.tsx`)
350
+ 7. Write unit tests
765
351
 
766
- ### 統一測試環境
767
- ```
768
- MerchantID: 請至後台申請
769
- HashKey: 請至後台申請
770
- HashIV: 請至後台申請
771
- 測試 URL: https://sandbox-api.payuni.com.tw/api/upp
772
- 後台: https://sandbox.payuni.com.tw/
773
- ```
352
+ ## References
774
353
 
775
- ## 開發檢查清單
776
-
777
- 使用此清單確保實作完整:
778
-
779
- ### 基礎設定
780
- - [ ] 實作 `PaymentService` 介面
781
- - [ ] 實作各服務商加密機制(SHA256 / AES-CBC / AES-GCM)
782
- - [ ] 設定環境變數(測試/正式)
783
- - [ ] 配置 ReturnURL(HTTPS)
784
- - [ ] 配置 OrderResultURL(付款完成導向頁)
785
-
786
- ### 訂單處理
787
- - [ ] 儲存 `paymentProvider` 欄位(查詢/退款時需要)
788
- - [ ] 儲存 `merchantTradeNo`(唯一訂單編號)
789
- - [ ] 儲存 `tradeNo`(金流商訂單號,退款時需要)
790
- - [ ] 金額驗證(正整數、最小金額)
791
- - [ ] 商品金額總和驗證
792
-
793
- ### 付款方式
794
- - [ ] 支援信用卡一次付清
795
- - [ ] 支援 ATM 虛擬帳號(可選)
796
- - [ ] 支援超商代碼/條碼(可選)
797
- - [ ] 支援電子錢包(可選)
798
- - [ ] 支援信用卡分期(可選)
799
- - [ ] 支援定期定額(可選)
800
-
801
- ### 回呼處理
802
- - [ ] 驗證 CheckMacValue / TradeSha / HashInfo
803
- - [ ] 更新訂單狀態
804
- - [ ] 回應 "1|OK" 格式
805
- - [ ] 防止重複通知處理(冪等性)
806
-
807
- ### 查詢退款
808
- - [ ] 實作訂單查詢 API
809
- - [ ] 實作退款 API
810
- - [ ] 處理部分退款邏輯
811
- - [ ] 檢查退款期限
812
-
813
- ### 錯誤處理
814
- - [ ] 實作錯誤處理與 logger
815
- - [ ] 記錄完整請求/回應(除敏感資訊)
816
- - [ ] 處理加密錯誤
817
- - [ ] 處理網路逾時
818
-
819
- ### 測試驗證
820
- - [ ] 測試環境驗證
821
- - [ ] 使用官方測試卡測試
822
- - [ ] 測試付款通知接收
823
- - [ ] 測試查詢功能
824
- - [ ] 測試退款功能
825
-
826
- ## 新增服務商步驟
827
-
828
- 1. 在 `lib/services/` 建立 `{provider}-payment-service.ts`
829
- 2. 實作 `PaymentService` 介面的所有方法
830
- 3. 在 `PaymentServiceFactory` 註冊新服務商
831
- 4. 在 `prisma/schema.prisma` 的 `PaymentProvider` enum 新增選項
832
- 5. 執行 `prisma migrate` 或 `prisma db push`
833
- 6. 更新前端設定頁面
834
- 7. 撰寫單元測試
835
- 8. 更新文檔
836
-
837
- ## 參考資料
838
-
839
- 詳細 API 規格請查看 `references/` 目錄:
840
- - [綠界 ECPay Payment API 規格](./references/ECPAY_PAYMENT_REFERENCE.md)
841
- - [藍新 NewebPay Payment API 規格](./references/NEWEBPAY_PAYMENT_REFERENCE.md)
842
- - [統一 PAYUNi Payment API 規格](./references/PAYUNI_PAYMENT_REFERENCE.md)
843
-
844
- 官方文檔:
845
- - ECPay: https://developers.ecpay.com.tw/
846
- - NewebPay: https://www.newebpay.com/website/Page/content/download_api
847
- - PAYUNi: https://www.payuni.com.tw/docs/
354
+ For detailed API specifications, see the `references/` directory:
355
+ - [ECPay API Specification](./references/ECPAY_API_REFERENCE.md)
356
+ - [SmilePay API Specification](./references/SMILEPAY_API_REFERENCE.md)
357
+ - [Amego API Specification](./references/AMEGO_API_REFERENCE.md)
848
358
 
849
359
  ---
850
360
 
851
- 最後更新:2026/01/29
361
+ Last updated: 2026/01/28