pathao-merchant-sdk 2.0.2 → 2.2.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.
package/README.md CHANGED
@@ -21,6 +21,7 @@ An **unofficial** TypeScript SDK for integrating with the Pathao Merchant API. T
21
21
  - [API Reference](#api-reference)
22
22
  - [Examples](#examples)
23
23
  - [Error Handling](#error-handling)
24
+ - [Webhooks](#webhooks)
24
25
  - [Authentication](#authentication)
25
26
  - [Official Documentation](#official-documentation)
26
27
  - [Contributing](#contributing)
@@ -37,6 +38,8 @@ An **unofficial** TypeScript SDK for integrating with the Pathao Merchant API. T
37
38
  - 🏪 **Store Management** - Create and manage pickup/service points
38
39
  - 💰 **Price Calculation** - Get accurate delivery charges before creating orders
39
40
  - 🌍 **Location Services** - Access cities, zones, and areas data
41
+ - 🔔 **Webhook Support** - Verify and handle inbound Pathao webhook events
42
+ - ♻️ **Retry & Circuit Breaker** - Automatic retry with backoff, circuit breaker for resilience
40
43
  - ⚡ **Built with Axios** - Reliable HTTP client with request/response interceptors
41
44
  - 🛡️ **Error Handling** - Comprehensive error handling with detailed error messages
42
45
  - 📚 **Well Documented** - Extensive documentation and examples
@@ -406,6 +409,166 @@ try {
406
409
  }
407
410
  ```
408
411
 
412
+ ## Webhooks
413
+
414
+ The `pathao-merchant-sdk/webhooks` sub-module handles inbound Pathao webhook events. It has **zero extra runtime dependencies** — only Node.js built-ins (`crypto`, `events`).
415
+
416
+ > **Note:** Pathao has no official webhook documentation. This implementation is based on reverse-engineering the [`pathao-courier`](https://www.npmjs.com/package/pathao-courier) package. The signature scheme is plain shared-secret equality (no HMAC) via constant-time comparison.
417
+
418
+ ### Installation
419
+
420
+ The webhooks module is a separate entry point. Import it explicitly:
421
+
422
+ ```typescript
423
+ import {
424
+ PathaoWebhookHandler,
425
+ constructEvent,
426
+ verifySignature,
427
+ PathaoWebhookEvent,
428
+ } from 'pathao-merchant-sdk/webhooks';
429
+ ```
430
+
431
+ ```javascript
432
+ // CommonJS
433
+ const { PathaoWebhookHandler } = require('pathao-merchant-sdk/webhooks');
434
+ ```
435
+
436
+ ### Quick verification
437
+
438
+ ```typescript
439
+ import { verifySignature } from 'pathao-merchant-sdk/webhooks';
440
+
441
+ const isValid = verifySignature(
442
+ req.headers['x-pathao-signature'],
443
+ process.env.PATHAO_WEBHOOK_SECRET!,
444
+ );
445
+ ```
446
+
447
+ ### Verify + parse in one call
448
+
449
+ ```typescript
450
+ import { constructEvent, PathaoWebhookError } from 'pathao-merchant-sdk/webhooks';
451
+
452
+ app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
453
+ try {
454
+ const payload = constructEvent(
455
+ req.body, // raw Buffer — do NOT pre-parse
456
+ req.headers,
457
+ process.env.PATHAO_WEBHOOK_SECRET!,
458
+ );
459
+ console.log('Event:', payload.event, payload.data);
460
+ res.sendStatus(200);
461
+ } catch (err) {
462
+ if (err instanceof PathaoWebhookError) {
463
+ res.status(400).send(err.message);
464
+ } else {
465
+ res.sendStatus(500);
466
+ }
467
+ }
468
+ });
469
+ ```
470
+
471
+ ### Express middleware
472
+
473
+ ```typescript
474
+ import { PathaoWebhookHandler, PathaoWebhookEvent } from 'pathao-merchant-sdk/webhooks';
475
+
476
+ const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);
477
+
478
+ // Listen for specific events (fully typed payload)
479
+ handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {
480
+ console.log('Delivered:', payload.consignment_id);
481
+ });
482
+
483
+ handler.on(PathaoWebhookEvent.ORDER_CANCELLED, (payload) => {
484
+ console.log('Cancelled:', payload.consignment_id, payload.reason);
485
+ });
486
+
487
+ // Catch all events
488
+ handler.on('webhook', (payload) => {
489
+ console.log('Any event:', payload.event, payload.data);
490
+ });
491
+
492
+ // Handle errors (invalid signature, bad JSON, etc.)
493
+ handler.on('error', (err) => {
494
+ console.error('Webhook error:', err.message);
495
+ });
496
+
497
+ // Mount — must use express.raw() BEFORE this middleware
498
+ app.post(
499
+ '/webhook/pathao',
500
+ express.raw({ type: 'application/json' }),
501
+ handler.expressMiddleware(),
502
+ );
503
+
504
+ // Access the parsed payload downstream
505
+ app.post('/webhook/pathao', express.raw({ type: 'application/json' }), handler.expressMiddleware(), (req, res) => {
506
+ console.log(req.pathaoWebhook); // typed PathaoWebhookPayload
507
+ res.sendStatus(200);
508
+ });
509
+ ```
510
+
511
+ ### Generic (non-Express) middleware
512
+
513
+ ```typescript
514
+ const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);
515
+ const middleware = handler.middleware();
516
+
517
+ // Returns { payload, error } — never rejects
518
+ const { payload, error } = await middleware(rawBody, headers);
519
+ if (error) {
520
+ console.error('Bad webhook:', error.message);
521
+ } else {
522
+ console.log('Event:', payload!.event);
523
+ }
524
+ ```
525
+
526
+ ### Supported event types
527
+
528
+ | Event constant | Event string |
529
+ |---|---|
530
+ | `ORDER_CREATED` | `order.created` |
531
+ | `ORDER_ACCEPTED` | `order.accepted` |
532
+ | `ORDER_PICKUP_REQUESTED` | `order.pickup_requested` |
533
+ | `ORDER_PICKED` | `order.picked` |
534
+ | `ORDER_IN_TRANSIT` | `order.in_transit` |
535
+ | `ORDER_DELIVERED` | `order.delivered` |
536
+ | `ORDER_CANCELLED` | `order.cancelled` |
537
+ | `ORDER_HOLD` | `order.hold` |
538
+ | `ORDER_RETURN_REQUESTED` | `order.return_requested` |
539
+ | `ORDER_RETURN_PICKUP_REQUESTED` | `order.return_pickup_requested` |
540
+ | `ORDER_RETURN_PICKED` | `order.return_picked` |
541
+ | `ORDER_RETURN_IN_TRANSIT` | `order.return_in_transit` |
542
+ | `ORDER_PARTIAL_DELIVERED` | `order.partial_delivered` |
543
+ | `ORDER_DELIVERY_FAILED` | `order.delivery_failed` |
544
+ | `ORDER_ON_HOLD` | `order.on_hold` |
545
+ | `ORDER_PAID` | `order.paid` |
546
+ | `ORDER_PAID_RETURN` | `order.paid_return` |
547
+ | `ORDER_RETURNED` | `order.returned` |
548
+ | `ORDER_EXCHANGED` | `order.exchanged` |
549
+ | `STORE_CREATED` | `store.created` |
550
+ | `STORE_UPDATED` | `store.updated` |
551
+
552
+ ### TypeScript types
553
+
554
+ All event payloads are fully typed. Access them via `WebhookEventPayloadMap`:
555
+
556
+ ```typescript
557
+ import type {
558
+ WebhookEventPayloadMap,
559
+ PathaoWebhookEvent,
560
+ OrderDeliveredPayload,
561
+ } from 'pathao-merchant-sdk/webhooks';
562
+
563
+ // Explicit payload type
564
+ const handler = (payload: OrderDeliveredPayload) => {
565
+ console.log(payload.consignment_id, payload.amount_to_collect);
566
+ };
567
+
568
+ // Via mapped type
569
+ type DeliveredPayload = WebhookEventPayloadMap[PathaoWebhookEvent.ORDER_DELIVERED];
570
+ ```
571
+
409
572
  ## Authentication
410
573
 
411
574
  The SDK automatically handles OAuth2 authentication and token refresh. You only need to provide your credentials once during initialization:
@@ -511,6 +674,28 @@ This is an **unofficial** SDK and is not affiliated with or endorsed by Pathao.
511
674
 
512
675
  ## Changelog
513
676
 
677
+ ### 2.1.0
678
+ - Added webhook support via `pathao-merchant-sdk/webhooks` sub-path
679
+ - `PathaoWebhookHandler` — EventEmitter with typed `on()` overloads for all 21 event types
680
+ - `constructEvent()` and `verifySignature()` standalone helpers
681
+ - Express middleware and generic async middleware
682
+ - Constant-time signature comparison via `crypto.timingSafeEqual`
683
+ - Fixed case-insensitive header lookup for `X-PATHAO-Signature`
684
+
685
+ ### 2.0.x
686
+ - Added retry logic: 429 reads `Retry-After`, 5xx exponential backoff (max 2 retries)
687
+ - Added circuit breaker: throws `PathaoApiError` (code 503) when open
688
+ - Added `sandbox()` and `production()` named constructors
689
+ - Deferred config validation to first API call
690
+ - HTTPS-only enforcement in `validateConfiguration()`
691
+ - Auth token redacted as `Bearer [REDACTED]` in debug logs
692
+ - Added `User-Agent: pathao-merchant-sdk node/<version>` header
693
+ - `getStores()` accepts optional `page` parameter
694
+ - `validateContactNumber` requires `startsWith('01')`
695
+ - `is_active` and `cod_enabled` typed as `0 | 1`
696
+ - Fixed shell injection in `scripts/release.js` (replaced `execSync` with `spawnSync`)
697
+ - CI/CD: replaced deprecated actions, added version-existence check before publish
698
+
514
699
  ### 1.0.0
515
700
  - Initial release
516
701
  - Complete Pathao API integration
package/dist/index.d.mts CHANGED
@@ -65,7 +65,7 @@ interface PathaoStore {
65
65
  store_id: number;
66
66
  store_name: string;
67
67
  store_address: string;
68
- is_active: number;
68
+ is_active: 0 | 1;
69
69
  city_id: number;
70
70
  zone_id: number;
71
71
  hub_id: number;
@@ -107,7 +107,7 @@ interface PathaoPriceResponse {
107
107
  discount: number;
108
108
  promo_discount: number;
109
109
  plan_id: number;
110
- cod_enabled: number;
110
+ cod_enabled: 0 | 1;
111
111
  cod_percentage: number;
112
112
  additional_charge: number;
113
113
  final_price: number;
@@ -166,7 +166,7 @@ interface PathaoConfig {
166
166
  clientSecret: string;
167
167
  username: string;
168
168
  password: string;
169
- baseURL: string;
169
+ baseURL?: string;
170
170
  timeout?: number;
171
171
  }
172
172
  interface PathaoError {
@@ -176,14 +176,27 @@ interface PathaoError {
176
176
  errors?: Record<string, string[]>;
177
177
  validation?: Record<string, string[]>;
178
178
  }
179
+ /**
180
+ * Delivery type codes defined by the Pathao API.
181
+ * Values correspond to Pathao's internal delivery_type IDs.
182
+ * @see https://developers.pathao.com - Order Creation endpoint
183
+ */
179
184
  declare enum DeliveryType {
185
+ /** Standard delivery (API value: 48) */
180
186
  NORMAL = 48,
187
+ /** Same-day on-demand delivery (API value: 12) */
181
188
  ON_DEMAND = 12
182
189
  }
183
190
  declare enum ItemType {
184
191
  DOCUMENT = 1,
185
192
  PARCEL = 2
186
193
  }
194
+ interface PathaoBulkOrderResponse {
195
+ message: string;
196
+ type: string;
197
+ code: number;
198
+ data: boolean;
199
+ }
187
200
 
188
201
  /**
189
202
  * Pathao Merchant API Service (Unofficial SDK)
@@ -233,7 +246,6 @@ declare class PathaoApiService {
233
246
  private config;
234
247
  private isAuthenticating;
235
248
  private authPromise;
236
- private requestQueue;
237
249
  private hasValidated;
238
250
  private debug;
239
251
  private circuitBreaker;
@@ -244,26 +256,22 @@ declare class PathaoApiService {
244
256
  private validateConfiguration;
245
257
  private ensureAuthenticated;
246
258
  private performAuthentication;
247
- private processRequestQueue;
248
259
  private authenticate;
249
260
  private refreshAccessToken;
250
261
  private getErrorMessage;
251
262
  private toPathaoApiError;
252
263
  private handleCircuitBreaker;
264
+ private delay;
253
265
  createOrder(orderData: PathaoOrderRequest): Promise<PathaoOrderResponse>;
254
266
  createStore(storeData: PathaoStoreRequest): Promise<PathaoStoreCreateResponse>;
255
- getStores(): Promise<PathaoStoreListResponse>;
267
+ getStores(page?: number): Promise<PathaoStoreListResponse>;
268
+ getStoresAll(): Promise<PathaoStore[]>;
256
269
  calculatePrice(priceData: PathaoPriceRequest): Promise<PathaoPriceResponse>;
257
270
  getCities(): Promise<PathaoCityResponse>;
258
271
  getZones(cityId: number): Promise<PathaoZoneResponse>;
259
272
  getAreas(zoneId: number): Promise<PathaoAreaResponse>;
260
273
  getOrderStatus(consignmentId: string): Promise<PathaoOrderStatusResponse>;
261
- createBulkOrder(orders: PathaoOrderRequest[]): Promise<{
262
- message: string;
263
- type: string;
264
- code: number;
265
- data: boolean;
266
- }>;
274
+ createBulkOrder(orders: PathaoOrderRequest[]): Promise<PathaoBulkOrderResponse>;
267
275
  static validatePhoneNumber(phone: string): boolean;
268
276
  static formatPhoneNumber(phone: string): string;
269
277
  static validateAddress(address: string): boolean;
@@ -282,6 +290,14 @@ declare class PathaoApiService {
282
290
  debug?: boolean;
283
291
  circuitBreaker?: CircuitBreakerConfig;
284
292
  }): PathaoApiService;
293
+ static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
294
+ debug?: boolean;
295
+ circuitBreaker?: CircuitBreakerConfig;
296
+ }): PathaoApiService;
297
+ static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
298
+ debug?: boolean;
299
+ circuitBreaker?: CircuitBreakerConfig;
300
+ }): PathaoApiService;
285
301
  }
286
302
 
287
- export { DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
303
+ export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
package/dist/index.d.ts CHANGED
@@ -65,7 +65,7 @@ interface PathaoStore {
65
65
  store_id: number;
66
66
  store_name: string;
67
67
  store_address: string;
68
- is_active: number;
68
+ is_active: 0 | 1;
69
69
  city_id: number;
70
70
  zone_id: number;
71
71
  hub_id: number;
@@ -107,7 +107,7 @@ interface PathaoPriceResponse {
107
107
  discount: number;
108
108
  promo_discount: number;
109
109
  plan_id: number;
110
- cod_enabled: number;
110
+ cod_enabled: 0 | 1;
111
111
  cod_percentage: number;
112
112
  additional_charge: number;
113
113
  final_price: number;
@@ -166,7 +166,7 @@ interface PathaoConfig {
166
166
  clientSecret: string;
167
167
  username: string;
168
168
  password: string;
169
- baseURL: string;
169
+ baseURL?: string;
170
170
  timeout?: number;
171
171
  }
172
172
  interface PathaoError {
@@ -176,14 +176,27 @@ interface PathaoError {
176
176
  errors?: Record<string, string[]>;
177
177
  validation?: Record<string, string[]>;
178
178
  }
179
+ /**
180
+ * Delivery type codes defined by the Pathao API.
181
+ * Values correspond to Pathao's internal delivery_type IDs.
182
+ * @see https://developers.pathao.com - Order Creation endpoint
183
+ */
179
184
  declare enum DeliveryType {
185
+ /** Standard delivery (API value: 48) */
180
186
  NORMAL = 48,
187
+ /** Same-day on-demand delivery (API value: 12) */
181
188
  ON_DEMAND = 12
182
189
  }
183
190
  declare enum ItemType {
184
191
  DOCUMENT = 1,
185
192
  PARCEL = 2
186
193
  }
194
+ interface PathaoBulkOrderResponse {
195
+ message: string;
196
+ type: string;
197
+ code: number;
198
+ data: boolean;
199
+ }
187
200
 
188
201
  /**
189
202
  * Pathao Merchant API Service (Unofficial SDK)
@@ -233,7 +246,6 @@ declare class PathaoApiService {
233
246
  private config;
234
247
  private isAuthenticating;
235
248
  private authPromise;
236
- private requestQueue;
237
249
  private hasValidated;
238
250
  private debug;
239
251
  private circuitBreaker;
@@ -244,26 +256,22 @@ declare class PathaoApiService {
244
256
  private validateConfiguration;
245
257
  private ensureAuthenticated;
246
258
  private performAuthentication;
247
- private processRequestQueue;
248
259
  private authenticate;
249
260
  private refreshAccessToken;
250
261
  private getErrorMessage;
251
262
  private toPathaoApiError;
252
263
  private handleCircuitBreaker;
264
+ private delay;
253
265
  createOrder(orderData: PathaoOrderRequest): Promise<PathaoOrderResponse>;
254
266
  createStore(storeData: PathaoStoreRequest): Promise<PathaoStoreCreateResponse>;
255
- getStores(): Promise<PathaoStoreListResponse>;
267
+ getStores(page?: number): Promise<PathaoStoreListResponse>;
268
+ getStoresAll(): Promise<PathaoStore[]>;
256
269
  calculatePrice(priceData: PathaoPriceRequest): Promise<PathaoPriceResponse>;
257
270
  getCities(): Promise<PathaoCityResponse>;
258
271
  getZones(cityId: number): Promise<PathaoZoneResponse>;
259
272
  getAreas(zoneId: number): Promise<PathaoAreaResponse>;
260
273
  getOrderStatus(consignmentId: string): Promise<PathaoOrderStatusResponse>;
261
- createBulkOrder(orders: PathaoOrderRequest[]): Promise<{
262
- message: string;
263
- type: string;
264
- code: number;
265
- data: boolean;
266
- }>;
274
+ createBulkOrder(orders: PathaoOrderRequest[]): Promise<PathaoBulkOrderResponse>;
267
275
  static validatePhoneNumber(phone: string): boolean;
268
276
  static formatPhoneNumber(phone: string): string;
269
277
  static validateAddress(address: string): boolean;
@@ -282,6 +290,14 @@ declare class PathaoApiService {
282
290
  debug?: boolean;
283
291
  circuitBreaker?: CircuitBreakerConfig;
284
292
  }): PathaoApiService;
293
+ static sandbox(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
294
+ debug?: boolean;
295
+ circuitBreaker?: CircuitBreakerConfig;
296
+ }): PathaoApiService;
297
+ static production(credentials: Omit<PathaoConfig, 'baseURL'>, options?: {
298
+ debug?: boolean;
299
+ circuitBreaker?: CircuitBreakerConfig;
300
+ }): PathaoApiService;
285
301
  }
286
302
 
287
- export { DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };
303
+ export { type CircuitBreakerConfig, DeliveryType, ItemType, PathaoApiError, PathaoApiService, type PathaoAreaResponse, type PathaoAuthResponse, type PathaoBulkOrderResponse, type PathaoCityResponse, type PathaoConfig, type PathaoError, type PathaoOrderRequest, type PathaoOrderResponse, type PathaoOrderStatusResponse, type PathaoPriceRequest, type PathaoPriceResponse, type PathaoStore, type PathaoStoreCreateResponse, type PathaoStoreListResponse, type PathaoStoreRequest, type PathaoZoneResponse };