@waffo/waffo-node 2.0.2

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 ADDED
@@ -0,0 +1,580 @@
1
+ # Waffo PSP SDK for Node.js
2
+
3
+ Official Node.js SDK for Waffo PSP (Payment Service Provider) acquiring services. This SDK provides secure API communication with RSA signing and comprehensive type definitions for payment acquiring operations.
4
+
5
+ **Language**: [English](./README.md) | [中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
+
7
+ ## Quick Start
8
+
9
+ ```typescript
10
+ import {
11
+ Waffo,
12
+ Environment,
13
+ CurrencyCode,
14
+ ProductName,
15
+ } from '@waffo/waffo-node';
16
+
17
+ // 1. Initialize SDK
18
+ const waffo = new Waffo({
19
+ apiKey: 'your-api-key',
20
+ privateKey: 'your-base64-encoded-private-key',
21
+ environment: Environment.SANDBOX,
22
+ });
23
+
24
+ // 2. Create an order
25
+ const result = await waffo.order.create({
26
+ paymentRequestId: 'REQ_001',
27
+ merchantOrderId: 'ORDER_001',
28
+ orderCurrency: CurrencyCode.IDR,
29
+ orderAmount: '100000',
30
+ orderDescription: 'Product purchase',
31
+ notifyUrl: 'https://merchant.com/notify',
32
+ merchantInfo: { merchantId: 'your-merchant-id' },
33
+ userInfo: {
34
+ userId: 'user_001',
35
+ userEmail: 'user@example.com',
36
+ },
37
+ paymentInfo: {
38
+ productName: ProductName.ONE_TIME_PAYMENT,
39
+ payMethodType: 'EWALLET',
40
+ payMethodName: 'DANA',
41
+ },
42
+ });
43
+
44
+ // 3. Handle response
45
+ if (result.success) {
46
+ console.log('Order created:', result.data);
47
+ // Redirect user to payment page
48
+ if (result.data?.orderAction) {
49
+ const action = JSON.parse(result.data.orderAction);
50
+ window.location.href = action.webUrl;
51
+ }
52
+ }
53
+ ```
54
+
55
+ > **Tip**: Need to generate a new RSA key pair? Use `Waffo.generateKeyPair()` to create one:
56
+ > ```typescript
57
+ > const keyPair = Waffo.generateKeyPair();
58
+ > console.log(keyPair.privateKey); // Keep this secure, use for SDK initialization
59
+ > console.log(keyPair.publicKey); // Share this with Waffo
60
+ > ```
61
+
62
+ ## Features
63
+
64
+ - RSA-2048 request signing and response verification
65
+ - Full TypeScript support with comprehensive type definitions
66
+ - Zero production dependencies (uses only Node.js built-in `crypto` module)
67
+ - Support for Sandbox and Production environments
68
+ - Dual ESM/CommonJS module support
69
+ - Order management (create, query, cancel, refund, capture)
70
+ - Subscription management (create, query, cancel, manage)
71
+ - Refund status inquiry
72
+ - Merchant configuration inquiry (transaction limits, daily limits)
73
+ - Payment method configuration inquiry (availability, maintenance schedules)
74
+ - Webhook handler with automatic signature verification and event routing
75
+ - Webhook signature verification utilities
76
+ - Direct HTTP client access for custom API requests
77
+ - Automatic timestamp defaults for request parameters
78
+
79
+ ## Automatic Timestamp Defaults
80
+
81
+ All timestamp parameters (`orderRequestedAt`, `requestedAt`, `captureRequestedAt`) are **optional** and will automatically default to the current time (`new Date().toISOString()`) if not provided:
82
+
83
+ ```typescript
84
+ // Timestamp is automatically set to current time
85
+ await waffo.order.create({
86
+ paymentRequestId: 'REQ_001',
87
+ merchantOrderId: 'ORDER_001',
88
+ // ... other required fields
89
+ // orderRequestedAt is automatically set
90
+ });
91
+
92
+ // Or explicitly provide a custom timestamp
93
+ await waffo.order.create({
94
+ paymentRequestId: 'REQ_001',
95
+ merchantOrderId: 'ORDER_001',
96
+ orderRequestedAt: '2025-01-01T00:00:00.000Z', // Custom timestamp
97
+ // ... other required fields
98
+ });
99
+ ```
100
+
101
+ This applies to:
102
+ - `CreateOrderParams.orderRequestedAt`
103
+ - `CancelOrderParams.orderRequestedAt`
104
+ - `RefundOrderParams.requestedAt`
105
+ - `CaptureOrderParams.captureRequestedAt`
106
+ - `CreateSubscriptionParams.requestedAt`
107
+ - `CancelSubscriptionParams.requestedAt`
108
+
109
+ ## Installation
110
+
111
+ ```bash
112
+ npm install @waffo/waffo-node
113
+ ```
114
+
115
+ ## Usage
116
+
117
+ ### Initialize the SDK
118
+
119
+ ```typescript
120
+ import { Waffo, Environment } from '@waffo/waffo-node';
121
+
122
+ const waffo = new Waffo({
123
+ apiKey: 'your-api-key',
124
+ privateKey: 'your-base64-encoded-private-key',
125
+ environment: Environment.SANDBOX, // or Environment.PRODUCTION
126
+ });
127
+ ```
128
+
129
+ ### Generate RSA Key Pair
130
+
131
+ ```typescript
132
+ import { Waffo } from '@waffo/waffo-node';
133
+
134
+ const keyPair = Waffo.generateKeyPair();
135
+ console.log(keyPair.privateKey); // Base64 encoded PKCS8 private key
136
+ console.log(keyPair.publicKey); // Base64 encoded X509 public key
137
+ ```
138
+
139
+ ### Create an Order
140
+
141
+ ```typescript
142
+ const result = await waffo.order.create({
143
+ paymentRequestId: 'REQ_001',
144
+ merchantOrderId: 'ORDER_001',
145
+ orderCurrency: CurrencyCode.IDR,
146
+ orderAmount: '100000',
147
+ orderDescription: 'Product purchase',
148
+ notifyUrl: 'https://merchant.com/notify',
149
+ merchantInfo: {
150
+ merchantId: 'your-merchant-id',
151
+ },
152
+ userInfo: {
153
+ userId: 'user_001',
154
+ userEmail: 'user@example.com',
155
+ userPhone: '+62-81234567890',
156
+ userTerminal: UserTerminalType.WEB,
157
+ },
158
+ paymentInfo: {
159
+ productName: ProductName.ONE_TIME_PAYMENT,
160
+ payMethodType: 'EWALLET',
161
+ payMethodName: 'DANA',
162
+ },
163
+ });
164
+
165
+ if (result.success) {
166
+ console.log('Order created:', result.data);
167
+ } else {
168
+ console.error('Error:', result.error);
169
+ }
170
+ ```
171
+
172
+ ### Query Order Status
173
+
174
+ ```typescript
175
+ const result = await waffo.order.inquiry({
176
+ acquiringOrderId: 'A202512230000001',
177
+ // or use paymentRequestId: 'REQ_001'
178
+ });
179
+
180
+ if (result.success) {
181
+ console.log('Order status:', result.data?.orderStatus);
182
+ }
183
+ ```
184
+
185
+ ### Cancel an Order
186
+
187
+ ```typescript
188
+ const result = await waffo.order.cancel({
189
+ acquiringOrderId: 'A202512230000001',
190
+ merchantId: 'your-merchant-id',
191
+ // orderRequestedAt is optional, defaults to current time
192
+ });
193
+ ```
194
+
195
+ ### Refund an Order
196
+
197
+ ```typescript
198
+ const result = await waffo.order.refund({
199
+ refundRequestId: 'REFUND_001',
200
+ acquiringOrderId: 'A202512230000001',
201
+ merchantId: 'your-merchant-id',
202
+ refundAmount: '50000',
203
+ refundReason: 'Customer requested refund',
204
+ refundNotifyUrl: 'https://merchant.com/refund-notify',
205
+ // requestedAt is optional, defaults to current time
206
+ });
207
+ ```
208
+
209
+ ### Query Refund Status
210
+
211
+ ```typescript
212
+ const result = await waffo.refund.inquiry({
213
+ refundRequestId: 'REFUND_001',
214
+ // or use acquiringRefundOrderId: 'R202512230000001'
215
+ });
216
+
217
+ if (result.success) {
218
+ console.log('Refund status:', result.data?.refundStatus);
219
+ }
220
+ ```
221
+
222
+ ### Capture a Pre-authorized Payment
223
+
224
+ ```typescript
225
+ const result = await waffo.order.capture({
226
+ acquiringOrderId: 'A202512230000001',
227
+ merchantId: 'your-merchant-id',
228
+ captureAmount: '100000',
229
+ // captureRequestedAt is optional, defaults to current time
230
+ });
231
+ ```
232
+
233
+ ### Create a Subscription
234
+
235
+ ```typescript
236
+ const result = await waffo.subscription.create({
237
+ subscriptionRequest: 'SUB_REQ_001',
238
+ merchantSubscriptionId: 'MERCHANT_SUB_001',
239
+ currency: CurrencyCode.PHP,
240
+ amount: '100',
241
+ productInfo: {
242
+ periodType: PeriodType.MONTHLY,
243
+ periodInterval: '1',
244
+ numberOfPeriod: '12',
245
+ description: 'Monthly subscription',
246
+ },
247
+ paymentInfo: {
248
+ productName: ProductName.SUBSCRIPTION,
249
+ payMethodType: 'EWALLET',
250
+ payMethodName: 'GCASH',
251
+ },
252
+ merchantInfo: { merchantId: 'your-merchant-id' },
253
+ userInfo: {
254
+ userId: 'user_001',
255
+ userEmail: 'user@example.com',
256
+ },
257
+ goodsInfo: {
258
+ goodsId: 'GOODS_001',
259
+ goodsName: 'Premium Plan',
260
+ },
261
+ notifyUrl: 'https://merchant.com/subscription/notify',
262
+ // requestedAt is optional, defaults to current time
263
+ });
264
+
265
+ if (result.success) {
266
+ console.log('Subscription created:', result.data);
267
+ // Redirect user to complete subscription signing
268
+ if (result.data?.subscriptionAction?.webUrl) {
269
+ window.location.href = result.data.subscriptionAction.webUrl;
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### Query Subscription Status
275
+
276
+ ```typescript
277
+ const result = await waffo.subscription.inquiry({
278
+ merchantId: 'your-merchant-id',
279
+ subscriptionId: 'SUB_202512230000001',
280
+ paymentDetails: '1', // Include payment history
281
+ });
282
+
283
+ if (result.success) {
284
+ console.log('Subscription status:', result.data?.subscriptionStatus);
285
+ }
286
+ ```
287
+
288
+ ### Cancel a Subscription
289
+
290
+ ```typescript
291
+ const result = await waffo.subscription.cancel({
292
+ merchantId: 'your-merchant-id',
293
+ subscriptionId: 'SUB_202512230000001',
294
+ // requestedAt is optional, defaults to current time
295
+ });
296
+ ```
297
+
298
+ ### Get Subscription Management URL
299
+
300
+ ```typescript
301
+ const result = await waffo.subscription.manage({
302
+ subscriptionId: 'SUB_202512230000001',
303
+ // or use subscriptionRequest: 'SUB_REQ_001'
304
+ });
305
+
306
+ if (result.success) {
307
+ console.log('Management URL:', result.data?.managementUrl);
308
+ console.log('Expires at:', result.data?.expiresAt);
309
+ // Redirect user to manage their subscription
310
+ window.location.href = result.data?.managementUrl;
311
+ }
312
+ ```
313
+
314
+ ### Query Merchant Configuration
315
+
316
+ ```typescript
317
+ const result = await waffo.merchantConfig.inquiry({
318
+ merchantId: 'your-merchant-id',
319
+ });
320
+
321
+ if (result.success) {
322
+ console.log('Daily Limit:', result.data?.totalDailyLimit);
323
+ console.log('Remaining Daily Limit:', result.data?.remainingDailyLimit);
324
+ console.log('Transaction Limit:', result.data?.transactionLimit);
325
+ }
326
+ ```
327
+
328
+ ### Query Payment Method Configuration
329
+
330
+ ```typescript
331
+ const result = await waffo.payMethodConfig.inquiry({
332
+ merchantId: 'your-merchant-id',
333
+ });
334
+
335
+ if (result.success) {
336
+ result.data?.payMethodDetails.forEach(method => {
337
+ console.log(`${method.payMethodName}: ${method.currentStatus === '1' ? 'Available' : 'Unavailable'}`);
338
+ if (method.fixedMaintenanceRules) {
339
+ console.log('Maintenance periods:', method.fixedMaintenanceRules);
340
+ }
341
+ });
342
+ }
343
+ ```
344
+
345
+ ### Webhook Handler
346
+
347
+ The SDK provides a built-in webhook handler that automatically handles signature verification, event routing, and response signing:
348
+
349
+ ```typescript
350
+ // In your webhook handler
351
+ app.post('/webhook', async (req, res) => {
352
+ const signature = req.headers['x-signature'] as string;
353
+ const body = JSON.stringify(req.body);
354
+
355
+ const result = await waffo.webhook.handle(body, signature, {
356
+ onPayment: async ({ notification }) => {
357
+ console.log('Payment status:', notification.result.orderStatus);
358
+ // Process payment notification
359
+ },
360
+ onRefund: async ({ notification }) => {
361
+ console.log('Refund status:', notification.result.refundStatus);
362
+ // Process refund notification
363
+ },
364
+ onSubscriptionStatus: async ({ notification }) => {
365
+ console.log('Subscription status:', notification.result.subscriptionStatus);
366
+ // Process subscription status change
367
+ },
368
+ onSubscriptionPayment: async ({ notification }) => {
369
+ console.log('Subscription payment:', notification.result.orderStatus);
370
+ // Process subscription recurring payment
371
+ },
372
+ onError: async (error) => {
373
+ console.error('Webhook error:', error.message);
374
+ },
375
+ });
376
+
377
+ return res.json(result.response);
378
+ });
379
+ ```
380
+
381
+ ### Manual Webhook Signature Verification
382
+
383
+ For more control, you can use the low-level webhook utilities:
384
+
385
+ ```typescript
386
+ import {
387
+ verifyWebhookSignature,
388
+ buildSuccessResponse,
389
+ buildFailedResponse,
390
+ isPaymentNotification,
391
+ isRefundNotification,
392
+ } from '@waffo/waffo-node';
393
+
394
+ // In your webhook handler
395
+ app.post('/webhook', (req, res) => {
396
+ const signature = req.headers['x-signature'];
397
+ const body = JSON.stringify(req.body);
398
+
399
+ // Verify signature
400
+ const verifyResult = verifyWebhookSignature(body, signature, waffoPublicKey);
401
+
402
+ if (!verifyResult.valid) {
403
+ return res.json(buildFailedResponse('Signature verification failed', merchantPrivateKey));
404
+ }
405
+
406
+ // Handle notification based on type
407
+ const notification = verifyResult.notification;
408
+
409
+ if (isPaymentNotification(notification)) {
410
+ // Handle payment notification
411
+ console.log('Payment status:', notification.result.orderStatus);
412
+ } else if (isRefundNotification(notification)) {
413
+ // Handle refund notification
414
+ console.log('Refund status:', notification.result.refundStatus);
415
+ }
416
+
417
+ // Return success response
418
+ return res.json(buildSuccessResponse(merchantPrivateKey));
419
+ });
420
+ ```
421
+
422
+ ### Direct HTTP Client Access
423
+
424
+ For custom API requests not covered by the SDK methods:
425
+
426
+ ```typescript
427
+ const response = await waffo.httpClient.post<CustomResponseType>('/custom/endpoint', {
428
+ body: { key: 'value' }
429
+ });
430
+
431
+ if (response.success) {
432
+ console.log(response.data);
433
+ }
434
+ ```
435
+
436
+ ## Configuration Options
437
+
438
+ | Option | Type | Required | Default | Description |
439
+ |--------|------|----------|---------|-------------|
440
+ | `apiKey` | string | Yes | - | API key provided by Waffo |
441
+ | `privateKey` | string | Yes | - | Base64 encoded PKCS8 private key |
442
+ | `waffoPublicKey` | string | No | Built-in | Custom Waffo public key for response verification |
443
+ | `environment` | Environment | No | PRODUCTION | API environment (SANDBOX or PRODUCTION) |
444
+ | `timeout` | number | No | 30000 | Request timeout in milliseconds |
445
+ | `logger` | Logger | No | - | Logger instance for debugging (can use `console`) |
446
+
447
+ ## API Response Format
448
+
449
+ All API methods return an `ApiResponse<T>` object:
450
+
451
+ ```typescript
452
+ interface ApiResponse<T> {
453
+ success: boolean; // Whether the request was successful
454
+ statusCode: number; // HTTP status code
455
+ data?: T; // Response data (on success)
456
+ error?: string; // Error message (on failure)
457
+ }
458
+ ```
459
+
460
+ ## Type Definitions
461
+
462
+ The SDK exports comprehensive TypeScript types including:
463
+
464
+ - `Environment` - SDK environment enum
465
+ - `CountryCode` - ISO 3166-1 alpha-3 country codes
466
+ - `CurrencyCode` - ISO 4217 currency codes
467
+ - `ProductName` - Payment product type enum (ONE_TIME_PAYMENT, SUBSCRIPTION)
468
+ - `payMethodType` - Payment method category (string: "EWALLET", "CREDITCARD", "BANKTRANSFER", "ONLINE_BANKING", "DIGITAL_BANKING", "OTC", "DEBITCARD")
469
+ - `payMethodName` - Specific payment method (string: "OVO", "DANA", "GOPAY", "GCASH", "CC_VISA", "CC_MASTERCARD", "VA_BCA", "VA_BNI", etc.)
470
+ - `OrderStatus` - Order status enum (PAY_IN_PROGRESS, AUTHORIZATION_REQUIRED, PAY_SUCCESS, ORDER_CLOSE, etc.)
471
+ - `RefundStatus` - Refund status enum (REFUND_IN_PROGRESS, ORDER_PARTIALLY_REFUNDED, ORDER_FULLY_REFUNDED, ORDER_REFUND_FAILED)
472
+ - `SubscriptionStatus` - Subscription status enum (AUTHORIZATION_REQUIRED, ACTIVE, PAUSED, MERCHANT_CANCELLED, etc.)
473
+ - `PeriodType` - Subscription period type enum (DAILY, WEEKLY, MONTHLY, YEARLY)
474
+ - `UserTerminalType` - User terminal type enum (WEB, APP, IN_WALLET_APP, IN_MINI_PROGRAM)
475
+ - Request/Response interfaces for all API operations
476
+
477
+ ## Development
478
+
479
+ ```bash
480
+ # Install dependencies
481
+ npm install
482
+
483
+ # Build (generates both ESM and CJS)
484
+ npm run build
485
+
486
+ # Run tests
487
+ npm test
488
+
489
+ # Run tests with coverage
490
+ npm run test:coverage
491
+
492
+ # Run tests in watch mode
493
+ npm run test:watch
494
+
495
+ # Lint code
496
+ npm run lint
497
+
498
+ # Lint and auto-fix
499
+ npm run lint:fix
500
+
501
+ # Format code
502
+ npm run format
503
+
504
+ # Check formatting
505
+ npm run format:check
506
+ ```
507
+
508
+ ### Code Quality
509
+
510
+ This project uses:
511
+ - **ESLint** - Code linting with TypeScript support
512
+ - **Prettier** - Code formatting
513
+ - **Husky** - Git hooks
514
+ - **lint-staged** - Run linters on staged files
515
+
516
+ Pre-commit hooks automatically run ESLint and Prettier on staged `.ts` files.
517
+
518
+ ### Running E2E Tests
519
+
520
+ E2E tests require Waffo sandbox credentials. The SDK supports multiple merchant configurations for different test scenarios and provides comprehensive test coverage based on the official Waffo test cases document.
521
+
522
+ ```bash
523
+ # Copy the template and fill in your credentials
524
+ cp .env.template .env
525
+ # Edit .env with your credentials
526
+ ```
527
+
528
+ Environment variables:
529
+
530
+ | Variable | Required | Description |
531
+ |----------|----------|-------------|
532
+ | `WAFFO_PUBLIC_KEY` | No | Waffo public key for signature verification (shared) |
533
+ | `ACQUIRING_MERCHANT_ID` | Yes* | Merchant ID for payment/order tests |
534
+ | `ACQUIRING_API_KEY` | Yes* | API key for payment/order tests |
535
+ | `ACQUIRING_MERCHANT_PRIVATE_KEY` | Yes* | Private key for payment/order tests |
536
+ | `SUBSCRIPTION_MERCHANT_ID` | Yes** | Merchant ID for subscription tests |
537
+ | `SUBSCRIPTION_API_KEY` | Yes** | API key for subscription tests |
538
+ | `SUBSCRIPTION_MERCHANT_PRIVATE_KEY` | Yes** | Private key for subscription tests |
539
+
540
+ \* Required for running acquiring/payment E2E tests
541
+ \** Required for running subscription E2E tests
542
+
543
+ **E2E Test Coverage:**
544
+
545
+ | Module | Test Cases |
546
+ |--------|------------|
547
+ | Create Order | Payment success/failure, channel rejection (C0005), idempotency error (A0011), system error (C0001), unknown status (E0001) |
548
+ | Inquiry Order | Query before/after payment |
549
+ | Cancel Order | Cancel before payment, channel not supported (A0015), already paid (A0013) |
550
+ | Refund Order | Full/partial refund, parameter validation (A0003), refund rules (A0014) |
551
+ | Create Subscription | Subscription success/failure, next period payment simulation |
552
+ | Cancel Subscription | Merchant-initiated cancellation |
553
+ | Webhook Notifications | Signature verification for payment, refund, and subscription notifications |
554
+
555
+ **Sandbox Amount Triggers:**
556
+
557
+ | Amount Pattern | Error Code | Description |
558
+ |----------------|------------|-------------|
559
+ | 9, 90, 990, 1990, 19990 | C0005 | Channel rejection |
560
+ | 9.1, 91, 991, 1991, 19991 | C0001 | System error |
561
+ | 9.2, 92, 992, 1992, 19992 | E0001 | Unknown status |
562
+ | 9.3, 93, 993, 1993, 19993 | C0001 | Cancel system error |
563
+ | 9.4, 94, 994, 1994, 19994 | E0001 | Cancel unknown status |
564
+ | 9.5, 95, 995, 1995, 19995 | C0001 | Refund system error |
565
+ | 9.6, 96, 996, 1996, 199996 | E0001 | Refund unknown status |
566
+
567
+ ```bash
568
+ # Run all tests
569
+ npm test
570
+ ```
571
+
572
+ ## Build Output
573
+
574
+ The SDK is built using [tsup](https://tsup.egoist.dev/) and outputs:
575
+
576
+ | File | Format | Description |
577
+ |------|--------|-------------|
578
+ | `dist/index.js` | CommonJS | For `require()` imports |
579
+ | `dist/index.mjs` | ESM | For `import` statements |
580
+ | `dist/index.d.ts` | TypeScript | Type declarations |