taiwan-logistics-skill 1.0.0

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,3183 @@
1
+ # Taiwan Logistics Code Examples
2
+
3
+ **Production-ready code examples for Taiwan Logistics integration**
4
+
5
+ Supporting NewebPay Logistics, ECPay Logistics, and PAYUNi Logistics with comprehensive TypeScript, Python, and PHP implementations.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [NewebPay Logistics Examples](#newebpay-logistics-examples)
12
+ - [Basic Integration](#1-basic-integration-newebpay)
13
+ - [Store Map Query](#2-store-map-query)
14
+ - [Create Shipment](#3-create-shipment)
15
+ - [Get Shipment Number](#4-get-shipment-number)
16
+ - [Print Label](#5-print-label)
17
+ - [Query Shipment](#6-query-shipment)
18
+ - [Modify Shipment](#7-modify-shipment)
19
+ - [Track Shipment](#8-track-shipment)
20
+ - [Status Notification](#9-status-notification-callback)
21
+
22
+ 2. [ECPay Logistics Examples](#ecpay-logistics-examples)
23
+ 3. [Real-World Scenarios](#real-world-scenarios)
24
+ 4. [Error Handling](#error-handling)
25
+
26
+ ---
27
+
28
+ ## NewebPay Logistics Examples
29
+
30
+ ### 1. Basic Integration (NewebPay)
31
+
32
+ #### TypeScript - Encryption Helper
33
+
34
+ ```typescript
35
+ import crypto from 'crypto';
36
+
37
+ interface NewebPayConfig {
38
+ merchantId: string;
39
+ hashKey: string;
40
+ hashIV: string;
41
+ isProduction?: boolean;
42
+ }
43
+
44
+ class NewebPayLogistics {
45
+ private config: Required<NewebPayConfig>;
46
+ private baseUrl: string;
47
+
48
+ constructor(config: NewebPayConfig) {
49
+ this.config = {
50
+ ...config,
51
+ isProduction: config.isProduction ?? false,
52
+ };
53
+
54
+ this.baseUrl = this.config.isProduction
55
+ ? 'https://core.newebpay.com/API/Logistic'
56
+ : 'https://ccore.newebpay.com/API/Logistic';
57
+ }
58
+
59
+ /**
60
+ * AES-256-CBC Encryption
61
+ */
62
+ private aesEncrypt(data: string): string {
63
+ const cipher = crypto.createCipheriv(
64
+ 'aes-256-cbc',
65
+ this.config.hashKey,
66
+ this.config.hashIV
67
+ );
68
+
69
+ let encrypted = cipher.update(data, 'utf8', 'hex');
70
+ encrypted += cipher.final('hex');
71
+
72
+ return encrypted;
73
+ }
74
+
75
+ /**
76
+ * AES-256-CBC Decryption
77
+ */
78
+ private aesDecrypt(encryptedData: string): string {
79
+ const decipher = crypto.createDecipheriv(
80
+ 'aes-256-cbc',
81
+ this.config.hashKey,
82
+ this.config.hashIV
83
+ );
84
+
85
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
86
+ decrypted += decipher.final('utf8');
87
+
88
+ return decrypted;
89
+ }
90
+
91
+ /**
92
+ * Generate Hash Data (SHA256)
93
+ */
94
+ private generateHashData(encryptData: string): string {
95
+ const raw = `${this.config.hashKey}${encryptData}${this.config.hashIV}`;
96
+ return crypto.createHash('sha256').update(raw).digest('hex').toUpperCase();
97
+ }
98
+
99
+ /**
100
+ * Encrypt request data
101
+ */
102
+ encryptData(data: Record<string, any>): { EncryptData_: string; HashData_: string } {
103
+ const jsonStr = JSON.stringify(data);
104
+ const encryptData = this.aesEncrypt(jsonStr);
105
+ const hashData = this.generateHashData(encryptData);
106
+
107
+ return {
108
+ EncryptData_: encryptData,
109
+ HashData_: hashData,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Decrypt response data
115
+ */
116
+ decryptData(encryptData: string, hashData: string): any {
117
+ // Verify hash
118
+ const calculatedHash = this.generateHashData(encryptData);
119
+ if (calculatedHash !== hashData.toUpperCase()) {
120
+ throw new Error('Hash verification failed');
121
+ }
122
+
123
+ const decrypted = this.aesDecrypt(encryptData);
124
+ return JSON.parse(decrypted);
125
+ }
126
+
127
+ /**
128
+ * Get current Unix timestamp
129
+ */
130
+ getTimestamp(): string {
131
+ return Math.floor(Date.now() / 1000).toString();
132
+ }
133
+ }
134
+
135
+ export { NewebPayLogistics };
136
+ ```
137
+
138
+ #### Python - Encryption Helper
139
+
140
+ ```python
141
+ """NewebPay Logistics Encryption Helper - Python"""
142
+
143
+ import json
144
+ import hashlib
145
+ import time
146
+ from typing import Dict, Any, Tuple
147
+ from Crypto.Cipher import AES
148
+ from Crypto.Util.Padding import pad, unpad
149
+
150
+
151
+ class NewebPayLogistics:
152
+ """NewebPay Logistics API Client"""
153
+
154
+ def __init__(
155
+ self,
156
+ merchant_id: str,
157
+ hash_key: str,
158
+ hash_iv: str,
159
+ is_production: bool = False
160
+ ):
161
+ self.merchant_id = merchant_id
162
+ self.hash_key = hash_key.encode('utf-8')
163
+ self.hash_iv = hash_iv.encode('utf-8')
164
+
165
+ self.base_url = (
166
+ 'https://core.newebpay.com/API/Logistic'
167
+ if is_production
168
+ else 'https://ccore.newebpay.com/API/Logistic'
169
+ )
170
+
171
+ def aes_encrypt(self, data: str) -> str:
172
+ """AES-256-CBC Encryption"""
173
+ cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
174
+ padded_data = pad(data.encode('utf-8'), AES.block_size)
175
+ encrypted = cipher.encrypt(padded_data)
176
+ return encrypted.hex()
177
+
178
+ def aes_decrypt(self, encrypted_data: str) -> str:
179
+ """AES-256-CBC Decryption"""
180
+ cipher = AES.new(self.hash_key, AES.MODE_CBC, self.hash_iv)
181
+ decrypted = cipher.decrypt(bytes.fromhex(encrypted_data))
182
+ unpadded = unpad(decrypted, AES.block_size)
183
+ return unpadded.decode('utf-8')
184
+
185
+ def generate_hash_data(self, encrypt_data: str) -> str:
186
+ """Generate Hash Data (SHA256)"""
187
+ raw = f"{self.hash_key.decode('utf-8')}{encrypt_data}{self.hash_iv.decode('utf-8')}"
188
+ return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
189
+
190
+ def encrypt_data(self, data: Dict[str, Any]) -> Dict[str, str]:
191
+ """Encrypt request data"""
192
+ json_str = json.dumps(data, ensure_ascii=False)
193
+ encrypt_data = self.aes_encrypt(json_str)
194
+ hash_data = self.generate_hash_data(encrypt_data)
195
+
196
+ return {
197
+ 'EncryptData_': encrypt_data,
198
+ 'HashData_': hash_data,
199
+ }
200
+
201
+ def decrypt_data(self, encrypt_data: str, hash_data: str) -> Dict[str, Any]:
202
+ """Decrypt response data"""
203
+ # Verify hash
204
+ calculated_hash = self.generate_hash_data(encrypt_data)
205
+ if calculated_hash != hash_data.upper():
206
+ raise ValueError('Hash verification failed')
207
+
208
+ decrypted = self.aes_decrypt(encrypt_data)
209
+ return json.loads(decrypted)
210
+
211
+ @staticmethod
212
+ def get_timestamp() -> str:
213
+ """Get current Unix timestamp"""
214
+ return str(int(time.time()))
215
+ ```
216
+
217
+ ---
218
+
219
+ ### 2. Store Map Query
220
+
221
+ Query convenience store locations for pickup or sender.
222
+
223
+ #### TypeScript Example
224
+
225
+ ```typescript
226
+ import axios from 'axios';
227
+
228
+ interface StoreMapRequest {
229
+ merchantOrderNo: string;
230
+ lgsType: 'B2C' | 'C2C';
231
+ shipType: '1' | '2' | '3' | '4'; // 1=7-11, 2=FamilyMart, 3=Hi-Life, 4=OK Mart
232
+ returnURL: string;
233
+ extraData?: string;
234
+ }
235
+
236
+ class NewebPayStoreMap extends NewebPayLogistics {
237
+ /**
238
+ * Query store map
239
+ */
240
+ async queryStoreMap(params: StoreMapRequest): Promise<string> {
241
+ const data = {
242
+ MerchantOrderNo: params.merchantOrderNo,
243
+ LgsType: params.lgsType,
244
+ ShipType: params.shipType,
245
+ ReturnURL: params.returnURL,
246
+ TimeStamp: this.getTimestamp(),
247
+ ExtraData: params.extraData || '',
248
+ };
249
+
250
+ const { EncryptData_, HashData_ } = this.encryptData(data);
251
+
252
+ const requestData = {
253
+ UID_: this.config.merchantId,
254
+ EncryptData_,
255
+ HashData_,
256
+ Version_: '1.0',
257
+ RespondType_: 'JSON',
258
+ };
259
+
260
+ const response = await axios.post(
261
+ `${this.baseUrl}/storeMap`,
262
+ new URLSearchParams(requestData as any),
263
+ {
264
+ headers: {
265
+ 'Content-Type': 'application/x-www-form-urlencoded',
266
+ },
267
+ }
268
+ );
269
+
270
+ // NewebPay will redirect to store selection page
271
+ // Return the redirect URL or HTML
272
+ return response.data;
273
+ }
274
+
275
+ /**
276
+ * Handle store map callback
277
+ */
278
+ handleStoreMapCallback(
279
+ encryptData: string,
280
+ hashData: string
281
+ ): {
282
+ lgsType: string;
283
+ shipType: string;
284
+ merchantOrderNo: string;
285
+ storeName: string;
286
+ storeTel: string;
287
+ storeAddr: string;
288
+ storeID: string;
289
+ extraData: string;
290
+ } {
291
+ const decrypted = this.decryptData(encryptData, hashData);
292
+
293
+ return {
294
+ lgsType: decrypted.LgsType,
295
+ shipType: decrypted.ShipType,
296
+ merchantOrderNo: decrypted.MerchantOrderNo,
297
+ storeName: decrypted.StoreName,
298
+ storeTel: decrypted.StoreTel,
299
+ storeAddr: decrypted.StoreAddr,
300
+ storeID: decrypted.StoreID,
301
+ extraData: decrypted.ExtraData,
302
+ };
303
+ }
304
+ }
305
+
306
+ // Usage Example
307
+ const logistics = new NewebPayStoreMap({
308
+ merchantId: 'YOUR_MERCHANT_ID',
309
+ hashKey: 'YOUR_HASH_KEY',
310
+ hashIV: 'YOUR_HASH_IV',
311
+ isProduction: false,
312
+ });
313
+
314
+ // Open store map
315
+ await logistics.queryStoreMap({
316
+ merchantOrderNo: `ORD${Date.now()}`,
317
+ lgsType: 'C2C',
318
+ shipType: '1', // 7-ELEVEN
319
+ returnURL: 'https://your-site.com/callback/store-map',
320
+ extraData: 'order_id=123',
321
+ });
322
+
323
+ export { NewebPayStoreMap };
324
+ ```
325
+
326
+ #### Python Example
327
+
328
+ ```python
329
+ """Store Map Query - Python Example"""
330
+
331
+ import requests
332
+ from typing import Dict, Optional
333
+
334
+
335
+ class NewebPayStoreMap(NewebPayLogistics):
336
+ """Store Map Query Operations"""
337
+
338
+ def query_store_map(
339
+ self,
340
+ merchant_order_no: str,
341
+ lgs_type: str, # 'B2C' or 'C2C'
342
+ ship_type: str, # '1'=7-11, '2'=FamilyMart, '3'=Hi-Life, '4'=OK Mart
343
+ return_url: str,
344
+ extra_data: str = '',
345
+ ) -> str:
346
+ """Query store map"""
347
+
348
+ data = {
349
+ 'MerchantOrderNo': merchant_order_no,
350
+ 'LgsType': lgs_type,
351
+ 'ShipType': ship_type,
352
+ 'ReturnURL': return_url,
353
+ 'TimeStamp': self.get_timestamp(),
354
+ 'ExtraData': extra_data,
355
+ }
356
+
357
+ encrypted = self.encrypt_data(data)
358
+
359
+ request_data = {
360
+ 'UID_': self.merchant_id,
361
+ 'EncryptData_': encrypted['EncryptData_'],
362
+ 'HashData_': encrypted['HashData_'],
363
+ 'Version_': '1.0',
364
+ 'RespondType_': 'JSON',
365
+ }
366
+
367
+ response = requests.post(
368
+ f'{self.base_url}/storeMap',
369
+ data=request_data,
370
+ )
371
+
372
+ return response.text
373
+
374
+ def handle_store_map_callback(
375
+ self,
376
+ encrypt_data: str,
377
+ hash_data: str,
378
+ ) -> Dict[str, str]:
379
+ """Handle store map callback"""
380
+
381
+ decrypted = self.decrypt_data(encrypt_data, hash_data)
382
+
383
+ return {
384
+ 'lgs_type': decrypted['LgsType'],
385
+ 'ship_type': decrypted['ShipType'],
386
+ 'merchant_order_no': decrypted['MerchantOrderNo'],
387
+ 'store_name': decrypted['StoreName'],
388
+ 'store_tel': decrypted['StoreTel'],
389
+ 'store_addr': decrypted['StoreAddr'],
390
+ 'store_id': decrypted['StoreID'],
391
+ 'extra_data': decrypted.get('ExtraData', ''),
392
+ }
393
+
394
+
395
+ # Usage Example
396
+ logistics = NewebPayStoreMap(
397
+ merchant_id='YOUR_MERCHANT_ID',
398
+ hash_key='YOUR_HASH_KEY',
399
+ hash_iv='YOUR_HASH_IV',
400
+ is_production=False,
401
+ )
402
+
403
+ # Open store map
404
+ html = logistics.query_store_map(
405
+ merchant_order_no=f'ORD{int(time.time())}',
406
+ lgs_type='C2C',
407
+ ship_type='1', # 7-ELEVEN
408
+ return_url='https://your-site.com/callback/store-map',
409
+ extra_data='order_id=123',
410
+ )
411
+ ```
412
+
413
+ ---
414
+
415
+ ### 3. Create Shipment
416
+
417
+ Create logistics shipment order.
418
+
419
+ #### TypeScript Example
420
+
421
+ ```typescript
422
+ interface CreateShipmentRequest {
423
+ merchantOrderNo: string;
424
+ tradeType: 1 | 3; // 1=COD, 3=No Payment
425
+ userName: string;
426
+ userTel: string;
427
+ userEmail: string;
428
+ storeID: string;
429
+ amt: number;
430
+ itemDesc?: string;
431
+ notifyURL?: string;
432
+ lgsType: 'B2C' | 'C2C';
433
+ shipType: '1' | '2' | '3' | '4';
434
+ }
435
+
436
+ class NewebPayShipment extends NewebPayLogistics {
437
+ /**
438
+ * Create shipment order
439
+ */
440
+ async createShipment(params: CreateShipmentRequest) {
441
+ const data = {
442
+ MerchantOrderNo: params.merchantOrderNo,
443
+ TradeType: params.tradeType,
444
+ UserName: params.userName,
445
+ UserTel: params.userTel,
446
+ UserEmail: params.userEmail,
447
+ StoreID: params.storeID,
448
+ Amt: params.amt,
449
+ NotifyURL: params.notifyURL || '',
450
+ ItemDesc: params.itemDesc || '',
451
+ LgsType: params.lgsType,
452
+ ShipType: params.shipType,
453
+ TimeStamp: this.getTimestamp(),
454
+ };
455
+
456
+ const { EncryptData_, HashData_ } = this.encryptData(data);
457
+
458
+ const requestData = {
459
+ UID_: this.config.merchantId,
460
+ EncryptData_,
461
+ HashData_,
462
+ Version_: '1.0',
463
+ RespondType_: 'JSON',
464
+ };
465
+
466
+ const response = await axios.post(
467
+ `${this.baseUrl}/createShipment`,
468
+ new URLSearchParams(requestData as any),
469
+ {
470
+ headers: {
471
+ 'Content-Type': 'application/x-www-form-urlencoded',
472
+ },
473
+ }
474
+ );
475
+
476
+ const result = response.data;
477
+
478
+ if (result.Status !== 'SUCCESS') {
479
+ throw new Error(`Create shipment failed: ${result.Message}`);
480
+ }
481
+
482
+ // Decrypt response
483
+ const decrypted = this.decryptData(result.EncryptData, result.HashData);
484
+
485
+ return {
486
+ merchantID: decrypted.MerchantID,
487
+ amt: decrypted.Amt,
488
+ merchantOrderNo: decrypted.MerchantOrderNo,
489
+ tradeNo: decrypted.TradeNo,
490
+ lgsType: decrypted.LgsType,
491
+ shipType: decrypted.ShipType,
492
+ storeID: decrypted.StoreID,
493
+ tradeType: decrypted.TradeType,
494
+ };
495
+ }
496
+ }
497
+
498
+ // Usage Example
499
+ const shipment = new NewebPayShipment({
500
+ merchantId: 'YOUR_MERCHANT_ID',
501
+ hashKey: 'YOUR_HASH_KEY',
502
+ hashIV: 'YOUR_HASH_IV',
503
+ });
504
+
505
+ const result = await shipment.createShipment({
506
+ merchantOrderNo: `ORD${Date.now()}`,
507
+ tradeType: 1, // Cash on Delivery
508
+ userName: 'John Doe',
509
+ userTel: '0912345678',
510
+ userEmail: 'john@example.com',
511
+ storeID: '123456', // From store map query
512
+ amt: 1500,
513
+ itemDesc: 'T-shirt x 2',
514
+ notifyURL: 'https://your-site.com/callback/shipment',
515
+ lgsType: 'C2C',
516
+ shipType: '1', // 7-ELEVEN
517
+ });
518
+
519
+ console.log('Trade No:', result.tradeNo);
520
+
521
+ export { NewebPayShipment };
522
+ ```
523
+
524
+ #### Python Example
525
+
526
+ ```python
527
+ """Create Shipment - Python Example"""
528
+
529
+ from typing import Dict, Optional
530
+
531
+
532
+ class NewebPayShipment(NewebPayLogistics):
533
+ """Shipment Creation Operations"""
534
+
535
+ def create_shipment(
536
+ self,
537
+ merchant_order_no: str,
538
+ trade_type: int, # 1=COD, 3=No Payment
539
+ user_name: str,
540
+ user_tel: str,
541
+ user_email: str,
542
+ store_id: str,
543
+ amt: int,
544
+ lgs_type: str, # 'B2C' or 'C2C'
545
+ ship_type: str, # '1'=7-11, '2'=FamilyMart, '3'=Hi-Life, '4'=OK Mart
546
+ item_desc: str = '',
547
+ notify_url: str = '',
548
+ ) -> Dict[str, any]:
549
+ """Create shipment order"""
550
+
551
+ data = {
552
+ 'MerchantOrderNo': merchant_order_no,
553
+ 'TradeType': trade_type,
554
+ 'UserName': user_name,
555
+ 'UserTel': user_tel,
556
+ 'UserEmail': user_email,
557
+ 'StoreID': store_id,
558
+ 'Amt': amt,
559
+ 'NotifyURL': notify_url,
560
+ 'ItemDesc': item_desc,
561
+ 'LgsType': lgs_type,
562
+ 'ShipType': ship_type,
563
+ 'TimeStamp': self.get_timestamp(),
564
+ }
565
+
566
+ encrypted = self.encrypt_data(data)
567
+
568
+ request_data = {
569
+ 'UID_': self.merchant_id,
570
+ 'EncryptData_': encrypted['EncryptData_'],
571
+ 'HashData_': encrypted['HashData_'],
572
+ 'Version_': '1.0',
573
+ 'RespondType_': 'JSON',
574
+ }
575
+
576
+ response = requests.post(
577
+ f'{self.base_url}/createShipment',
578
+ data=request_data,
579
+ )
580
+
581
+ result = response.json()
582
+
583
+ if result['Status'] != 'SUCCESS':
584
+ raise Exception(f"Create shipment failed: {result['Message']}")
585
+
586
+ # Decrypt response
587
+ decrypted = self.decrypt_data(result['EncryptData'], result['HashData'])
588
+
589
+ return {
590
+ 'merchant_id': decrypted['MerchantID'],
591
+ 'amt': decrypted['Amt'],
592
+ 'merchant_order_no': decrypted['MerchantOrderNo'],
593
+ 'trade_no': decrypted['TradeNo'],
594
+ 'lgs_type': decrypted['LgsType'],
595
+ 'ship_type': decrypted['ShipType'],
596
+ 'store_id': decrypted['StoreID'],
597
+ 'trade_type': decrypted['TradeType'],
598
+ }
599
+
600
+
601
+ # Usage Example
602
+ shipment = NewebPayShipment(
603
+ merchant_id='YOUR_MERCHANT_ID',
604
+ hash_key='YOUR_HASH_KEY',
605
+ hash_iv='YOUR_HASH_IV',
606
+ )
607
+
608
+ result = shipment.create_shipment(
609
+ merchant_order_no=f'ORD{int(time.time())}',
610
+ trade_type=1, # Cash on Delivery
611
+ user_name='John Doe',
612
+ user_tel='0912345678',
613
+ user_email='john@example.com',
614
+ store_id='123456', # From store map query
615
+ amt=1500,
616
+ item_desc='T-shirt x 2',
617
+ notify_url='https://your-site.com/callback/shipment',
618
+ lgs_type='C2C',
619
+ ship_type='1', # 7-ELEVEN
620
+ )
621
+
622
+ print(f"Trade No: {result['trade_no']}")
623
+ ```
624
+
625
+ ---
626
+
627
+ ### 4. Get Shipment Number
628
+
629
+ Get shipping code for Kiosk printing.
630
+
631
+ #### TypeScript Example
632
+
633
+ ```typescript
634
+ class NewebPayShipmentNumber extends NewebPayLogistics {
635
+ /**
636
+ * Get shipment numbers (max 10 orders)
637
+ */
638
+ async getShipmentNumbers(merchantOrderNos: string[]) {
639
+ if (merchantOrderNos.length > 10) {
640
+ throw new Error('Maximum 10 orders per request');
641
+ }
642
+
643
+ const data = {
644
+ MerchantOrderNo: merchantOrderNos,
645
+ TimeStamp: this.getTimestamp(),
646
+ };
647
+
648
+ const { EncryptData_, HashData_ } = this.encryptData(data);
649
+
650
+ const requestData = {
651
+ UID_: this.config.merchantId,
652
+ EncryptData_,
653
+ HashData_,
654
+ Version_: '1.0',
655
+ RespondType_: 'JSON',
656
+ };
657
+
658
+ const response = await axios.post(
659
+ `${this.baseUrl}/getShipmentNo`,
660
+ new URLSearchParams(requestData as any),
661
+ {
662
+ headers: {
663
+ 'Content-Type': 'application/x-www-form-urlencoded',
664
+ },
665
+ }
666
+ );
667
+
668
+ const result = response.data;
669
+
670
+ if (result.Status !== 'SUCCESS') {
671
+ throw new Error(`Get shipment number failed: ${result.Message}`);
672
+ }
673
+
674
+ // Decrypt response
675
+ const decrypted = this.decryptData(result.EncryptData, result.HashData);
676
+
677
+ return {
678
+ success: decrypted.SUCCESS || [],
679
+ error: decrypted.ERROR || [],
680
+ };
681
+ }
682
+ }
683
+
684
+ // Usage Example
685
+ const shipmentNum = new NewebPayShipmentNumber({
686
+ merchantId: 'YOUR_MERCHANT_ID',
687
+ hashKey: 'YOUR_HASH_KEY',
688
+ hashIV: 'YOUR_HASH_IV',
689
+ });
690
+
691
+ const result = await shipmentNum.getShipmentNumbers([
692
+ 'ORD001',
693
+ 'ORD002',
694
+ 'ORD003',
695
+ ]);
696
+
697
+ result.success.forEach((item: any) => {
698
+ console.log(`Order ${item.MerchantOrderNo}:`);
699
+ console.log(` Shipment No: ${item.LgsNo}`);
700
+ console.log(` Store Print No: ${item.StorePrintNo}`);
701
+ });
702
+
703
+ result.error.forEach((item: any) => {
704
+ console.error(`Order ${item.MerchantOrderNo}: ${item.ErrorCode}`);
705
+ });
706
+
707
+ export { NewebPayShipmentNumber };
708
+ ```
709
+
710
+ ---
711
+
712
+ ### 5. Print Label
713
+
714
+ Print shipping labels (Form POST method).
715
+
716
+ #### TypeScript Example
717
+
718
+ ```typescript
719
+ class NewebPayPrintLabel extends NewebPayLogistics {
720
+ /**
721
+ * Generate print label HTML form
722
+ */
723
+ generatePrintLabelForm(params: {
724
+ merchantOrderNos: string[];
725
+ lgsType: 'B2C' | 'C2C';
726
+ shipType: '1' | '2' | '3' | '4';
727
+ }): string {
728
+ // Validate batch limits
729
+ const limits: Record<string, number> = {
730
+ '1': 18, // 7-ELEVEN
731
+ '2': 8, // FamilyMart
732
+ '3': 18, // Hi-Life
733
+ '4': 18, // OK Mart
734
+ };
735
+
736
+ if (params.merchantOrderNos.length > limits[params.shipType]) {
737
+ throw new Error(`Maximum ${limits[params.shipType]} labels for this provider`);
738
+ }
739
+
740
+ const data = {
741
+ LgsType: params.lgsType,
742
+ ShipType: params.shipType,
743
+ MerchantOrderNo: params.merchantOrderNos,
744
+ TimeStamp: this.getTimestamp(),
745
+ };
746
+
747
+ const { EncryptData_, HashData_ } = this.encryptData(data);
748
+
749
+ const formData = {
750
+ UID_: this.config.merchantId,
751
+ EncryptData_,
752
+ HashData_,
753
+ Version_: '1.0',
754
+ RespondType_: 'JSON',
755
+ };
756
+
757
+ // Generate HTML form for auto-submit
758
+ const inputs = Object.entries(formData)
759
+ .map(([key, value]) => `<input type="hidden" name="${key}" value="${value}">`)
760
+ .join('\n');
761
+
762
+ return `
763
+ <!DOCTYPE html>
764
+ <html>
765
+ <head>
766
+ <meta charset="UTF-8">
767
+ <title>Print Shipping Label</title>
768
+ </head>
769
+ <body onload="document.getElementById('printForm').submit();">
770
+ <form id="printForm" method="post" action="${this.baseUrl}/printLabel">
771
+ ${inputs}
772
+ </form>
773
+ <p>Redirecting to print page...</p>
774
+ </body>
775
+ </html>
776
+ `;
777
+ }
778
+ }
779
+
780
+ // Usage Example
781
+ const printLabel = new NewebPayPrintLabel({
782
+ merchantId: 'YOUR_MERCHANT_ID',
783
+ hashKey: 'YOUR_HASH_KEY',
784
+ hashIV: 'YOUR_HASH_IV',
785
+ });
786
+
787
+ const html = printLabel.generatePrintLabelForm({
788
+ merchantOrderNos: ['ORD001', 'ORD002'],
789
+ lgsType: 'C2C',
790
+ shipType: '1', // 7-ELEVEN (max 18 labels)
791
+ });
792
+
793
+ // Send HTML to browser or save to file
794
+ export { NewebPayPrintLabel };
795
+ ```
796
+
797
+ ---
798
+
799
+ ### 6. Query Shipment
800
+
801
+ Query logistics order status.
802
+
803
+ #### TypeScript Example
804
+
805
+ ```typescript
806
+ class NewebPayQueryShipment extends NewebPayLogistics {
807
+ /**
808
+ * Query shipment status
809
+ */
810
+ async queryShipment(merchantOrderNo: string) {
811
+ const data = {
812
+ MerchantOrderNo: merchantOrderNo,
813
+ TimeStamp: this.getTimestamp(),
814
+ };
815
+
816
+ const { EncryptData_, HashData_ } = this.encryptData(data);
817
+
818
+ const requestData = {
819
+ UID_: this.config.merchantId,
820
+ EncryptData_,
821
+ HashData_,
822
+ Version_: '1.0',
823
+ RespondType_: 'JSON',
824
+ };
825
+
826
+ const response = await axios.post(
827
+ `${this.baseUrl}/queryShipment`,
828
+ new URLSearchParams(requestData as any),
829
+ {
830
+ headers: {
831
+ 'Content-Type': 'application/x-www-form-urlencoded',
832
+ },
833
+ }
834
+ );
835
+
836
+ const result = response.data;
837
+
838
+ if (result.Status !== 'SUCCESS') {
839
+ throw new Error(`Query shipment failed: ${result.Message}`);
840
+ }
841
+
842
+ // Decrypt response
843
+ const decrypted = this.decryptData(result.EncryptData, result.HashData);
844
+
845
+ return {
846
+ merchantID: decrypted.MerchantID,
847
+ lgsType: decrypted.LgsType,
848
+ tradeNo: decrypted.TradeNo,
849
+ merchantOrderNo: decrypted.MerchantOrderNo,
850
+ amt: decrypted.Amt,
851
+ itemDesc: decrypted.ItemDesc,
852
+ lgsNo: decrypted.LgsNo,
853
+ storePrintNo: decrypted.StorePrintNo,
854
+ collectionAmt: decrypted.collectionAmt,
855
+ tradeType: decrypted.TradeType,
856
+ type: decrypted.Type,
857
+ shopDate: decrypted.ShopDate,
858
+ userName: decrypted.UserName,
859
+ userTel: decrypted.UserTel,
860
+ userEmail: decrypted.UserEmail,
861
+ storeID: decrypted.StoreID,
862
+ shipType: decrypted.ShipType,
863
+ storeName: decrypted.StoreName,
864
+ retId: decrypted.Retld,
865
+ retString: decrypted.RetString,
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Get human-readable status
871
+ */
872
+ getStatusDescription(retId: string): string {
873
+ const statusMap: Record<string, string> = {
874
+ '0_1': 'Order not processed',
875
+ '0_2': 'Shipment number expired',
876
+ '0_3': 'Shipment canceled',
877
+ '1': 'Order processing',
878
+ '2': 'Store received shipment',
879
+ '3': 'Store reselected',
880
+ '4': 'Arrived at logistics center',
881
+ '5': 'Arrived at pickup store',
882
+ '6': 'Customer picked up',
883
+ '-1': 'Returned to merchant',
884
+ // ... more statuses
885
+ };
886
+
887
+ return statusMap[retId] || 'Unknown status';
888
+ }
889
+ }
890
+
891
+ // Usage Example
892
+ const query = new NewebPayQueryShipment({
893
+ merchantId: 'YOUR_MERCHANT_ID',
894
+ hashKey: 'YOUR_HASH_KEY',
895
+ hashIV: 'YOUR_HASH_IV',
896
+ });
897
+
898
+ const status = await query.queryShipment('ORD123456');
899
+
900
+ console.log(`Order: ${status.merchantOrderNo}`);
901
+ console.log(`Status: ${status.retString} (${status.retId})`);
902
+ console.log(`Tracking No: ${status.lgsNo}`);
903
+ console.log(`Store: ${status.storeName}`);
904
+
905
+ export { NewebPayQueryShipment };
906
+ ```
907
+
908
+ ---
909
+
910
+ ### 7. Modify Shipment
911
+
912
+ Modify shipment order details.
913
+
914
+ #### TypeScript Example
915
+
916
+ ```typescript
917
+ interface ModifyShipmentRequest {
918
+ merchantOrderNo: string;
919
+ lgsType: 'B2C' | 'C2C';
920
+ shipType: '1' | '2' | '3' | '4';
921
+ userName?: string;
922
+ userTel?: string;
923
+ userEmail?: string;
924
+ storeID?: string;
925
+ }
926
+
927
+ class NewebPayModifyShipment extends NewebPayLogistics {
928
+ /**
929
+ * Modify shipment order
930
+ */
931
+ async modifyShipment(params: ModifyShipmentRequest) {
932
+ const data: any = {
933
+ MerchantOrderNo: params.merchantOrderNo,
934
+ LgsType: params.lgsType,
935
+ ShipType: params.shipType,
936
+ TimeStamp: this.getTimestamp(),
937
+ };
938
+
939
+ // Add optional fields
940
+ if (params.userName) data.UserName = params.userName;
941
+ if (params.userTel) data.UserTel = params.userTel;
942
+ if (params.userEmail) data.UserEmail = params.userEmail;
943
+ if (params.storeID) data.StoreID = params.storeID;
944
+
945
+ const { EncryptData_, HashData_ } = this.encryptData(data);
946
+
947
+ const requestData = {
948
+ UID_: this.config.merchantId,
949
+ EncryptData_,
950
+ HashData_,
951
+ Version_: '1.0',
952
+ RespondType_: 'JSON',
953
+ };
954
+
955
+ const response = await axios.post(
956
+ `${this.baseUrl}/modifyShipment`,
957
+ new URLSearchParams(requestData as any),
958
+ {
959
+ headers: {
960
+ 'Content-Type': 'application/x-www-form-urlencoded',
961
+ },
962
+ }
963
+ );
964
+
965
+ const result = response.data;
966
+
967
+ if (result.Status !== 'SUCCESS') {
968
+ throw new Error(`Modify shipment failed: ${result.Message}`);
969
+ }
970
+
971
+ // Decrypt response
972
+ const decrypted = this.decryptData(result.EncryptData, result.HashData);
973
+
974
+ return {
975
+ merchantID: decrypted.MerchantID,
976
+ merchantOrderNo: decrypted.MerchantOrderNo,
977
+ lgsType: decrypted.LgsType,
978
+ shipType: decrypted.ShipType,
979
+ };
980
+ }
981
+ }
982
+
983
+ // Usage Example
984
+ const modify = new NewebPayModifyShipment({
985
+ merchantId: 'YOUR_MERCHANT_ID',
986
+ hashKey: 'YOUR_HASH_KEY',
987
+ hashIV: 'YOUR_HASH_IV',
988
+ });
989
+
990
+ // Change recipient information
991
+ await modify.modifyShipment({
992
+ merchantOrderNo: 'ORD123456',
993
+ lgsType: 'C2C',
994
+ shipType: '1',
995
+ userName: 'Jane Doe',
996
+ userTel: '0987654321',
997
+ userEmail: 'jane@example.com',
998
+ });
999
+
1000
+ // Change pickup store
1001
+ await modify.modifyShipment({
1002
+ merchantOrderNo: 'ORD123456',
1003
+ lgsType: 'C2C',
1004
+ shipType: '1',
1005
+ storeID: '654321',
1006
+ });
1007
+
1008
+ export { NewebPayModifyShipment };
1009
+ ```
1010
+
1011
+ ---
1012
+
1013
+ ### 8. Track Shipment
1014
+
1015
+ Track logistics delivery history.
1016
+
1017
+ #### Python Example
1018
+
1019
+ ```python
1020
+ """Track Shipment History - Python Example"""
1021
+
1022
+
1023
+ class NewebPayTrackShipment(NewebPayLogistics):
1024
+ """Track Shipment Operations"""
1025
+
1026
+ def track_shipment(self, merchant_order_no: str) -> Dict[str, any]:
1027
+ """Track shipment history"""
1028
+
1029
+ data = {
1030
+ 'MerchantOrderNo': merchant_order_no,
1031
+ 'TimeStamp': self.get_timestamp(),
1032
+ }
1033
+
1034
+ encrypted = self.encrypt_data(data)
1035
+
1036
+ request_data = {
1037
+ 'UID_': self.merchant_id,
1038
+ 'EncryptData_': encrypted['EncryptData_'],
1039
+ 'HashData_': encrypted['HashData_'],
1040
+ 'Version_': '1.0',
1041
+ 'RespondType_': 'JSON',
1042
+ }
1043
+
1044
+ response = requests.post(
1045
+ f'{self.base_url}/trace',
1046
+ data=request_data,
1047
+ )
1048
+
1049
+ result = response.json()
1050
+
1051
+ if result['Status'] != 'SUCCESS':
1052
+ raise Exception(f"Track shipment failed: {result['Message']}")
1053
+
1054
+ # Decrypt response
1055
+ decrypted = self.decrypt_data(result['EncryptData'], result['HashData'])
1056
+
1057
+ return {
1058
+ 'lgs_type': decrypted['LgsType'],
1059
+ 'merchant_order_no': decrypted['MerchantOrderNo'],
1060
+ 'lgs_no': decrypted['LgsNo'],
1061
+ 'trade_type': decrypted['TradeType'],
1062
+ 'ship_type': decrypted['ShipType'],
1063
+ 'history': decrypted.get('History', []),
1064
+ 'ret_id': decrypted.get('Retld', ''),
1065
+ 'ret_string': decrypted.get('RetString', ''),
1066
+ }
1067
+
1068
+ def print_tracking_history(self, merchant_order_no: str):
1069
+ """Print tracking history in readable format"""
1070
+
1071
+ tracking = self.track_shipment(merchant_order_no)
1072
+
1073
+ print(f"Order: {tracking['merchant_order_no']}")
1074
+ print(f"Tracking No: {tracking['lgs_no']}")
1075
+ print(f"Current Status: {tracking['ret_string']}")
1076
+ print("\nHistory:")
1077
+
1078
+ for event in tracking['history']:
1079
+ print(f" {event.get('EventTime')}: {event.get('RetString')}")
1080
+
1081
+
1082
+ # Usage Example
1083
+ track = NewebPayTrackShipment(
1084
+ merchant_id='YOUR_MERCHANT_ID',
1085
+ hash_key='YOUR_HASH_KEY',
1086
+ hash_iv='YOUR_HASH_IV',
1087
+ )
1088
+
1089
+ # Get tracking history
1090
+ tracking = track.track_shipment('ORD123456')
1091
+
1092
+ # Print formatted history
1093
+ track.print_tracking_history('ORD123456')
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ ### 9. Status Notification (Callback)
1099
+
1100
+ Handle real-time status notifications from NewebPay.
1101
+
1102
+ #### Express.js Example
1103
+
1104
+ ```typescript
1105
+ import express from 'express';
1106
+
1107
+ const app = express();
1108
+
1109
+ app.use(express.urlencoded({ extended: true }));
1110
+ app.use(express.json());
1111
+
1112
+ const logistics = new NewebPayLogistics({
1113
+ merchantId: 'YOUR_MERCHANT_ID',
1114
+ hashKey: 'YOUR_HASH_KEY',
1115
+ hashIV: 'YOUR_HASH_IV',
1116
+ });
1117
+
1118
+ /**
1119
+ * Handle shipment status notification
1120
+ */
1121
+ app.post('/callback/shipment-status', async (req, res) => {
1122
+ try {
1123
+ const { Status, Message, EncryptData_, HashData_, UID_, Version_ } = req.body;
1124
+
1125
+ console.log('Received notification:', {
1126
+ Status,
1127
+ Message,
1128
+ UID: UID_,
1129
+ Version: Version_,
1130
+ });
1131
+
1132
+ if (Status !== 'SUCCESS') {
1133
+ console.error('Notification error:', Message);
1134
+ return res.send('0|Error');
1135
+ }
1136
+
1137
+ // Decrypt data
1138
+ const data = logistics.decryptData(EncryptData_, HashData_);
1139
+
1140
+ console.log('Notification data:', data);
1141
+
1142
+ // Process the notification
1143
+ await processShipmentStatusUpdate({
1144
+ lgsType: data.LgsType,
1145
+ merchantOrderNo: data.MerchantOrderNo,
1146
+ lgsNo: data.LgsNo,
1147
+ tradeType: data.TradeType,
1148
+ shipType: data.ShipType,
1149
+ retId: data.Retld,
1150
+ retString: data.RetString,
1151
+ eventTime: data.EventTime,
1152
+ });
1153
+
1154
+ // Return success
1155
+ res.send('1|OK');
1156
+ } catch (error) {
1157
+ console.error('Callback error:', error);
1158
+ res.send('0|Error');
1159
+ }
1160
+ });
1161
+
1162
+ /**
1163
+ * Process shipment status update
1164
+ */
1165
+ async function processShipmentStatusUpdate(data: {
1166
+ lgsType: string;
1167
+ merchantOrderNo: string;
1168
+ lgsNo: string;
1169
+ tradeType: number;
1170
+ shipType: string;
1171
+ retId: string;
1172
+ retString: string;
1173
+ eventTime: string;
1174
+ }) {
1175
+ console.log(`Processing status update for order ${data.merchantOrderNo}`);
1176
+
1177
+ // Update database
1178
+ // await db.orders.updateOne(
1179
+ // { orderNo: data.merchantOrderNo },
1180
+ // {
1181
+ // $set: {
1182
+ // 'logistics.status': data.retId,
1183
+ // 'logistics.statusDesc': data.retString,
1184
+ // 'logistics.trackingNo': data.lgsNo,
1185
+ // 'logistics.lastUpdate': new Date(data.eventTime),
1186
+ // },
1187
+ // }
1188
+ // );
1189
+
1190
+ // Send notification to customer
1191
+ if (data.retId === '6') {
1192
+ // Customer picked up
1193
+ // await sendEmail({
1194
+ // to: customerEmail,
1195
+ // subject: 'Order Delivered',
1196
+ // body: `Your order ${data.merchantOrderNo} has been picked up.`,
1197
+ // });
1198
+ }
1199
+
1200
+ console.log(`Status update completed for order ${data.merchantOrderNo}`);
1201
+ }
1202
+
1203
+ app.listen(3000, () => {
1204
+ console.log('Callback server listening on port 3000');
1205
+ });
1206
+ ```
1207
+
1208
+ #### Flask Example
1209
+
1210
+ ```python
1211
+ """Status Notification Callback - Flask Example"""
1212
+
1213
+ from flask import Flask, request
1214
+
1215
+
1216
+ app = Flask(__name__)
1217
+
1218
+ logistics = NewebPayLogistics(
1219
+ merchant_id='YOUR_MERCHANT_ID',
1220
+ hash_key='YOUR_HASH_KEY',
1221
+ hash_iv='YOUR_HASH_IV',
1222
+ )
1223
+
1224
+
1225
+ @app.route('/callback/shipment-status', methods=['POST'])
1226
+ def shipment_status_callback():
1227
+ """Handle shipment status notification"""
1228
+
1229
+ try:
1230
+ data = request.form.to_dict()
1231
+
1232
+ status = data.get('Status')
1233
+ message = data.get('Message')
1234
+ encrypt_data = data.get('EncryptData_')
1235
+ hash_data = data.get('HashData_')
1236
+
1237
+ app.logger.info(f'Received notification: {status} - {message}')
1238
+
1239
+ if status != 'SUCCESS':
1240
+ app.logger.error(f'Notification error: {message}')
1241
+ return '0|Error'
1242
+
1243
+ # Decrypt data
1244
+ decrypted = logistics.decrypt_data(encrypt_data, hash_data)
1245
+
1246
+ app.logger.info(f'Notification data: {decrypted}')
1247
+
1248
+ # Process the notification
1249
+ process_shipment_status_update(decrypted)
1250
+
1251
+ # Return success
1252
+ return '1|OK'
1253
+
1254
+ except Exception as e:
1255
+ app.logger.error(f'Callback error: {str(e)}')
1256
+ return '0|Error'
1257
+
1258
+
1259
+ def process_shipment_status_update(data: Dict[str, any]):
1260
+ """Process shipment status update"""
1261
+
1262
+ merchant_order_no = data['MerchantOrderNo']
1263
+ ret_id = data.get('Retld')
1264
+ ret_string = data.get('RetString')
1265
+
1266
+ app.logger.info(f'Processing status update for order {merchant_order_no}')
1267
+
1268
+ # Update database
1269
+ # db.orders.update_one(
1270
+ # {'order_no': merchant_order_no},
1271
+ # {
1272
+ # '$set': {
1273
+ # 'logistics.status': ret_id,
1274
+ # 'logistics.status_desc': ret_string,
1275
+ # 'logistics.tracking_no': data['LgsNo'],
1276
+ # 'logistics.last_update': datetime.now(),
1277
+ # }
1278
+ # }
1279
+ # )
1280
+
1281
+ # Send notification to customer
1282
+ if ret_id == '6':
1283
+ # Customer picked up
1284
+ # send_email(
1285
+ # to=customer_email,
1286
+ # subject='Order Delivered',
1287
+ # body=f'Your order {merchant_order_no} has been picked up.',
1288
+ # )
1289
+ pass
1290
+
1291
+ app.logger.info(f'Status update completed for order {merchant_order_no}')
1292
+
1293
+
1294
+ if __name__ == '__main__':
1295
+ app.run(port=3000)
1296
+ ```
1297
+
1298
+ ---
1299
+
1300
+ ## PAYUNi Logistics Examples
1301
+
1302
+ ### 1. Basic Integration (PAYUNi)
1303
+
1304
+ #### TypeScript - AES-256-GCM Encryption Helper
1305
+
1306
+ ```typescript
1307
+ import crypto from 'crypto';
1308
+
1309
+ interface PAYUNiConfig {
1310
+ merchantId: string;
1311
+ hashKey: string;
1312
+ hashIV: string;
1313
+ isProduction?: boolean;
1314
+ }
1315
+
1316
+ class PAYUNiLogistics {
1317
+ private config: Required<PAYUNiConfig>;
1318
+ private baseUrl: string;
1319
+
1320
+ constructor(config: PAYUNiConfig) {
1321
+ this.config = {
1322
+ ...config,
1323
+ isProduction: config.isProduction ?? false,
1324
+ };
1325
+
1326
+ this.baseUrl = this.config.isProduction
1327
+ ? 'https://api.payuni.com.tw/api'
1328
+ : 'https://sandbox-api.payuni.com.tw/api';
1329
+ }
1330
+
1331
+ /**
1332
+ * AES-256-GCM Encryption
1333
+ */
1334
+ encrypt(data: Record<string, any>): string {
1335
+ // Convert to query string
1336
+ const queryString = new URLSearchParams(data).toString();
1337
+
1338
+ // Create cipher
1339
+ const cipher = crypto.createCipheriv(
1340
+ 'aes-256-gcm',
1341
+ this.config.hashKey,
1342
+ this.config.hashIV
1343
+ );
1344
+
1345
+ // Encrypt
1346
+ let encrypted = cipher.update(queryString, 'utf8', 'hex');
1347
+ encrypted += cipher.final('hex');
1348
+
1349
+ // Get auth tag
1350
+ const authTag = cipher.getAuthTag().toString('hex');
1351
+
1352
+ // Combine encrypted + tag
1353
+ return encrypted + authTag;
1354
+ }
1355
+
1356
+ /**
1357
+ * AES-256-GCM Decryption
1358
+ */
1359
+ decrypt(encryptInfo: string): Record<string, any> {
1360
+ // Split encrypted data and tag (tag is last 32 hex chars = 16 bytes)
1361
+ const encrypted = encryptInfo.slice(0, -32);
1362
+ const authTag = encryptInfo.slice(-32);
1363
+
1364
+ // Create decipher
1365
+ const decipher = crypto.createDecipheriv(
1366
+ 'aes-256-gcm',
1367
+ this.config.hashKey,
1368
+ this.config.hashIV
1369
+ );
1370
+
1371
+ // Set auth tag
1372
+ decipher.setAuthTag(Buffer.from(authTag, 'hex'));
1373
+
1374
+ // Decrypt
1375
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
1376
+ decrypted += decipher.final('utf8');
1377
+
1378
+ // Parse query string
1379
+ const params = new URLSearchParams(decrypted);
1380
+ const result: Record<string, any> = {};
1381
+ params.forEach((value, key) => {
1382
+ result[key] = value;
1383
+ });
1384
+
1385
+ return result;
1386
+ }
1387
+
1388
+ /**
1389
+ * Generate HashInfo (SHA256)
1390
+ */
1391
+ generateHashInfo(encryptInfo: string): string {
1392
+ const raw = encryptInfo + this.config.hashKey + this.config.hashIV;
1393
+ return crypto.createHash('sha256').update(raw).digest('hex').toUpperCase();
1394
+ }
1395
+
1396
+ /**
1397
+ * Get current Unix timestamp
1398
+ */
1399
+ getTimestamp(): number {
1400
+ return Math.floor(Date.now() / 1000);
1401
+ }
1402
+ }
1403
+
1404
+ export { PAYUNiLogistics };
1405
+ ```
1406
+
1407
+ #### Python - AES-256-GCM Encryption Helper
1408
+
1409
+ ```python
1410
+ """PAYUNi Logistics Encryption Helper - Python"""
1411
+
1412
+ from Crypto.Cipher import AES
1413
+ from urllib.parse import urlencode, parse_qs
1414
+ import hashlib
1415
+ import time
1416
+ from typing import Dict, Any
1417
+
1418
+
1419
+ class PAYUNiLogistics:
1420
+ """PAYUNi Logistics API Client"""
1421
+
1422
+ def __init__(
1423
+ self,
1424
+ merchant_id: str,
1425
+ hash_key: str,
1426
+ hash_iv: str,
1427
+ is_production: bool = False
1428
+ ):
1429
+ self.merchant_id = merchant_id
1430
+ self.hash_key = hash_key.encode('utf-8')
1431
+ self.hash_iv = hash_iv.encode('utf-8')
1432
+
1433
+ self.base_url = (
1434
+ 'https://api.payuni.com.tw/api'
1435
+ if is_production
1436
+ else 'https://sandbox-api.payuni.com.tw/api'
1437
+ )
1438
+
1439
+ def encrypt(self, data: Dict[str, Any]) -> str:
1440
+ """AES-256-GCM Encryption"""
1441
+ # Convert to query string
1442
+ query_string = urlencode(data)
1443
+
1444
+ # Create cipher
1445
+ cipher = AES.new(self.hash_key, AES.MODE_GCM, nonce=self.hash_iv)
1446
+
1447
+ # Encrypt and get tag
1448
+ encrypted, tag = cipher.encrypt_and_digest(query_string.encode('utf-8'))
1449
+
1450
+ # Combine encrypted + tag and convert to hex
1451
+ return (encrypted + tag).hex()
1452
+
1453
+ def decrypt(self, encrypt_info: str) -> Dict[str, Any]:
1454
+ """AES-256-GCM Decryption"""
1455
+ # Convert hex to binary
1456
+ data = bytes.fromhex(encrypt_info)
1457
+
1458
+ # Split encrypted data and tag (last 16 bytes)
1459
+ encrypted = data[:-16]
1460
+ tag = data[-16:]
1461
+
1462
+ # Create cipher
1463
+ cipher = AES.new(self.hash_key, AES.MODE_GCM, nonce=self.hash_iv)
1464
+
1465
+ # Decrypt and verify
1466
+ decrypted = cipher.decrypt_and_verify(encrypted, tag)
1467
+
1468
+ # Parse query string
1469
+ result = dict(parse_qs(decrypted.decode('utf-8')))
1470
+ return {k: v[0] if len(v) == 1 else v for k, v in result.items()}
1471
+
1472
+ def generate_hash_info(self, encrypt_info: str) -> str:
1473
+ """Generate HashInfo (SHA256)"""
1474
+ raw = encrypt_info + self.hash_key.decode('utf-8') + self.hash_iv.decode('utf-8')
1475
+ return hashlib.sha256(raw.encode('utf-8')).hexdigest().upper()
1476
+
1477
+ @staticmethod
1478
+ def get_timestamp() -> int:
1479
+ """Get current Unix timestamp"""
1480
+ return int(time.time())
1481
+ ```
1482
+
1483
+ ---
1484
+
1485
+ ### 2. Create 7-11 C2C Shipment
1486
+
1487
+ #### TypeScript Example
1488
+
1489
+ ```typescript
1490
+ import axios from 'axios';
1491
+
1492
+ interface Create711ShipmentRequest {
1493
+ merTradeNo: string;
1494
+ goodsType: 1 | 2; // 1=Normal, 2=Frozen
1495
+ goodsAmount: number;
1496
+ goodsName: string;
1497
+ senderName: string;
1498
+ senderPhone: string;
1499
+ senderStoreID: string;
1500
+ receiverName: string;
1501
+ receiverPhone: string;
1502
+ receiverStoreID: string;
1503
+ notifyURL: string;
1504
+ }
1505
+
1506
+ class PAYUNi711Logistics extends PAYUNiLogistics {
1507
+ /**
1508
+ * Create 7-11 C2C shipment
1509
+ */
1510
+ async create711Shipment(params: Create711ShipmentRequest) {
1511
+ const data = {
1512
+ MerID: this.config.merchantId,
1513
+ MerTradeNo: params.merTradeNo,
1514
+ LogisticsType: 'PAYUNi_Logistic_711',
1515
+ GoodsType: params.goodsType,
1516
+ GoodsAmount: params.goodsAmount,
1517
+ GoodsName: params.goodsName,
1518
+ SenderName: params.senderName,
1519
+ SenderPhone: params.senderPhone,
1520
+ SenderStoreID: params.senderStoreID,
1521
+ ReceiverName: params.receiverName,
1522
+ ReceiverPhone: params.receiverPhone,
1523
+ ReceiverStoreID: params.receiverStoreID,
1524
+ NotifyURL: params.notifyURL,
1525
+ Timestamp: this.getTimestamp(),
1526
+ };
1527
+
1528
+ const encryptInfo = this.encrypt(data);
1529
+ const hashInfo = this.generateHashInfo(encryptInfo);
1530
+
1531
+ const response = await axios.post(
1532
+ `${this.baseUrl}/logistics/create`,
1533
+ new URLSearchParams({
1534
+ MerID: this.config.merchantId,
1535
+ Version: '1.0',
1536
+ EncryptInfo: encryptInfo,
1537
+ HashInfo: hashInfo,
1538
+ }),
1539
+ {
1540
+ headers: {
1541
+ 'Content-Type': 'application/x-www-form-urlencoded',
1542
+ },
1543
+ }
1544
+ );
1545
+
1546
+ const result = response.data;
1547
+
1548
+ if (result.Status !== 'SUCCESS') {
1549
+ throw new Error(`Create shipment failed: ${result.Message}`);
1550
+ }
1551
+
1552
+ // Decrypt response
1553
+ const decrypted = this.decrypt(result.EncryptInfo);
1554
+
1555
+ return {
1556
+ logisticsID: decrypted.LogisticsID,
1557
+ merTradeNo: decrypted.MerTradeNo,
1558
+ cvsPaymentNo: decrypted.CVSPaymentNo,
1559
+ cvsValidationNo: decrypted.CVSValidationNo,
1560
+ expireDate: decrypted.ExpireDate,
1561
+ };
1562
+ }
1563
+ }
1564
+
1565
+ // Usage Example
1566
+ const logistics = new PAYUNi711Logistics({
1567
+ merchantId: 'YOUR_MERCHANT_ID',
1568
+ hashKey: 'YOUR_HASH_KEY',
1569
+ hashIV: 'YOUR_HASH_IV',
1570
+ isProduction: false,
1571
+ });
1572
+
1573
+ const result = await logistics.create711Shipment({
1574
+ merTradeNo: `LOG${Date.now()}`,
1575
+ goodsType: 1, // Normal temperature
1576
+ goodsAmount: 500,
1577
+ goodsName: 'T-shirt',
1578
+ senderName: 'John Doe',
1579
+ senderPhone: '0912345678',
1580
+ senderStoreID: '123456', // 7-11 sender store
1581
+ receiverName: 'Jane Doe',
1582
+ receiverPhone: '0987654321',
1583
+ receiverStoreID: '654321', // 7-11 receiver store
1584
+ notifyURL: 'https://your-site.com/callback/payuni-711',
1585
+ });
1586
+
1587
+ console.log('Logistics ID:', result.logisticsID);
1588
+ console.log('Payment Code:', result.cvsPaymentNo);
1589
+
1590
+ export { PAYUNi711Logistics };
1591
+ ```
1592
+
1593
+ #### Python Example
1594
+
1595
+ ```python
1596
+ """Create 7-11 C2C Shipment - Python Example"""
1597
+
1598
+ import requests
1599
+ from typing import Dict
1600
+
1601
+
1602
+ class PAYUNi711Logistics(PAYUNiLogistics):
1603
+ """7-11 C2C Logistics Operations"""
1604
+
1605
+ def create_711_shipment(
1606
+ self,
1607
+ mer_trade_no: str,
1608
+ goods_type: int, # 1=Normal, 2=Frozen
1609
+ goods_amount: int,
1610
+ goods_name: str,
1611
+ sender_name: str,
1612
+ sender_phone: str,
1613
+ sender_store_id: str,
1614
+ receiver_name: str,
1615
+ receiver_phone: str,
1616
+ receiver_store_id: str,
1617
+ notify_url: str,
1618
+ ) -> Dict[str, any]:
1619
+ """Create 7-11 C2C shipment"""
1620
+
1621
+ data = {
1622
+ 'MerID': self.merchant_id,
1623
+ 'MerTradeNo': mer_trade_no,
1624
+ 'LogisticsType': 'PAYUNi_Logistic_711',
1625
+ 'GoodsType': goods_type,
1626
+ 'GoodsAmount': goods_amount,
1627
+ 'GoodsName': goods_name,
1628
+ 'SenderName': sender_name,
1629
+ 'SenderPhone': sender_phone,
1630
+ 'SenderStoreID': sender_store_id,
1631
+ 'ReceiverName': receiver_name,
1632
+ 'ReceiverPhone': receiver_phone,
1633
+ 'ReceiverStoreID': receiver_store_id,
1634
+ 'NotifyURL': notify_url,
1635
+ 'Timestamp': self.get_timestamp(),
1636
+ }
1637
+
1638
+ encrypt_info = self.encrypt(data)
1639
+ hash_info = self.generate_hash_info(encrypt_info)
1640
+
1641
+ response = requests.post(
1642
+ f'{self.base_url}/logistics/create',
1643
+ data={
1644
+ 'MerID': self.merchant_id,
1645
+ 'Version': '1.0',
1646
+ 'EncryptInfo': encrypt_info,
1647
+ 'HashInfo': hash_info,
1648
+ },
1649
+ )
1650
+
1651
+ result = response.json()
1652
+
1653
+ if result['Status'] != 'SUCCESS':
1654
+ raise Exception(f"Create shipment failed: {result['Message']}")
1655
+
1656
+ # Decrypt response
1657
+ decrypted = self.decrypt(result['EncryptInfo'])
1658
+
1659
+ return {
1660
+ 'logistics_id': decrypted['LogisticsID'],
1661
+ 'mer_trade_no': decrypted['MerTradeNo'],
1662
+ 'cvs_payment_no': decrypted['CVSPaymentNo'],
1663
+ 'cvs_validation_no': decrypted['CVSValidationNo'],
1664
+ 'expire_date': decrypted['ExpireDate'],
1665
+ }
1666
+
1667
+
1668
+ # Usage Example
1669
+ logistics = PAYUNi711Logistics(
1670
+ merchant_id='YOUR_MERCHANT_ID',
1671
+ hash_key='YOUR_HASH_KEY',
1672
+ hash_iv='YOUR_HASH_IV',
1673
+ is_production=False,
1674
+ )
1675
+
1676
+ result = logistics.create_711_shipment(
1677
+ mer_trade_no=f'LOG{int(time.time())}',
1678
+ goods_type=1, # Normal temperature
1679
+ goods_amount=500,
1680
+ goods_name='T-shirt',
1681
+ sender_name='John Doe',
1682
+ sender_phone='0912345678',
1683
+ sender_store_id='123456',
1684
+ receiver_name='Jane Doe',
1685
+ receiver_phone='0987654321',
1686
+ receiver_store_id='654321',
1687
+ notify_url='https://your-site.com/callback/payuni-711',
1688
+ )
1689
+
1690
+ print(f"Logistics ID: {result['logistics_id']}")
1691
+ print(f"Payment Code: {result['cvs_payment_no']}")
1692
+ ```
1693
+
1694
+ ---
1695
+
1696
+ ### 3. Create T-Cat Home Delivery
1697
+
1698
+ #### TypeScript Example
1699
+
1700
+ ```typescript
1701
+ interface CreateTCatShipmentRequest {
1702
+ merTradeNo: string;
1703
+ goodsType: 1 | 2 | 3; // 1=Normal, 2=Frozen, 3=Refrigerated
1704
+ goodsAmount: number;
1705
+ goodsName: string;
1706
+ goodsWeight?: number;
1707
+ senderName: string;
1708
+ senderPhone: string;
1709
+ senderZipCode: string;
1710
+ senderAddress: string;
1711
+ receiverName: string;
1712
+ receiverPhone: string;
1713
+ receiverZipCode: string;
1714
+ receiverAddress: string;
1715
+ scheduledDeliveryTime?: '01' | '02' | '03';
1716
+ notifyURL: string;
1717
+ }
1718
+
1719
+ class PAYUNiTCatLogistics extends PAYUNiLogistics {
1720
+ /**
1721
+ * Create T-Cat home delivery shipment
1722
+ */
1723
+ async createTCatShipment(params: CreateTCatShipmentRequest) {
1724
+ const logisticsType =
1725
+ params.goodsType === 2 ? 'PAYUNi_Logistic_Tcat_Freeze' :
1726
+ params.goodsType === 3 ? 'PAYUNi_Logistic_Tcat_Cold' :
1727
+ 'PAYUNi_Logistic_Tcat';
1728
+
1729
+ const data: any = {
1730
+ MerID: this.config.merchantId,
1731
+ MerTradeNo: params.merTradeNo,
1732
+ LogisticsType: logisticsType,
1733
+ GoodsType: params.goodsType,
1734
+ GoodsAmount: params.goodsAmount,
1735
+ GoodsName: params.goodsName,
1736
+ SenderName: params.senderName,
1737
+ SenderPhone: params.senderPhone,
1738
+ SenderZipCode: params.senderZipCode,
1739
+ SenderAddress: params.senderAddress,
1740
+ ReceiverName: params.receiverName,
1741
+ ReceiverPhone: params.receiverPhone,
1742
+ ReceiverZipCode: params.receiverZipCode,
1743
+ ReceiverAddress: params.receiverAddress,
1744
+ NotifyURL: params.notifyURL,
1745
+ Timestamp: this.getTimestamp(),
1746
+ };
1747
+
1748
+ if (params.goodsWeight) data.GoodsWeight = params.goodsWeight;
1749
+ if (params.scheduledDeliveryTime) data.ScheduledDeliveryTime = params.scheduledDeliveryTime;
1750
+
1751
+ const encryptInfo = this.encrypt(data);
1752
+ const hashInfo = this.generateHashInfo(encryptInfo);
1753
+
1754
+ const response = await axios.post(
1755
+ `${this.baseUrl}/logistics/create`,
1756
+ new URLSearchParams({
1757
+ MerID: this.config.merchantId,
1758
+ Version: '1.0',
1759
+ EncryptInfo: encryptInfo,
1760
+ HashInfo: hashInfo,
1761
+ }),
1762
+ {
1763
+ headers: {
1764
+ 'Content-Type': 'application/x-www-form-urlencoded',
1765
+ },
1766
+ }
1767
+ );
1768
+
1769
+ const result = response.data;
1770
+
1771
+ if (result.Status !== 'SUCCESS') {
1772
+ throw new Error(`Create shipment failed: ${result.Message}`);
1773
+ }
1774
+
1775
+ const decrypted = this.decrypt(result.EncryptInfo);
1776
+
1777
+ return {
1778
+ logisticsID: decrypted.LogisticsID,
1779
+ merTradeNo: decrypted.MerTradeNo,
1780
+ shipmentNo: decrypted.ShipmentNo,
1781
+ bookingNote: decrypted.BookingNote,
1782
+ };
1783
+ }
1784
+ }
1785
+
1786
+ // Usage Example
1787
+ const tcat = new PAYUNiTCatLogistics({
1788
+ merchantId: 'YOUR_MERCHANT_ID',
1789
+ hashKey: 'YOUR_HASH_KEY',
1790
+ hashIV: 'YOUR_HASH_IV',
1791
+ });
1792
+
1793
+ const result = await tcat.createTCatShipment({
1794
+ merTradeNo: `TCAT${Date.now()}`,
1795
+ goodsType: 1, // Normal temperature
1796
+ goodsAmount: 1000,
1797
+ goodsName: 'Electronics',
1798
+ goodsWeight: 500, // 500g
1799
+ senderName: 'Sender Name',
1800
+ senderPhone: '0912345678',
1801
+ senderZipCode: '100',
1802
+ senderAddress: 'Taipei City, Zhongzheng Dist., XXX Road No.1',
1803
+ receiverName: 'Receiver Name',
1804
+ receiverPhone: '0987654321',
1805
+ receiverZipCode: '300',
1806
+ receiverAddress: 'Hsinchu City, East Dist., YYY Road No.2',
1807
+ scheduledDeliveryTime: '02', // 14:00-18:00
1808
+ notifyURL: 'https://your-site.com/callback/payuni-tcat',
1809
+ });
1810
+
1811
+ console.log('Tracking No:', result.shipmentNo);
1812
+
1813
+ export { PAYUNiTCatLogistics };
1814
+ ```
1815
+
1816
+ ---
1817
+
1818
+ ### 4. Query Shipment Status
1819
+
1820
+ #### TypeScript Example
1821
+
1822
+ ```typescript
1823
+ class PAYUNiQueryLogistics extends PAYUNiLogistics {
1824
+ /**
1825
+ * Query shipment status
1826
+ */
1827
+ async queryShipment(merTradeNo: string) {
1828
+ const data = {
1829
+ MerID: this.config.merchantId,
1830
+ MerTradeNo: merTradeNo,
1831
+ Timestamp: this.getTimestamp(),
1832
+ };
1833
+
1834
+ const encryptInfo = this.encrypt(data);
1835
+ const hashInfo = this.generateHashInfo(encryptInfo);
1836
+
1837
+ const response = await axios.post(
1838
+ `${this.baseUrl}/logistics/query`,
1839
+ new URLSearchParams({
1840
+ MerID: this.config.merchantId,
1841
+ Version: '1.0',
1842
+ EncryptInfo: encryptInfo,
1843
+ HashInfo: hashInfo,
1844
+ }),
1845
+ {
1846
+ headers: {
1847
+ 'Content-Type': 'application/x-www-form-urlencoded',
1848
+ },
1849
+ }
1850
+ );
1851
+
1852
+ const result = response.data;
1853
+
1854
+ if (result.Status !== 'SUCCESS') {
1855
+ throw new Error(`Query shipment failed: ${result.Message}`);
1856
+ }
1857
+
1858
+ const decrypted = this.decrypt(result.EncryptInfo);
1859
+
1860
+ return {
1861
+ logisticsID: decrypted.LogisticsID,
1862
+ merTradeNo: decrypted.MerTradeNo,
1863
+ logisticsType: decrypted.LogisticsType,
1864
+ logisticsStatus: decrypted.LogisticsStatus,
1865
+ logisticsStatusMsg: decrypted.LogisticsStatusMsg,
1866
+ shipmentNo: decrypted.ShipmentNo,
1867
+ receiverStoreID: decrypted.ReceiverStoreID,
1868
+ updateTime: decrypted.UpdateTime,
1869
+ };
1870
+ }
1871
+
1872
+ /**
1873
+ * Get human-readable status
1874
+ */
1875
+ getStatusDescription(statusCode: string): string {
1876
+ const statusMap: Record<string, string> = {
1877
+ '11': 'Shipped',
1878
+ '21': 'Arrived at store / In delivery',
1879
+ '22': 'Picked up / Delivered',
1880
+ '31': 'Returning',
1881
+ '32': 'Return completed',
1882
+ };
1883
+
1884
+ return statusMap[statusCode] || 'Unknown status';
1885
+ }
1886
+ }
1887
+
1888
+ // Usage Example
1889
+ const query = new PAYUNiQueryLogistics({
1890
+ merchantId: 'YOUR_MERCHANT_ID',
1891
+ hashKey: 'YOUR_HASH_KEY',
1892
+ hashIV: 'YOUR_HASH_IV',
1893
+ });
1894
+
1895
+ const status = await query.queryShipment('LOG123456');
1896
+
1897
+ console.log(`Order: ${status.merTradeNo}`);
1898
+ console.log(`Status: ${status.logisticsStatusMsg} (${status.logisticsStatus})`);
1899
+ console.log(`Tracking No: ${status.shipmentNo}`);
1900
+
1901
+ export { PAYUNiQueryLogistics };
1902
+ ```
1903
+
1904
+ ---
1905
+
1906
+ ### 5. Status Notification Callback
1907
+
1908
+ #### Express.js Example
1909
+
1910
+ ```typescript
1911
+ import express from 'express';
1912
+
1913
+ const app = express();
1914
+
1915
+ app.use(express.urlencoded({ extended: true }));
1916
+ app.use(express.json());
1917
+
1918
+ const logistics = new PAYUNiLogistics({
1919
+ merchantId: 'YOUR_MERCHANT_ID',
1920
+ hashKey: 'YOUR_HASH_KEY',
1921
+ hashIV: 'YOUR_HASH_IV',
1922
+ });
1923
+
1924
+ /**
1925
+ * Handle PAYUNi logistics status notification
1926
+ */
1927
+ app.post('/callback/payuni-logistics', async (req, res) => {
1928
+ try {
1929
+ const { MerID, EncryptInfo, HashInfo } = req.body;
1930
+
1931
+ console.log('Received notification from PAYUNi');
1932
+
1933
+ // Verify HashInfo
1934
+ const calculatedHash = logistics.generateHashInfo(EncryptInfo);
1935
+ if (calculatedHash !== HashInfo.toUpperCase()) {
1936
+ console.error('Hash verification failed');
1937
+ return res.send('Hash Error');
1938
+ }
1939
+
1940
+ // Decrypt data
1941
+ const data = logistics.decrypt(EncryptInfo);
1942
+
1943
+ console.log('Notification data:', data);
1944
+
1945
+ // Process the notification
1946
+ await processLogisticsStatusUpdate({
1947
+ merTradeNo: data.MerTradeNo,
1948
+ logisticsID: data.LogisticsID,
1949
+ logisticsType: data.LogisticsType,
1950
+ logisticsStatus: data.LogisticsStatus,
1951
+ logisticsStatusMsg: data.LogisticsStatusMsg,
1952
+ updateTime: data.UpdateTime,
1953
+ });
1954
+
1955
+ // Return SUCCESS
1956
+ res.send('SUCCESS');
1957
+ } catch (error) {
1958
+ console.error('Callback error:', error);
1959
+ res.send('Error');
1960
+ }
1961
+ });
1962
+
1963
+ /**
1964
+ * Process logistics status update
1965
+ */
1966
+ async function processLogisticsStatusUpdate(data: {
1967
+ merTradeNo: string;
1968
+ logisticsID: string;
1969
+ logisticsType: string;
1970
+ logisticsStatus: string;
1971
+ logisticsStatusMsg: string;
1972
+ updateTime: string;
1973
+ }) {
1974
+ console.log(`Processing status update for order ${data.merTradeNo}`);
1975
+
1976
+ // Update database
1977
+ // await db.orders.updateOne(
1978
+ // { orderNo: data.merTradeNo },
1979
+ // {
1980
+ // $set: {
1981
+ // 'logistics.status': data.logisticsStatus,
1982
+ // 'logistics.statusMsg': data.logisticsStatusMsg,
1983
+ // 'logistics.logisticsID': data.logisticsID,
1984
+ // 'logistics.lastUpdate': new Date(data.updateTime),
1985
+ // },
1986
+ // }
1987
+ // );
1988
+
1989
+ // Send notification to customer based on status
1990
+ switch (data.logisticsStatus) {
1991
+ case '11':
1992
+ // Shipped
1993
+ console.log('Order shipped');
1994
+ break;
1995
+ case '21':
1996
+ // Arrived
1997
+ console.log('Arrived at destination');
1998
+ break;
1999
+ case '22':
2000
+ // Picked up / Delivered
2001
+ console.log('Delivery completed');
2002
+ // await sendEmail({ ... });
2003
+ break;
2004
+ case '31':
2005
+ case '32':
2006
+ // Return
2007
+ console.log('Order returned');
2008
+ break;
2009
+ }
2010
+ }
2011
+
2012
+ app.listen(3000, () => {
2013
+ console.log('PAYUNi callback server listening on port 3000');
2014
+ });
2015
+ ```
2016
+
2017
+ ---
2018
+
2019
+ ## ECPay Logistics Examples
2020
+
2021
+ ### 1. Basic Integration (ECPay)
2022
+
2023
+ #### TypeScript - MD5 CheckMacValue Helper
2024
+
2025
+ ```typescript
2026
+ import crypto from 'crypto';
2027
+
2028
+ interface ECPayConfig {
2029
+ merchantId: string;
2030
+ hashKey: string;
2031
+ hashIV: string;
2032
+ isProduction?: boolean;
2033
+ }
2034
+
2035
+ class ECPayLogistics {
2036
+ private config: Required<ECPayConfig>;
2037
+ private baseUrl: string;
2038
+
2039
+ constructor(config: ECPayConfig) {
2040
+ this.config = {
2041
+ ...config,
2042
+ isProduction: config.isProduction ?? false,
2043
+ };
2044
+
2045
+ this.baseUrl = this.config.isProduction
2046
+ ? 'https://logistics.ecpay.com.tw'
2047
+ : 'https://logistics-stage.ecpay.com.tw';
2048
+ }
2049
+
2050
+ /**
2051
+ * Generate CheckMacValue (MD5)
2052
+ */
2053
+ generateCheckMacValue(params: Record<string, any>): string {
2054
+ // Sort parameters
2055
+ const sorted = Object.keys(params)
2056
+ .sort()
2057
+ .reduce((acc, key) => {
2058
+ acc[key] = params[key];
2059
+ return acc;
2060
+ }, {} as Record<string, any>);
2061
+
2062
+ // Create query string
2063
+ const paramStr = Object.entries(sorted)
2064
+ .map(([key, value]) => `${key}=${value}`)
2065
+ .join('&');
2066
+
2067
+ // Add HashKey and HashIV
2068
+ const raw = `HashKey=${this.config.hashKey}&${paramStr}&HashIV=${this.config.hashIV}`;
2069
+
2070
+ // URL encode and convert to lowercase
2071
+ const encoded = encodeURIComponent(raw).toLowerCase();
2072
+
2073
+ // MD5 hash and uppercase
2074
+ return crypto.createHash('md5').update(encoded).digest('hex').toUpperCase();
2075
+ }
2076
+
2077
+ /**
2078
+ * Verify CheckMacValue from callback
2079
+ */
2080
+ verifyCheckMacValue(params: Record<string, any>): boolean {
2081
+ const { CheckMacValue, ...data } = params;
2082
+ const calculated = this.generateCheckMacValue(data);
2083
+ return calculated === CheckMacValue;
2084
+ }
2085
+ }
2086
+
2087
+ export { ECPayLogistics };
2088
+ ```
2089
+
2090
+ #### Python - MD5 CheckMacValue Helper
2091
+
2092
+ ```python
2093
+ """ECPay Logistics Encryption Helper - Python"""
2094
+
2095
+ import hashlib
2096
+ import urllib.parse
2097
+ from typing import Dict, Any
2098
+
2099
+
2100
+ class ECPayLogistics:
2101
+ """ECPay Logistics API Client"""
2102
+
2103
+ def __init__(
2104
+ self,
2105
+ merchant_id: str,
2106
+ hash_key: str,
2107
+ hash_iv: str,
2108
+ is_production: bool = False
2109
+ ):
2110
+ self.merchant_id = merchant_id
2111
+ self.hash_key = hash_key
2112
+ self.hash_iv = hash_iv
2113
+
2114
+ self.base_url = (
2115
+ 'https://logistics.ecpay.com.tw'
2116
+ if is_production
2117
+ else 'https://logistics-stage.ecpay.com.tw'
2118
+ )
2119
+
2120
+ def generate_check_mac_value(self, params: Dict[str, Any]) -> str:
2121
+ """Generate CheckMacValue (MD5)"""
2122
+ # Sort parameters
2123
+ sorted_params = sorted(params.items())
2124
+
2125
+ # Create query string
2126
+ param_str = '&'.join(f'{k}={v}' for k, v in sorted_params)
2127
+
2128
+ # Add HashKey and HashIV
2129
+ raw = f'HashKey={self.hash_key}&{param_str}&HashIV={self.hash_iv}'
2130
+
2131
+ # URL encode and convert to lowercase
2132
+ encoded = urllib.parse.quote_plus(raw).lower()
2133
+
2134
+ # MD5 hash and uppercase
2135
+ return hashlib.md5(encoded.encode('utf-8')).hexdigest().upper()
2136
+
2137
+ def verify_check_mac_value(self, params: Dict[str, Any]) -> bool:
2138
+ """Verify CheckMacValue from callback"""
2139
+ check_mac = params.pop('CheckMacValue', None)
2140
+ if not check_mac:
2141
+ return False
2142
+
2143
+ calculated = self.generate_check_mac_value(params)
2144
+ return calculated == check_mac
2145
+ ```
2146
+
2147
+ ---
2148
+
2149
+ ### 2. Create CVS C2C Shipment (ECPay)
2150
+
2151
+ #### TypeScript Example
2152
+
2153
+ ```typescript
2154
+ import axios from 'axios';
2155
+
2156
+ interface CreateCVSShipmentRequest {
2157
+ merTradeNo: string;
2158
+ logisticsSubType: 'FAMI' | 'UNIMART' | 'UNIMARTFREEZE' | 'HILIFE' | 'OKMART';
2159
+ goodsAmount: number;
2160
+ goodsName: string;
2161
+ senderName: string;
2162
+ senderCellPhone: string;
2163
+ receiverName: string;
2164
+ receiverCellPhone: string;
2165
+ receiverStoreID: string;
2166
+ isCollection?: 'Y' | 'N';
2167
+ serverReplyURL: string;
2168
+ }
2169
+
2170
+ class ECPayCVSLogistics extends ECPayLogistics {
2171
+ /**
2172
+ * Create CVS C2C shipment
2173
+ */
2174
+ async createCVSShipment(params: CreateCVSShipmentRequest) {
2175
+ const tradeDate = new Date().toLocaleString('zh-TW', {
2176
+ year: 'numeric',
2177
+ month: '2-digit',
2178
+ day: '2-digit',
2179
+ hour: '2-digit',
2180
+ minute: '2-digit',
2181
+ second: '2-digit',
2182
+ hour12: false,
2183
+ }).replace(/\//g, '/');
2184
+
2185
+ const data: Record<string, any> = {
2186
+ MerchantID: this.config.merchantId,
2187
+ MerchantTradeNo: params.merTradeNo,
2188
+ MerchantTradeDate: tradeDate,
2189
+ LogisticsType: 'CVS',
2190
+ LogisticsSubType: params.logisticsSubType,
2191
+ GoodsAmount: params.goodsAmount,
2192
+ GoodsName: params.goodsName,
2193
+ SenderName: params.senderName,
2194
+ SenderCellPhone: params.senderCellPhone,
2195
+ ReceiverName: params.receiverName,
2196
+ ReceiverCellPhone: params.receiverCellPhone,
2197
+ ReceiverStoreID: params.receiverStoreID,
2198
+ IsCollection: params.isCollection || 'N',
2199
+ ServerReplyURL: params.serverReplyURL,
2200
+ };
2201
+
2202
+ // Generate CheckMacValue
2203
+ data.CheckMacValue = this.generateCheckMacValue(data);
2204
+
2205
+ const response = await axios.post(
2206
+ `${this.baseUrl}/Express/Create`,
2207
+ new URLSearchParams(data),
2208
+ {
2209
+ headers: {
2210
+ 'Content-Type': 'application/x-www-form-urlencoded',
2211
+ },
2212
+ }
2213
+ );
2214
+
2215
+ // Parse response (1=key&value format)
2216
+ const result: Record<string, string> = {};
2217
+ response.data.split('&').forEach((item: string) => {
2218
+ const [key, value] = item.split('=');
2219
+ if (key && value) {
2220
+ result[key] = decodeURIComponent(value);
2221
+ }
2222
+ });
2223
+
2224
+ if (result.RtnCode !== '300' && result.RtnCode !== '2001') {
2225
+ throw new Error(`Create shipment failed: ${result.RtnMsg}`);
2226
+ }
2227
+
2228
+ return {
2229
+ allPayLogisticsID: result.AllPayLogisticsID,
2230
+ cvsPaymentNo: result.CVSPaymentNo,
2231
+ cvsValidationNo: result.CVSValidationNo,
2232
+ bookingNote: result.BookingNote,
2233
+ };
2234
+ }
2235
+ }
2236
+
2237
+ // Usage Example
2238
+ const logistics = new ECPayCVSLogistics({
2239
+ merchantId: '2000132',
2240
+ hashKey: '5294y06JbISpM5x9',
2241
+ hashIV: 'v77hoKGq4kWxNNIS',
2242
+ isProduction: false,
2243
+ });
2244
+
2245
+ const result = await logistics.createCVSShipment({
2246
+ merTradeNo: `CVS${Date.now()}`,
2247
+ logisticsSubType: 'UNIMART', // 7-11
2248
+ goodsAmount: 500,
2249
+ goodsName: 'Test Product',
2250
+ senderName: 'Sender Name',
2251
+ senderCellPhone: '0912345678',
2252
+ receiverName: 'Receiver Name',
2253
+ receiverCellPhone: '0987654321',
2254
+ receiverStoreID: '131386', // 7-11 store code
2255
+ isCollection: 'N',
2256
+ serverReplyURL: 'https://your-site.com/callback/ecpay-cvs',
2257
+ });
2258
+
2259
+ console.log('Logistics ID:', result.allPayLogisticsID);
2260
+ console.log('Payment No:', result.cvsPaymentNo);
2261
+
2262
+ export { ECPayCVSLogistics };
2263
+ ```
2264
+
2265
+ #### Python Example
2266
+
2267
+ ```python
2268
+ #!/usr/bin/env python3
2269
+ """
2270
+ Create CVS C2C Shipment - ECPay Python Example
2271
+
2272
+ 依照 taiwan-logistics-skill 嚴格規範撰寫
2273
+ """
2274
+
2275
+ import requests
2276
+ import urllib.parse
2277
+ import time
2278
+ from datetime import datetime
2279
+ from typing import Dict, Literal, Optional
2280
+ from dataclasses import dataclass
2281
+
2282
+
2283
+ @dataclass
2284
+ class CVSShipmentData:
2285
+ """CVS C2C 物流訂單資料"""
2286
+ mer_trade_no: str
2287
+ logistics_sub_type: Literal['FAMI', 'UNIMART', 'UNIMARTFREEZE', 'HILIFE', 'OKMART']
2288
+ goods_amount: int
2289
+ goods_name: str
2290
+ sender_name: str
2291
+ sender_cell_phone: str
2292
+ receiver_name: str
2293
+ receiver_cell_phone: str
2294
+ receiver_store_id: str
2295
+ is_collection: str = 'N'
2296
+ server_reply_url: str = ''
2297
+
2298
+
2299
+ @dataclass
2300
+ class CVSShipmentResponse:
2301
+ """CVS C2C 物流訂單回應"""
2302
+ success: bool
2303
+ rtn_code: str
2304
+ rtn_msg: str
2305
+ all_pay_logistics_id: str = ''
2306
+ cvs_payment_no: str = ''
2307
+ cvs_validation_no: str = ''
2308
+ booking_note: str = ''
2309
+ raw: Dict[str, str] = None
2310
+
2311
+
2312
+ class ECPayCVSLogistics(ECPayLogistics):
2313
+ """
2314
+ ECPay CVS 超商物流服務
2315
+
2316
+ 支援超商類型:
2317
+ - FAMI: FamilyMart 全家便利商店
2318
+ - UNIMART: 7-ELEVEN 統一超商 (常溫)
2319
+ - UNIMARTFREEZE: 7-ELEVEN 統一超商 (冷凍)
2320
+ - HILIFE: Hi-Life 萊爾富便利商店
2321
+ - OKMART: OK Mart OK 便利商店
2322
+
2323
+ 回傳碼:
2324
+ - 300: 訂單建立成功 (尚未寄貨)
2325
+ - 2001: 訂單建立成功 (門市已出貨)
2326
+ """
2327
+
2328
+ def create_cvs_shipment(
2329
+ self,
2330
+ data: CVSShipmentData,
2331
+ ) -> CVSShipmentResponse:
2332
+ """
2333
+ 建立 CVS C2C 超商物流訂單
2334
+
2335
+ Args:
2336
+ data: CVS 物流訂單資料 (CVSShipmentData)
2337
+
2338
+ Returns:
2339
+ CVSShipmentResponse: 物流訂單回應
2340
+
2341
+ Raises:
2342
+ Exception: API 請求失敗或訂單建立失敗
2343
+
2344
+ Example:
2345
+ >>> shipment_data = CVSShipmentData(
2346
+ ... mer_trade_no=f'CVS{int(time.time())}',
2347
+ ... logistics_sub_type='UNIMART',
2348
+ ... goods_amount=500,
2349
+ ... goods_name='Test Product',
2350
+ ... sender_name='Sender Name',
2351
+ ... sender_cell_phone='0912345678',
2352
+ ... receiver_name='Receiver Name',
2353
+ ... receiver_cell_phone='0987654321',
2354
+ ... receiver_store_id='131386',
2355
+ ... is_collection='N',
2356
+ ... server_reply_url='https://your-site.com/callback',
2357
+ ... )
2358
+ >>> result = logistics.create_cvs_shipment(shipment_data)
2359
+ >>> print(result.all_pay_logistics_id)
2360
+ """
2361
+ # 產生交易日期時間 (格式: YYYY/MM/DD HH:MM:SS)
2362
+ trade_date = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
2363
+
2364
+ # 準備 API 請求參數
2365
+ api_params = {
2366
+ 'MerchantID': self.merchant_id,
2367
+ 'MerchantTradeNo': data.mer_trade_no,
2368
+ 'MerchantTradeDate': trade_date,
2369
+ 'LogisticsType': 'CVS',
2370
+ 'LogisticsSubType': data.logistics_sub_type,
2371
+ 'GoodsAmount': data.goods_amount,
2372
+ 'GoodsName': data.goods_name,
2373
+ 'SenderName': data.sender_name,
2374
+ 'SenderCellPhone': data.sender_cell_phone,
2375
+ 'ReceiverName': data.receiver_name,
2376
+ 'ReceiverCellPhone': data.receiver_cell_phone,
2377
+ 'ReceiverStoreID': data.receiver_store_id,
2378
+ 'IsCollection': data.is_collection,
2379
+ 'ServerReplyURL': data.server_reply_url,
2380
+ }
2381
+
2382
+ # 產生 CheckMacValue (MD5 雜湊驗證碼)
2383
+ api_params['CheckMacValue'] = self.generate_check_mac_value(api_params)
2384
+
2385
+ # 發送 API 請求
2386
+ try:
2387
+ response = requests.post(
2388
+ f'{self.base_url}/Express/Create',
2389
+ data=api_params,
2390
+ timeout=30,
2391
+ )
2392
+ response.raise_for_status()
2393
+ except requests.RequestException as e:
2394
+ raise Exception(f"API 請求失敗: {str(e)}")
2395
+
2396
+ # 解析回應 (格式: key1=value1&key2=value2)
2397
+ result = {}
2398
+ for item in response.text.split('&'):
2399
+ if '=' in item:
2400
+ key, value = item.split('=', 1)
2401
+ result[key] = urllib.parse.unquote(value)
2402
+
2403
+ # 檢查訂單狀態
2404
+ rtn_code = result.get('RtnCode', '')
2405
+ success = rtn_code in ['300', '2001']
2406
+
2407
+ if not success:
2408
+ return CVSShipmentResponse(
2409
+ success=False,
2410
+ rtn_code=rtn_code,
2411
+ rtn_msg=result.get('RtnMsg', '未知錯誤'),
2412
+ raw=result,
2413
+ )
2414
+
2415
+ # 回傳成功結果
2416
+ return CVSShipmentResponse(
2417
+ success=True,
2418
+ rtn_code=rtn_code,
2419
+ rtn_msg=result.get('RtnMsg', ''),
2420
+ all_pay_logistics_id=result.get('AllPayLogisticsID', ''),
2421
+ cvs_payment_no=result.get('CVSPaymentNo', ''),
2422
+ cvs_validation_no=result.get('CVSValidationNo', ''),
2423
+ booking_note=result.get('BookingNote', ''),
2424
+ raw=result,
2425
+ )
2426
+
2427
+
2428
+ # Usage Example
2429
+ if __name__ == '__main__':
2430
+ # 初始化物流服務 (使用測試環境)
2431
+ logistics = ECPayCVSLogistics(
2432
+ merchant_id='2000132', # ECPay 測試商店代號
2433
+ hash_key='5294y06JbISpM5x9', # ECPay 測試 HashKey
2434
+ hash_iv='v77hoKGq4kWxNNIS', # ECPay 測試 HashIV
2435
+ is_production=False, # 使用測試環境
2436
+ )
2437
+
2438
+ # 準備物流訂單資料
2439
+ shipment_data = CVSShipmentData(
2440
+ mer_trade_no=f'CVS{int(time.time())}', # 訂單編號 (唯一值)
2441
+ logistics_sub_type='UNIMART', # 7-11 超商
2442
+ goods_amount=500, # 商品金額
2443
+ goods_name='測試商品', # 商品名稱
2444
+ sender_name='寄件人姓名', # 寄件人姓名
2445
+ sender_cell_phone='0912345678', # 寄件人手機
2446
+ receiver_name='收件人姓名', # 收件人姓名
2447
+ receiver_cell_phone='0987654321', # 收件人手機
2448
+ receiver_store_id='131386', # 收件門市代號 (7-11)
2449
+ is_collection='N', # 不代收貨款
2450
+ server_reply_url='https://your-site.com/callback/ecpay-cvs', # 回傳網址
2451
+ )
2452
+
2453
+ # 建立物流訂單
2454
+ try:
2455
+ result = logistics.create_cvs_shipment(shipment_data)
2456
+
2457
+ if result.success:
2458
+ print(f"✓ 訂單建立成功")
2459
+ print(f" 物流編號: {result.all_pay_logistics_id}")
2460
+ print(f" 寄貨編號: {result.cvs_payment_no}")
2461
+ print(f" 驗證碼: {result.cvs_validation_no}")
2462
+ print(f" 托運單號: {result.booking_note}")
2463
+ else:
2464
+ print(f"✗ 訂單建立失敗")
2465
+ print(f" 錯誤代碼: {result.rtn_code}")
2466
+ print(f" 錯誤訊息: {result.rtn_msg}")
2467
+ except Exception as e:
2468
+ print(f"✗ 發生例外: {str(e)}")
2469
+ ```
2470
+
2471
+ ---
2472
+
2473
+ ### 3. Create Home Delivery (ECPay)
2474
+
2475
+ #### TypeScript Example
2476
+
2477
+ ```typescript
2478
+ interface CreateHomeShipmentRequest {
2479
+ merTradeNo: string;
2480
+ logisticsSubType: 'TCAT' | 'ECAN' | 'POST';
2481
+ goodsAmount: number;
2482
+ goodsName: string;
2483
+ senderName: string;
2484
+ senderCellPhone: string;
2485
+ senderZipCode: string;
2486
+ senderAddress: string;
2487
+ receiverName: string;
2488
+ receiverCellPhone: string;
2489
+ receiverZipCode: string;
2490
+ receiverAddress: string;
2491
+ temperature?: '0001' | '0002' | '0003';
2492
+ specification?: '0001' | '0002' | '0003' | '0004';
2493
+ distance?: '00' | '01' | '02' | '03';
2494
+ scheduledPickupTime?: '1' | '2' | '3' | '4';
2495
+ scheduledDeliveryTime?: '1' | '2' | '3' | '4' | '5';
2496
+ serverReplyURL: string;
2497
+ }
2498
+
2499
+ class ECPayHomeLogistics extends ECPayLogistics {
2500
+ async createHomeShipment(params: CreateHomeShipmentRequest) {
2501
+ const tradeDate = new Date().toLocaleString('zh-TW', {
2502
+ year: 'numeric',
2503
+ month: '2-digit',
2504
+ day: '2-digit',
2505
+ hour: '2-digit',
2506
+ minute: '2-digit',
2507
+ second: '2-digit',
2508
+ hour12: false,
2509
+ }).replace(/\//g, '/');
2510
+
2511
+ const data: Record<string, any> = {
2512
+ MerchantID: this.config.merchantId,
2513
+ MerchantTradeNo: params.merTradeNo,
2514
+ MerchantTradeDate: tradeDate,
2515
+ LogisticsType: 'HOME',
2516
+ LogisticsSubType: params.logisticsSubType,
2517
+ GoodsAmount: params.goodsAmount,
2518
+ GoodsName: params.goodsName,
2519
+ SenderName: params.senderName,
2520
+ SenderCellPhone: params.senderCellPhone,
2521
+ SenderZipCode: params.senderZipCode,
2522
+ SenderAddress: params.senderAddress,
2523
+ ReceiverName: params.receiverName,
2524
+ ReceiverCellPhone: params.receiverCellPhone,
2525
+ ReceiverZipCode: params.receiverZipCode,
2526
+ ReceiverAddress: params.receiverAddress,
2527
+ ServerReplyURL: params.serverReplyURL,
2528
+ };
2529
+
2530
+ // Optional parameters
2531
+ if (params.temperature) data.Temperature = params.temperature;
2532
+ if (params.specification) data.Specification = params.specification;
2533
+ if (params.distance) data.Distance = params.distance;
2534
+ if (params.scheduledPickupTime) data.ScheduledPickupTime = params.scheduledPickupTime;
2535
+ if (params.scheduledDeliveryTime) data.ScheduledDeliveryTime = params.scheduledDeliveryTime;
2536
+
2537
+ data.CheckMacValue = this.generateCheckMacValue(data);
2538
+
2539
+ const response = await axios.post(
2540
+ `${this.baseUrl}/Express/Create`,
2541
+ new URLSearchParams(data),
2542
+ {
2543
+ headers: {
2544
+ 'Content-Type': 'application/x-www-form-urlencoded',
2545
+ },
2546
+ }
2547
+ );
2548
+
2549
+ const result: Record<string, string> = {};
2550
+ response.data.split('&').forEach((item: string) => {
2551
+ const [key, value] = item.split('=');
2552
+ if (key && value) {
2553
+ result[key] = decodeURIComponent(value);
2554
+ }
2555
+ });
2556
+
2557
+ if (result.RtnCode !== '300') {
2558
+ throw new Error(`Create shipment failed: ${result.RtnMsg}`);
2559
+ }
2560
+
2561
+ return {
2562
+ allPayLogisticsID: result.AllPayLogisticsID,
2563
+ bookingNote: result.BookingNote,
2564
+ };
2565
+ }
2566
+ }
2567
+
2568
+ // Usage Example
2569
+ const home = new ECPayHomeLogistics({
2570
+ merchantId: '2000132',
2571
+ hashKey: '5294y06JbISpM5x9',
2572
+ hashIV: 'v77hoKGq4kWxNNIS',
2573
+ });
2574
+
2575
+ const result = await home.createHomeShipment({
2576
+ merTradeNo: `HOME${Date.now()}`,
2577
+ logisticsSubType: 'TCAT',
2578
+ goodsAmount: 1000,
2579
+ goodsName: 'Electronics',
2580
+ senderName: 'Store Name',
2581
+ senderCellPhone: '0912345678',
2582
+ senderZipCode: '100',
2583
+ senderAddress: 'Taipei City, Zhongzheng Dist., XXX Road',
2584
+ receiverName: 'Customer Name',
2585
+ receiverCellPhone: '0987654321',
2586
+ receiverZipCode: '300',
2587
+ receiverAddress: 'Hsinchu City, East Dist., YYY Road',
2588
+ temperature: '0001', // Normal
2589
+ specification: '0001', // 60cm
2590
+ distance: '02', // Cross city
2591
+ scheduledDeliveryTime: '4', // No preference
2592
+ serverReplyURL: 'https://your-site.com/callback/ecpay-home',
2593
+ });
2594
+
2595
+ console.log('Logistics ID:', result.allPayLogisticsID);
2596
+ console.log('Booking Note:', result.bookingNote);
2597
+
2598
+ export { ECPayHomeLogistics };
2599
+ ```
2600
+
2601
+ ---
2602
+
2603
+ ### 4. Query Shipment Status (ECPay)
2604
+
2605
+ #### TypeScript Example
2606
+
2607
+ ```typescript
2608
+ class ECPayQueryLogistics extends ECPayLogistics {
2609
+ /**
2610
+ * Query shipment status
2611
+ */
2612
+ async queryShipment(allPayLogisticsID: string) {
2613
+ const data = {
2614
+ MerchantID: this.config.merchantId,
2615
+ AllPayLogisticsID: allPayLogisticsID,
2616
+ TimeStamp: Math.floor(Date.now() / 1000).toString(),
2617
+ };
2618
+
2619
+ data['CheckMacValue'] = this.generateCheckMacValue(data);
2620
+
2621
+ const response = await axios.post(
2622
+ `${this.baseUrl}/Helper/QueryLogisticsTradeInfo/V2`,
2623
+ new URLSearchParams(data),
2624
+ {
2625
+ headers: {
2626
+ 'Content-Type': 'application/x-www-form-urlencoded',
2627
+ },
2628
+ }
2629
+ );
2630
+
2631
+ const result: Record<string, string> = {};
2632
+ response.data.split('&').forEach((item: string) => {
2633
+ const [key, value] = item.split('=');
2634
+ if (key && value) {
2635
+ result[key] = decodeURIComponent(value);
2636
+ }
2637
+ });
2638
+
2639
+ return {
2640
+ merchantTradeNo: result.MerchantTradeNo,
2641
+ goodsAmount: result.GoodsAmount,
2642
+ logisticsStatus: result.LogisticsStatus,
2643
+ receiverName: result.ReceiverName,
2644
+ receiverStoreID: result.ReceiverStoreID,
2645
+ updateStatusDate: result.UpdateStatusDate,
2646
+ };
2647
+ }
2648
+ }
2649
+
2650
+ // Usage Example
2651
+ const query = new ECPayQueryLogistics({
2652
+ merchantId: '2000132',
2653
+ hashKey: '5294y06JbISpM5x9',
2654
+ hashIV: 'v77hoKGq4kWxNNIS',
2655
+ });
2656
+
2657
+ const status = await query.queryShipment('1718546');
2658
+
2659
+ console.log(`Order: ${status.merchantTradeNo}`);
2660
+ console.log(`Status: ${status.logisticsStatus}`);
2661
+ console.log(`Store: ${status.receiverStoreID}`);
2662
+
2663
+ export { ECPayQueryLogistics };
2664
+ ```
2665
+
2666
+ ---
2667
+
2668
+ ## Real-World Scenarios
2669
+
2670
+ ### Scenario 1: E-commerce Checkout Flow
2671
+
2672
+ Complete integration from store selection to shipment creation.
2673
+
2674
+ ```typescript
2675
+ class EcommerceLogistics {
2676
+ private logistics: NewebPayStoreMap & NewebPayShipment;
2677
+
2678
+ constructor() {
2679
+ this.logistics = new (class extends NewebPayStoreMap {})({
2680
+ merchantId: process.env.NEWEBPAY_MERCHANT_ID!,
2681
+ hashKey: process.env.NEWEBPAY_HASH_KEY!,
2682
+ hashIV: process.env.NEWEBPAY_HASH_IV!,
2683
+ isProduction: process.env.NODE_ENV === 'production',
2684
+ }) as any;
2685
+ }
2686
+
2687
+ /**
2688
+ * Step 1: Customer selects convenience store
2689
+ */
2690
+ async showStoreSelection(orderId: string) {
2691
+ const html = await this.logistics.queryStoreMap({
2692
+ merchantOrderNo: orderId,
2693
+ lgsType: 'C2C',
2694
+ shipType: '1', // 7-ELEVEN
2695
+ returnURL: `https://your-site.com/api/store-selected`,
2696
+ extraData: orderId,
2697
+ });
2698
+
2699
+ return html;
2700
+ }
2701
+
2702
+ /**
2703
+ * Step 2: Handle store selection callback
2704
+ */
2705
+ async handleStoreSelection(encryptData: string, hashData: string) {
2706
+ const storeInfo = this.logistics.handleStoreMapCallback(encryptData, hashData);
2707
+
2708
+ // Save store info to order
2709
+ await this.saveStoreToOrder(storeInfo.merchantOrderNo, {
2710
+ storeID: storeInfo.storeID,
2711
+ storeName: storeInfo.storeName,
2712
+ storeAddr: storeInfo.storeAddr,
2713
+ storeTel: storeInfo.storeTel,
2714
+ });
2715
+
2716
+ return storeInfo;
2717
+ }
2718
+
2719
+ /**
2720
+ * Step 3: Create shipment after payment
2721
+ */
2722
+ async createShipmentAfterPayment(orderId: string) {
2723
+ // Get order details from database
2724
+ const order = await this.getOrder(orderId);
2725
+
2726
+ // Create shipment
2727
+ const shipment = await (this.logistics as any).createShipment({
2728
+ merchantOrderNo: orderId,
2729
+ tradeType: 1, // COD
2730
+ userName: order.customer.name,
2731
+ userTel: order.customer.phone,
2732
+ userEmail: order.customer.email,
2733
+ storeID: order.logistics.storeID,
2734
+ amt: order.total,
2735
+ itemDesc: order.items.map((i: any) => i.name).join(', '),
2736
+ notifyURL: 'https://your-site.com/api/shipment-status',
2737
+ lgsType: 'C2C',
2738
+ shipType: '1',
2739
+ });
2740
+
2741
+ // Save trade number
2742
+ await this.saveTradeNumber(orderId, shipment.tradeNo);
2743
+
2744
+ return shipment;
2745
+ }
2746
+
2747
+ /**
2748
+ * Step 4: Get shipment number for printing
2749
+ */
2750
+ async getShipmentNumberForPrinting(orderIds: string[]) {
2751
+ const shipmentNums = await (this.logistics as any).getShipmentNumbers(orderIds);
2752
+
2753
+ return shipmentNums.success.map((item: any) => ({
2754
+ orderId: item.MerchantOrderNo,
2755
+ trackingNo: item.LgsNo,
2756
+ printCode: item.StorePrintNo,
2757
+ }));
2758
+ }
2759
+
2760
+ // Helper methods
2761
+ private async saveStoreToOrder(orderId: string, storeInfo: any) {
2762
+ // Implementation
2763
+ }
2764
+
2765
+ private async getOrder(orderId: string) {
2766
+ // Implementation
2767
+ return {} as any;
2768
+ }
2769
+
2770
+ private async saveTradeNumber(orderId: string, tradeNo: string) {
2771
+ // Implementation
2772
+ }
2773
+ }
2774
+ ```
2775
+
2776
+ ---
2777
+
2778
+ ### Scenario 2: Batch Shipment Processing
2779
+
2780
+ Process multiple orders in batch.
2781
+
2782
+ ```python
2783
+ """Batch Shipment Processing - Python Example"""
2784
+
2785
+ from typing import List, Dict
2786
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2787
+
2788
+
2789
+ class BatchLogisticsProcessor:
2790
+ """Process multiple shipments in batch"""
2791
+
2792
+ def __init__(self, logistics: NewebPayShipment):
2793
+ self.logistics = logistics
2794
+
2795
+ def create_shipments_batch(
2796
+ self,
2797
+ orders: List[Dict[str, any]],
2798
+ max_workers: int = 5,
2799
+ ) -> Dict[str, any]:
2800
+ """Create shipments for multiple orders"""
2801
+
2802
+ results = {
2803
+ 'success': [],
2804
+ 'failed': [],
2805
+ }
2806
+
2807
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
2808
+ futures = {
2809
+ executor.submit(
2810
+ self.logistics.create_shipment,
2811
+ **self._prepare_shipment_data(order)
2812
+ ): order['order_id']
2813
+ for order in orders
2814
+ }
2815
+
2816
+ for future in as_completed(futures):
2817
+ order_id = futures[future]
2818
+ try:
2819
+ result = future.result()
2820
+ results['success'].append({
2821
+ 'order_id': order_id,
2822
+ 'trade_no': result['trade_no'],
2823
+ })
2824
+ except Exception as e:
2825
+ results['failed'].append({
2826
+ 'order_id': order_id,
2827
+ 'error': str(e),
2828
+ })
2829
+
2830
+ return results
2831
+
2832
+ def get_shipment_numbers_batch(
2833
+ self,
2834
+ order_ids: List[str],
2835
+ ) -> Dict[str, List[Dict]]:
2836
+ """Get shipment numbers for multiple orders (max 10 per request)"""
2837
+
2838
+ results = {
2839
+ 'success': [],
2840
+ 'failed': [],
2841
+ }
2842
+
2843
+ # Split into batches of 10
2844
+ for i in range(0, len(order_ids), 10):
2845
+ batch = order_ids[i:i+10]
2846
+
2847
+ try:
2848
+ result = self.logistics.get_shipment_numbers(batch)
2849
+ results['success'].extend(result['success'])
2850
+ results['failed'].extend(result['error'])
2851
+ except Exception as e:
2852
+ results['failed'].extend([
2853
+ {'order_id': order_id, 'error': str(e)}
2854
+ for order_id in batch
2855
+ ])
2856
+
2857
+ return results
2858
+
2859
+ def _prepare_shipment_data(self, order: Dict) -> Dict:
2860
+ """Prepare shipment data from order"""
2861
+ return {
2862
+ 'merchant_order_no': order['order_id'],
2863
+ 'trade_type': 1 if order['cod'] else 3,
2864
+ 'user_name': order['customer']['name'],
2865
+ 'user_tel': order['customer']['phone'],
2866
+ 'user_email': order['customer']['email'],
2867
+ 'store_id': order['logistics']['store_id'],
2868
+ 'amt': order['total'],
2869
+ 'item_desc': order['description'],
2870
+ 'lgs_type': 'C2C',
2871
+ 'ship_type': '1',
2872
+ }
2873
+
2874
+
2875
+ # Usage Example
2876
+ logistics = NewebPayShipment(
2877
+ merchant_id='YOUR_MERCHANT_ID',
2878
+ hash_key='YOUR_HASH_KEY',
2879
+ hash_iv='YOUR_HASH_IV',
2880
+ )
2881
+
2882
+ processor = BatchLogisticsProcessor(logistics)
2883
+
2884
+ # Create shipments for 100 orders
2885
+ orders = [
2886
+ {
2887
+ 'order_id': f'ORD{i:05d}',
2888
+ 'cod': True,
2889
+ 'customer': {
2890
+ 'name': f'Customer {i}',
2891
+ 'phone': '0912345678',
2892
+ 'email': f'customer{i}@example.com',
2893
+ },
2894
+ 'logistics': {
2895
+ 'store_id': '123456',
2896
+ },
2897
+ 'total': 1000 + i * 10,
2898
+ 'description': f'Order {i} items',
2899
+ }
2900
+ for i in range(100)
2901
+ ]
2902
+
2903
+ results = processor.create_shipments_batch(orders)
2904
+
2905
+ print(f"Success: {len(results['success'])}")
2906
+ print(f"Failed: {len(results['failed'])}")
2907
+ ```
2908
+
2909
+ ---
2910
+
2911
+ ### Scenario 3: Order Modification Workflow
2912
+
2913
+ Handle customer requests to change delivery details.
2914
+
2915
+ ```typescript
2916
+ class OrderModificationWorkflow {
2917
+ private query: NewebPayQueryShipment;
2918
+ private modify: NewebPayModifyShipment;
2919
+ private storeMap: NewebPayStoreMap;
2920
+
2921
+ constructor() {
2922
+ const config = {
2923
+ merchantId: process.env.NEWEBPAY_MERCHANT_ID!,
2924
+ hashKey: process.env.NEWEBPAY_HASH_KEY!,
2925
+ hashIV: process.env.NEWEBPAY_HASH_IV!,
2926
+ };
2927
+
2928
+ this.query = new NewebPayQueryShipment(config);
2929
+ this.modify = new NewebPayModifyShipment(config);
2930
+ this.storeMap = new NewebPayStoreMap(config);
2931
+ }
2932
+
2933
+ /**
2934
+ * Check if order can be modified
2935
+ */
2936
+ async canModifyOrder(orderId: string): Promise<boolean> {
2937
+ const status = await this.query.queryShipment(orderId);
2938
+
2939
+ // Can only modify if not yet shipped
2940
+ const modifiableStatuses = ['0_1', '0_2', '0_3'];
2941
+ return modifiableStatuses.includes(status.retId);
2942
+ }
2943
+
2944
+ /**
2945
+ * Change recipient information
2946
+ */
2947
+ async changeRecipient(
2948
+ orderId: string,
2949
+ newRecipient: {
2950
+ name: string;
2951
+ phone: string;
2952
+ email: string;
2953
+ }
2954
+ ) {
2955
+ // Check if can modify
2956
+ if (!(await this.canModifyOrder(orderId))) {
2957
+ throw new Error('Order cannot be modified at current status');
2958
+ }
2959
+
2960
+ // Get current order info
2961
+ const current = await this.query.queryShipment(orderId);
2962
+
2963
+ // Modify order
2964
+ await this.modify.modifyShipment({
2965
+ merchantOrderNo: orderId,
2966
+ lgsType: current.lgsType as 'B2C' | 'C2C',
2967
+ shipType: current.shipType as '1' | '2' | '3' | '4',
2968
+ userName: newRecipient.name,
2969
+ userTel: newRecipient.phone,
2970
+ userEmail: newRecipient.email,
2971
+ });
2972
+
2973
+ return { success: true };
2974
+ }
2975
+
2976
+ /**
2977
+ * Change pickup store
2978
+ */
2979
+ async changePickupStore(orderId: string) {
2980
+ // Check if can modify
2981
+ if (!(await this.canModifyOrder(orderId))) {
2982
+ throw new Error('Order cannot be modified at current status');
2983
+ }
2984
+
2985
+ // Get current order info
2986
+ const current = await this.query.queryShipment(orderId);
2987
+
2988
+ // Show store map for new selection
2989
+ const html = await this.storeMap.queryStoreMap({
2990
+ merchantOrderNo: orderId,
2991
+ lgsType: current.lgsType as 'B2C' | 'C2C',
2992
+ shipType: current.shipType as '1' | '2' | '3' | '4',
2993
+ returnURL: `https://your-site.com/api/store-changed`,
2994
+ extraData: orderId,
2995
+ });
2996
+
2997
+ return html;
2998
+ }
2999
+
3000
+ /**
3001
+ * Handle new store selection
3002
+ */
3003
+ async handleStoreChange(encryptData: string, hashData: string) {
3004
+ const storeInfo = this.storeMap.handleStoreMapCallback(encryptData, hashData);
3005
+
3006
+ // Get current order info
3007
+ const current = await this.query.queryShipment(storeInfo.merchantOrderNo);
3008
+
3009
+ // Modify order with new store
3010
+ await this.modify.modifyShipment({
3011
+ merchantOrderNo: storeInfo.merchantOrderNo,
3012
+ lgsType: storeInfo.lgsType as 'B2C' | 'C2C',
3013
+ shipType: storeInfo.shipType as '1' | '2' | '3' | '4',
3014
+ storeID: storeInfo.storeID,
3015
+ });
3016
+
3017
+ // Update database
3018
+ await this.updateOrderStore(storeInfo.merchantOrderNo, {
3019
+ storeID: storeInfo.storeID,
3020
+ storeName: storeInfo.storeName,
3021
+ storeAddr: storeInfo.storeAddr,
3022
+ storeTel: storeInfo.storeTel,
3023
+ });
3024
+
3025
+ return storeInfo;
3026
+ }
3027
+
3028
+ private async updateOrderStore(orderId: string, storeInfo: any) {
3029
+ // Implementation
3030
+ }
3031
+ }
3032
+ ```
3033
+
3034
+ ---
3035
+
3036
+ ## Error Handling
3037
+
3038
+ ### Comprehensive Error Handler
3039
+
3040
+ ```typescript
3041
+ class LogisticsError extends Error {
3042
+ constructor(
3043
+ public code: string,
3044
+ public message: string,
3045
+ public originalError?: any
3046
+ ) {
3047
+ super(message);
3048
+ this.name = 'LogisticsError';
3049
+ }
3050
+ }
3051
+
3052
+ class LogisticsErrorHandler {
3053
+ /**
3054
+ * Error code mapping
3055
+ */
3056
+ private static errorMessages: Record<string, string> = {
3057
+ '1101': 'Failed to create logistics order',
3058
+ '1102': 'Merchant not found',
3059
+ '1103': 'Duplicate merchant order number',
3060
+ '1104': 'Logistics service not enabled',
3061
+ '1105': 'Store information invalid or empty',
3062
+ '1106': 'IP not allowed',
3063
+ '1107': 'Payment order not found',
3064
+ '1108': 'System error, cannot query logistics order',
3065
+ '1109': 'Logistics order not found',
3066
+ '1110': 'System error, cannot modify logistics order',
3067
+ '1111': 'Order status cannot be modified',
3068
+ '1112': 'Failed to modify logistics order',
3069
+ '1113': 'System error, cannot query tracking history',
3070
+ '1114': 'Insufficient prepaid balance',
3071
+ '1115': 'Failed to get shipment number',
3072
+ '1116': 'Shipment already created for this transaction',
3073
+ '2100': 'Data format error',
3074
+ '2101': 'Version error',
3075
+ '2102': 'UID_ cannot be empty',
3076
+ '2103': 'COD amount limit: 20000 NTD',
3077
+ '2104': 'No payment amount limit: 20000 NTD',
3078
+ '2105': 'Max 10 shipment numbers per request',
3079
+ '2106': 'Max labels exceeded for this provider',
3080
+ '4101': 'IP restricted',
3081
+ '4103': 'HashData_ verification failed',
3082
+ '4104': 'Encryption error, check Hash_Key and Hash_IV',
3083
+ };
3084
+
3085
+ /**
3086
+ * Handle API error
3087
+ */
3088
+ static handleError(errorCode: string, originalError?: any): LogisticsError {
3089
+ const message = this.errorMessages[errorCode] || 'Unknown error';
3090
+ return new LogisticsError(errorCode, message, originalError);
3091
+ }
3092
+
3093
+ /**
3094
+ * Retry logic for transient errors
3095
+ */
3096
+ static async withRetry<T>(
3097
+ fn: () => Promise<T>,
3098
+ maxRetries: number = 3,
3099
+ delay: number = 1000
3100
+ ): Promise<T> {
3101
+ let lastError: any;
3102
+
3103
+ for (let i = 0; i < maxRetries; i++) {
3104
+ try {
3105
+ return await fn();
3106
+ } catch (error: any) {
3107
+ lastError = error;
3108
+
3109
+ // Don't retry for non-transient errors
3110
+ if (this.isNonTransientError(error.code)) {
3111
+ throw error;
3112
+ }
3113
+
3114
+ // Wait before retry
3115
+ if (i < maxRetries - 1) {
3116
+ await new Promise((resolve) => setTimeout(resolve, delay * (i + 1)));
3117
+ }
3118
+ }
3119
+ }
3120
+
3121
+ throw lastError;
3122
+ }
3123
+
3124
+ /**
3125
+ * Check if error is non-transient
3126
+ */
3127
+ private static isNonTransientError(code: string): boolean {
3128
+ const nonTransientErrors = [
3129
+ '1102', // Merchant not found
3130
+ '1103', // Duplicate order number
3131
+ '1104', // Service not enabled
3132
+ '1106', // IP not allowed
3133
+ '2100', // Data format error
3134
+ '2101', // Version error
3135
+ '2102', // UID_ empty
3136
+ '2103', // Amount limit exceeded
3137
+ '2104', // Amount limit exceeded
3138
+ '2105', // Batch limit exceeded
3139
+ '2106', // Batch limit exceeded
3140
+ '4101', // IP restricted
3141
+ '4103', // Hash verification failed
3142
+ '4104', // Encryption error
3143
+ ];
3144
+
3145
+ return nonTransientErrors.includes(code);
3146
+ }
3147
+ }
3148
+
3149
+ // Usage Example
3150
+ try {
3151
+ const result = await LogisticsErrorHandler.withRetry(async () => {
3152
+ return await logistics.createShipment({
3153
+ /* ... */
3154
+ });
3155
+ });
3156
+ } catch (error) {
3157
+ if (error instanceof LogisticsError) {
3158
+ console.error(`Logistics Error [${error.code}]: ${error.message}`);
3159
+
3160
+ // Handle specific errors
3161
+ switch (error.code) {
3162
+ case '1103':
3163
+ // Duplicate order - use different order number
3164
+ break;
3165
+ case '2103':
3166
+ // Amount too high - split into multiple shipments
3167
+ break;
3168
+ case '4103':
3169
+ // Hash failed - check credentials
3170
+ break;
3171
+ default:
3172
+ // Generic error handling
3173
+ break;
3174
+ }
3175
+ }
3176
+ }
3177
+ ```
3178
+
3179
+ ---
3180
+
3181
+ **Total Lines**: 1400+
3182
+
3183
+ This comprehensive guide covers all major NewebPay Logistics integration scenarios with production-ready code examples.