@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/LICENSE +1 -1
- package/README.md +1063 -390
- package/dist/index.d.mts +1567 -4258
- package/dist/index.d.ts +1567 -4258
- package/dist/index.js +1128 -1597
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1105 -1560
- package/dist/index.mjs.map +1 -1
- package/package.json +28 -43
- package/README.ja.md +0 -588
- package/README.zh-CN.md +0 -588
package/README.md
CHANGED
|
@@ -1,588 +1,1261 @@
|
|
|
1
1
|
# Waffo Node.js SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](https://nodejs.org/)
|
|
3
|
+
<!-- Synced with waffo-sdk/README.md @ commit 9971ef7 -->
|
|
4
|
+
|
|
5
|
+
**English** | [中文](README_CN.md)
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@waffo/waffo-node)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](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
|
-
|
|
69
|
+
## Installation
|
|
12
70
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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: '
|
|
112
|
+
userId: 'user_123',
|
|
43
113
|
userEmail: 'user@example.com',
|
|
114
|
+
userTerminal: 'WEB',
|
|
44
115
|
},
|
|
45
116
|
paymentInfo: {
|
|
46
|
-
productName:
|
|
47
|
-
|
|
48
|
-
|
|
117
|
+
productName: 'ONE_TIME_PAYMENT',
|
|
118
|
+
},
|
|
119
|
+
goodsInfo: {
|
|
120
|
+
goodsUrl: 'https://your-site.com/product/001',
|
|
49
121
|
},
|
|
50
122
|
});
|
|
51
123
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log('
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
await waffo.order.
|
|
102
|
-
|
|
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
|
-
|
|
110
|
-
- `CreateOrderParams.orderRequestedAt`
|
|
111
|
-
- `CancelOrderParams.orderRequestedAt`
|
|
112
|
-
- `RefundOrderParams.requestedAt`
|
|
113
|
-
- `CaptureOrderParams.captureRequestedAt`
|
|
114
|
-
- `CreateSubscriptionParams.requestedAt`
|
|
115
|
-
- `CancelSubscriptionParams.requestedAt`
|
|
151
|
+
## Configuration
|
|
116
152
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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:
|
|
132
|
-
privateKey:
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
+
## API Usage
|
|
333
|
+
|
|
334
|
+
### Order Management
|
|
335
|
+
|
|
336
|
+
#### Create Order
|
|
148
337
|
|
|
149
338
|
```typescript
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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: '
|
|
352
|
+
userId: 'user_123',
|
|
162
353
|
userEmail: 'user@example.com',
|
|
163
|
-
|
|
164
|
-
userTerminal: UserTerminalType.WEB,
|
|
354
|
+
userTerminal: 'WEB',
|
|
165
355
|
},
|
|
166
356
|
paymentInfo: {
|
|
167
|
-
productName:
|
|
168
|
-
payMethodType: '
|
|
169
|
-
payMethodName: '
|
|
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 (
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
376
|
+
#### Combine Multiple Payment Methods
|
|
181
377
|
|
|
182
378
|
```typescript
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
386
|
+
#### Query Order
|
|
194
387
|
|
|
195
388
|
```typescript
|
|
196
|
-
const
|
|
197
|
-
acquiringOrderId: '
|
|
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
|
-
|
|
394
|
+
#### Cancel Order
|
|
204
395
|
|
|
205
396
|
```typescript
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
403
|
+
#### Refund Order
|
|
218
404
|
|
|
219
405
|
```typescript
|
|
220
|
-
|
|
221
|
-
refundRequestId: 'REFUND_001',
|
|
222
|
-
// or use acquiringRefundOrderId: 'R202512230000001'
|
|
223
|
-
});
|
|
406
|
+
import { randomUUID } from 'crypto';
|
|
224
407
|
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
420
|
+
#### Capture Order
|
|
231
421
|
|
|
232
422
|
```typescript
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
###
|
|
431
|
+
### Subscription Management
|
|
432
|
+
|
|
433
|
+
#### Create Subscription
|
|
242
434
|
|
|
243
435
|
```typescript
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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: '
|
|
452
|
+
userId: 'user_123',
|
|
263
453
|
userEmail: 'user@example.com',
|
|
264
454
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
notifyUrl: 'https://
|
|
270
|
-
|
|
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 (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
471
|
+
#### Subscription with Trial Period
|
|
283
472
|
|
|
284
473
|
```typescript
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
496
|
+
// Or query by subscriptionRequest
|
|
497
|
+
const response = await waffo.subscription().inquiry({
|
|
498
|
+
subscriptionRequest: 'subscription_request',
|
|
499
|
+
});
|
|
294
500
|
```
|
|
295
501
|
|
|
296
|
-
|
|
502
|
+
#### Cancel Subscription
|
|
297
503
|
|
|
298
504
|
```typescript
|
|
299
|
-
const
|
|
300
|
-
|
|
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
|
-
|
|
510
|
+
#### Get Subscription Management URL
|
|
307
511
|
|
|
308
512
|
```typescript
|
|
309
|
-
const
|
|
310
|
-
subscriptionId: '
|
|
311
|
-
// or use subscriptionRequest: 'SUB_REQ_001'
|
|
513
|
+
const response = await waffo.subscription().manage({
|
|
514
|
+
subscriptionId: 'subscription_id',
|
|
312
515
|
});
|
|
313
516
|
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
|
|
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 (
|
|
330
|
-
|
|
331
|
-
console.log('
|
|
332
|
-
console.log('
|
|
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
|
|
630
|
+
### Refund Query
|
|
337
631
|
|
|
338
632
|
```typescript
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
674
|
+
## Webhook Handling
|
|
354
675
|
|
|
355
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
828
|
+
// Specify exact payment method
|
|
829
|
+
paymentInfo: {
|
|
830
|
+
payMethodType: 'CREDITCARD',
|
|
831
|
+
payMethodName: 'CC_VISA',
|
|
832
|
+
}
|
|
406
833
|
|
|
407
|
-
|
|
408
|
-
|
|
834
|
+
// Combine multiple types
|
|
835
|
+
paymentInfo: {
|
|
836
|
+
payMethodType: 'CREDITCARD,DEBITCARD',
|
|
837
|
+
}
|
|
409
838
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
839
|
+
// E-wallet with specific channel
|
|
840
|
+
paymentInfo: {
|
|
841
|
+
payMethodType: 'EWALLET',
|
|
842
|
+
payMethodName: 'GCASH',
|
|
843
|
+
}
|
|
844
|
+
```
|
|
413
845
|
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
###
|
|
918
|
+
### TLS Security Configuration
|
|
431
919
|
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
941
|
+
### Debug Logging
|
|
445
942
|
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
956
|
+
Or configure programmatically:
|
|
458
957
|
|
|
459
958
|
```typescript
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1222
|
+
# Build the SDK
|
|
492
1223
|
npm run build
|
|
493
1224
|
|
|
494
1225
|
# Run tests
|
|
495
1226
|
npm test
|
|
496
1227
|
|
|
497
|
-
#
|
|
498
|
-
npm run
|
|
1228
|
+
# Type check
|
|
1229
|
+
npm run typecheck
|
|
499
1230
|
|
|
500
|
-
#
|
|
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
|
-
###
|
|
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
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
577
|
-
npm test
|
|
1248
|
+
# Run cross-language test vectors
|
|
1249
|
+
npm run test:vectors
|
|
578
1250
|
```
|
|
579
1251
|
|
|
580
|
-
##
|
|
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
|
-
|
|
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 |
|