@waffo/waffo-node 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,588 +1,1317 @@
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 48b44fc -->
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
+ - [Handling New API Fields (ExtraParams)](#handling-new-api-fields-extraparams)
50
+ - [Error Handling](#error-handling)
51
+ - [Support](#support)
52
+ - [License](#license)
53
+ - [Development & Testing](#development-testing)
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
- > ```
134
+ ### 3. Query Order Status
69
135
 
70
- ## Features
136
+ ```typescript
137
+ const response = await waffo.order().inquiry({
138
+ acquiringOrderId: 'acquiring_order_id',
139
+ });
71
140
 
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
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
+ }
149
+ ```
86
150
 
87
- ## Automatic Timestamp Defaults
151
+ ## Configuration
88
152
 
89
- All timestamp parameters (`orderRequestedAt`, `requestedAt`, `captureRequestedAt`) are **optional** and will automatically default to the current time (`new Date().toISOString()`) if not provided:
153
+ ### Full Configuration Options
90
154
 
91
155
  ```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
- });
156
+ import { Waffo, Environment } from '@waffo/waffo-node';
99
157
 
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
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
106
171
  });
107
172
  ```
108
173
 
109
- This applies to:
110
- - `CreateOrderParams.orderRequestedAt`
111
- - `CancelOrderParams.orderRequestedAt`
112
- - `RefundOrderParams.requestedAt`
113
- - `CaptureOrderParams.captureRequestedAt`
114
- - `CreateSubscriptionParams.requestedAt`
115
- - `CancelSubscriptionParams.requestedAt`
116
-
117
- ## Installation
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
124
192
 
125
- ### 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
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,
134
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);
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
+ });
141
314
 
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
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 });
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);
468
+ }
469
+ ```
470
+
471
+ #### Subscription with Trial Period
472
+
473
+ ```typescript
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',
279
484
  }
280
485
  ```
281
486
 
282
- ### Query Subscription Status
487
+ #### Query Subscription
283
488
 
284
489
  ```typescript
285
- const result = await waffo.subscription.inquiry({
286
- merchantId: 'your-merchant-id',
287
- subscriptionId: 'SUB_202512230000001',
288
- paymentDetails: '1', // Include payment history
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);
350
657
  }
351
658
  ```
352
659
 
353
- ### Webhook Handler
660
+ #### Query Available Payment Methods
354
661
 
355
- The SDK provides a built-in webhook handler that automatically handles signature verification, event routing, and response signing:
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
+ }
671
+ }
672
+ ```
673
+
674
+ ## Webhook Handling
675
+
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
390
774
 
391
- For more control, you can use the low-level webhook utilities:
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`
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
919
+
920
+ The SDK enforces **TLS 1.2 or higher** by default for all HTTPS communication.
431
921
 
432
- For custom API requests not covered by the SDK methods:
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
+ });
939
+ ```
940
+
941
+ ### Debug Logging
942
+
943
+ Enable debug logging to troubleshoot issues during development:
944
+
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
+ ```
955
+
956
+ Or configure programmatically:
957
+
958
+ ```typescript
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
+ });
442
968
  ```
443
969
 
444
- ## Configuration Options
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 |
445
986
 
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`) |
987
+ ### Instance Reuse
454
988
 
455
- ## API Response Format
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
+ ```
456
997
 
457
- All API methods return an `ApiResponse<T>` object:
998
+ ### RSA Utilities
458
999
 
459
1000
  ```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)
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
+ ```
1014
+
1015
+ ## Handling New API Fields (ExtraParams)
1016
+
1017
+ When Waffo API adds new fields that are not yet defined in the SDK, you can use the ExtraParams feature to access these fields without waiting for an SDK update.
1018
+
1019
+ ### Reading Unknown Fields from Responses
1020
+
1021
+ ```typescript
1022
+ // Get extra field from response
1023
+ const response = await waffo.order().inquiry({ paymentRequestId: 'REQ001' });
1024
+ if (response.isSuccess()) {
1025
+ const data = response.getData();
1026
+
1027
+ // Access field not yet defined in SDK
1028
+ const newField = data.extraParams?.['newField'];
1029
+
1030
+ // Or use type assertion if you know the type
1031
+ const typedValue = data.extraParams?.['newField'] as string;
465
1032
  }
1033
+
1034
+ // Get extra field from webhook notification
1035
+ webhookHandler.onPaymentNotification((notification) => {
1036
+ const result = notification.result;
1037
+ const newField = result.extraParams?.['newField'];
1038
+ });
466
1039
  ```
467
1040
 
468
- ## Type Definitions
1041
+ ### Sending Extra Fields in Requests
469
1042
 
470
- The SDK exports comprehensive TypeScript types including:
1043
+ ```typescript
1044
+ // TypeScript types include index signature [key: string]: unknown
1045
+ // You can directly add extra fields to any request
1046
+ const response = await waffo.order().create({
1047
+ paymentRequestId: 'REQ001',
1048
+ merchantOrderId: 'ORDER001',
1049
+ // ... other required fields
1050
+ newField: 'value', // Extra field - no type error
1051
+ nested: { key: 123 } // Nested object - works too
1052
+ });
1053
+ ```
471
1054
 
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
1055
+ ### Important Notes
1056
+
1057
+ > **Upgrade SDK Promptly**
1058
+ >
1059
+ > ExtraParams is designed as a **temporary solution** for accessing new API fields before SDK updates.
1060
+ >
1061
+ > **Best Practices:**
1062
+ > 1. Check SDK release notes regularly for new field support
1063
+ > 2. Once SDK officially supports the field, migrate from `getExtraParam("field")` to the official getter (e.g., `getField()`)
1064
+ > 3. The SDK logs a warning when you use `getExtraParam()` on officially supported fields
1065
+ >
1066
+ > **Why migrate?**
1067
+ > - Official getters provide type safety
1068
+ > - Better IDE auto-completion and documentation
1069
+ > - Reduced risk of typos in field names
1070
+
1071
+ ## Error Handling
1072
+
1073
+ ### Error Handling Pattern
1074
+
1075
+ SDKs use a hybrid error handling approach:
1076
+ - **Business errors**: Returned via `ApiResponse`, check with `response.isSuccess()`
1077
+ - **Unknown status exceptions**: Only for **write operations** (may affect funds or status), network timeout or server returning E0001 error code throws `WaffoUnknownStatusError`
1078
+
1079
+ ### Methods That Throw Unknown Status Exception
1080
+
1081
+ Only these methods that may affect funds or status throw `WaffoUnknownStatusError`:
1082
+
1083
+ | Method | Description |
1084
+ |--------|-------------|
1085
+ | `order().create()` | Create order, may initiate payment |
1086
+ | `order().refund()` | Refund, may cause fund changes |
1087
+ | `order().cancel()` | Cancel order, affects order status |
1088
+ | `subscription().create()` | Create subscription, may cause initial charge |
1089
+ | `subscription().cancel()` | Cancel subscription, affects subscription status |
1090
+
1091
+ **Query methods do not throw this exception** (e.g., `inquiry()`), because query operations can be safely retried without affecting funds or status.
1092
+
1093
+ ### WaffoUnknownStatusError Handling
1094
+
1095
+ > ⚠️ **IMPORTANT WARNING**
1096
+ >
1097
+ > When `WaffoUnknownStatusError` is caught, it means **the operation result is uncertain**.
1098
+ >
1099
+ > **DO NOT directly close the order or assume payment failed!** The user may have already completed payment.
1100
+ >
1101
+ > **Correct handling:**
1102
+ > 1. Call `waffo.order().inquiry()` to query actual order status
1103
+ > 2. Or wait for Waffo webhook callback notification
1104
+ > 3. Use Waffo's returned order status as the final authority
484
1105
 
485
- ## Development
1106
+ ```typescript
1107
+ import { Waffo, WaffoUnknownStatusError } from '@waffo/waffo-node';
1108
+
1109
+ try {
1110
+ const response = await waffo.order().create(params);
1111
+
1112
+ if (response.isSuccess()) {
1113
+ // Handle success
1114
+ const data = response.getData();
1115
+ console.log('Redirect URL:', data.orderAction);
1116
+ } else {
1117
+ // Handle business error (non-E0001 error code)
1118
+ console.log('Error:', response.getMessage());
1119
+ }
1120
+ } catch (error) {
1121
+ if (error instanceof WaffoUnknownStatusError) {
1122
+ // ⚠️ IMPORTANT: Payment status unknown
1123
+ //
1124
+ // [WRONG] Do not close order directly! User may have paid
1125
+ // [CORRECT]
1126
+ // 1. Call inquiry API to query actual order status
1127
+ // 2. Or wait for Waffo webhook callback
1128
+ // 3. Use Waffo's returned status as authority
1129
+
1130
+ console.warn('Status unknown, need to query:', error.message);
1131
+
1132
+ // Query order status (inquiry doesn't throw, can call directly)
1133
+ const inquiryResponse = await waffo.order().inquiry({
1134
+ paymentRequestId: params.paymentRequestId,
1135
+ });
1136
+
1137
+ if (inquiryResponse.isSuccess()) {
1138
+ const status = inquiryResponse.getData().orderStatus;
1139
+ console.log('Actual order status:', status);
1140
+ } else {
1141
+ // Query failed, wait for webhook callback
1142
+ console.error('Query failed, waiting for webhook callback');
1143
+ }
1144
+ } else {
1145
+ throw error;
1146
+ }
1147
+ }
1148
+ ```
1149
+
1150
+ ### WaffoUnknownStatusError Trigger Scenarios
1151
+
1152
+ | Scenario | Description |
1153
+ |----------|-------------|
1154
+ | Network Timeout | Request timeout, cannot determine if server received request |
1155
+ | Connection Failed | Network connection failed, cannot determine server status |
1156
+ | E0001 Error Code | Server returned E0001, indicating processing status unknown |
1157
+
1158
+ ### Error Code Classification
1159
+
1160
+ Error codes are classified by first letter:
1161
+
1162
+ | Prefix | Category | Description |
1163
+ |--------|----------|-------------|
1164
+ | **S** | SDK Internal Error | SDK client internal error such as network timeout, signing failure, etc. |
1165
+ | **A** | Merchant Related | Parameter, signature, permission, contract issues on merchant side |
1166
+ | **B** | User Related | User status, balance, authorization issues |
1167
+ | **C** | System Related | Waffo system or payment channel issues |
1168
+ | **D** | Risk Related | Risk control rejection |
1169
+ | **E** | Unknown Status | Server returned unknown status |
1170
+
1171
+ ### Complete Error Code Table
1172
+
1173
+ #### SDK Internal Errors (Sxxxx)
1174
+
1175
+ | Code | Description | Exception Type | Handling Suggestion |
1176
+ |------|-------------|----------------|---------------------|
1177
+ | `S0001` | Network Error | `WaffoUnknownStatusError` | **Status unknown**, need to query order to confirm |
1178
+ | `S0002` | Invalid Public Key | `WaffoError` | Check if public key is valid Base64 encoded X509 format |
1179
+ | `S0003` | RSA Signing Failed | `WaffoError` | Check if private key format is correct |
1180
+ | `S0004` | Response Signature Verification Failed | `ApiResponse.error()` | Check Waffo public key config, contact Waffo |
1181
+ | `S0005` | Request Serialization Failed | `ApiResponse.error()` | Check request parameter format |
1182
+ | `S0006` | SDK Unknown Error | `ApiResponse.error()` | Check logs, contact technical support |
1183
+ | `S0007` | Invalid Private Key | `WaffoError` | Check if private key is valid Base64 encoded PKCS8 format |
1184
+
1185
+ > **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.
1186
+
1187
+ #### Merchant Related Errors (Axxxxx)
1188
+
1189
+ | Code | Description | HTTP Status |
1190
+ |------|-------------|-------------|
1191
+ | `0` | Success | 200 |
1192
+ | `A0001` | Invalid API Key | 401 |
1193
+ | `A0002` | Invalid Signature | 401 |
1194
+ | `A0003` | Parameter Validation Failed | 400 |
1195
+ | `A0004` | Insufficient Permission | 401 |
1196
+ | `A0005` | Merchant Limit Exceeded | 400 |
1197
+ | `A0006` | Merchant Status Abnormal | 400 |
1198
+ | `A0007` | Unsupported Transaction Currency | 400 |
1199
+ | `A0008` | Transaction Amount Exceeded | 400 |
1200
+ | `A0009` | Order Not Found | 400 |
1201
+ | `A0010` | Merchant Contract Does Not Allow This Operation | 400 |
1202
+ | `A0011` | Idempotent Parameter Mismatch | 400 |
1203
+ | `A0012` | Merchant Account Insufficient Balance | 400 |
1204
+ | `A0013` | Order Already Paid, Cannot Cancel | 400 |
1205
+ | `A0014` | Refund Rules Do Not Allow Refund | 400 |
1206
+ | `A0015` | Payment Channel Does Not Support Cancel | 400 |
1207
+ | `A0016` | Payment Channel Rejected Cancel | 400 |
1208
+ | `A0017` | Payment Channel Does Not Support Refund | 400 |
1209
+ | `A0018` | Payment Method Does Not Match Merchant Contract | 400 |
1210
+ | `A0019` | Cannot Refund Due to Chargeback Dispute | 400 |
1211
+ | `A0020` | Payment Amount Exceeds Single Transaction Limit | 400 |
1212
+ | `A0021` | Cumulative Payment Amount Exceeds Daily Limit | 400 |
1213
+ | `A0022` | Multiple Products Exist, Need to Specify Product Name | 400 |
1214
+ | `A0023` | Token Expired, Cannot Create Order | 400 |
1215
+ | `A0024` | Exchange Rate Expired, Cannot Process Order | 400 |
1216
+ | `A0026` | Unsupported Checkout Language | 400 |
1217
+ | `A0027` | Refund Count Reached Limit (50 times) | 400 |
1218
+ | `A0029` | Invalid Card Data Provided by Merchant | 400 |
1219
+ | `A0030` | Card BIN Not Found | 400 |
1220
+ | `A0031` | Unsupported Card Scheme or Card Type | 400 |
1221
+ | `A0032` | Invalid Payment Token Data | 400 |
1222
+ | `A0033` | Multiple Payment Methods with Same Name, Need to Specify Country | 400 |
1223
+ | `A0034` | Order Expiry Time Provided by Merchant Has Passed | 400 |
1224
+ | `A0035` | Current Order Does Not Support Capture Operation | 400 |
1225
+ | `A0036` | Current Order Status Does Not Allow Capture Operation | 400 |
1226
+ | `A0037` | User Payment Token Invalid or Expired | 400 |
1227
+ | `A0038` | MIT Transaction Requires Verified User Payment Token | 400 |
1228
+ | `A0039` | Order Already Refunded by Chargeback Prevention Service | 400 |
1229
+ | `A0040` | Order Cannot Be Created Concurrently | 400 |
1230
+ | `A0045` | MIT Transaction Cannot Process, tokenId Status Unverified | 400 |
1231
+
1232
+ #### User Related Errors (Bxxxxx)
1233
+
1234
+ | Code | Description | HTTP Status |
1235
+ |------|-------------|-------------|
1236
+ | `B0001` | User Status Abnormal | 400 |
1237
+ | `B0002` | User Limit Exceeded | 400 |
1238
+ | `B0003` | User Insufficient Balance | 400 |
1239
+ | `B0004` | User Did Not Pay Within Timeout | 400 |
1240
+ | `B0005` | User Authorization Failed | 400 |
1241
+ | `B0006` | Invalid Phone Number | 400 |
1242
+ | `B0007` | Invalid Email Format | 400 |
1243
+
1244
+ #### System Related Errors (Cxxxxx)
1245
+
1246
+ | Code | Description | HTTP Status |
1247
+ |------|-------------|-------------|
1248
+ | `C0001` | System Error | 500 |
1249
+ | `C0002` | Merchant Contract Invalid | 500 |
1250
+ | `C0003` | Order Status Invalid, Cannot Continue Processing | 500 |
1251
+ | `C0004` | Order Information Mismatch | 500 |
1252
+ | `C0005` | Payment Channel Rejected | 503 |
1253
+ | `C0006` | Payment Channel Error | 503 |
1254
+ | `C0007` | Payment Channel Under Maintenance | 503 |
1255
+
1256
+ #### Risk Related Errors (Dxxxxx)
1257
+
1258
+ | Code | Description | HTTP Status |
1259
+ |------|-------------|-------------|
1260
+ | `D0001` | Risk Control Rejected | 406 |
1261
+
1262
+ #### Unknown Status Errors (Exxxxx)
1263
+
1264
+ | Code | Description | HTTP Status |
1265
+ |------|-------------|-------------|
1266
+ | `E0001` | Unknown Status (Need to query or wait for callback) | 500 |
1267
+
1268
+ > **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.
1269
+
1270
+ ## Development & Testing
1271
+
1272
+ ### Build Commands
486
1273
 
487
1274
  ```bash
488
1275
  # Install dependencies
489
1276
  npm install
490
1277
 
491
- # Build (generates both ESM and CJS)
1278
+ # Build the SDK
492
1279
  npm run build
493
1280
 
494
1281
  # Run tests
495
1282
  npm test
496
1283
 
497
- # Run tests with coverage
498
- npm run test:coverage
1284
+ # Type check
1285
+ npm run typecheck
499
1286
 
500
- # Run tests in watch mode
501
- npm run test:watch
502
-
503
- # Lint code
1287
+ # Lint
504
1288
  npm run lint
505
1289
 
506
- # Lint and auto-fix
507
- npm run lint:fix
508
-
509
1290
  # Format code
510
1291
  npm run format
511
-
512
- # Check formatting
513
- npm run format:check
514
1292
  ```
515
1293
 
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.
1294
+ ### Generate Types from OpenAPI
529
1295
 
530
1296
  ```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 |
1297
+ # From monorepo root
1298
+ ./scripts/generate-types.sh node
1299
+ ```
1300
+
1301
+ ### Run Test Vectors
574
1302
 
575
1303
  ```bash
576
- # Run all tests
577
- npm test
1304
+ # Run cross-language test vectors
1305
+ npm run test:vectors
578
1306
  ```
579
1307
 
580
- ## Build Output
1308
+ ## Support
1309
+
1310
+ - Documentation: [Waffo Developer Docs](https://dashboard-sandbox.waffo.com/docs/)
1311
+ - Issues: [GitHub Issues](https://github.com/waffo-com/waffo-sdk/issues)
1312
+ - Technical Support: merchant.support@waffo.com
1313
+
1314
+ ## License
581
1315
 
582
- The SDK is built using [tsup](https://tsup.egoist.dev/) and outputs:
1316
+ MIT License - See [LICENSE](LICENSE) file for details.
583
1317
 
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 |