taiwan-invoice-skill 2.6.1 → 2.6.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.
@@ -0,0 +1,598 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ECPay 綠界電子發票 Python 完整範例
4
+
5
+ 依照 taiwan-invoice-skill 最高規範撰寫
6
+ 支援: B2C 二聯式、B2B 三聯式、發票作廢、折讓、列印
7
+
8
+ API 文件: https://developers.ecpay.com.tw
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import urllib.parse
14
+ import base64
15
+ import time
16
+ import requests
17
+ from datetime import datetime
18
+ from typing import Dict, Literal, Optional, List
19
+ from dataclasses import dataclass, field
20
+ from Crypto.Cipher import AES
21
+ from Crypto.Util.Padding import pad, unpad
22
+
23
+
24
+ @dataclass
25
+ class InvoiceIssueData:
26
+ """ECPay 發票開立資料"""
27
+ merchant_id: str
28
+ relate_number: str # 訂單編號 (唯一)
29
+ customer_identifier: str # 買受人統編 (B2C: 0000000000, B2B: 8碼統編)
30
+ customer_name: str # 買受人名稱
31
+ customer_addr: str # 買受人地址
32
+ customer_phone: str # 買受人電話
33
+ customer_email: str # 買受人 Email
34
+ sales_amount: int # 銷售額 (B2C: 含稅, B2B: 未稅)
35
+ tax_type: Literal['1', '2', '3', '9'] = '1' # 1=應稅, 2=零稅率, 3=免稅, 9=混合
36
+ tax_rate: int = 5 # 稅率 (預設5%)
37
+ tax_amount: int = 0 # 稅額 (B2B 必填)
38
+ total_amount: int = 0 # 總計 (含稅)
39
+ carrier_type: Optional[Literal['', '1', '2', '3']] = '' # 載具類型 (B2B不可使用)
40
+ carrier_num: Optional[str] = '' # 載具號碼
41
+ donation: Literal['0', '1'] = '0' # 是否捐贈
42
+ love_code: Optional[str] = '' # 捐贈碼
43
+ print: Literal['0', '1'] = '0' # 是否列印
44
+ items: List[Dict[str, any]] = field(default_factory=list)
45
+ inv_type: Literal['07', '08'] = '07' # 07=一般稅額, 08=特種稅額
46
+ vat: Literal['1', '2', '3', '4'] = '1' # 1=課稅, 2=零稅率, 3=免稅, 4=應稅(特種)
47
+ remark: Optional[str] = ''
48
+
49
+
50
+ @dataclass
51
+ class InvoiceVoidData:
52
+ """ECPay 發票作廢資料"""
53
+ merchant_id: str
54
+ invoice_no: str # 發票號碼
55
+ invoice_date: str # 發票開立日期 (YYYY-MM-DD)
56
+ reason: str # 作廢原因
57
+
58
+
59
+ @dataclass
60
+ class InvoiceAllowanceData:
61
+ """ECPay 發票折讓資料"""
62
+ merchant_id: str
63
+ invoice_no: str # 發票號碼
64
+ invoice_date: str # 發票開立日期 (YYYY-MM-DD)
65
+ allowance_notify: Literal['E', 'S', 'N', 'A'] = 'E' # E=Email, S=簡訊, N=不通知, A=Email+簡訊
66
+ customer_name: str
67
+ notify_mail: Optional[str] = ''
68
+ notify_phone: Optional[str] = ''
69
+ allowance_amount: int = 0 # 折讓金額
70
+ items: List[Dict[str, any]] = field(default_factory=list)
71
+
72
+
73
+ @dataclass
74
+ class InvoiceIssueResponse:
75
+ """ECPay 發票開立回應"""
76
+ success: bool
77
+ invoice_number: str = ''
78
+ invoice_date: str = ''
79
+ random_number: str = ''
80
+ rtn_code: int = 0
81
+ rtn_msg: str = ''
82
+ error_message: str = ''
83
+ raw: Dict[str, any] = field(default_factory=dict)
84
+
85
+
86
+ class ECPayInvoiceService:
87
+ """
88
+ ECPay 綠界電子發票服務
89
+
90
+ 認證方式: AES-128-CBC 加密
91
+ 加密金鑰: HashKey (16 bytes), HashIV (16 bytes)
92
+
93
+ 支援功能:
94
+ - 開立發票 (B2C 二聯式 / B2B 三聯式)
95
+ - 作廢發票
96
+ - 折讓發票
97
+ - 列印發票
98
+ - 查詢發票
99
+
100
+ 測試環境:
101
+ - 商店代號: 2000132
102
+ - HashKey: ejCk326UnaZWKisg
103
+ - HashIV: q9jcZX8Ib9LM8wYk
104
+ - 測試 URL: https://einvoice-stage.ecpay.com.tw
105
+
106
+ B2C vs B2B 差異:
107
+ - B2C: CustomerIdentifier = 0000000000, 金額為含稅價, 可用載具/捐贈
108
+ - B2B: CustomerIdentifier = 8碼統編, 金額為未稅價, 需計算稅額, 不可用載具/捐贈
109
+ """
110
+
111
+ # 測試環境
112
+ TEST_MERCHANT_ID = '2000132'
113
+ TEST_HASH_KEY = 'ejCk326UnaZWKisg'
114
+ TEST_HASH_IV = 'q9jcZX8Ib9LM8wYk'
115
+ TEST_API_URL = 'https://einvoice-stage.ecpay.com.tw/B2CInvoice/Issue'
116
+ TEST_VOID_URL = 'https://einvoice-stage.ecpay.com.tw/B2CInvoice/Invalid'
117
+ TEST_ALLOWANCE_URL = 'https://einvoice-stage.ecpay.com.tw/B2CInvoice/Allowance'
118
+ TEST_ALLOWANCE_VOID_URL = 'https://einvoice-stage.ecpay.com.tw/B2CInvoice/AllowanceInvalid'
119
+ TEST_QUERY_URL = 'https://einvoice-stage.ecpay.com.tw/B2CInvoice/GetIssue'
120
+
121
+ # 正式環境
122
+ PROD_API_URL = 'https://einvoice.ecpay.com.tw/B2CInvoice/Issue'
123
+ PROD_VOID_URL = 'https://einvoice.ecpay.com.tw/B2CInvoice/Invalid'
124
+ PROD_ALLOWANCE_URL = 'https://einvoice.ecpay.com.tw/B2CInvoice/Allowance'
125
+ PROD_ALLOWANCE_VOID_URL = 'https://einvoice.ecpay.com.tw/B2CInvoice/AllowanceInvalid'
126
+ PROD_QUERY_URL = 'https://einvoice.ecpay.com.tw/B2CInvoice/GetIssue'
127
+
128
+ def __init__(self, merchant_id: str, hash_key: str, hash_iv: str, is_test: bool = True):
129
+ """
130
+ 初始化 ECPay 電子發票服務
131
+
132
+ Args:
133
+ merchant_id: 商店代號
134
+ hash_key: HashKey (16 bytes)
135
+ hash_iv: HashIV (16 bytes)
136
+ is_test: 是否為測試環境
137
+ """
138
+ self.merchant_id = merchant_id
139
+ self.hash_key = hash_key.encode('utf-8')
140
+ self.hash_iv = hash_iv.encode('utf-8')
141
+ self.is_test = is_test
142
+
143
+ # 設定 API URL
144
+ if is_test:
145
+ self.api_url = self.TEST_API_URL
146
+ self.void_url = self.TEST_VOID_URL
147
+ self.allowance_url = self.TEST_ALLOWANCE_URL
148
+ self.allowance_void_url = self.TEST_ALLOWANCE_VOID_URL
149
+ self.query_url = self.TEST_QUERY_URL
150
+ else:
151
+ self.api_url = self.PROD_API_URL
152
+ self.void_url = self.PROD_VOID_URL
153
+ self.allowance_url = self.PROD_ALLOWANCE_URL
154
+ self.allowance_void_url = self.PROD_ALLOWANCE_VOID_URL
155
+ self.query_url = self.PROD_QUERY_URL
156
+
157
+ def _encrypt_aes(self, data: str) -> str:
158
+ """
159
+ AES-128-CBC 加密
160
+
161
+ Args:
162
+ data: 要加密的字串
163
+
164
+ Returns:
165
+ Base64 編碼的加密字串
166
+ """
167
+ # URL Encode
168
+ url_encoded = urllib.parse.quote(data)
169
+
170
+ # AES-128-CBC 加密
171
+ cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
172
+ encrypted = cipher.encrypt(pad(url_encoded.encode('utf-8'), AES.block_size))
173
+
174
+ # Base64 編碼
175
+ return base64.b64encode(encrypted).decode('utf-8')
176
+
177
+ def _decrypt_aes(self, encrypted_data: str) -> Dict[str, any]:
178
+ """
179
+ AES-128-CBC 解密
180
+
181
+ Args:
182
+ encrypted_data: Base64 編碼的加密字串
183
+
184
+ Returns:
185
+ 解密後的 JSON 物件
186
+ """
187
+ # Base64 解碼
188
+ encrypted = base64.b64decode(encrypted_data)
189
+
190
+ # AES-128-CBC 解密
191
+ cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
192
+ decrypted = unpad(cipher.decrypt(encrypted), AES.block_size).decode('utf-8')
193
+
194
+ # URL Decode
195
+ url_decoded = urllib.parse.unquote(decrypted)
196
+
197
+ # JSON Parse
198
+ return json.loads(url_decoded)
199
+
200
+ def calculate_b2b_amounts(self, total_amount: int) -> Dict[str, int]:
201
+ """
202
+ 計算 B2B 發票金額 (含稅總額 → 未稅金額 + 稅額)
203
+
204
+ Args:
205
+ total_amount: 含稅總額
206
+
207
+ Returns:
208
+ { 'sales_amount': 未稅金額, 'tax_amount': 稅額, 'total_amount': 總額 }
209
+
210
+ Example:
211
+ >>> svc.calculate_b2b_amounts(1050)
212
+ {'sales_amount': 1000, 'tax_amount': 50, 'total_amount': 1050}
213
+ """
214
+ tax_amount = round(total_amount - (total_amount / 1.05))
215
+ sales_amount = total_amount - tax_amount
216
+
217
+ return {
218
+ 'sales_amount': sales_amount,
219
+ 'tax_amount': tax_amount,
220
+ 'total_amount': total_amount
221
+ }
222
+
223
+ def issue_invoice(self, data: InvoiceIssueData) -> InvoiceIssueResponse:
224
+ """
225
+ 開立電子發票
226
+
227
+ Args:
228
+ data: 發票開立資料
229
+
230
+ Returns:
231
+ 發票開立回應
232
+
233
+ Raises:
234
+ ValueError: 資料驗證失敗
235
+ ConnectionError: API 連線失敗
236
+
237
+ Example:
238
+ >>> # B2C 二聯式發票
239
+ >>> data = InvoiceIssueData(
240
+ ... merchant_id='2000132',
241
+ ... relate_number='ORD20240129001',
242
+ ... customer_identifier='0000000000',
243
+ ... customer_name='王小明',
244
+ ... customer_addr='台北市信義區',
245
+ ... customer_phone='0912345678',
246
+ ... customer_email='test@example.com',
247
+ ... sales_amount=1050,
248
+ ... total_amount=1050,
249
+ ... items=[
250
+ ... {'ItemName': '商品A', 'ItemCount': 1, 'ItemWord': '個', 'ItemPrice': 1000, 'ItemAmount': 1050}
251
+ ... ]
252
+ ... )
253
+ >>> response = svc.issue_invoice(data)
254
+ """
255
+ # 驗證資料
256
+ if data.customer_identifier != '0000000000':
257
+ # B2B 三聯式
258
+ if len(data.customer_identifier) != 8:
259
+ raise ValueError('B2B 發票統編必須為 8 碼')
260
+ if data.carrier_type or data.love_code:
261
+ raise ValueError('B2B 發票不可使用載具或捐贈')
262
+ if data.tax_amount == 0:
263
+ raise ValueError('B2B 發票必須計算稅額')
264
+
265
+ # 準備 API 資料
266
+ api_data = {
267
+ 'MerchantID': data.merchant_id,
268
+ 'RelateNumber': data.relate_number,
269
+ 'CustomerID': '',
270
+ 'CustomerIdentifier': data.customer_identifier,
271
+ 'CustomerName': data.customer_name,
272
+ 'CustomerAddr': data.customer_addr,
273
+ 'CustomerPhone': data.customer_phone,
274
+ 'CustomerEmail': data.customer_email,
275
+ 'ClearanceMark': '',
276
+ 'Print': data.print,
277
+ 'Donation': data.donation,
278
+ 'LoveCode': data.love_code,
279
+ 'CarrierType': data.carrier_type,
280
+ 'CarrierNum': data.carrier_num,
281
+ 'TaxType': data.tax_type,
282
+ 'SalesAmount': data.sales_amount,
283
+ 'InvType': data.inv_type,
284
+ 'vat': data.vat,
285
+ 'Items': data.items,
286
+ 'InvoiceRemark': data.remark,
287
+ 'TimeStamp': int(time.time())
288
+ }
289
+
290
+ # 加密資料
291
+ json_string = json.dumps(api_data, ensure_ascii=False)
292
+ encrypted_data = self._encrypt_aes(json_string)
293
+
294
+ # 發送請求
295
+ try:
296
+ response = requests.post(
297
+ self.api_url,
298
+ data={'MerchantID': data.merchant_id, 'RqHeader': {'Timestamp': int(time.time())}, 'Data': encrypted_data},
299
+ headers={'Content-Type': 'application/x-www-form-urlencoded'},
300
+ timeout=30
301
+ )
302
+ response.raise_for_status()
303
+
304
+ # 解析回應
305
+ result = response.json()
306
+
307
+ if 'Data' in result:
308
+ decrypted = self._decrypt_aes(result['Data'])
309
+
310
+ if decrypted.get('RtnCode') == 1:
311
+ return InvoiceIssueResponse(
312
+ success=True,
313
+ invoice_number=decrypted.get('InvoiceNo', ''),
314
+ invoice_date=decrypted.get('InvoiceDate', ''),
315
+ random_number=decrypted.get('RandomNumber', ''),
316
+ rtn_code=decrypted.get('RtnCode', 0),
317
+ rtn_msg=decrypted.get('RtnMsg', ''),
318
+ raw=decrypted
319
+ )
320
+ else:
321
+ return InvoiceIssueResponse(
322
+ success=False,
323
+ rtn_code=decrypted.get('RtnCode', 0),
324
+ rtn_msg=decrypted.get('RtnMsg', ''),
325
+ error_message=f"發票開立失敗: {decrypted.get('RtnMsg', '未知錯誤')}",
326
+ raw=decrypted
327
+ )
328
+ else:
329
+ return InvoiceIssueResponse(
330
+ success=False,
331
+ error_message='API 回應格式錯誤',
332
+ raw=result
333
+ )
334
+
335
+ except requests.exceptions.RequestException as e:
336
+ raise ConnectionError(f'API 連線失敗: {str(e)}')
337
+
338
+ def void_invoice(self, data: InvoiceVoidData, reason: str = '訂單取消') -> Dict[str, any]:
339
+ """
340
+ 作廢電子發票
341
+
342
+ Args:
343
+ data: 發票作廢資料
344
+ reason: 作廢原因
345
+
346
+ Returns:
347
+ 作廢結果
348
+
349
+ Example:
350
+ >>> void_data = InvoiceVoidData(
351
+ ... merchant_id='2000132',
352
+ ... invoice_no='AA12345678',
353
+ ... invoice_date='2024-01-29',
354
+ ... reason='訂單取消'
355
+ ... )
356
+ >>> result = svc.void_invoice(void_data)
357
+ """
358
+ api_data = {
359
+ 'MerchantID': data.merchant_id,
360
+ 'InvoiceNo': data.invoice_no,
361
+ 'InvoiceDate': data.invoice_date,
362
+ 'Reason': reason,
363
+ 'TimeStamp': int(time.time())
364
+ }
365
+
366
+ json_string = json.dumps(api_data, ensure_ascii=False)
367
+ encrypted_data = self._encrypt_aes(json_string)
368
+
369
+ try:
370
+ response = requests.post(
371
+ self.void_url,
372
+ data={'MerchantID': data.merchant_id, 'RqHeader': {'Timestamp': int(time.time())}, 'Data': encrypted_data},
373
+ headers={'Content-Type': 'application/x-www-form-urlencoded'},
374
+ timeout=30
375
+ )
376
+ response.raise_for_status()
377
+
378
+ result = response.json()
379
+
380
+ if 'Data' in result:
381
+ return self._decrypt_aes(result['Data'])
382
+ else:
383
+ return result
384
+
385
+ except requests.exceptions.RequestException as e:
386
+ raise ConnectionError(f'API 連線失敗: {str(e)}')
387
+
388
+ def issue_allowance(self, data: InvoiceAllowanceData) -> Dict[str, any]:
389
+ """
390
+ 開立折讓證明單
391
+
392
+ Args:
393
+ data: 折讓資料
394
+
395
+ Returns:
396
+ 折讓結果
397
+
398
+ Example:
399
+ >>> allowance_data = InvoiceAllowanceData(
400
+ ... merchant_id='2000132',
401
+ ... invoice_no='AA12345678',
402
+ ... invoice_date='2024-01-29',
403
+ ... customer_name='王小明',
404
+ ... notify_mail='test@example.com',
405
+ ... allowance_amount=100,
406
+ ... items=[
407
+ ... {'ItemName': '商品A', 'ItemCount': 1, 'ItemWord': '個', 'ItemPrice': 100, 'ItemAmount': 100}
408
+ ... ]
409
+ ... )
410
+ >>> result = svc.issue_allowance(allowance_data)
411
+ """
412
+ api_data = {
413
+ 'MerchantID': data.merchant_id,
414
+ 'InvoiceNo': data.invoice_no,
415
+ 'InvoiceDate': data.invoice_date,
416
+ 'AllowanceNotify': data.allowance_notify,
417
+ 'CustomerName': data.customer_name,
418
+ 'NotifyMail': data.notify_mail,
419
+ 'NotifyPhone': data.notify_phone,
420
+ 'AllowanceAmount': data.allowance_amount,
421
+ 'Items': data.items,
422
+ 'TimeStamp': int(time.time())
423
+ }
424
+
425
+ json_string = json.dumps(api_data, ensure_ascii=False)
426
+ encrypted_data = self._encrypt_aes(json_string)
427
+
428
+ try:
429
+ response = requests.post(
430
+ self.allowance_url,
431
+ data={'MerchantID': data.merchant_id, 'RqHeader': {'Timestamp': int(time.time())}, 'Data': encrypted_data},
432
+ headers={'Content-Type': 'application/x-www-form-urlencoded'},
433
+ timeout=30
434
+ )
435
+ response.raise_for_status()
436
+
437
+ result = response.json()
438
+
439
+ if 'Data' in result:
440
+ return self._decrypt_aes(result['Data'])
441
+ else:
442
+ return result
443
+
444
+ except requests.exceptions.RequestException as e:
445
+ raise ConnectionError(f'API 連線失敗: {str(e)}')
446
+
447
+
448
+ # ============================================================================
449
+ # 使用範例
450
+ # ============================================================================
451
+
452
+ def example_b2c_invoice():
453
+ """範例: B2C 二聯式發票開立"""
454
+ print("=== B2C 二聯式發票開立範例 ===\n")
455
+
456
+ # 初始化服務 (測試環境)
457
+ service = ECPayInvoiceService(
458
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
459
+ hash_key=ECPayInvoiceService.TEST_HASH_KEY,
460
+ hash_iv=ECPayInvoiceService.TEST_HASH_IV,
461
+ is_test=True
462
+ )
463
+
464
+ # 準備發票資料
465
+ invoice_data = InvoiceIssueData(
466
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
467
+ relate_number=f'ORD{int(time.time())}', # 唯一訂單編號
468
+ customer_identifier='0000000000', # B2C 固定值
469
+ customer_name='王小明',
470
+ customer_addr='台北市信義區信義路五段7號',
471
+ customer_phone='0912345678',
472
+ customer_email='test@example.com',
473
+ sales_amount=1050, # B2C 含稅金額
474
+ total_amount=1050,
475
+ print='0', # 不列印
476
+ donation='0', # 不捐贈
477
+ carrier_type='', # 不使用載具
478
+ items=[
479
+ {
480
+ 'ItemName': '測試商品A',
481
+ 'ItemCount': 1,
482
+ 'ItemWord': '個',
483
+ 'ItemPrice': 1000,
484
+ 'ItemTaxType': '1',
485
+ 'ItemAmount': 1050
486
+ }
487
+ ]
488
+ )
489
+
490
+ # 開立發票
491
+ try:
492
+ response = service.issue_invoice(invoice_data)
493
+
494
+ if response.success:
495
+ print(f"✓ 發票開立成功!")
496
+ print(f" 發票號碼: {response.invoice_number}")
497
+ print(f" 發票日期: {response.invoice_date}")
498
+ print(f" 隨機碼: {response.random_number}")
499
+ else:
500
+ print(f"✗ 發票開立失敗: {response.error_message}")
501
+ print(f" 錯誤碼: {response.rtn_code}")
502
+ print(f" 錯誤訊息: {response.rtn_msg}")
503
+
504
+ except Exception as e:
505
+ print(f"✗ 發生錯誤: {str(e)}")
506
+
507
+
508
+ def example_b2b_invoice():
509
+ """範例: B2B 三聯式發票開立"""
510
+ print("\n=== B2B 三聯式發票開立範例 ===\n")
511
+
512
+ service = ECPayInvoiceService(
513
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
514
+ hash_key=ECPayInvoiceService.TEST_HASH_KEY,
515
+ hash_iv=ECPayInvoiceService.TEST_HASH_IV,
516
+ is_test=True
517
+ )
518
+
519
+ # 計算 B2B 金額 (含稅 1050 → 未稅 1000 + 稅額 50)
520
+ amounts = service.calculate_b2b_amounts(1050)
521
+
522
+ invoice_data = InvoiceIssueData(
523
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
524
+ relate_number=f'ORD{int(time.time())}',
525
+ customer_identifier='80129529', # 8 碼統編
526
+ customer_name='測試公司股份有限公司',
527
+ customer_addr='台北市中正區重慶南路一段122號',
528
+ customer_phone='02-23113456',
529
+ customer_email='company@example.com',
530
+ sales_amount=amounts['sales_amount'], # 未稅金額
531
+ tax_amount=amounts['tax_amount'], # 稅額
532
+ total_amount=amounts['total_amount'], # 總額
533
+ print='0',
534
+ donation='0',
535
+ carrier_type='', # B2B 不可使用載具
536
+ items=[
537
+ {
538
+ 'ItemName': '測試商品B',
539
+ 'ItemCount': 1,
540
+ 'ItemWord': '個',
541
+ 'ItemPrice': 1000, # 未稅單價
542
+ 'ItemTaxType': '1',
543
+ 'ItemAmount': 1000 # 未稅小計
544
+ }
545
+ ]
546
+ )
547
+
548
+ try:
549
+ response = service.issue_invoice(invoice_data)
550
+
551
+ if response.success:
552
+ print(f"✓ B2B 發票開立成功!")
553
+ print(f" 發票號碼: {response.invoice_number}")
554
+ print(f" 未稅金額: {amounts['sales_amount']}")
555
+ print(f" 稅額: {amounts['tax_amount']}")
556
+ print(f" 總額: {amounts['total_amount']}")
557
+ else:
558
+ print(f"✗ B2B 發票開立失敗: {response.error_message}")
559
+
560
+ except Exception as e:
561
+ print(f"✗ 發生錯誤: {str(e)}")
562
+
563
+
564
+ def example_void_invoice():
565
+ """範例: 發票作廢"""
566
+ print("\n=== 發票作廢範例 ===\n")
567
+
568
+ service = ECPayInvoiceService(
569
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
570
+ hash_key=ECPayInvoiceService.TEST_HASH_KEY,
571
+ hash_iv=ECPayInvoiceService.TEST_HASH_IV,
572
+ is_test=True
573
+ )
574
+
575
+ void_data = InvoiceVoidData(
576
+ merchant_id=ECPayInvoiceService.TEST_MERCHANT_ID,
577
+ invoice_no='AA12345678', # 實際發票號碼
578
+ invoice_date='2024/01/29',
579
+ reason='訂單取消'
580
+ )
581
+
582
+ try:
583
+ result = service.void_invoice(void_data)
584
+ print(f"作廢結果: {result}")
585
+
586
+ except Exception as e:
587
+ print(f"✗ 發生錯誤: {str(e)}")
588
+
589
+
590
+ if __name__ == '__main__':
591
+ # 執行範例
592
+ example_b2c_invoice()
593
+ example_b2b_invoice()
594
+ # example_void_invoice() # 需要有效的發票號碼
595
+
596
+ print("\n" + "="*50)
597
+ print("範例執行完畢!")
598
+ print("="*50)