@waffo/waffo-node 2.0.3 → 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,588 +1,1261 @@
1
1
  # Waffo Node.js SDK
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@waffo/waffo-node.svg)](https://www.npmjs.com/package/@waffo/waffo-node)
4
- [![npm downloads](https://img.shields.io/npm/dm/@waffo/waffo-node.svg)](https://www.npmjs.com/package/@waffo/waffo-node)
5
- [![license](https://img.shields.io/npm/l/@waffo/waffo-node.svg)](https://github.com/AcquireNow/waffo-node/blob/main/LICENSE)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
- [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
3
+ <!-- Synced with waffo-sdk/README.md @ commit 9971ef7 -->
4
+
5
+ **English** | [中文](README_CN.md)
8
6
 
9
- Official Node.js SDK for Waffo acquiring services. This SDK provides secure API communication with RSA signing and comprehensive type definitions for payment acquiring operations.
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 |
10
68
 
11
- **npm**: https://www.npmjs.com/package/@waffo/waffo-node
69
+ ## Installation
12
70
 
13
- **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
+ ```
14
78
 
15
79
  ## Quick Start
16
80
 
81
+ ### 1. Initialize the SDK
82
+
17
83
  ```typescript
18
- import {
19
- Waffo,
20
- Environment,
21
- CurrencyCode,
22
- ProductName,
23
- } from '@waffo/waffo-node';
84
+ import { Waffo, Environment } from '@waffo/waffo-node';
24
85
 
25
- // 1. Initialize SDK
26
86
  const waffo = new Waffo({
27
87
  apiKey: 'your-api-key',
28
88
  privateKey: 'your-base64-encoded-private-key',
29
- 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
30
92
  });
93
+ ```
94
+
95
+ ### 2. Create a Payment Order
31
96
 
32
- // 2. Create an order
33
- const result = await waffo.order.create({
34
- paymentRequestId: 'REQ_001',
35
- merchantOrderId: 'ORDER_001',
36
- orderCurrency: CurrencyCode.IDR,
37
- orderAmount: '100000',
38
- orderDescription: 'Product purchase',
39
- notifyUrl: 'https://merchant.com/notify',
40
- merchantInfo: { merchantId: 'your-merchant-id' },
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',
41
111
  userInfo: {
42
- userId: 'user_001',
112
+ userId: 'user_123',
43
113
  userEmail: 'user@example.com',
114
+ userTerminal: 'WEB',
44
115
  },
45
116
  paymentInfo: {
46
- productName: ProductName.ONE_TIME_PAYMENT,
47
- payMethodType: 'EWALLET',
48
- payMethodName: 'DANA',
117
+ productName: 'ONE_TIME_PAYMENT',
118
+ },
119
+ goodsInfo: {
120
+ goodsUrl: 'https://your-site.com/product/001',
49
121
  },
50
122
  });
51
123
 
52
- // 3. Handle response
53
- if (result.success) {
54
- console.log('Order created:', result.data);
55
- // Redirect user to payment page
56
- if (result.data?.orderAction) {
57
- const action = JSON.parse(result.data.orderAction);
58
- window.location.href = action.webUrl;
59
- }
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());
60
131
  }
61
132
  ```
62
133
 
63
- > **Tip**: Need to generate a new RSA key pair? Use `Waffo.generateKeyPair()` to create one:
64
- > ```typescript
65
- > const keyPair = Waffo.generateKeyPair();
66
- > console.log(keyPair.privateKey); // Keep this secure, use for SDK initialization
67
- > console.log(keyPair.publicKey); // Share this with Waffo
68
- > ```
69
-
70
- ## Features
71
-
72
- - RSA-2048 request signing and response verification
73
- - Full TypeScript support with comprehensive type definitions
74
- - Zero production dependencies (uses only Node.js built-in `crypto` module)
75
- - Support for Sandbox and Production environments
76
- - Dual ESM/CommonJS module support
77
- - Order management (create, query, cancel, refund, capture)
78
- - Subscription management (create, query, cancel, manage)
79
- - Refund status inquiry
80
- - Merchant configuration inquiry (transaction limits, daily limits)
81
- - Payment method configuration inquiry (availability, maintenance schedules)
82
- - Webhook handler with automatic signature verification and event routing
83
- - Webhook signature verification utilities
84
- - Direct HTTP client access for custom API requests
85
- - Automatic timestamp defaults for request parameters
86
-
87
- ## Automatic Timestamp Defaults
88
-
89
- All timestamp parameters (`orderRequestedAt`, `requestedAt`, `captureRequestedAt`) are **optional** and will automatically default to the current time (`new Date().toISOString()`) if not provided:
90
-
91
- ```typescript
92
- // Timestamp is automatically set to current time
93
- await waffo.order.create({
94
- paymentRequestId: 'REQ_001',
95
- merchantOrderId: 'ORDER_001',
96
- // ... other required fields
97
- // orderRequestedAt is automatically set
98
- });
134
+ ### 3. Query Order Status
99
135
 
100
- // Or explicitly provide a custom timestamp
101
- await waffo.order.create({
102
- paymentRequestId: 'REQ_001',
103
- merchantOrderId: 'ORDER_001',
104
- orderRequestedAt: '2025-01-01T00:00:00.000Z', // Custom timestamp
105
- // ... other required fields
136
+ ```typescript
137
+ const response = await waffo.order().inquiry({
138
+ acquiringOrderId: 'acquiring_order_id',
106
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
+ }
107
149
  ```
108
150
 
109
- This applies to:
110
- - `CreateOrderParams.orderRequestedAt`
111
- - `CancelOrderParams.orderRequestedAt`
112
- - `RefundOrderParams.requestedAt`
113
- - `CaptureOrderParams.captureRequestedAt`
114
- - `CreateSubscriptionParams.requestedAt`
115
- - `CancelSubscriptionParams.requestedAt`
151
+ ## Configuration
116
152
 
117
- ## 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
118
175
 
119
176
  ```bash
120
- 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();
121
189
  ```
122
190
 
123
- ## Usage
191
+ ### Environment URLs
192
+
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.
124
199
 
125
- ### Initialize the SDK
200
+ ### Request-Level Configuration
126
201
 
127
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';
128
215
  import { Waffo, Environment } from '@waffo/waffo-node';
129
216
 
217
+ const app = express();
130
218
  const waffo = new Waffo({
131
- apiKey: 'your-api-key',
132
- privateKey: 'your-base64-encoded-private-key',
133
- 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,
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);
134
238
  });
239
+
240
+ app.listen(3000);
135
241
  ```
136
242
 
137
- ### Generate RSA Key Pair
243
+ #### NestJS Integration
138
244
 
139
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';
140
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
297
+
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
+ });
141
328
 
142
- const keyPair = Waffo.generateKeyPair();
143
- console.log(keyPair.privateKey); // Base64 encoded PKCS8 private key
144
- console.log(keyPair.publicKey); // Base64 encoded X509 public key
329
+ fastify.listen({ port: 3000 });
145
330
  ```
146
331
 
147
- ### Create an Order
332
+ ## API Usage
333
+
334
+ ### Order Management
335
+
336
+ #### Create Order
148
337
 
149
338
  ```typescript
150
- const result = await waffo.order.create({
151
- paymentRequestId: 'REQ_001',
152
- merchantOrderId: 'ORDER_001',
153
- orderCurrency: CurrencyCode.IDR,
154
- orderAmount: '100000',
155
- orderDescription: 'Product purchase',
156
- 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(),
157
348
  merchantInfo: {
158
349
  merchantId: 'your-merchant-id',
159
350
  },
160
351
  userInfo: {
161
- userId: 'user_001',
352
+ userId: 'user_123',
162
353
  userEmail: 'user@example.com',
163
- userPhone: '+62-81234567890',
164
- userTerminal: UserTerminalType.WEB,
354
+ userTerminal: 'WEB',
165
355
  },
166
356
  paymentInfo: {
167
- productName: ProductName.ONE_TIME_PAYMENT,
168
- payMethodType: 'EWALLET',
169
- payMethodName: 'DANA',
357
+ productName: 'ONE_TIME_PAYMENT',
358
+ payMethodType: 'CREDITCARD', // CREDITCARD, DEBITCARD, EWALLET, VA, etc.
359
+ // payMethodName: 'CC_VISA', // Optional: specify exact payment method
360
+ },
361
+ goodsInfo: {
362
+ goodsUrl: 'https://your-site.com/product/001',
170
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',
171
368
  });
172
369
 
173
- if (result.success) {
174
- console.log('Order created:', result.data);
175
- } else {
176
- console.error('Error:', result.error);
370
+ if (response.isSuccess()) {
371
+ const data = response.getData();
372
+ console.log('Checkout URL:', data.orderAction);
177
373
  }
178
374
  ```
179
375
 
180
- ### Query Order Status
376
+ #### Combine Multiple Payment Methods
181
377
 
182
378
  ```typescript
183
- const result = await waffo.order.inquiry({
184
- acquiringOrderId: 'A202512230000001',
185
- // or use paymentRequestId: 'REQ_001'
186
- });
187
-
188
- if (result.success) {
189
- 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
190
383
  }
191
384
  ```
192
385
 
193
- ### Cancel an Order
386
+ #### Query Order
194
387
 
195
388
  ```typescript
196
- const result = await waffo.order.cancel({
197
- acquiringOrderId: 'A202512230000001',
198
- merchantId: 'your-merchant-id',
199
- // orderRequestedAt is optional, defaults to current time
389
+ const response = await waffo.order().inquiry({
390
+ acquiringOrderId: 'acquiring_order_id',
200
391
  });
201
392
  ```
202
393
 
203
- ### Refund an Order
394
+ #### Cancel Order
204
395
 
205
396
  ```typescript
206
- const result = await waffo.order.refund({
207
- refundRequestId: 'REFUND_001',
208
- acquiringOrderId: 'A202512230000001',
209
- merchantId: 'your-merchant-id',
210
- refundAmount: '50000',
211
- refundReason: 'Customer requested refund',
212
- refundNotifyUrl: 'https://merchant.com/refund-notify',
213
- // requestedAt is optional, defaults to current time
397
+ const response = await waffo.order().cancel({
398
+ acquiringOrderId: 'acquiring_order_id',
399
+ orderRequestedAt: new Date().toISOString(),
214
400
  });
215
401
  ```
216
402
 
217
- ### Query Refund Status
403
+ #### Refund Order
218
404
 
219
405
  ```typescript
220
- const result = await waffo.refund.inquiry({
221
- refundRequestId: 'REFUND_001',
222
- // or use acquiringRefundOrderId: 'R202512230000001'
223
- });
406
+ import { randomUUID } from 'crypto';
224
407
 
225
- if (result.success) {
226
- console.log('Refund status:', result.data?.refundStatus);
227
- }
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
+ });
228
418
  ```
229
419
 
230
- ### Capture a Pre-authorized Payment
420
+ #### Capture Order
231
421
 
232
422
  ```typescript
233
- const result = await waffo.order.capture({
234
- acquiringOrderId: 'A202512230000001',
235
- merchantId: 'your-merchant-id',
236
- captureAmount: '100000',
237
- // 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',
238
428
  });
239
429
  ```
240
430
 
241
- ### Create a Subscription
431
+ ### Subscription Management
432
+
433
+ #### Create Subscription
242
434
 
243
435
  ```typescript
244
- const result = await waffo.subscription.create({
245
- subscriptionRequest: 'SUB_REQ_001',
246
- merchantSubscriptionId: 'MERCHANT_SUB_001',
247
- currency: CurrencyCode.PHP,
248
- 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',
249
446
  productInfo: {
250
- periodType: PeriodType.MONTHLY,
447
+ description: 'Monthly Subscription',
448
+ periodType: 'MONTHLY',
251
449
  periodInterval: '1',
252
- numberOfPeriod: '12',
253
- description: 'Monthly subscription',
254
- },
255
- paymentInfo: {
256
- productName: ProductName.SUBSCRIPTION,
257
- payMethodType: 'EWALLET',
258
- payMethodName: 'GCASH',
259
450
  },
260
- merchantInfo: { merchantId: 'your-merchant-id' },
261
451
  userInfo: {
262
- userId: 'user_001',
452
+ userId: 'user_123',
263
453
  userEmail: 'user@example.com',
264
454
  },
265
- goodsInfo: {
266
- goodsId: 'GOODS_001',
267
- goodsName: 'Premium Plan',
268
- },
269
- notifyUrl: 'https://merchant.com/subscription/notify',
270
- // 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',
271
461
  });
272
462
 
273
- if (result.success) {
274
- console.log('Subscription created:', result.data);
275
- // Redirect user to complete subscription signing
276
- if (result.data?.subscriptionAction?.webUrl) {
277
- window.location.href = result.data.subscriptionAction.webUrl;
278
- }
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);
279
468
  }
280
469
  ```
281
470
 
282
- ### Query Subscription Status
471
+ #### Subscription with Trial Period
283
472
 
284
473
  ```typescript
285
- const result = await waffo.subscription.inquiry({
286
- merchantId: 'your-merchant-id',
287
- subscriptionId: 'SUB_202512230000001',
288
- 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
289
494
  });
290
495
 
291
- if (result.success) {
292
- console.log('Subscription status:', result.data?.subscriptionStatus);
293
- }
496
+ // Or query by subscriptionRequest
497
+ const response = await waffo.subscription().inquiry({
498
+ subscriptionRequest: 'subscription_request',
499
+ });
294
500
  ```
295
501
 
296
- ### Cancel a Subscription
502
+ #### Cancel Subscription
297
503
 
298
504
  ```typescript
299
- const result = await waffo.subscription.cancel({
300
- merchantId: 'your-merchant-id',
301
- subscriptionId: 'SUB_202512230000001',
302
- // requestedAt is optional, defaults to current time
505
+ const response = await waffo.subscription().cancel({
506
+ subscriptionId: 'subscription_id',
303
507
  });
304
508
  ```
305
509
 
306
- ### Get Subscription Management URL
510
+ #### Get Subscription Management URL
307
511
 
308
512
  ```typescript
309
- const result = await waffo.subscription.manage({
310
- subscriptionId: 'SUB_202512230000001',
311
- // or use subscriptionRequest: 'SUB_REQ_001'
513
+ const response = await waffo.subscription().manage({
514
+ subscriptionId: 'subscription_id',
312
515
  });
313
516
 
314
- if (result.success) {
315
- console.log('Management URL:', result.data?.managementUrl);
316
- console.log('Expires at:', result.data?.expiresAt);
317
- // Redirect user to manage their subscription
318
- 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
520
+ }
521
+ ```
522
+
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
+ }
319
601
  }
320
602
  ```
321
603
 
322
- ### Query Merchant Configuration
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
323
614
 
324
615
  ```typescript
325
- const result = await waffo.merchantConfig.inquiry({
326
- merchantId: 'your-merchant-id',
616
+ const response = await waffo.subscription().changeInquiry({
617
+ subscriptionRequest: 'new-subscription-request-id',
618
+ originSubscriptionRequest: 'original-subscription-request-id',
327
619
  });
328
620
 
329
- if (result.success) {
330
- console.log('Daily Limit:', result.data?.totalDailyLimit);
331
- console.log('Remaining Daily Limit:', result.data?.remainingDailyLimit);
332
- 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);
333
627
  }
334
628
  ```
335
629
 
336
- ### Query Payment Method Configuration
630
+ ### Refund Query
337
631
 
338
632
  ```typescript
339
- const result = await waffo.payMethodConfig.inquiry({
340
- merchantId: 'your-merchant-id',
633
+ // Query by refundRequestId (merchant-generated idempotency key)
634
+ const response = await waffo.refund().inquiry({
635
+ refundRequestId: 'refund_request_id',
341
636
  });
342
637
 
343
- if (result.success) {
344
- result.data?.payMethodDetails.forEach(method => {
345
- console.log(`${method.payMethodName}: ${method.currentStatus === '1' ? 'Available' : 'Unavailable'}`);
346
- if (method.fixedMaintenanceRules) {
347
- console.log('Maintenance periods:', method.fixedMaintenanceRules);
348
- }
349
- });
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
+ }
350
671
  }
351
672
  ```
352
673
 
353
- ### Webhook Handler
674
+ ## Webhook Handling
354
675
 
355
- 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
356
679
 
357
680
  ```typescript
358
- // In your webhook handler
359
- app.post('/webhook', async (req, res) => {
360
- const signature = req.headers['x-signature'] as string;
361
- const body = JSON.stringify(req.body);
681
+ import { Waffo, Environment } from '@waffo/waffo-node';
682
+ import express from 'express';
362
683
 
363
- const result = await waffo.webhook.handle(body, signature, {
364
- onPayment: async ({ notification }) => {
365
- console.log('Payment status:', notification.result.orderStatus);
366
- // Process payment notification
367
- },
368
- onRefund: async ({ notification }) => {
369
- console.log('Refund status:', notification.result.refundStatus);
370
- // Process refund notification
371
- },
372
- onSubscriptionStatus: async ({ notification }) => {
373
- console.log('Subscription status:', notification.result.subscriptionStatus);
374
- // Process subscription status change
375
- },
376
- onSubscriptionPayment: async ({ notification }) => {
377
- console.log('Subscription payment:', notification.result.orderStatus);
378
- // Process subscription recurring payment
379
- },
380
- onError: async (error) => {
381
- console.error('Webhook error:', error.message);
382
- },
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
+ }
383
759
  });
384
760
 
385
- 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);
386
770
  });
387
771
  ```
388
772
 
389
- ### 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 |
390
791
 
391
- For more control, you can use the low-level webhook utilities:
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`
797
+
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
392
821
 
393
822
  ```typescript
394
- import {
395
- verifyWebhookSignature,
396
- buildSuccessResponse,
397
- buildFailedResponse,
398
- isPaymentNotification,
399
- isRefundNotification,
400
- } from '@waffo/waffo-node';
823
+ // Specify type only, let user choose on checkout page
824
+ paymentInfo: {
825
+ payMethodType: 'CREDITCARD',
826
+ }
401
827
 
402
- // In your webhook handler
403
- app.post('/webhook', (req, res) => {
404
- const signature = req.headers['x-signature'];
405
- const body = JSON.stringify(req.body);
828
+ // Specify exact payment method
829
+ paymentInfo: {
830
+ payMethodType: 'CREDITCARD',
831
+ payMethodName: 'CC_VISA',
832
+ }
406
833
 
407
- // Verify signature
408
- const verifyResult = verifyWebhookSignature(body, signature, waffoPublicKey);
834
+ // Combine multiple types
835
+ paymentInfo: {
836
+ payMethodType: 'CREDITCARD,DEBITCARD',
837
+ }
409
838
 
410
- if (!verifyResult.valid) {
411
- return res.json(buildFailedResponse('Signature verification failed', merchantPrivateKey));
412
- }
839
+ // E-wallet with specific channel
840
+ paymentInfo: {
841
+ payMethodType: 'EWALLET',
842
+ payMethodName: 'GCASH',
843
+ }
844
+ ```
413
845
 
414
- // Handle notification based on type
415
- 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).
416
847
 
417
- if (isPaymentNotification(notification)) {
418
- // Handle payment notification
419
- console.log('Payment status:', notification.result.orderStatus);
420
- } else if (isRefundNotification(notification)) {
421
- // Handle refund notification
422
- 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
+ });
423
872
  }
424
873
 
425
- // Return success response
426
- 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(),
427
915
  });
428
916
  ```
429
917
 
430
- ### Direct HTTP Client Access
918
+ ### TLS Security Configuration
431
919
 
432
- For custom API requests not covered by the SDK methods:
920
+ The SDK enforces **TLS 1.2 or higher** by default for all HTTPS communication.
921
+
922
+ When implementing custom HTTP transport, ensure TLS 1.2+ is configured:
433
923
 
434
924
  ```typescript
435
- const response = await waffo.httpClient.post<CustomResponseType>('/custom/endpoint', {
436
- 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,
437
932
  });
438
933
 
439
- if (response.success) {
440
- console.log(response.data);
441
- }
934
+ // Use with axios or other HTTP clients
935
+ import axios from 'axios';
936
+ const client = axios.create({
937
+ httpsAgent,
938
+ });
442
939
  ```
443
940
 
444
- ## Configuration Options
941
+ ### Debug Logging
445
942
 
446
- | Option | Type | Required | Default | Description |
447
- |--------|------|----------|---------|-------------|
448
- | `apiKey` | string | Yes | - | API key provided by Waffo |
449
- | `privateKey` | string | Yes | - | Base64 encoded PKCS8 private key |
450
- | `waffoPublicKey` | string | No | Built-in | Custom Waffo public key for response verification |
451
- | `environment` | Environment | No | PRODUCTION | API environment (SANDBOX or PRODUCTION) |
452
- | `timeout` | number | No | 30000 | Request timeout in milliseconds |
453
- | `logger` | Logger | No | - | Logger instance for debugging (can use `console`) |
943
+ Enable debug logging to troubleshoot issues during development:
454
944
 
455
- ## API Response Format
945
+ ```bash
946
+ # Enable all Waffo SDK debug logs
947
+ DEBUG=waffo:* npm start
948
+
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
+ ```
456
955
 
457
- All API methods return an `ApiResponse<T>` object:
956
+ Or configure programmatically:
458
957
 
459
958
  ```typescript
460
- interface ApiResponse<T> {
461
- success: boolean; // Whether the request was successful
462
- statusCode: number; // HTTP status code
463
- data?: T; // Response data (on success)
464
- error?: string; // Error message (on failure)
465
- }
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
+ });
968
+ ```
969
+
970
+ ### Timeout Configuration Recommendations
971
+
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 |
978
+
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);
466
1013
  ```
467
1014
 
468
- ## Type Definitions
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`:
469
1026
 
470
- The SDK exports comprehensive TypeScript types including:
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 |
471
1034
 
472
- - `Environment` - SDK environment enum
473
- - `CountryCode` - ISO 3166-1 alpha-3 country codes
474
- - `CurrencyCode` - ISO 4217 currency codes
475
- - `ProductName` - Payment product type enum (ONE_TIME_PAYMENT, SUBSCRIPTION)
476
- - `payMethodType` - Payment method category (string: "EWALLET", "CREDITCARD", "BANKTRANSFER", "ONLINE_BANKING", "DIGITAL_BANKING", "OTC", "DEBITCARD")
477
- - `payMethodName` - Specific payment method (string: "OVO", "DANA", "GOPAY", "GCASH", "CC_VISA", "CC_MASTERCARD", "VA_BCA", "VA_BNI", etc.)
478
- - `OrderStatus` - Order status enum (PAY_IN_PROGRESS, AUTHORIZATION_REQUIRED, PAY_SUCCESS, ORDER_CLOSE, etc.)
479
- - `RefundStatus` - Refund status enum (REFUND_IN_PROGRESS, ORDER_PARTIALLY_REFUNDED, ORDER_FULLY_REFUNDED, ORDER_REFUND_FAILED)
480
- - `SubscriptionStatus` - Subscription status enum (AUTHORIZATION_REQUIRED, ACTIVE, PAUSED, MERCHANT_CANCELLED, etc.)
481
- - `PeriodType` - Subscription period type enum (DAILY, WEEKLY, MONTHLY, YEARLY)
482
- - `UserTerminalType` - User terminal type enum (WEB, APP, IN_WALLET_APP, IN_MINI_PROGRAM)
483
- - Request/Response interfaces for all API operations
1035
+ **Query methods do not throw this exception** (e.g., `inquiry()`), because query operations can be safely retried without affecting funds or status.
484
1036
 
485
- ## Development
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
486
1217
 
487
1218
  ```bash
488
1219
  # Install dependencies
489
1220
  npm install
490
1221
 
491
- # Build (generates both ESM and CJS)
1222
+ # Build the SDK
492
1223
  npm run build
493
1224
 
494
1225
  # Run tests
495
1226
  npm test
496
1227
 
497
- # Run tests with coverage
498
- npm run test:coverage
1228
+ # Type check
1229
+ npm run typecheck
499
1230
 
500
- # Run tests in watch mode
501
- npm run test:watch
502
-
503
- # Lint code
1231
+ # Lint
504
1232
  npm run lint
505
1233
 
506
- # Lint and auto-fix
507
- npm run lint:fix
508
-
509
1234
  # Format code
510
1235
  npm run format
511
-
512
- # Check formatting
513
- npm run format:check
514
1236
  ```
515
1237
 
516
- ### Code Quality
517
-
518
- This project uses:
519
- - **ESLint** - Code linting with TypeScript support
520
- - **Prettier** - Code formatting
521
- - **Husky** - Git hooks
522
- - **lint-staged** - Run linters on staged files
523
-
524
- Pre-commit hooks automatically run ESLint and Prettier on staged `.ts` files.
525
-
526
- ### Running E2E Tests
527
-
528
- 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
529
1239
 
530
1240
  ```bash
531
- # Copy the template and fill in your credentials
532
- cp .env.template .env
533
- # Edit .env with your credentials
534
- ```
535
-
536
- Environment variables:
537
-
538
- | Variable | Required | Description |
539
- |----------|----------|-------------|
540
- | `WAFFO_PUBLIC_KEY` | No | Waffo public key for signature verification (shared) |
541
- | `ACQUIRING_MERCHANT_ID` | Yes* | Merchant ID for payment/order tests |
542
- | `ACQUIRING_API_KEY` | Yes* | API key for payment/order tests |
543
- | `ACQUIRING_MERCHANT_PRIVATE_KEY` | Yes* | Private key for payment/order tests |
544
- | `SUBSCRIPTION_MERCHANT_ID` | Yes** | Merchant ID for subscription tests |
545
- | `SUBSCRIPTION_API_KEY` | Yes** | API key for subscription tests |
546
- | `SUBSCRIPTION_MERCHANT_PRIVATE_KEY` | Yes** | Private key for subscription tests |
547
-
548
- \* Required for running acquiring/payment E2E tests
549
- \** Required for running subscription E2E tests
550
-
551
- **E2E Test Coverage:**
552
-
553
- | Module | Test Cases |
554
- |--------|------------|
555
- | Create Order | Payment success/failure, channel rejection (C0005), idempotency error (A0011), system error (C0001), unknown status (E0001) |
556
- | Inquiry Order | Query before/after payment |
557
- | Cancel Order | Cancel before payment, channel not supported (A0015), already paid (A0013) |
558
- | Refund Order | Full/partial refund, parameter validation (A0003), refund rules (A0014) |
559
- | Create Subscription | Subscription success/failure, next period payment simulation |
560
- | Cancel Subscription | Merchant-initiated cancellation |
561
- | Webhook Notifications | Signature verification for payment, refund, and subscription notifications |
562
-
563
- **Sandbox Amount Triggers:**
564
-
565
- | Amount Pattern | Error Code | Description |
566
- |----------------|------------|-------------|
567
- | 9, 90, 990, 1990, 19990 | C0005 | Channel rejection |
568
- | 9.1, 91, 991, 1991, 19991 | C0001 | System error |
569
- | 9.2, 92, 992, 1992, 19992 | E0001 | Unknown status |
570
- | 9.3, 93, 993, 1993, 19993 | C0001 | Cancel system error |
571
- | 9.4, 94, 994, 1994, 19994 | E0001 | Cancel unknown status |
572
- | 9.5, 95, 995, 1995, 19995 | C0001 | Refund system error |
573
- | 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
574
1246
 
575
1247
  ```bash
576
- # Run all tests
577
- npm test
1248
+ # Run cross-language test vectors
1249
+ npm run test:vectors
578
1250
  ```
579
1251
 
580
- ## 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
581
1259
 
582
- The SDK is built using [tsup](https://tsup.egoist.dev/) and outputs:
1260
+ MIT License - See [LICENSE](LICENSE) file for details.
583
1261
 
584
- | File | Format | Description |
585
- |------|--------|-------------|
586
- | `dist/index.js` | CommonJS | For `require()` imports |
587
- | `dist/index.mjs` | ESM | For `import` statements |
588
- | `dist/index.d.ts` | TypeScript | Type declarations |