@waffo/waffo-node 2.0.2 → 2.0.4

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
@@ -1,580 +1,1261 @@
1
- # Waffo PSP SDK for Node.js
1
+ # Waffo Node.js SDK
2
+
3
+ <!-- Synced with waffo-sdk/README.md @ commit 9971ef7 -->
4
+
5
+ **English** | [中文](README_CN.md)
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@waffo/waffo-node.svg)](https://www.npmjs.com/package/@waffo/waffo-node)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
+ [![Node.js Version](https://img.shields.io/node/v/@waffo/waffo-node.svg)](https://nodejs.org)
10
+
11
+ Official Node.js/TypeScript SDK for [Waffo Payment Platform](https://www.waffo.com), providing one-stop global payment solutions for AI products, SaaS services, and more.
12
+
13
+ ## Introduction
14
+
15
+ ### Core Features
16
+
17
+ - **Global Payments**: Support for credit cards, debit cards, e-wallets, virtual accounts, and more payment methods covering mainstream global payment channels
18
+ - **Subscription Management**: Complete subscription lifecycle management with trial periods, recurring billing, and subscription upgrades/downgrades
19
+ - **Refund Processing**: Flexible full/partial refund capabilities with refund status tracking
20
+ - **Webhook Notifications**: Real-time payment result push notifications for payments, refunds, subscription status changes, and more
21
+ - **Security & Reliability**: PCI DSS certified, RSA signature verification, enforced TLS 1.2+ encryption
22
+
23
+ ### Use Cases
24
+
25
+ | Scenario | Description |
26
+ |----------|-------------|
27
+ | **AI Products** | ChatGPT-like applications, AI writing tools, AI image generation with usage-based billing or subscriptions |
28
+ | **SaaS Services** | Enterprise software subscriptions, online collaboration tools, cloud services with periodic payments |
29
+ | **Content Platforms** | Membership subscriptions, paid content, tipping scenarios |
30
+
31
+ ## Table of Contents
32
+
33
+ - [Requirements](#requirements)
34
+ - [Installation](#installation)
35
+ - [Quick Start](#quick-start)
36
+ - [Configuration](#configuration)
37
+ - [Framework Integration](#framework-integration)
38
+ - [API Usage](#api-usage)
39
+ - [Order Management](#order-management)
40
+ - [Subscription Management](#subscription-management)
41
+ - [Refund Query](#refund-query)
42
+ - [Merchant Configuration](#merchant-configuration)
43
+ - [Webhook Handling](#webhook-handling)
44
+ - [Payment Method Types](#payment-method-types)
45
+ - [Advanced Configuration](#advanced-configuration)
46
+ - [Custom HTTP Transport](#custom-http-transport-axios)
47
+ - [TLS Security Configuration](#tls-security-configuration)
48
+ - [Debug Logging](#debug-logging)
49
+ - [Error Handling](#error-handling)
50
+ - [TypeScript Support](#typescript-support)
51
+ - [Development & Testing](#development--testing)
52
+ - [Support](#support)
53
+ - [License](#license)
54
+
55
+ ## Requirements
56
+
57
+ - Node.js >= 18.0.0
58
+ - npm, yarn, or pnpm
59
+
60
+ ### Version Compatibility
61
+
62
+ | Node.js Version | Support Status |
63
+ |-----------------|----------------|
64
+ | 22.x | ✅ Fully Supported |
65
+ | 20.x LTS | ✅ Fully Supported (Recommended) |
66
+ | 18.x LTS | ✅ Fully Supported |
67
+ | < 18.x | ❌ Not Supported |
2
68
 
3
- Official Node.js SDK for Waffo PSP (Payment Service Provider) acquiring services. This SDK provides secure API communication with RSA signing and comprehensive type definitions for payment acquiring operations.
69
+ ## Installation
4
70
 
5
- **Language**: [English](./README.md) | [中文](./README.zh-CN.md) | [日本語](./README.ja.md)
71
+ ```bash
72
+ npm install @waffo/waffo-node
73
+ # or
74
+ yarn add @waffo/waffo-node
75
+ # or
76
+ pnpm add @waffo/waffo-node
77
+ ```
6
78
 
7
79
  ## Quick Start
8
80
 
81
+ ### 1. Initialize the SDK
82
+
9
83
  ```typescript
10
- import {
11
- Waffo,
12
- Environment,
13
- CurrencyCode,
14
- ProductName,
15
- } from '@waffo/waffo-node';
84
+ import { Waffo, Environment } from '@waffo/waffo-node';
16
85
 
17
- // 1. Initialize SDK
18
86
  const waffo = new Waffo({
19
87
  apiKey: 'your-api-key',
20
88
  privateKey: 'your-base64-encoded-private-key',
21
- environment: Environment.SANDBOX,
89
+ waffoPublicKey: 'waffo-public-key', // From Waffo Dashboard
90
+ merchantId: 'your-merchant-id', // Auto-injected into requests
91
+ environment: Environment.SANDBOX, // SANDBOX or PRODUCTION
22
92
  });
93
+ ```
23
94
 
24
- // 2. Create an order
25
- const result = await waffo.order.create({
26
- paymentRequestId: 'REQ_001',
27
- merchantOrderId: 'ORDER_001',
28
- orderCurrency: CurrencyCode.IDR,
29
- orderAmount: '100000',
30
- orderDescription: 'Product purchase',
31
- notifyUrl: 'https://merchant.com/notify',
32
- merchantInfo: { merchantId: 'your-merchant-id' },
95
+ ### 2. Create a Payment Order
96
+
97
+ ```typescript
98
+ import { randomUUID } from 'crypto';
99
+
100
+ // Generate idempotency key (max 32 chars)
101
+ const paymentRequestId = randomUUID().replace(/-/g, '');
102
+ // Important: Persist this ID to database for retry and query
103
+
104
+ const response = await waffo.order().create({
105
+ paymentRequestId,
106
+ merchantOrderId: `ORDER_${Date.now()}`,
107
+ orderCurrency: 'HKD',
108
+ orderAmount: '100.00',
109
+ orderDescription: 'Test Product',
110
+ notifyUrl: 'https://your-site.com/webhook',
33
111
  userInfo: {
34
- userId: 'user_001',
112
+ userId: 'user_123',
35
113
  userEmail: 'user@example.com',
114
+ userTerminal: 'WEB',
36
115
  },
37
116
  paymentInfo: {
38
- productName: ProductName.ONE_TIME_PAYMENT,
39
- payMethodType: 'EWALLET',
40
- payMethodName: 'DANA',
117
+ productName: 'ONE_TIME_PAYMENT',
118
+ },
119
+ goodsInfo: {
120
+ goodsUrl: 'https://your-site.com/product/001',
41
121
  },
42
122
  });
43
123
 
44
- // 3. Handle response
45
- if (result.success) {
46
- console.log('Order created:', result.data);
47
- // Redirect user to payment page
48
- if (result.data?.orderAction) {
49
- const action = JSON.parse(result.data.orderAction);
50
- window.location.href = action.webUrl;
51
- }
124
+ if (response.isSuccess()) {
125
+ const data = response.getData();
126
+ console.log('Redirect URL:', data.orderAction);
127
+ console.log('Acquiring Order ID:', data.acquiringOrderId);
128
+ console.log('Order Status:', data.orderStatus);
129
+ } else {
130
+ console.log('Error:', response.getMessage());
52
131
  }
53
132
  ```
54
133
 
55
- > **Tip**: Need to generate a new RSA key pair? Use `Waffo.generateKeyPair()` to create one:
56
- > ```typescript
57
- > const keyPair = Waffo.generateKeyPair();
58
- > console.log(keyPair.privateKey); // Keep this secure, use for SDK initialization
59
- > console.log(keyPair.publicKey); // Share this with Waffo
60
- > ```
61
-
62
- ## Features
63
-
64
- - RSA-2048 request signing and response verification
65
- - Full TypeScript support with comprehensive type definitions
66
- - Zero production dependencies (uses only Node.js built-in `crypto` module)
67
- - Support for Sandbox and Production environments
68
- - Dual ESM/CommonJS module support
69
- - Order management (create, query, cancel, refund, capture)
70
- - Subscription management (create, query, cancel, manage)
71
- - Refund status inquiry
72
- - Merchant configuration inquiry (transaction limits, daily limits)
73
- - Payment method configuration inquiry (availability, maintenance schedules)
74
- - Webhook handler with automatic signature verification and event routing
75
- - Webhook signature verification utilities
76
- - Direct HTTP client access for custom API requests
77
- - Automatic timestamp defaults for request parameters
78
-
79
- ## Automatic Timestamp Defaults
80
-
81
- All timestamp parameters (`orderRequestedAt`, `requestedAt`, `captureRequestedAt`) are **optional** and will automatically default to the current time (`new Date().toISOString()`) if not provided:
82
-
83
- ```typescript
84
- // Timestamp is automatically set to current time
85
- await waffo.order.create({
86
- paymentRequestId: 'REQ_001',
87
- merchantOrderId: 'ORDER_001',
88
- // ... other required fields
89
- // orderRequestedAt is automatically set
90
- });
134
+ ### 3. Query Order Status
91
135
 
92
- // Or explicitly provide a custom timestamp
93
- await waffo.order.create({
94
- paymentRequestId: 'REQ_001',
95
- merchantOrderId: 'ORDER_001',
96
- orderRequestedAt: '2025-01-01T00:00:00.000Z', // Custom timestamp
97
- // ... other required fields
136
+ ```typescript
137
+ const response = await waffo.order().inquiry({
138
+ acquiringOrderId: 'acquiring_order_id',
98
139
  });
140
+
141
+ if (response.isSuccess()) {
142
+ const data = response.getData();
143
+ console.log('Order Status:', data.orderStatus);
144
+ console.log('Merchant Order ID:', data.merchantOrderId);
145
+ console.log('Order Amount:', data.orderAmount);
146
+ console.log('Order Currency:', data.orderCurrency);
147
+ // Tip: Verify amount and currency match your records
148
+ }
99
149
  ```
100
150
 
101
- This applies to:
102
- - `CreateOrderParams.orderRequestedAt`
103
- - `CancelOrderParams.orderRequestedAt`
104
- - `RefundOrderParams.requestedAt`
105
- - `CaptureOrderParams.captureRequestedAt`
106
- - `CreateSubscriptionParams.requestedAt`
107
- - `CancelSubscriptionParams.requestedAt`
151
+ ## Configuration
108
152
 
109
- ## Installation
153
+ ### Full Configuration Options
154
+
155
+ ```typescript
156
+ import { Waffo, Environment } from '@waffo/waffo-node';
157
+
158
+ const waffo = new Waffo({
159
+ // Required
160
+ apiKey: 'your-api-key', // API Key
161
+ privateKey: 'your-base64-private-key', // Base64 encoded merchant private key
162
+ waffoPublicKey: 'waffo-public-key', // Waffo public key (from Dashboard)
163
+ environment: Environment.SANDBOX, // SANDBOX or PRODUCTION (required)
164
+
165
+ // Optional
166
+ merchantId: 'your-merchant-id', // Default merchant ID (auto-injected)
167
+ connectTimeout: 10000, // Connection timeout in ms (default: 10000)
168
+ readTimeout: 30000, // Read timeout in ms (default: 30000)
169
+ logger: console, // Custom logger
170
+ httpTransport: customTransport, // Custom HTTP transport
171
+ });
172
+ ```
173
+
174
+ ### Environment Variables
110
175
 
111
176
  ```bash
112
- npm install @waffo/waffo-node
177
+ # Set environment variables
178
+ export WAFFO_API_KEY=your-api-key
179
+ export WAFFO_PRIVATE_KEY=your-private-key
180
+ export WAFFO_PUBLIC_KEY=waffo-public-key # Waffo public key
181
+ export WAFFO_ENVIRONMENT=SANDBOX # Required: SANDBOX or PRODUCTION
182
+ export WAFFO_MERCHANT_ID=your-merchant-id # Optional
183
+ ```
184
+
185
+ ```typescript
186
+ import { Waffo } from '@waffo/waffo-node';
187
+
188
+ const waffo = Waffo.fromEnv();
113
189
  ```
114
190
 
115
- ## Usage
191
+ ### Environment URLs
116
192
 
117
- ### Initialize the SDK
193
+ | Environment | Base URL | Description |
194
+ |-------------|----------|-------------|
195
+ | `SANDBOX` | `https://api-sandbox.waffo.com` | Test environment |
196
+ | `PRODUCTION` | `https://api.waffo.com` | Production environment |
197
+
198
+ > **Important**: Environment must be explicitly specified. SDKs do not default to any environment to prevent accidental requests to wrong environments.
199
+
200
+ ### Request-Level Configuration
118
201
 
119
202
  ```typescript
203
+ const response = await waffo.order().create(params, {
204
+ connectTimeout: 10000,
205
+ readTimeout: 30000,
206
+ });
207
+ ```
208
+
209
+ ### Framework Integration
210
+
211
+ #### Express Integration
212
+
213
+ ```typescript
214
+ import express from 'express';
120
215
  import { Waffo, Environment } from '@waffo/waffo-node';
121
216
 
217
+ const app = express();
122
218
  const waffo = new Waffo({
123
- apiKey: 'your-api-key',
124
- privateKey: 'your-base64-encoded-private-key',
125
- environment: Environment.SANDBOX, // or Environment.PRODUCTION
219
+ apiKey: process.env.WAFFO_API_KEY!,
220
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
221
+ waffoPublicKey: process.env.WAFFO_PUBLIC_KEY!,
222
+ environment: Environment.SANDBOX,
126
223
  });
224
+
225
+ // Webhook endpoint
226
+ app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
227
+ const body = req.body.toString();
228
+ const signature = req.headers['x-signature'] as string;
229
+
230
+ const webhookHandler = waffo.webhook()
231
+ .onPayment((notification) => {
232
+ console.log('Payment received:', notification.acquiringOrderId);
233
+ });
234
+
235
+ const result = await webhookHandler.handleWebhook(body, signature);
236
+ res.setHeader('X-SIGNATURE', result.responseSignature);
237
+ res.status(200).json(result.responseBody);
238
+ });
239
+
240
+ app.listen(3000);
127
241
  ```
128
242
 
129
- ### Generate RSA Key Pair
243
+ #### NestJS Integration
130
244
 
131
245
  ```typescript
246
+ // waffo.module.ts
247
+ import { Module, Global } from '@nestjs/common';
248
+ import { Waffo, Environment } from '@waffo/waffo-node';
249
+
250
+ @Global()
251
+ @Module({
252
+ providers: [
253
+ {
254
+ provide: 'WAFFO',
255
+ useFactory: () => {
256
+ return new Waffo({
257
+ apiKey: process.env.WAFFO_API_KEY!,
258
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
259
+ waffoPublicKey: process.env.WAFFO_PUBLIC_KEY!,
260
+ environment: Environment.SANDBOX,
261
+ });
262
+ },
263
+ },
264
+ ],
265
+ exports: ['WAFFO'],
266
+ })
267
+ export class WaffoModule {}
268
+
269
+ // payment.controller.ts
270
+ import { Controller, Post, Body, Headers, Inject, Res } from '@nestjs/common';
132
271
  import { Waffo } from '@waffo/waffo-node';
272
+ import { Response } from 'express';
273
+
274
+ @Controller('payment')
275
+ export class PaymentController {
276
+ constructor(@Inject('WAFFO') private readonly waffo: Waffo) {}
277
+
278
+ @Post('webhook')
279
+ async handleWebhook(
280
+ @Body() body: string,
281
+ @Headers('x-signature') signature: string,
282
+ @Res() res: Response,
283
+ ) {
284
+ const webhookHandler = this.waffo.webhook()
285
+ .onPayment((notification) => {
286
+ console.log('Payment received:', notification.acquiringOrderId);
287
+ });
288
+
289
+ const result = await webhookHandler.handleWebhook(body, signature);
290
+ res.setHeader('X-SIGNATURE', result.responseSignature);
291
+ res.status(200).json(result.responseBody);
292
+ }
293
+ }
294
+ ```
295
+
296
+ #### Fastify Integration
133
297
 
134
- const keyPair = Waffo.generateKeyPair();
135
- console.log(keyPair.privateKey); // Base64 encoded PKCS8 private key
136
- console.log(keyPair.publicKey); // Base64 encoded X509 public key
298
+ ```typescript
299
+ import Fastify from 'fastify';
300
+ import { Waffo, Environment } from '@waffo/waffo-node';
301
+
302
+ const fastify = Fastify();
303
+ const waffo = new Waffo({
304
+ apiKey: process.env.WAFFO_API_KEY!,
305
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
306
+ waffoPublicKey: process.env.WAFFO_PUBLIC_KEY!,
307
+ environment: Environment.SANDBOX,
308
+ });
309
+
310
+ // Register raw body parser for webhooks
311
+ fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
312
+ done(null, body);
313
+ });
314
+
315
+ fastify.post('/webhook', async (request, reply) => {
316
+ const body = request.body as string;
317
+ const signature = request.headers['x-signature'] as string;
318
+
319
+ const webhookHandler = waffo.webhook()
320
+ .onPayment((notification) => {
321
+ console.log('Payment received:', notification.acquiringOrderId);
322
+ });
323
+
324
+ const result = await webhookHandler.handleWebhook(body, signature);
325
+ reply.header('X-SIGNATURE', result.responseSignature);
326
+ return result.responseBody;
327
+ });
328
+
329
+ fastify.listen({ port: 3000 });
137
330
  ```
138
331
 
139
- ### Create an Order
332
+ ## API Usage
333
+
334
+ ### Order Management
335
+
336
+ #### Create Order
140
337
 
141
338
  ```typescript
142
- const result = await waffo.order.create({
143
- paymentRequestId: 'REQ_001',
144
- merchantOrderId: 'ORDER_001',
145
- orderCurrency: CurrencyCode.IDR,
146
- orderAmount: '100000',
147
- orderDescription: 'Product purchase',
148
- notifyUrl: 'https://merchant.com/notify',
339
+ import { randomUUID } from 'crypto';
340
+
341
+ const response = await waffo.order().create({
342
+ paymentRequestId: randomUUID().replace(/-/g, ''), // Idempotency key, persist it
343
+ merchantOrderId: `ORDER_${Date.now()}`,
344
+ orderCurrency: 'BRL', // Brazilian Real
345
+ orderAmount: '100.00',
346
+ orderDescription: 'Product Name',
347
+ orderRequestedAt: new Date().toISOString(),
149
348
  merchantInfo: {
150
349
  merchantId: 'your-merchant-id',
151
350
  },
152
351
  userInfo: {
153
- userId: 'user_001',
352
+ userId: 'user_123',
154
353
  userEmail: 'user@example.com',
155
- userPhone: '+62-81234567890',
156
- userTerminal: UserTerminalType.WEB,
354
+ userTerminal: 'WEB',
157
355
  },
158
356
  paymentInfo: {
159
- productName: ProductName.ONE_TIME_PAYMENT,
160
- payMethodType: 'EWALLET',
161
- payMethodName: 'DANA',
357
+ productName: 'ONE_TIME_PAYMENT',
358
+ payMethodType: 'CREDITCARD', // CREDITCARD, DEBITCARD, EWALLET, VA, etc.
359
+ // payMethodName: 'CC_VISA', // Optional: specify exact payment method
162
360
  },
361
+ goodsInfo: {
362
+ goodsUrl: 'https://your-site.com/product/001',
363
+ },
364
+ notifyUrl: 'https://your-site.com/webhook',
365
+ successRedirectUrl: 'https://your-site.com/success',
366
+ failedRedirectUrl: 'https://your-site.com/failed',
367
+ cancelRedirectUrl: 'https://your-site.com/cancel',
163
368
  });
164
369
 
165
- if (result.success) {
166
- console.log('Order created:', result.data);
167
- } else {
168
- console.error('Error:', result.error);
370
+ if (response.isSuccess()) {
371
+ const data = response.getData();
372
+ console.log('Checkout URL:', data.orderAction);
169
373
  }
170
374
  ```
171
375
 
172
- ### Query Order Status
376
+ #### Combine Multiple Payment Methods
173
377
 
174
378
  ```typescript
175
- const result = await waffo.order.inquiry({
176
- acquiringOrderId: 'A202512230000001',
177
- // or use paymentRequestId: 'REQ_001'
178
- });
179
-
180
- if (result.success) {
181
- console.log('Order status:', result.data?.orderStatus);
379
+ // Allow user to choose between credit card or debit card
380
+ paymentInfo: {
381
+ productName: 'ONE_TIME_PAYMENT',
382
+ payMethodType: 'CREDITCARD,DEBITCARD', // Comma-separated for multiple types
182
383
  }
183
384
  ```
184
385
 
185
- ### Cancel an Order
386
+ #### Query Order
186
387
 
187
388
  ```typescript
188
- const result = await waffo.order.cancel({
189
- acquiringOrderId: 'A202512230000001',
190
- merchantId: 'your-merchant-id',
191
- // orderRequestedAt is optional, defaults to current time
389
+ const response = await waffo.order().inquiry({
390
+ acquiringOrderId: 'acquiring_order_id',
192
391
  });
193
392
  ```
194
393
 
195
- ### Refund an Order
394
+ #### Cancel Order
196
395
 
197
396
  ```typescript
198
- const result = await waffo.order.refund({
199
- refundRequestId: 'REFUND_001',
200
- acquiringOrderId: 'A202512230000001',
201
- merchantId: 'your-merchant-id',
202
- refundAmount: '50000',
203
- refundReason: 'Customer requested refund',
204
- refundNotifyUrl: 'https://merchant.com/refund-notify',
205
- // requestedAt is optional, defaults to current time
397
+ const response = await waffo.order().cancel({
398
+ acquiringOrderId: 'acquiring_order_id',
399
+ orderRequestedAt: new Date().toISOString(),
206
400
  });
207
401
  ```
208
402
 
209
- ### Query Refund Status
403
+ #### Refund Order
210
404
 
211
405
  ```typescript
212
- const result = await waffo.refund.inquiry({
213
- refundRequestId: 'REFUND_001',
214
- // or use acquiringRefundOrderId: 'R202512230000001'
215
- });
406
+ import { randomUUID } from 'crypto';
216
407
 
217
- if (result.success) {
218
- console.log('Refund status:', result.data?.refundStatus);
219
- }
408
+ // Generate idempotency key (max 32 chars)
409
+ const refundRequestId = randomUUID().replace(/-/g, '');
410
+ // Important: Persist this ID for retry and query
411
+
412
+ const response = await waffo.order().refund({
413
+ refundRequestId,
414
+ acquiringOrderId: 'acquiring_order_id',
415
+ refundAmount: '50.00',
416
+ refundReason: 'Customer requested refund',
417
+ });
220
418
  ```
221
419
 
222
- ### Capture a Pre-authorized Payment
420
+ #### Capture Order
223
421
 
224
422
  ```typescript
225
- const result = await waffo.order.capture({
226
- acquiringOrderId: 'A202512230000001',
227
- merchantId: 'your-merchant-id',
228
- captureAmount: '100000',
229
- // captureRequestedAt is optional, defaults to current time
423
+ // For pre-authorized payments
424
+ const response = await waffo.order().capture({
425
+ paymentRequestId: 'unique-request-id',
426
+ merchantId: 'merchant-123',
427
+ captureAmount: '10.00',
230
428
  });
231
429
  ```
232
430
 
233
- ### Create a Subscription
431
+ ### Subscription Management
432
+
433
+ #### Create Subscription
234
434
 
235
435
  ```typescript
236
- const result = await waffo.subscription.create({
237
- subscriptionRequest: 'SUB_REQ_001',
238
- merchantSubscriptionId: 'MERCHANT_SUB_001',
239
- currency: CurrencyCode.PHP,
240
- amount: '100',
436
+ import { randomUUID } from 'crypto';
437
+
438
+ const subscriptionRequest = randomUUID().replace(/-/g, '');
439
+
440
+ const response = await waffo.subscription().create({
441
+ subscriptionRequest,
442
+ merchantSubscriptionId: `MSUB_${Date.now()}`,
443
+ currency: 'HKD',
444
+ amount: '99.00',
445
+ payMethodType: 'CREDITCARD,DEBITCARD,APPLEPAY,GOOGLEPAY',
241
446
  productInfo: {
242
- periodType: PeriodType.MONTHLY,
447
+ description: 'Monthly Subscription',
448
+ periodType: 'MONTHLY',
243
449
  periodInterval: '1',
244
- numberOfPeriod: '12',
245
- description: 'Monthly subscription',
246
450
  },
247
- paymentInfo: {
248
- productName: ProductName.SUBSCRIPTION,
249
- payMethodType: 'EWALLET',
250
- payMethodName: 'GCASH',
251
- },
252
- merchantInfo: { merchantId: 'your-merchant-id' },
253
451
  userInfo: {
254
- userId: 'user_001',
452
+ userId: 'user_123',
255
453
  userEmail: 'user@example.com',
256
454
  },
257
- goodsInfo: {
258
- goodsId: 'GOODS_001',
259
- goodsName: 'Premium Plan',
260
- },
261
- notifyUrl: 'https://merchant.com/subscription/notify',
262
- // requestedAt is optional, defaults to current time
455
+ requestedAt: new Date().toISOString(),
456
+ successRedirectUrl: 'https://your-site.com/subscription/success',
457
+ failedRedirectUrl: 'https://your-site.com/subscription/failed',
458
+ cancelRedirectUrl: 'https://your-site.com/subscription/cancel',
459
+ notifyUrl: 'https://your-site.com/webhook/subscription',
460
+ subscriptionManagementUrl: 'https://your-site.com/subscription/manage',
263
461
  });
264
462
 
265
- if (result.success) {
266
- console.log('Subscription created:', result.data);
267
- // Redirect user to complete subscription signing
268
- if (result.data?.subscriptionAction?.webUrl) {
269
- window.location.href = result.data.subscriptionAction.webUrl;
270
- }
463
+ if (response.isSuccess()) {
464
+ const data = response.getData();
465
+ console.log('Waffo Subscription ID:', data.subscriptionId);
466
+ console.log('Status:', data.subscriptionStatus);
467
+ console.log('Action:', data.subscriptionAction);
271
468
  }
272
469
  ```
273
470
 
274
- ### Query Subscription Status
471
+ #### Subscription with Trial Period
275
472
 
276
473
  ```typescript
277
- const result = await waffo.subscription.inquiry({
278
- merchantId: 'your-merchant-id',
279
- subscriptionId: 'SUB_202512230000001',
280
- paymentDetails: '1', // Include payment history
474
+ productInfo: {
475
+ description: 'Monthly subscription with 7-day free trial',
476
+ periodType: 'MONTHLY',
477
+ periodInterval: '1',
478
+ numberOfPeriod: '12',
479
+ // Trial period configuration
480
+ trialPeriodType: 'DAILY',
481
+ trialPeriodInterval: '7',
482
+ trialPeriodAmount: '0', // Free trial
483
+ numberOfTrialPeriod: '1',
484
+ }
485
+ ```
486
+
487
+ #### Query Subscription
488
+
489
+ ```typescript
490
+ // Query by subscriptionId
491
+ const response = await waffo.subscription().inquiry({
492
+ subscriptionId: 'subscription_id',
493
+ paymentDetails: 1, // 1: include payment details, 0: exclude
281
494
  });
282
495
 
283
- if (result.success) {
284
- console.log('Subscription status:', result.data?.subscriptionStatus);
285
- }
496
+ // Or query by subscriptionRequest
497
+ const response = await waffo.subscription().inquiry({
498
+ subscriptionRequest: 'subscription_request',
499
+ });
286
500
  ```
287
501
 
288
- ### Cancel a Subscription
502
+ #### Cancel Subscription
289
503
 
290
504
  ```typescript
291
- const result = await waffo.subscription.cancel({
292
- merchantId: 'your-merchant-id',
293
- subscriptionId: 'SUB_202512230000001',
294
- // requestedAt is optional, defaults to current time
505
+ const response = await waffo.subscription().cancel({
506
+ subscriptionId: 'subscription_id',
295
507
  });
296
508
  ```
297
509
 
298
- ### Get Subscription Management URL
510
+ #### Get Subscription Management URL
299
511
 
300
512
  ```typescript
301
- const result = await waffo.subscription.manage({
302
- subscriptionId: 'SUB_202512230000001',
303
- // or use subscriptionRequest: 'SUB_REQ_001'
513
+ const response = await waffo.subscription().manage({
514
+ subscriptionId: 'subscription_id',
304
515
  });
305
516
 
306
- if (result.success) {
307
- console.log('Management URL:', result.data?.managementUrl);
308
- console.log('Expires at:', result.data?.expiresAt);
309
- // Redirect user to manage their subscription
310
- window.location.href = result.data?.managementUrl;
517
+ if (response.isSuccess()) {
518
+ const managementUrl = response.getData().managementUrl;
519
+ // Redirect user to this URL to manage subscription
311
520
  }
312
521
  ```
313
522
 
314
- ### Query Merchant Configuration
523
+ ### Subscription Change (Upgrade/Downgrade)
524
+
525
+ Change an existing subscription to a new plan (upgrade or downgrade).
526
+
527
+ #### Change Subscription
528
+
529
+ ```typescript
530
+ import { randomUUID } from 'crypto';
531
+ import { WaffoUnknownStatusError } from '@waffo/waffo-node';
532
+
533
+ // New subscription request ID for the change
534
+ const subscriptionRequest = randomUUID().replace(/-/g, '');
535
+ const originSubscriptionRequest = 'original-subscription-request-id';
536
+
537
+ try {
538
+ const response = await waffo.subscription().change({
539
+ subscriptionRequest,
540
+ originSubscriptionRequest,
541
+ remainingAmount: '50.00', // Remaining value from original subscription
542
+ currency: 'HKD',
543
+ requestedAt: new Date().toISOString(),
544
+ notifyUrl: 'https://your-site.com/webhook/subscription',
545
+ productInfoList: [
546
+ {
547
+ description: 'Yearly Premium Subscription',
548
+ periodType: 'YEAR',
549
+ periodInterval: '1',
550
+ amount: '999.00',
551
+ },
552
+ ],
553
+ userInfo: {
554
+ userId: 'user_123',
555
+ userEmail: 'user@example.com',
556
+ },
557
+ goodsInfo: {
558
+ goodsId: 'GOODS_PREMIUM',
559
+ goodsName: 'Premium Plan',
560
+ },
561
+ paymentInfo: {
562
+ productName: 'SUBSCRIPTION',
563
+ },
564
+ // Optional fields
565
+ merchantSubscriptionId: `MSUB_UPGRADE_${Date.now()}`,
566
+ successRedirectUrl: 'https://your-site.com/subscription/upgrade/success',
567
+ failedRedirectUrl: 'https://your-site.com/subscription/upgrade/failed',
568
+ cancelRedirectUrl: 'https://your-site.com/subscription/upgrade/cancel',
569
+ subscriptionManagementUrl: 'https://your-site.com/subscription/manage',
570
+ });
571
+
572
+ if (response.isSuccess()) {
573
+ const data = response.getData();
574
+ console.log('Change Status:', data.subscriptionChangeStatus);
575
+ console.log('New Subscription ID:', data.subscriptionId);
576
+
577
+ // Handle different statuses
578
+ if (data.subscriptionChangeStatus === 'AUTHORIZATION_REQUIRED') {
579
+ // User needs to authorize the change
580
+ const action = JSON.parse(data.subscriptionAction);
581
+ console.log('Redirect user to:', action.webUrl);
582
+ } else if (data.subscriptionChangeStatus === 'SUCCESS') {
583
+ // Change completed successfully
584
+ console.log('Subscription upgraded successfully');
585
+ }
586
+ }
587
+ } catch (error) {
588
+ if (error instanceof WaffoUnknownStatusError) {
589
+ // Status unknown - DO NOT assume failure! User may have completed payment
590
+ console.error('Unknown status, need to query:', error.message);
591
+
592
+ // Correct handling: Call inquiry API to confirm actual status
593
+ const inquiryResponse = await waffo.subscription().changeInquiry({
594
+ subscriptionRequest,
595
+ originSubscriptionRequest,
596
+ });
597
+ // Or wait for Webhook callback
598
+ } else {
599
+ throw error;
600
+ }
601
+ }
602
+ ```
603
+
604
+ #### Subscription Change Status Values
605
+
606
+ | Status | Description |
607
+ |--------|-------------|
608
+ | `IN_PROGRESS` | Change is being processed |
609
+ | `AUTHORIZATION_REQUIRED` | User needs to authorize the change (redirect to webUrl) |
610
+ | `SUCCESS` | Change completed successfully |
611
+ | `CLOSED` | Change was closed (timeout or failed) |
612
+
613
+ #### Query Subscription Change Status
315
614
 
316
615
  ```typescript
317
- const result = await waffo.merchantConfig.inquiry({
318
- merchantId: 'your-merchant-id',
616
+ const response = await waffo.subscription().changeInquiry({
617
+ subscriptionRequest: 'new-subscription-request-id',
618
+ originSubscriptionRequest: 'original-subscription-request-id',
319
619
  });
320
620
 
321
- if (result.success) {
322
- console.log('Daily Limit:', result.data?.totalDailyLimit);
323
- console.log('Remaining Daily Limit:', result.data?.remainingDailyLimit);
324
- console.log('Transaction Limit:', result.data?.transactionLimit);
621
+ if (response.isSuccess()) {
622
+ const data = response.getData();
623
+ console.log('Change Status:', data.subscriptionChangeStatus);
624
+ console.log('New Subscription ID:', data.subscriptionId);
625
+ console.log('Remaining Amount:', data.remainingAmount);
626
+ console.log('Currency:', data.currency);
325
627
  }
326
628
  ```
327
629
 
328
- ### Query Payment Method Configuration
630
+ ### Refund Query
329
631
 
330
632
  ```typescript
331
- const result = await waffo.payMethodConfig.inquiry({
332
- merchantId: 'your-merchant-id',
633
+ // Query by refundRequestId (merchant-generated idempotency key)
634
+ const response = await waffo.refund().inquiry({
635
+ refundRequestId: 'refund_request_id',
333
636
  });
334
637
 
335
- if (result.success) {
336
- result.data?.payMethodDetails.forEach(method => {
337
- console.log(`${method.payMethodName}: ${method.currentStatus === '1' ? 'Available' : 'Unavailable'}`);
338
- if (method.fixedMaintenanceRules) {
339
- console.log('Maintenance periods:', method.fixedMaintenanceRules);
340
- }
341
- });
638
+ // Or query by acquiringRefundOrderId (Waffo refund order ID)
639
+ const response = await waffo.refund().inquiry({
640
+ acquiringRefundOrderId: 'acquiring_refund_order_id',
641
+ });
642
+ ```
643
+
644
+ ### Merchant Configuration
645
+
646
+ #### Query Merchant Configuration
647
+
648
+ ```typescript
649
+ // merchantId is auto-injected from config if not provided
650
+ const response = await waffo.merchantConfig().inquiry({});
651
+
652
+ if (response.isSuccess()) {
653
+ const data = response.getData();
654
+ console.log('Daily Limit:', data.totalDailyLimit);
655
+ console.log('Remaining Daily Limit:', data.remainingDailyLimit);
656
+ console.log('Transaction Limit:', data.transactionLimit);
657
+ }
658
+ ```
659
+
660
+ #### Query Available Payment Methods
661
+
662
+ ```typescript
663
+ // merchantId is auto-injected from config if not provided
664
+ const response = await waffo.payMethodConfig().inquiry({});
665
+
666
+ if (response.isSuccess()) {
667
+ const data = response.getData();
668
+ for (const detail of data.payMethodDetails) {
669
+ console.log(`Payment Method: ${detail.payMethodName}, Country: ${detail.country}, Status: ${detail.currentStatus}`);
670
+ }
342
671
  }
343
672
  ```
344
673
 
345
- ### Webhook Handler
674
+ ## Webhook Handling
346
675
 
347
- The SDK provides a built-in webhook handler that automatically handles signature verification, event routing, and response signing:
676
+ Waffo pushes payment results, refund results, subscription status changes, and more via webhooks.
677
+
678
+ ### Webhook Handler Example
348
679
 
349
680
  ```typescript
350
- // In your webhook handler
351
- app.post('/webhook', async (req, res) => {
352
- const signature = req.headers['x-signature'] as string;
353
- const body = JSON.stringify(req.body);
681
+ import { Waffo, Environment } from '@waffo/waffo-node';
682
+ import express from 'express';
354
683
 
355
- const result = await waffo.webhook.handle(body, signature, {
356
- onPayment: async ({ notification }) => {
357
- console.log('Payment status:', notification.result.orderStatus);
358
- // Process payment notification
359
- },
360
- onRefund: async ({ notification }) => {
361
- console.log('Refund status:', notification.result.refundStatus);
362
- // Process refund notification
363
- },
364
- onSubscriptionStatus: async ({ notification }) => {
365
- console.log('Subscription status:', notification.result.subscriptionStatus);
366
- // Process subscription status change
367
- },
368
- onSubscriptionPayment: async ({ notification }) => {
369
- console.log('Subscription payment:', notification.result.orderStatus);
370
- // Process subscription recurring payment
371
- },
372
- onError: async (error) => {
373
- console.error('Webhook error:', error.message);
374
- },
684
+ const app = express();
685
+ const waffo = new Waffo({ /* config */ });
686
+
687
+ // Create webhook handler
688
+ const webhookHandler = waffo.webhook()
689
+ .onPayment((notification) => {
690
+ console.log('Payment notification received:');
691
+ console.log(' Acquiring Order ID:', notification.acquiringOrderId);
692
+ console.log(' Order Status:', notification.orderStatus);
693
+ console.log(' Payment Amount:', notification.orderAmount);
694
+ console.log(' Payment Currency:', notification.orderCurrency);
695
+
696
+ // Tip: First verify amount and currency match your records
697
+ // Then handle based on orderStatus
698
+
699
+ if (notification.orderStatus === 'PAY_SUCCESS') {
700
+ // Payment successful - update order status, deliver goods, etc.
701
+ }
702
+ })
703
+ .onRefund((notification) => {
704
+ console.log('Refund notification:', notification.acquiringRefundOrderId);
705
+ // Handle refund notification
706
+ })
707
+ .onSubscriptionStatus((notification) => {
708
+ console.log('Subscription status notification:');
709
+ console.log(' Subscription ID:', notification.subscriptionId);
710
+ console.log(' Subscription Status:', notification.subscriptionStatus);
711
+
712
+ switch (notification.subscriptionStatus) {
713
+ case 'ACTIVE':
714
+ // Subscription activated - grant membership privileges
715
+ break;
716
+ case 'CLOSE':
717
+ // Subscription closed (timeout or failed)
718
+ break;
719
+ case 'MERCHANT_CANCELLED':
720
+ // Merchant cancelled subscription
721
+ break;
722
+ case 'USER_CANCELLED':
723
+ // User cancelled subscription
724
+ break;
725
+ case 'CHANNEL_CANCELLED':
726
+ // Channel cancelled subscription
727
+ break;
728
+ case 'EXPIRED':
729
+ // Subscription expired
730
+ break;
731
+ }
732
+ })
733
+ .onSubscriptionPeriodChanged((notification) => {
734
+ console.log('Subscription period changed:', notification.subscriptionId);
735
+ // Key fields to track:
736
+ // - notification.period: Current period number
737
+ // - notification.nextChargeAt: Next billing time
738
+ // - notification.subscriptionStatus: Subscription status
739
+ // - notification.orderStatus: Current billing order status (SUCCESS/FAILED)
740
+ // - notification.orderAmount: Billing amount
741
+ // - notification.orderCurrency: Billing currency
742
+ })
743
+ .onSubscriptionChange((notification) => {
744
+ console.log('Subscription change notification:');
745
+ console.log(' Change Request ID:', notification.subscriptionRequest);
746
+ console.log(' Change Status:', notification.subscriptionChangeStatus);
747
+ console.log(' Origin Subscription:', notification.originSubscriptionId);
748
+ console.log(' New Subscription:', notification.subscriptionId);
749
+
750
+ if (notification.subscriptionChangeStatus === 'SUCCESS') {
751
+ // Subscription change successful
752
+ // - Original subscription is now MERCHANT_CANCELLED
753
+ // - New subscription is now ACTIVE
754
+ // Update user's subscription level accordingly
755
+ } else if (notification.subscriptionChangeStatus === 'CLOSED') {
756
+ // Subscription change failed/closed
757
+ // Original subscription remains unchanged
758
+ }
375
759
  });
376
760
 
377
- return res.json(result.response);
761
+ // Express route
762
+ app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
763
+ const body = req.body.toString();
764
+ const signature = req.headers['x-signature'] as string;
765
+
766
+ const result = await webhookHandler.handleWebhook(body, signature);
767
+
768
+ res.setHeader('X-SIGNATURE', result.responseSignature);
769
+ res.status(200).json(result.responseBody);
378
770
  });
379
771
  ```
380
772
 
381
- ### Manual Webhook Signature Verification
773
+ ### Webhook Notification Types
774
+
775
+ | Event Type | Handler Method | Description |
776
+ |------------|----------------|-------------|
777
+ | `PAYMENT_NOTIFICATION` | `onPayment()` | Payment result notification (triggered on every payment attempt, including retries) |
778
+ | `REFUND_NOTIFICATION` | `onRefund()` | Refund result notification |
779
+ | `SUBSCRIPTION_STATUS_NOTIFICATION` | `onSubscriptionStatus()` | Subscription status change notification (triggered when subscription main record status changes) |
780
+ | `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | `onSubscriptionPeriodChanged()` | Subscription period change notification (final result of each period) |
781
+ | `SUBSCRIPTION_CHANGE_NOTIFICATION` | `onSubscriptionChange()` | Subscription change (upgrade/downgrade) result notification |
782
+
783
+ ### Subscription Notification Types Explained
784
+
785
+ | Notification Type | Trigger Condition | Scope | Includes Retry Events | Typical Use Case |
786
+ |-------------------|-------------------|-------|----------------------|------------------|
787
+ | `SUBSCRIPTION_STATUS_NOTIFICATION` | Subscription main record status changes | Subscription level | No | Track subscription lifecycle: first payment success activation (ACTIVE), cancellation (MERCHANT_CANCELLED, CHANNEL_CANCELLED), first payment failure close (CLOSE), etc. |
788
+ | `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION` | Subscription period reaches final state | Period level | No (only final result) | Only need final result of each period, no intermediate retry events |
789
+ | `SUBSCRIPTION_CHANGE_NOTIFICATION` | Subscription change (upgrade/downgrade) completes | Change request level | No (only final result) | Track subscription change results: SUCCESS or CLOSED |
790
+ | `PAYMENT_NOTIFICATION` | Every payment order | Payment order level | Yes (includes all retries) | Need complete details of every payment attempt, including failure reasons, timestamps, retry details |
791
+
792
+ > **Selection Guide**:
793
+ > - If you only care about subscription activation/cancellation, use `SUBSCRIPTION_STATUS_NOTIFICATION`
794
+ > - If you only care about final renewal result of each period, use `SUBSCRIPTION_PERIOD_CHANGED_NOTIFICATION`
795
+ > - If you only care about subscription change (upgrade/downgrade) final result, use `SUBSCRIPTION_CHANGE_NOTIFICATION`
796
+ > - If you need to track every payment attempt (including retries), use `PAYMENT_NOTIFICATION`
382
797
 
383
- For more control, you can use the low-level webhook utilities:
798
+ > **Subscription Payment Note**: Each period's payment (including first payment and renewals) triggers `PAYMENT_NOTIFICATION` events. You can get subscription-related info (subscriptionId, period, etc.) from `subscriptionInfo`.
799
+
800
+ > **Subscription Change (Upgrade/Downgrade) Webhook Note**:
801
+ > When a subscription change is processed, the following notifications are triggered:
802
+ > - `SUBSCRIPTION_CHANGE_NOTIFICATION`: When subscription change completes (SUCCESS or CLOSED)
803
+ > - `SUBSCRIPTION_STATUS_NOTIFICATION`: When original subscription status changes to `MERCHANT_CANCELLED`
804
+ > - `SUBSCRIPTION_STATUS_NOTIFICATION`: When new subscription status changes to `ACTIVE`
805
+ > - `PAYMENT_NOTIFICATION`: If upgrade requires additional payment (price difference)
806
+
807
+ ## Payment Method Types
808
+
809
+ ### payMethodType Reference
810
+
811
+ | Type | Description | Example payMethodName |
812
+ |------|-------------|----------------------|
813
+ | `CREDITCARD` | Credit Card | CC_VISA, CC_MASTERCARD, CC_AMEX, CC_JCB, etc. |
814
+ | `DEBITCARD` | Debit Card | DC_VISA, DC_MASTERCARD, DC_ELO, etc. |
815
+ | `EWALLET` | E-Wallet | GCASH, DANA, PROMPTPAY, GRABPAY, etc. |
816
+ | `VA` | Virtual Account | BCA, BNI, BRI, MANDIRI, etc. |
817
+ | `APPLEPAY` | Apple Pay | APPLEPAY |
818
+ | `GOOGLEPAY` | Google Pay | GOOGLEPAY |
819
+
820
+ ### Usage Examples
384
821
 
385
822
  ```typescript
386
- import {
387
- verifyWebhookSignature,
388
- buildSuccessResponse,
389
- buildFailedResponse,
390
- isPaymentNotification,
391
- isRefundNotification,
392
- } from '@waffo/waffo-node';
823
+ // Specify type only, let user choose on checkout page
824
+ paymentInfo: {
825
+ payMethodType: 'CREDITCARD',
826
+ }
393
827
 
394
- // In your webhook handler
395
- app.post('/webhook', (req, res) => {
396
- const signature = req.headers['x-signature'];
397
- const body = JSON.stringify(req.body);
828
+ // Specify exact payment method
829
+ paymentInfo: {
830
+ payMethodType: 'CREDITCARD',
831
+ payMethodName: 'CC_VISA',
832
+ }
398
833
 
399
- // Verify signature
400
- const verifyResult = verifyWebhookSignature(body, signature, waffoPublicKey);
834
+ // Combine multiple types
835
+ paymentInfo: {
836
+ payMethodType: 'CREDITCARD,DEBITCARD',
837
+ }
401
838
 
402
- if (!verifyResult.valid) {
403
- return res.json(buildFailedResponse('Signature verification failed', merchantPrivateKey));
404
- }
839
+ // E-wallet with specific channel
840
+ paymentInfo: {
841
+ payMethodType: 'EWALLET',
842
+ payMethodName: 'GCASH',
843
+ }
844
+ ```
405
845
 
406
- // Handle notification based on type
407
- const notification = verifyResult.notification;
846
+ > **Note**: For available `ProductName`, `PayMethodType`, `PayMethodName` values, merchants can log in to [Waffo Portal](https://dashboard.waffo.com) to view contracted payment methods (Home → Service → Pay-in).
408
847
 
409
- if (isPaymentNotification(notification)) {
410
- // Handle payment notification
411
- console.log('Payment status:', notification.result.orderStatus);
412
- } else if (isRefundNotification(notification)) {
413
- // Handle refund notification
414
- console.log('Refund status:', notification.result.refundStatus);
848
+ ## Advanced Configuration
849
+
850
+ ### Custom HTTP Transport (axios)
851
+
852
+ The SDK uses native `fetch` by default. For connection pooling or advanced features, implement custom transport:
853
+
854
+ ```typescript
855
+ import { Waffo, Environment } from '@waffo/waffo-node';
856
+ import type { HttpTransport, HttpRequest, HttpResponse } from '@waffo/waffo-node';
857
+ import axios, { AxiosInstance } from 'axios';
858
+ import https from 'https';
859
+
860
+ // Create custom HTTP transport using axios
861
+ class AxiosHttpTransport implements HttpTransport {
862
+ private client: AxiosInstance;
863
+
864
+ constructor() {
865
+ this.client = axios.create({
866
+ timeout: 30000,
867
+ httpsAgent: new https.Agent({
868
+ minVersion: 'TLSv1.2',
869
+ maxVersion: 'TLSv1.3',
870
+ }),
871
+ });
415
872
  }
416
873
 
417
- // Return success response
418
- return res.json(buildSuccessResponse(merchantPrivateKey));
874
+ async send(request: HttpRequest): Promise<HttpResponse> {
875
+ try {
876
+ const response = await this.client.request({
877
+ method: request.method as 'POST' | 'GET',
878
+ url: request.url,
879
+ headers: request.headers,
880
+ data: request.body,
881
+ timeout: request.timeout,
882
+ validateStatus: () => true, // Don't throw on non-2xx
883
+ });
884
+
885
+ // Convert headers to Record<string, string>
886
+ const headers: Record<string, string> = {};
887
+ Object.entries(response.headers).forEach(([key, value]) => {
888
+ if (typeof value === 'string') {
889
+ headers[key] = value;
890
+ }
891
+ });
892
+
893
+ return {
894
+ statusCode: response.status,
895
+ headers,
896
+ body: typeof response.data === 'string'
897
+ ? response.data
898
+ : JSON.stringify(response.data),
899
+ };
900
+ } catch (error) {
901
+ if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
902
+ throw new Error('Request timeout');
903
+ }
904
+ throw error;
905
+ }
906
+ }
907
+ }
908
+
909
+ const waffo = new Waffo({
910
+ apiKey: 'your-api-key',
911
+ privateKey: 'your-private-key',
912
+ waffoPublicKey: 'waffo-public-key',
913
+ environment: Environment.SANDBOX,
914
+ httpTransport: new AxiosHttpTransport(),
419
915
  });
420
916
  ```
421
917
 
422
- ### Direct HTTP Client Access
918
+ ### TLS Security Configuration
919
+
920
+ The SDK enforces **TLS 1.2 or higher** by default for all HTTPS communication.
423
921
 
424
- For custom API requests not covered by the SDK methods:
922
+ When implementing custom HTTP transport, ensure TLS 1.2+ is configured:
425
923
 
426
924
  ```typescript
427
- const response = await waffo.httpClient.post<CustomResponseType>('/custom/endpoint', {
428
- body: { key: 'value' }
925
+ import https from 'https';
926
+
927
+ const httpsAgent = new https.Agent({
928
+ minVersion: 'TLSv1.2',
929
+ maxVersion: 'TLSv1.3',
930
+ // Optional: reject unauthorized certificates (default: true)
931
+ rejectUnauthorized: true,
429
932
  });
430
933
 
431
- if (response.success) {
432
- console.log(response.data);
433
- }
934
+ // Use with axios or other HTTP clients
935
+ import axios from 'axios';
936
+ const client = axios.create({
937
+ httpsAgent,
938
+ });
434
939
  ```
435
940
 
436
- ## Configuration Options
941
+ ### Debug Logging
437
942
 
438
- | Option | Type | Required | Default | Description |
439
- |--------|------|----------|---------|-------------|
440
- | `apiKey` | string | Yes | - | API key provided by Waffo |
441
- | `privateKey` | string | Yes | - | Base64 encoded PKCS8 private key |
442
- | `waffoPublicKey` | string | No | Built-in | Custom Waffo public key for response verification |
443
- | `environment` | Environment | No | PRODUCTION | API environment (SANDBOX or PRODUCTION) |
444
- | `timeout` | number | No | 30000 | Request timeout in milliseconds |
445
- | `logger` | Logger | No | - | Logger instance for debugging (can use `console`) |
943
+ Enable debug logging to troubleshoot issues during development:
446
944
 
447
- ## API Response Format
945
+ ```bash
946
+ # Enable all Waffo SDK debug logs
947
+ DEBUG=waffo:* npm start
448
948
 
449
- All API methods return an `ApiResponse<T>` object:
949
+ # Enable only HTTP request/response logs
950
+ DEBUG=waffo:http npm start
951
+
952
+ # Enable only signing logs
953
+ DEBUG=waffo:sign npm start
954
+ ```
955
+
956
+ Or configure programmatically:
450
957
 
451
958
  ```typescript
452
- interface ApiResponse<T> {
453
- success: boolean; // Whether the request was successful
454
- statusCode: number; // HTTP status code
455
- data?: T; // Response data (on success)
456
- error?: string; // Error message (on failure)
457
- }
959
+ const waffo = new Waffo({
960
+ // ... other config
961
+ logger: {
962
+ debug: (msg: string) => console.debug('[WAFFO]', msg),
963
+ info: (msg: string) => console.info('[WAFFO]', msg),
964
+ warn: (msg: string) => console.warn('[WAFFO]', msg),
965
+ error: (msg: string) => console.error('[WAFFO]', msg),
966
+ },
967
+ });
458
968
  ```
459
969
 
460
- ## Type Definitions
970
+ ### Timeout Configuration Recommendations
461
971
 
462
- The SDK exports comprehensive TypeScript types including:
972
+ | Operation Type | Connect Timeout | Read Timeout | Notes |
973
+ |----------------|-----------------|--------------|-------|
974
+ | Create Order | 5s | 30s | Recommended |
975
+ | Create Subscription | 5s | 30s | Recommended |
976
+ | Refund Operation | 5s | 30s | Recommended |
977
+ | Query Operations | 5s | 15s | Can be shorter |
463
978
 
464
- - `Environment` - SDK environment enum
465
- - `CountryCode` - ISO 3166-1 alpha-3 country codes
466
- - `CurrencyCode` - ISO 4217 currency codes
467
- - `ProductName` - Payment product type enum (ONE_TIME_PAYMENT, SUBSCRIPTION)
468
- - `payMethodType` - Payment method category (string: "EWALLET", "CREDITCARD", "BANKTRANSFER", "ONLINE_BANKING", "DIGITAL_BANKING", "OTC", "DEBITCARD")
469
- - `payMethodName` - Specific payment method (string: "OVO", "DANA", "GOPAY", "GCASH", "CC_VISA", "CC_MASTERCARD", "VA_BCA", "VA_BNI", etc.)
470
- - `OrderStatus` - Order status enum (PAY_IN_PROGRESS, AUTHORIZATION_REQUIRED, PAY_SUCCESS, ORDER_CLOSE, etc.)
471
- - `RefundStatus` - Refund status enum (REFUND_IN_PROGRESS, ORDER_PARTIALLY_REFUNDED, ORDER_FULLY_REFUNDED, ORDER_REFUND_FAILED)
472
- - `SubscriptionStatus` - Subscription status enum (AUTHORIZATION_REQUIRED, ACTIVE, PAUSED, MERCHANT_CANCELLED, etc.)
473
- - `PeriodType` - Subscription period type enum (DAILY, WEEKLY, MONTHLY, YEARLY)
474
- - `UserTerminalType` - User terminal type enum (WEB, APP, IN_WALLET_APP, IN_MINI_PROGRAM)
475
- - Request/Response interfaces for all API operations
979
+ ### Connection Pool Recommendations
980
+
981
+ | Scenario | Max Connections | Max Per Route | Notes |
982
+ |----------|-----------------|---------------|-------|
983
+ | Low Traffic (< 10 QPS) | 20 | 10 | Default config sufficient |
984
+ | Medium Traffic (10-100 QPS) | 50 | 20 | Consider using OkHttp |
985
+ | High Traffic (> 100 QPS) | 100-200 | 50 | Consider Apache HttpClient |
986
+
987
+ ### Instance Reuse
988
+
989
+ SDK instances are **thread-safe**. Recommended to use as singleton in your application:
990
+
991
+ ```typescript
992
+ // Create once, reuse everywhere
993
+ const waffo = new Waffo({ /* config */ });
994
+
995
+ export { waffo };
996
+ ```
997
+
998
+ ### RSA Utilities
999
+
1000
+ ```typescript
1001
+ import { RsaUtils } from '@waffo/waffo-node';
1002
+
1003
+ // Generate key pair (for testing)
1004
+ const keyPair = RsaUtils.generateKeyPair();
1005
+ console.log('Public Key (submit to Waffo):', keyPair.publicKey);
1006
+ console.log('Private Key (keep on your server):', keyPair.privateKey);
1007
+
1008
+ // Sign data
1009
+ const signature = RsaUtils.sign(data, privateKey);
1010
+
1011
+ // Verify signature
1012
+ const isValid = RsaUtils.verify(data, signature, publicKey);
1013
+ ```
476
1014
 
477
- ## Development
1015
+ ## Error Handling
1016
+
1017
+ ### Error Handling Pattern
1018
+
1019
+ SDKs use a hybrid error handling approach:
1020
+ - **Business errors**: Returned via `ApiResponse`, check with `response.isSuccess()`
1021
+ - **Unknown status exceptions**: Only for **write operations** (may affect funds or status), network timeout or server returning E0001 error code throws `WaffoUnknownStatusError`
1022
+
1023
+ ### Methods That Throw Unknown Status Exception
1024
+
1025
+ Only these methods that may affect funds or status throw `WaffoUnknownStatusError`:
1026
+
1027
+ | Method | Description |
1028
+ |--------|-------------|
1029
+ | `order().create()` | Create order, may initiate payment |
1030
+ | `order().refund()` | Refund, may cause fund changes |
1031
+ | `order().cancel()` | Cancel order, affects order status |
1032
+ | `subscription().create()` | Create subscription, may cause initial charge |
1033
+ | `subscription().cancel()` | Cancel subscription, affects subscription status |
1034
+
1035
+ **Query methods do not throw this exception** (e.g., `inquiry()`), because query operations can be safely retried without affecting funds or status.
1036
+
1037
+ ### WaffoUnknownStatusError Handling
1038
+
1039
+ > ⚠️ **IMPORTANT WARNING**
1040
+ >
1041
+ > When `WaffoUnknownStatusError` is caught, it means **the operation result is uncertain**.
1042
+ >
1043
+ > **DO NOT directly close the order or assume payment failed!** The user may have already completed payment.
1044
+ >
1045
+ > **Correct handling:**
1046
+ > 1. Call `waffo.order().inquiry()` to query actual order status
1047
+ > 2. Or wait for Waffo webhook callback notification
1048
+ > 3. Use Waffo's returned order status as the final authority
1049
+
1050
+ ```typescript
1051
+ import { Waffo, WaffoUnknownStatusError } from '@waffo/waffo-node';
1052
+
1053
+ try {
1054
+ const response = await waffo.order().create(params);
1055
+
1056
+ if (response.isSuccess()) {
1057
+ // Handle success
1058
+ const data = response.getData();
1059
+ console.log('Redirect URL:', data.orderAction);
1060
+ } else {
1061
+ // Handle business error (non-E0001 error code)
1062
+ console.log('Error:', response.getMessage());
1063
+ }
1064
+ } catch (error) {
1065
+ if (error instanceof WaffoUnknownStatusError) {
1066
+ // ⚠️ IMPORTANT: Payment status unknown
1067
+ //
1068
+ // [WRONG] Do not close order directly! User may have paid
1069
+ // [CORRECT]
1070
+ // 1. Call inquiry API to query actual order status
1071
+ // 2. Or wait for Waffo webhook callback
1072
+ // 3. Use Waffo's returned status as authority
1073
+
1074
+ console.warn('Status unknown, need to query:', error.message);
1075
+
1076
+ // Query order status (inquiry doesn't throw, can call directly)
1077
+ const inquiryResponse = await waffo.order().inquiry({
1078
+ paymentRequestId: params.paymentRequestId,
1079
+ });
1080
+
1081
+ if (inquiryResponse.isSuccess()) {
1082
+ const status = inquiryResponse.getData().orderStatus;
1083
+ console.log('Actual order status:', status);
1084
+ } else {
1085
+ // Query failed, wait for webhook callback
1086
+ console.error('Query failed, waiting for webhook callback');
1087
+ }
1088
+ } else {
1089
+ throw error;
1090
+ }
1091
+ }
1092
+ ```
1093
+
1094
+ ### WaffoUnknownStatusError Trigger Scenarios
1095
+
1096
+ | Scenario | Description |
1097
+ |----------|-------------|
1098
+ | Network Timeout | Request timeout, cannot determine if server received request |
1099
+ | Connection Failed | Network connection failed, cannot determine server status |
1100
+ | E0001 Error Code | Server returned E0001, indicating processing status unknown |
1101
+
1102
+ ### Error Code Classification
1103
+
1104
+ Error codes are classified by first letter:
1105
+
1106
+ | Prefix | Category | Description |
1107
+ |--------|----------|-------------|
1108
+ | **S** | SDK Internal Error | SDK client internal error such as network timeout, signing failure, etc. |
1109
+ | **A** | Merchant Related | Parameter, signature, permission, contract issues on merchant side |
1110
+ | **B** | User Related | User status, balance, authorization issues |
1111
+ | **C** | System Related | Waffo system or payment channel issues |
1112
+ | **D** | Risk Related | Risk control rejection |
1113
+ | **E** | Unknown Status | Server returned unknown status |
1114
+
1115
+ ### Complete Error Code Table
1116
+
1117
+ #### SDK Internal Errors (Sxxxx)
1118
+
1119
+ | Code | Description | Exception Type | Handling Suggestion |
1120
+ |------|-------------|----------------|---------------------|
1121
+ | `S0001` | Network Error | `WaffoUnknownStatusError` | **Status unknown**, need to query order to confirm |
1122
+ | `S0002` | Invalid Public Key | `WaffoError` | Check if public key is valid Base64 encoded X509 format |
1123
+ | `S0003` | RSA Signing Failed | `WaffoError` | Check if private key format is correct |
1124
+ | `S0004` | Response Signature Verification Failed | `ApiResponse.error()` | Check Waffo public key config, contact Waffo |
1125
+ | `S0005` | Request Serialization Failed | `ApiResponse.error()` | Check request parameter format |
1126
+ | `S0006` | SDK Unknown Error | `ApiResponse.error()` | Check logs, contact technical support |
1127
+ | `S0007` | Invalid Private Key | `WaffoError` | Check if private key is valid Base64 encoded PKCS8 format |
1128
+
1129
+ > **Important**: `S0001` and `E0001` (returned by server) indicate **unknown status**. Do not close order directly! Should call query API or wait for webhook to confirm actual status.
1130
+
1131
+ #### Merchant Related Errors (Axxxxx)
1132
+
1133
+ | Code | Description | HTTP Status |
1134
+ |------|-------------|-------------|
1135
+ | `0` | Success | 200 |
1136
+ | `A0001` | Invalid API Key | 401 |
1137
+ | `A0002` | Invalid Signature | 401 |
1138
+ | `A0003` | Parameter Validation Failed | 400 |
1139
+ | `A0004` | Insufficient Permission | 401 |
1140
+ | `A0005` | Merchant Limit Exceeded | 400 |
1141
+ | `A0006` | Merchant Status Abnormal | 400 |
1142
+ | `A0007` | Unsupported Transaction Currency | 400 |
1143
+ | `A0008` | Transaction Amount Exceeded | 400 |
1144
+ | `A0009` | Order Not Found | 400 |
1145
+ | `A0010` | Merchant Contract Does Not Allow This Operation | 400 |
1146
+ | `A0011` | Idempotent Parameter Mismatch | 400 |
1147
+ | `A0012` | Merchant Account Insufficient Balance | 400 |
1148
+ | `A0013` | Order Already Paid, Cannot Cancel | 400 |
1149
+ | `A0014` | Refund Rules Do Not Allow Refund | 400 |
1150
+ | `A0015` | Payment Channel Does Not Support Cancel | 400 |
1151
+ | `A0016` | Payment Channel Rejected Cancel | 400 |
1152
+ | `A0017` | Payment Channel Does Not Support Refund | 400 |
1153
+ | `A0018` | Payment Method Does Not Match Merchant Contract | 400 |
1154
+ | `A0019` | Cannot Refund Due to Chargeback Dispute | 400 |
1155
+ | `A0020` | Payment Amount Exceeds Single Transaction Limit | 400 |
1156
+ | `A0021` | Cumulative Payment Amount Exceeds Daily Limit | 400 |
1157
+ | `A0022` | Multiple Products Exist, Need to Specify Product Name | 400 |
1158
+ | `A0023` | Token Expired, Cannot Create Order | 400 |
1159
+ | `A0024` | Exchange Rate Expired, Cannot Process Order | 400 |
1160
+ | `A0026` | Unsupported Checkout Language | 400 |
1161
+ | `A0027` | Refund Count Reached Limit (50 times) | 400 |
1162
+ | `A0029` | Invalid Card Data Provided by Merchant | 400 |
1163
+ | `A0030` | Card BIN Not Found | 400 |
1164
+ | `A0031` | Unsupported Card Scheme or Card Type | 400 |
1165
+ | `A0032` | Invalid Payment Token Data | 400 |
1166
+ | `A0033` | Multiple Payment Methods with Same Name, Need to Specify Country | 400 |
1167
+ | `A0034` | Order Expiry Time Provided by Merchant Has Passed | 400 |
1168
+ | `A0035` | Current Order Does Not Support Capture Operation | 400 |
1169
+ | `A0036` | Current Order Status Does Not Allow Capture Operation | 400 |
1170
+ | `A0037` | User Payment Token Invalid or Expired | 400 |
1171
+ | `A0038` | MIT Transaction Requires Verified User Payment Token | 400 |
1172
+ | `A0039` | Order Already Refunded by Chargeback Prevention Service | 400 |
1173
+ | `A0040` | Order Cannot Be Created Concurrently | 400 |
1174
+ | `A0045` | MIT Transaction Cannot Process, tokenId Status Unverified | 400 |
1175
+
1176
+ #### User Related Errors (Bxxxxx)
1177
+
1178
+ | Code | Description | HTTP Status |
1179
+ |------|-------------|-------------|
1180
+ | `B0001` | User Status Abnormal | 400 |
1181
+ | `B0002` | User Limit Exceeded | 400 |
1182
+ | `B0003` | User Insufficient Balance | 400 |
1183
+ | `B0004` | User Did Not Pay Within Timeout | 400 |
1184
+ | `B0005` | User Authorization Failed | 400 |
1185
+ | `B0006` | Invalid Phone Number | 400 |
1186
+ | `B0007` | Invalid Email Format | 400 |
1187
+
1188
+ #### System Related Errors (Cxxxxx)
1189
+
1190
+ | Code | Description | HTTP Status |
1191
+ |------|-------------|-------------|
1192
+ | `C0001` | System Error | 500 |
1193
+ | `C0002` | Merchant Contract Invalid | 500 |
1194
+ | `C0003` | Order Status Invalid, Cannot Continue Processing | 500 |
1195
+ | `C0004` | Order Information Mismatch | 500 |
1196
+ | `C0005` | Payment Channel Rejected | 503 |
1197
+ | `C0006` | Payment Channel Error | 503 |
1198
+ | `C0007` | Payment Channel Under Maintenance | 503 |
1199
+
1200
+ #### Risk Related Errors (Dxxxxx)
1201
+
1202
+ | Code | Description | HTTP Status |
1203
+ |------|-------------|-------------|
1204
+ | `D0001` | Risk Control Rejected | 406 |
1205
+
1206
+ #### Unknown Status Errors (Exxxxx)
1207
+
1208
+ | Code | Description | HTTP Status |
1209
+ |------|-------------|-------------|
1210
+ | `E0001` | Unknown Status (Need to query or wait for callback) | 500 |
1211
+
1212
+ > **Note**: When receiving `E0001` error code, it indicates transaction status is unknown. **Do not close order directly**, should call query API to confirm actual status, or wait for webhook callback notification.
1213
+
1214
+ ## Development & Testing
1215
+
1216
+ ### Build Commands
478
1217
 
479
1218
  ```bash
480
1219
  # Install dependencies
481
1220
  npm install
482
1221
 
483
- # Build (generates both ESM and CJS)
1222
+ # Build the SDK
484
1223
  npm run build
485
1224
 
486
1225
  # Run tests
487
1226
  npm test
488
1227
 
489
- # Run tests with coverage
490
- npm run test:coverage
1228
+ # Type check
1229
+ npm run typecheck
491
1230
 
492
- # Run tests in watch mode
493
- npm run test:watch
494
-
495
- # Lint code
1231
+ # Lint
496
1232
  npm run lint
497
1233
 
498
- # Lint and auto-fix
499
- npm run lint:fix
500
-
501
1234
  # Format code
502
1235
  npm run format
503
-
504
- # Check formatting
505
- npm run format:check
506
1236
  ```
507
1237
 
508
- ### Code Quality
509
-
510
- This project uses:
511
- - **ESLint** - Code linting with TypeScript support
512
- - **Prettier** - Code formatting
513
- - **Husky** - Git hooks
514
- - **lint-staged** - Run linters on staged files
515
-
516
- Pre-commit hooks automatically run ESLint and Prettier on staged `.ts` files.
517
-
518
- ### Running E2E Tests
519
-
520
- E2E tests require Waffo sandbox credentials. The SDK supports multiple merchant configurations for different test scenarios and provides comprehensive test coverage based on the official Waffo test cases document.
1238
+ ### Generate Types from OpenAPI
521
1239
 
522
1240
  ```bash
523
- # Copy the template and fill in your credentials
524
- cp .env.template .env
525
- # Edit .env with your credentials
526
- ```
527
-
528
- Environment variables:
529
-
530
- | Variable | Required | Description |
531
- |----------|----------|-------------|
532
- | `WAFFO_PUBLIC_KEY` | No | Waffo public key for signature verification (shared) |
533
- | `ACQUIRING_MERCHANT_ID` | Yes* | Merchant ID for payment/order tests |
534
- | `ACQUIRING_API_KEY` | Yes* | API key for payment/order tests |
535
- | `ACQUIRING_MERCHANT_PRIVATE_KEY` | Yes* | Private key for payment/order tests |
536
- | `SUBSCRIPTION_MERCHANT_ID` | Yes** | Merchant ID for subscription tests |
537
- | `SUBSCRIPTION_API_KEY` | Yes** | API key for subscription tests |
538
- | `SUBSCRIPTION_MERCHANT_PRIVATE_KEY` | Yes** | Private key for subscription tests |
539
-
540
- \* Required for running acquiring/payment E2E tests
541
- \** Required for running subscription E2E tests
542
-
543
- **E2E Test Coverage:**
544
-
545
- | Module | Test Cases |
546
- |--------|------------|
547
- | Create Order | Payment success/failure, channel rejection (C0005), idempotency error (A0011), system error (C0001), unknown status (E0001) |
548
- | Inquiry Order | Query before/after payment |
549
- | Cancel Order | Cancel before payment, channel not supported (A0015), already paid (A0013) |
550
- | Refund Order | Full/partial refund, parameter validation (A0003), refund rules (A0014) |
551
- | Create Subscription | Subscription success/failure, next period payment simulation |
552
- | Cancel Subscription | Merchant-initiated cancellation |
553
- | Webhook Notifications | Signature verification for payment, refund, and subscription notifications |
554
-
555
- **Sandbox Amount Triggers:**
556
-
557
- | Amount Pattern | Error Code | Description |
558
- |----------------|------------|-------------|
559
- | 9, 90, 990, 1990, 19990 | C0005 | Channel rejection |
560
- | 9.1, 91, 991, 1991, 19991 | C0001 | System error |
561
- | 9.2, 92, 992, 1992, 19992 | E0001 | Unknown status |
562
- | 9.3, 93, 993, 1993, 19993 | C0001 | Cancel system error |
563
- | 9.4, 94, 994, 1994, 19994 | E0001 | Cancel unknown status |
564
- | 9.5, 95, 995, 1995, 19995 | C0001 | Refund system error |
565
- | 9.6, 96, 996, 1996, 199996 | E0001 | Refund unknown status |
1241
+ # From monorepo root
1242
+ ./scripts/generate-types.sh node
1243
+ ```
1244
+
1245
+ ### Run Test Vectors
566
1246
 
567
1247
  ```bash
568
- # Run all tests
569
- npm test
1248
+ # Run cross-language test vectors
1249
+ npm run test:vectors
570
1250
  ```
571
1251
 
572
- ## Build Output
1252
+ ## Support
1253
+
1254
+ - Documentation: [Waffo Developer Docs](https://dashboard-sandbox.waffo.com/docs/)
1255
+ - Issues: [GitHub Issues](https://github.com/waffo-com/waffo-sdk/issues)
1256
+ - Technical Support: merchant.support@waffo.com
1257
+
1258
+ ## License
573
1259
 
574
- The SDK is built using [tsup](https://tsup.egoist.dev/) and outputs:
1260
+ MIT License - See [LICENSE](LICENSE) file for details.
575
1261
 
576
- | File | Format | Description |
577
- |------|--------|-------------|
578
- | `dist/index.js` | CommonJS | For `require()` imports |
579
- | `dist/index.mjs` | ESM | For `import` statements |
580
- | `dist/index.d.ts` | TypeScript | Type declarations |