taiwan-payment-skill 1.0.0 → 1.0.1
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.
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ECPay 綠界金流 Python 完整範例
|
|
4
|
+
|
|
5
|
+
依照 taiwan-payment-skill 最高規範撰寫
|
|
6
|
+
支援: 信用卡、ATM 轉帳、超商代碼、超商條碼
|
|
7
|
+
|
|
8
|
+
API 文件: https://developers.ecpay.com.tw
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Dict, Literal, Optional, List
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PaymentOrderData:
|
|
21
|
+
"""ECPay 付款訂單資料"""
|
|
22
|
+
merchant_trade_no: str
|
|
23
|
+
total_amount: int
|
|
24
|
+
trade_desc: str
|
|
25
|
+
item_name: str
|
|
26
|
+
return_url: str
|
|
27
|
+
choose_payment: Literal['Credit', 'ATM', 'CVS', 'BARCODE', 'ALL']
|
|
28
|
+
merchant_trade_date: Optional[str] = None
|
|
29
|
+
client_back_url: Optional[str] = None
|
|
30
|
+
item_url: Optional[str] = None
|
|
31
|
+
remark: Optional[str] = None
|
|
32
|
+
choose_sub_payment: Optional[str] = None
|
|
33
|
+
order_result_url: Optional[str] = None
|
|
34
|
+
need_extra_paid_info: str = 'N'
|
|
35
|
+
device_source: Optional[str] = None
|
|
36
|
+
ignore_payment: Optional[str] = None
|
|
37
|
+
platform_id: Optional[str] = None
|
|
38
|
+
invoice_mark: str = 'N'
|
|
39
|
+
encrypt_type: int = 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PaymentOrderResponse:
|
|
44
|
+
"""ECPay 付款訂單回應"""
|
|
45
|
+
success: bool
|
|
46
|
+
merchant_trade_no: str
|
|
47
|
+
form_action: str = ''
|
|
48
|
+
form_data: Dict[str, str] = field(default_factory=dict)
|
|
49
|
+
error_message: str = ''
|
|
50
|
+
raw: Dict[str, str] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class PaymentCallbackData:
|
|
55
|
+
"""ECPay 付款回傳資料"""
|
|
56
|
+
merchant_trade_no: str
|
|
57
|
+
rtn_code: int
|
|
58
|
+
rtn_msg: str
|
|
59
|
+
trade_no: str
|
|
60
|
+
trade_amt: int
|
|
61
|
+
payment_date: str
|
|
62
|
+
payment_type: str
|
|
63
|
+
payment_type_charge_fee: str
|
|
64
|
+
trade_date: str
|
|
65
|
+
simulate_paid: int
|
|
66
|
+
check_mac_value: str
|
|
67
|
+
raw: Dict[str, str] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ECPayPaymentService:
|
|
71
|
+
"""
|
|
72
|
+
ECPay 綠界金流服務
|
|
73
|
+
|
|
74
|
+
認證方式: SHA256 CheckMacValue
|
|
75
|
+
加密方式: URL-encoded + SHA256
|
|
76
|
+
|
|
77
|
+
支援付款方式:
|
|
78
|
+
- Credit: 信用卡 (一次付清/分期/定期定額)
|
|
79
|
+
- ATM: ATM 轉帳
|
|
80
|
+
- CVS: 超商代碼繳費
|
|
81
|
+
- BARCODE: 超商條碼繳費
|
|
82
|
+
- ALL: 不指定付款方式
|
|
83
|
+
|
|
84
|
+
測試環境:
|
|
85
|
+
- 商店代號: 3002607
|
|
86
|
+
- HashKey: pwFHCqoQZGmho4w6
|
|
87
|
+
- HashIV: EkRm7iFT261dpevs
|
|
88
|
+
- 測試卡號: 4311-9522-2222-2222
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
# 測試環境
|
|
92
|
+
TEST_MERCHANT_ID = '3002607'
|
|
93
|
+
TEST_HASH_KEY = 'pwFHCqoQZGmho4w6'
|
|
94
|
+
TEST_HASH_IV = 'EkRm7iFT261dpevs'
|
|
95
|
+
TEST_API_URL = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
96
|
+
TEST_QUERY_URL = 'https://payment-stage.ecpay.com.tw/Cashier/QueryTradeInfo/V5'
|
|
97
|
+
|
|
98
|
+
# 正式環境
|
|
99
|
+
PROD_API_URL = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5'
|
|
100
|
+
PROD_QUERY_URL = 'https://payment.ecpay.com.tw/Cashier/QueryTradeInfo/V5'
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
merchant_id: str,
|
|
105
|
+
hash_key: str,
|
|
106
|
+
hash_iv: str,
|
|
107
|
+
is_production: bool = False
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
初始化 ECPay 金流服務
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
merchant_id: 商店代號
|
|
114
|
+
hash_key: HashKey
|
|
115
|
+
hash_iv: HashIV
|
|
116
|
+
is_production: 是否為正式環境 (預設 False)
|
|
117
|
+
"""
|
|
118
|
+
self.merchant_id = merchant_id
|
|
119
|
+
self.hash_key = hash_key
|
|
120
|
+
self.hash_iv = hash_iv
|
|
121
|
+
self.api_url = self.PROD_API_URL if is_production else self.TEST_API_URL
|
|
122
|
+
self.query_url = self.PROD_QUERY_URL if is_production else self.TEST_QUERY_URL
|
|
123
|
+
|
|
124
|
+
def generate_check_mac_value(self, params: Dict[str, any]) -> str:
|
|
125
|
+
"""
|
|
126
|
+
產生 CheckMacValue (SHA256)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
params: API 參數字典
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
str: SHA256 雜湊值 (大寫)
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> params = {'MerchantID': '3002607', 'TotalAmount': 100}
|
|
136
|
+
>>> mac = service.generate_check_mac_value(params)
|
|
137
|
+
>>> len(mac)
|
|
138
|
+
64
|
|
139
|
+
"""
|
|
140
|
+
# 步驟 1: 參數排序
|
|
141
|
+
sorted_params = sorted(params.items())
|
|
142
|
+
|
|
143
|
+
# 步驟 2: 組合查詢字串
|
|
144
|
+
param_str = '&'.join(f'{k}={v}' for k, v in sorted_params)
|
|
145
|
+
|
|
146
|
+
# 步驟 3: 加入 HashKey 和 HashIV
|
|
147
|
+
raw = f'HashKey={self.hash_key}&{param_str}&HashIV={self.hash_iv}'
|
|
148
|
+
|
|
149
|
+
# 步驟 4: URL Encode 並轉小寫
|
|
150
|
+
encoded = urllib.parse.quote_plus(raw).lower()
|
|
151
|
+
|
|
152
|
+
# 步驟 5: SHA256 雜湊並轉大寫
|
|
153
|
+
return hashlib.sha256(encoded.encode('utf-8')).hexdigest().upper()
|
|
154
|
+
|
|
155
|
+
def verify_check_mac_value(self, params: Dict[str, any]) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
驗證 CheckMacValue
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
params: 包含 CheckMacValue 的參數字典
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
bool: 驗證是否通過
|
|
164
|
+
"""
|
|
165
|
+
received_mac = params.pop('CheckMacValue', None)
|
|
166
|
+
if not received_mac:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
calculated_mac = self.generate_check_mac_value(params)
|
|
170
|
+
return calculated_mac == received_mac.upper()
|
|
171
|
+
|
|
172
|
+
def create_order(
|
|
173
|
+
self,
|
|
174
|
+
data: PaymentOrderData,
|
|
175
|
+
) -> PaymentOrderResponse:
|
|
176
|
+
"""
|
|
177
|
+
建立付款訂單
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
data: 付款訂單資料 (PaymentOrderData)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
PaymentOrderResponse: 付款訂單回應 (包含表單 HTML)
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: 參數驗證失敗
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> order_data = PaymentOrderData(
|
|
190
|
+
... merchant_trade_no=f'ORD{int(time.time())}',
|
|
191
|
+
... total_amount=1050,
|
|
192
|
+
... trade_desc='測試訂單',
|
|
193
|
+
... item_name='測試商品 x 1',
|
|
194
|
+
... return_url='https://your-site.com/callback',
|
|
195
|
+
... choose_payment='Credit',
|
|
196
|
+
... )
|
|
197
|
+
>>> result = service.create_order(order_data)
|
|
198
|
+
>>> print(result.form_action)
|
|
199
|
+
"""
|
|
200
|
+
# 參數驗證
|
|
201
|
+
if data.total_amount < 1:
|
|
202
|
+
raise ValueError('金額必須大於 0')
|
|
203
|
+
if len(data.merchant_trade_no) > 20:
|
|
204
|
+
raise ValueError('訂單編號不可超過 20 字元')
|
|
205
|
+
|
|
206
|
+
# 產生交易日期時間 (格式: YYYY/MM/DD HH:MM:SS)
|
|
207
|
+
if not data.merchant_trade_date:
|
|
208
|
+
data.merchant_trade_date = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
|
|
209
|
+
|
|
210
|
+
# 準備 API 參數
|
|
211
|
+
api_params = {
|
|
212
|
+
'MerchantID': self.merchant_id,
|
|
213
|
+
'MerchantTradeNo': data.merchant_trade_no,
|
|
214
|
+
'MerchantTradeDate': data.merchant_trade_date,
|
|
215
|
+
'PaymentType': 'aio',
|
|
216
|
+
'TotalAmount': data.total_amount,
|
|
217
|
+
'TradeDesc': data.trade_desc,
|
|
218
|
+
'ItemName': data.item_name,
|
|
219
|
+
'ReturnURL': data.return_url,
|
|
220
|
+
'ChoosePayment': data.choose_payment,
|
|
221
|
+
'EncryptType': data.encrypt_type,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# 加入可選參數
|
|
225
|
+
if data.client_back_url:
|
|
226
|
+
api_params['ClientBackURL'] = data.client_back_url
|
|
227
|
+
if data.item_url:
|
|
228
|
+
api_params['ItemURL'] = data.item_url
|
|
229
|
+
if data.remark:
|
|
230
|
+
api_params['Remark'] = data.remark
|
|
231
|
+
if data.choose_sub_payment:
|
|
232
|
+
api_params['ChooseSubPayment'] = data.choose_sub_payment
|
|
233
|
+
if data.order_result_url:
|
|
234
|
+
api_params['OrderResultURL'] = data.order_result_url
|
|
235
|
+
if data.need_extra_paid_info:
|
|
236
|
+
api_params['NeedExtraPaidInfo'] = data.need_extra_paid_info
|
|
237
|
+
if data.device_source:
|
|
238
|
+
api_params['DeviceSource'] = data.device_source
|
|
239
|
+
if data.ignore_payment:
|
|
240
|
+
api_params['IgnorePayment'] = data.ignore_payment
|
|
241
|
+
if data.platform_id:
|
|
242
|
+
api_params['PlatformID'] = data.platform_id
|
|
243
|
+
if data.invoice_mark:
|
|
244
|
+
api_params['InvoiceMark'] = data.invoice_mark
|
|
245
|
+
|
|
246
|
+
# 產生 CheckMacValue
|
|
247
|
+
api_params['CheckMacValue'] = self.generate_check_mac_value(api_params)
|
|
248
|
+
|
|
249
|
+
# 回傳表單資料
|
|
250
|
+
return PaymentOrderResponse(
|
|
251
|
+
success=True,
|
|
252
|
+
merchant_trade_no=data.merchant_trade_no,
|
|
253
|
+
form_action=self.api_url,
|
|
254
|
+
form_data=api_params,
|
|
255
|
+
raw=api_params,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def parse_callback(self, callback_data: Dict[str, str]) -> PaymentCallbackData:
|
|
259
|
+
"""
|
|
260
|
+
解析付款回傳資料
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
callback_data: POST 回傳的參數字典
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
PaymentCallbackData: 解析後的回傳資料
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ValueError: CheckMacValue 驗證失敗
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> callback = request.form.to_dict()
|
|
273
|
+
>>> result = service.parse_callback(callback)
|
|
274
|
+
>>> if result.rtn_code == 1:
|
|
275
|
+
... print(f"付款成功: {result.trade_no}")
|
|
276
|
+
"""
|
|
277
|
+
# 驗證 CheckMacValue
|
|
278
|
+
verify_data = callback_data.copy()
|
|
279
|
+
if not self.verify_check_mac_value(verify_data):
|
|
280
|
+
raise ValueError('CheckMacValue 驗證失敗')
|
|
281
|
+
|
|
282
|
+
# 解析回傳資料
|
|
283
|
+
return PaymentCallbackData(
|
|
284
|
+
merchant_trade_no=callback_data.get('MerchantTradeNo', ''),
|
|
285
|
+
rtn_code=int(callback_data.get('RtnCode', 0)),
|
|
286
|
+
rtn_msg=callback_data.get('RtnMsg', ''),
|
|
287
|
+
trade_no=callback_data.get('TradeNo', ''),
|
|
288
|
+
trade_amt=int(callback_data.get('TradeAmt', 0)),
|
|
289
|
+
payment_date=callback_data.get('PaymentDate', ''),
|
|
290
|
+
payment_type=callback_data.get('PaymentType', ''),
|
|
291
|
+
payment_type_charge_fee=callback_data.get('PaymentTypeChargeFee', '0'),
|
|
292
|
+
trade_date=callback_data.get('TradeDate', ''),
|
|
293
|
+
simulate_paid=int(callback_data.get('SimulatePaid', 0)),
|
|
294
|
+
check_mac_value=callback_data.get('CheckMacValue', ''),
|
|
295
|
+
raw=callback_data,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Usage Example
|
|
300
|
+
if __name__ == '__main__':
|
|
301
|
+
# 初始化金流服務 (使用測試環境)
|
|
302
|
+
service = ECPayPaymentService(
|
|
303
|
+
merchant_id=ECPayPaymentService.TEST_MERCHANT_ID,
|
|
304
|
+
hash_key=ECPayPaymentService.TEST_HASH_KEY,
|
|
305
|
+
hash_iv=ECPayPaymentService.TEST_HASH_IV,
|
|
306
|
+
is_production=False,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
print('=' * 60)
|
|
310
|
+
print('ECPay 綠界金流 - Python 範例')
|
|
311
|
+
print('=' * 60)
|
|
312
|
+
print()
|
|
313
|
+
|
|
314
|
+
# 範例 1: 建立信用卡付款訂單
|
|
315
|
+
print('[範例 1] 建立信用卡付款訂單')
|
|
316
|
+
print('-' * 60)
|
|
317
|
+
|
|
318
|
+
order_data = PaymentOrderData(
|
|
319
|
+
merchant_trade_no=f'ORD{int(time.time())}', # 訂單編號 (唯一值)
|
|
320
|
+
total_amount=1050, # 金額 (新台幣)
|
|
321
|
+
trade_desc='測試商品購買', # 交易描述
|
|
322
|
+
item_name='測試商品 x 1', # 商品名稱
|
|
323
|
+
return_url='https://your-site.com/api/payment/callback', # 付款結果通知網址
|
|
324
|
+
choose_payment='Credit', # 付款方式: 信用卡
|
|
325
|
+
client_back_url='https://your-site.com/order/complete', # 付款完成返回網址
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
result = service.create_order(order_data)
|
|
330
|
+
|
|
331
|
+
if result.success:
|
|
332
|
+
print(f'✓ 訂單建立成功')
|
|
333
|
+
print(f' 訂單編號: {result.merchant_trade_no}')
|
|
334
|
+
print(f' 表單網址: {result.form_action}')
|
|
335
|
+
print(f' CheckMacValue: {result.form_data["CheckMacValue"][:32]}...')
|
|
336
|
+
print()
|
|
337
|
+
print('請將以下表單資料 POST 到表單網址:')
|
|
338
|
+
for key, value in list(result.form_data.items())[:5]:
|
|
339
|
+
print(f' {key}: {value}')
|
|
340
|
+
print(f' ... (共 {len(result.form_data)} 個參數)')
|
|
341
|
+
else:
|
|
342
|
+
print(f'✗ 訂單建立失敗: {result.error_message}')
|
|
343
|
+
except Exception as e:
|
|
344
|
+
print(f'✗ 發生例外: {str(e)}')
|
|
345
|
+
|
|
346
|
+
print()
|
|
347
|
+
print('-' * 60)
|
|
348
|
+
|
|
349
|
+
# 範例 2: 驗證付款回傳
|
|
350
|
+
print('[範例 2] 驗證付款回傳資料')
|
|
351
|
+
print('-' * 60)
|
|
352
|
+
|
|
353
|
+
# 模擬 ECPay 回傳資料
|
|
354
|
+
mock_callback = {
|
|
355
|
+
'MerchantID': service.merchant_id,
|
|
356
|
+
'MerchantTradeNo': 'ORD1738123456',
|
|
357
|
+
'RtnCode': '1',
|
|
358
|
+
'RtnMsg': '交易成功',
|
|
359
|
+
'TradeNo': '2401291234567890',
|
|
360
|
+
'TradeAmt': '1050',
|
|
361
|
+
'PaymentDate': '2024/01/29 14:30:00',
|
|
362
|
+
'PaymentType': 'Credit_CreditCard',
|
|
363
|
+
'PaymentTypeChargeFee': '30',
|
|
364
|
+
'TradeDate': '2024/01/29 14:25:00',
|
|
365
|
+
'SimulatePaid': '0',
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# 計算正確的 CheckMacValue
|
|
369
|
+
mock_callback['CheckMacValue'] = service.generate_check_mac_value(mock_callback)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
callback_result = service.parse_callback(mock_callback)
|
|
373
|
+
|
|
374
|
+
if callback_result.rtn_code == 1:
|
|
375
|
+
print(f'✓ 付款成功')
|
|
376
|
+
print(f' 訂單編號: {callback_result.merchant_trade_no}')
|
|
377
|
+
print(f' ECPay 交易編號: {callback_result.trade_no}')
|
|
378
|
+
print(f' 付款金額: {callback_result.trade_amt} 元')
|
|
379
|
+
print(f' 付款時間: {callback_result.payment_date}')
|
|
380
|
+
print(f' 付款方式: {callback_result.payment_type}')
|
|
381
|
+
print(f' 手續費: {callback_result.payment_type_charge_fee} 元')
|
|
382
|
+
else:
|
|
383
|
+
print(f'✗ 付款失敗: {callback_result.rtn_msg}')
|
|
384
|
+
except ValueError as e:
|
|
385
|
+
print(f'✗ 驗證失敗: {str(e)}')
|
|
386
|
+
|
|
387
|
+
print()
|
|
388
|
+
print('=' * 60)
|
|
389
|
+
print('範例執行完成')
|
|
390
|
+
print('=' * 60)
|