taiwan-payment-skill 1.0.0 → 1.0.2

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)