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.
- package/assets/taiwan-payment/EXAMPLES.md +1425 -1425
- package/assets/taiwan-payment/SKILL.md +857 -857
- package/assets/taiwan-payment/references/ecpay-payment-api.md +880 -880
- package/assets/taiwan-payment/references/newebpay-payment-api.md +677 -677
- package/assets/taiwan-payment/references/payuni-payment-api.md +997 -997
- package/assets/templates/base/quick-reference.md +60 -345
- package/assets/templates/base/skill-content.md +248 -738
- package/assets/templates/platforms/agent.json +26 -0
- package/assets/templates/platforms/claude.json +26 -26
- package/assets/templates/platforms/codebuddy.json +25 -25
- package/assets/templates/platforms/codex.json +26 -25
- package/assets/templates/platforms/continue.json +25 -25
- package/assets/templates/platforms/copilot.json +25 -25
- package/assets/templates/platforms/cursor.json +26 -25
- package/assets/templates/platforms/gemini.json +25 -25
- package/assets/templates/platforms/kiro.json +25 -25
- package/assets/templates/platforms/opencode.json +25 -25
- package/assets/templates/platforms/qoder.json +25 -25
- package/assets/templates/platforms/roocode.json +25 -25
- package/assets/templates/platforms/trae.json +25 -25
- package/assets/templates/platforms/windsurf.json +25 -25
- package/dist/index.js +127 -29
- package/package.json +1 -1
|
@@ -1,851 +1,361 @@
|
|
|
1
1
|
# {{TITLE}}
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> {{DESCRIPTION}}
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Navigation
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
|
|
9
|
-
- `references/
|
|
10
|
-
- `references/
|
|
11
|
-
- `references/
|
|
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
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
###
|
|
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
|
-
|
|
38
|
+
## Provider Comparison
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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/
|
|
202
|
-
export interface
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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}-
|
|
210
|
-
export class
|
|
211
|
-
private
|
|
212
|
-
//
|
|
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
|
|
216
|
-
// 1.
|
|
217
|
-
// 2.
|
|
218
|
-
// 3.
|
|
219
|
-
// 4.
|
|
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
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
105
|
+
```typescript
|
|
106
|
+
import crypto from 'crypto'
|
|
314
107
|
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
//
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
126
|
+
const urlDecoded = decodeURIComponent(decrypted)
|
|
127
|
+
return JSON.parse(urlDecoded)
|
|
343
128
|
}
|
|
344
129
|
```
|
|
345
130
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
**關鍵:各服務商都使用 Form POST 導向付款頁**
|
|
131
|
+
**Amego - MD5 Signature:**
|
|
349
132
|
|
|
350
133
|
```typescript
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
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
|
-
**
|
|
143
|
+
**Key: When issuing invoice, must record the provider used so printing can call the correct one**
|
|
400
144
|
|
|
401
145
|
```typescript
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
###
|
|
197
|
+
### Issue 1: Invoice issuance failed with unclear error
|
|
484
198
|
|
|
485
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
const rawString = `HashKey=${hashKey}&${paramString}&HashIV=${hashIV}`
|
|
219
|
+
### Issue 2: Print shows "Invoice not found"
|
|
508
220
|
|
|
509
|
-
|
|
510
|
-
|
|
221
|
+
**Solution:**
|
|
222
|
+
Confirm `invoiceProvider` field is saved correctly, use the issuing provider when printing.
|
|
511
223
|
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
//
|
|
520
|
-
const
|
|
230
|
+
// Incorrect: Use user's current default provider
|
|
231
|
+
const service = await InvoiceServiceFactory.getServiceForUser(userId)
|
|
521
232
|
```
|
|
522
233
|
|
|
523
|
-
###
|
|
234
|
+
### Issue 3: B2B invoice amount error
|
|
524
235
|
|
|
525
|
-
|
|
236
|
+
**Amount fields by provider:**
|
|
526
237
|
|
|
527
|
-
**原因:** 使用相同的 MerchantTradeNo
|
|
528
|
-
|
|
529
|
-
**解決方案:**
|
|
530
238
|
```typescript
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
###
|
|
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
|
-
//
|
|
579
|
-
|
|
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
|
-
//
|
|
593
|
-
return
|
|
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
|
-
###
|
|
287
|
+
### Issue 5: Timestamp expired
|
|
617
288
|
|
|
618
|
-
**
|
|
289
|
+
**ECPay error 10000005:** Timestamp exceeds 10 minutes
|
|
619
290
|
|
|
291
|
+
**Solution:**
|
|
620
292
|
```typescript
|
|
621
|
-
//
|
|
622
|
-
const
|
|
623
|
-
const hashIV = 'your16BytesIV123' // 必須 16 bytes
|
|
293
|
+
// Ensure using current timestamp
|
|
294
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
624
295
|
|
|
625
|
-
//
|
|
626
|
-
const
|
|
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
|
-
|
|
300
|
+
## Test Accounts
|
|
631
301
|
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
361
|
+
Last updated: 2026/01/28
|