@waffo/waffo-integrate 1.0.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.
@@ -0,0 +1,576 @@
1
+ # Node.js Integration Templates
2
+
3
+ ## Package
4
+
5
+ ```
6
+ @waffo/waffo-node
7
+ ```
8
+
9
+ ## SDK Initialization
10
+
11
+ ```typescript
12
+ // src/config/waffo.ts
13
+ import { Waffo, Environment } from '@waffo/waffo-node';
14
+
15
+ let waffoInstance: Waffo | null = null;
16
+
17
+ export function getWaffo(): Waffo {
18
+ if (!waffoInstance) {
19
+ waffoInstance = new Waffo({
20
+ apiKey: process.env.WAFFO_API_KEY!,
21
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
22
+ waffoPublicKey: process.env.WAFFO_PUBLIC_KEY!,
23
+ environment: process.env.NODE_ENV === 'production'
24
+ ? Environment.PRODUCTION
25
+ : Environment.SANDBOX,
26
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
27
+ });
28
+ }
29
+ return waffoInstance;
30
+ }
31
+ ```
32
+
33
+ Alternatively, use the built-in env loader:
34
+
35
+ ```typescript
36
+ import { Waffo } from '@waffo/waffo-node';
37
+ const waffo = Waffo.fromEnv();
38
+ ```
39
+
40
+ Environment variables for `fromEnv()`:
41
+ - `WAFFO_API_KEY`
42
+ - `WAFFO_PRIVATE_KEY`
43
+ - `WAFFO_PUBLIC_KEY`
44
+ - `WAFFO_ENVIRONMENT` (SANDBOX or PRODUCTION)
45
+ - `WAFFO_MERCHANT_ID`
46
+
47
+ ---
48
+
49
+ ## Order Payment Service
50
+
51
+ ```typescript
52
+ // src/services/payment-service.ts
53
+ import { getWaffo } from '../config/waffo';
54
+ import { randomUUID } from 'crypto';
55
+
56
+ /** Generate a 32-char request ID (UUID without dashes, max length 32) */
57
+ function genRequestId(): string {
58
+ return randomUUID().replace(/-/g, '');
59
+ }
60
+
61
+ export interface CreatePaymentInput {
62
+ merchantOrderId: string;
63
+ amount: string;
64
+ currency: string;
65
+ description: string;
66
+ notifyUrl: string;
67
+ successRedirectUrl: string; // URL to redirect after payment
68
+ userId: string;
69
+ userEmail: string;
70
+ userTerminal?: string; // WEB | APP | WAP | SYSTEM (default: WEB)
71
+ payMethodType?: string; // e.g., 'CREDITCARD', 'EWALLET'
72
+ payMethodName?: string; // e.g., 'CC_VISA', 'DANA'
73
+ }
74
+
75
+ export async function createPayment(input: CreatePaymentInput) {
76
+ const waffo = getWaffo();
77
+ const response = await waffo.order().create({
78
+ paymentRequestId: genRequestId(),
79
+ merchantOrderId: input.merchantOrderId,
80
+ orderCurrency: input.currency,
81
+ orderAmount: input.amount,
82
+ orderDescription: input.description,
83
+ notifyUrl: input.notifyUrl,
84
+ successRedirectUrl: input.successRedirectUrl,
85
+ userInfo: {
86
+ userId: input.userId,
87
+ userEmail: input.userEmail,
88
+ userTerminal: input.userTerminal || 'WEB',
89
+ },
90
+ paymentInfo: {
91
+ productName: 'ONE_TIME_PAYMENT',
92
+ ...(input.payMethodType && { payMethodType: input.payMethodType }),
93
+ ...(input.payMethodName && { payMethodName: input.payMethodName }),
94
+ },
95
+ });
96
+
97
+ if (!response.isSuccess()) {
98
+ throw new Error(`Payment creation failed: ${response.getCode()} - ${response.getMessage()}`);
99
+ }
100
+
101
+ return response.getData();
102
+ }
103
+
104
+ export async function queryOrder(paymentRequestId: string) {
105
+ const waffo = getWaffo();
106
+ const response = await waffo.order().inquiry({ paymentRequestId });
107
+
108
+ if (!response.isSuccess()) {
109
+ throw new Error(`Order inquiry failed: ${response.getCode()} - ${response.getMessage()}`);
110
+ }
111
+
112
+ return response.getData();
113
+ }
114
+
115
+ export async function cancelOrder(paymentRequestId: string) {
116
+ const waffo = getWaffo();
117
+ const response = await waffo.order().cancel({ paymentRequestId });
118
+
119
+ if (!response.isSuccess()) {
120
+ throw new Error(`Order cancel failed: ${response.getCode()} - ${response.getMessage()}`);
121
+ }
122
+
123
+ return response.getData();
124
+ }
125
+
126
+ export async function captureOrder(paymentRequestId: string, amount: string, currency: string) {
127
+ const waffo = getWaffo();
128
+ const response = await waffo.order().capture({
129
+ paymentRequestId,
130
+ captureAmount: amount,
131
+ captureCurrency: currency,
132
+ });
133
+
134
+ if (!response.isSuccess()) {
135
+ throw new Error(`Order capture failed: ${response.getCode()} - ${response.getMessage()}`);
136
+ }
137
+
138
+ return response.getData();
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Refund Service
145
+
146
+ ```typescript
147
+ // src/services/refund-service.ts
148
+ import { getWaffo } from '../config/waffo';
149
+ import { v4 as uuidv4 } from 'uuid';
150
+
151
+ export async function refundOrder(
152
+ origPaymentRequestId: string,
153
+ refundAmount: string,
154
+ refundReason?: string,
155
+ ) {
156
+ const waffo = getWaffo();
157
+ const response = await waffo.order().refund({
158
+ refundRequestId: uuidv4(),
159
+ origPaymentRequestId,
160
+ refundAmount,
161
+ refundReason,
162
+ });
163
+
164
+ if (!response.isSuccess()) {
165
+ throw new Error(`Refund failed: ${response.getCode()} - ${response.getMessage()}`);
166
+ }
167
+
168
+ return response.getData();
169
+ }
170
+
171
+ export async function queryRefund(refundRequestId: string) {
172
+ const waffo = getWaffo();
173
+ const response = await waffo.refund().inquiry({ refundRequestId });
174
+
175
+ if (!response.isSuccess()) {
176
+ throw new Error(`Refund inquiry failed: ${response.getCode()} - ${response.getMessage()}`);
177
+ }
178
+
179
+ return response.getData();
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Subscription Service
186
+
187
+ ```typescript
188
+ // src/services/subscription-service.ts
189
+ import { getWaffo } from '../config/waffo';
190
+ import { v4 as uuidv4 } from 'uuid';
191
+
192
+ export interface CreateSubscriptionInput {
193
+ merchantSubscriptionId: string;
194
+ amount: string;
195
+ currency: string;
196
+ description: string;
197
+ notifyUrl: string;
198
+ userId: string;
199
+ userEmail: string;
200
+ productId: string;
201
+ productName: string;
202
+ periodType: 'DAILY' | 'WEEKLY' | 'MONTHLY';
203
+ periodInterval: string; // e.g., '1' for every period
204
+ goodsUrl: string;
205
+ successRedirectUrl: string;
206
+ }
207
+
208
+ export async function createSubscription(input: CreateSubscriptionInput) {
209
+ const waffo = getWaffo();
210
+ const response = await waffo.subscription().create({
211
+ subscriptionRequest: uuidv4(),
212
+ merchantSubscriptionId: input.merchantSubscriptionId,
213
+ currency: input.currency,
214
+ amount: input.amount,
215
+ orderDescription: input.description,
216
+ notifyUrl: input.notifyUrl,
217
+ successRedirectUrl: input.successRedirectUrl,
218
+ productInfo: {
219
+ description: input.productName,
220
+ periodType: input.periodType,
221
+ periodInterval: input.periodInterval,
222
+ },
223
+ userInfo: {
224
+ userId: input.userId,
225
+ userEmail: input.userEmail,
226
+ userTerminal: input.userTerminal || 'WEB', // WEB for PC, APP for mobile/tablet
227
+ },
228
+ goodsInfo: {
229
+ goodsId: input.productId,
230
+ goodsName: input.productName,
231
+ goodsUrl: input.goodsUrl,
232
+ },
233
+ paymentInfo: {
234
+ productName: 'SUBSCRIPTION',
235
+ payMethodType: 'CREDITCARD,DEBITCARD,APPLEPAY,GOOGLEPAY',
236
+ },
237
+ });
238
+
239
+ if (!response.isSuccess()) {
240
+ throw new Error(`Subscription creation failed: ${response.getCode()} - ${response.getMessage()}`);
241
+ }
242
+
243
+ return response.getData();
244
+ }
245
+
246
+ export async function querySubscription(subscriptionRequest: string) {
247
+ const waffo = getWaffo();
248
+ const response = await waffo.subscription().inquiry({ subscriptionRequest });
249
+
250
+ if (!response.isSuccess()) {
251
+ throw new Error(`Subscription inquiry failed: ${response.getCode()} - ${response.getMessage()}`);
252
+ }
253
+
254
+ return response.getData();
255
+ }
256
+
257
+ export async function cancelSubscription(subscriptionRequest: string) {
258
+ const waffo = getWaffo();
259
+ const response = await waffo.subscription().cancel({ subscriptionRequest });
260
+
261
+ if (!response.isSuccess()) {
262
+ throw new Error(`Subscription cancel failed: ${response.getCode()} - ${response.getMessage()}`);
263
+ }
264
+
265
+ return response.getData();
266
+ }
267
+
268
+ /**
269
+ * Get subscription management URL.
270
+ * Note: manage() only works when subscription is ACTIVE (after payment).
271
+ * Type assertion needed due to SDK type limitation.
272
+ */
273
+ export async function manageSubscription(subscriptionRequest: string) {
274
+ const waffo = getWaffo();
275
+ const response = await waffo.subscription().manage({ subscriptionRequest } as any);
276
+
277
+ if (!response.isSuccess()) {
278
+ throw new Error(`Subscription manage failed: ${response.getCode()} - ${response.getMessage()}`);
279
+ }
280
+
281
+ const data = response.getData() as any;
282
+ return data?.managementUrl;
283
+ }
284
+
285
+ export interface ChangeSubscriptionInput {
286
+ originSubscriptionRequest: string;
287
+ remainingAmount: string;
288
+ currency: string;
289
+ notifyUrl: string;
290
+ userId: string;
291
+ userEmail: string;
292
+ newProductName: string;
293
+ periodType: 'DAILY' | 'WEEKLY' | 'MONTHLY';
294
+ periodInterval: string;
295
+ newAmount: string;
296
+ }
297
+
298
+ export async function changeSubscription(input: ChangeSubscriptionInput) {
299
+ const waffo = getWaffo();
300
+ const response = await waffo.subscription().change({
301
+ subscriptionRequest: uuidv4(),
302
+ originSubscriptionRequest: input.originSubscriptionRequest,
303
+ remainingAmount: input.remainingAmount,
304
+ currency: input.currency,
305
+ notifyUrl: input.notifyUrl,
306
+ productInfoList: [{
307
+ description: input.newProductName,
308
+ periodType: input.periodType,
309
+ periodInterval: input.periodInterval,
310
+ amount: input.newAmount,
311
+ }],
312
+ userInfo: {
313
+ userId: input.userId,
314
+ userEmail: input.userEmail,
315
+ userTerminal: input.userTerminal || 'WEB',
316
+ },
317
+ goodsInfo: {
318
+ goodsId: 'subscription',
319
+ goodsName: input.newProductName,
320
+ },
321
+ paymentInfo: {
322
+ productName: 'SUBSCRIPTION',
323
+ },
324
+ });
325
+
326
+ if (!response.isSuccess()) {
327
+ throw new Error(`Subscription change failed: ${response.getCode()} - ${response.getMessage()}`);
328
+ }
329
+
330
+ return response.getData();
331
+ }
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Webhook Handler
337
+
338
+ ### Express
339
+
340
+ ```typescript
341
+ // src/webhooks/waffo-webhook.ts
342
+ import express from 'express';
343
+ import { getWaffo } from '../config/waffo';
344
+
345
+ const router = express.Router();
346
+
347
+ // Use raw body parser for webhook signature verification
348
+ router.post('/waffo/webhook',
349
+ express.raw({ type: 'application/json' }),
350
+ async (req, res) => {
351
+ const waffo = getWaffo();
352
+ const body = req.body.toString();
353
+ const signature = req.headers['x-signature'] as string;
354
+
355
+ const handler = waffo.webhook()
356
+ .onPayment((notification) => {
357
+ const result = notification.result;
358
+ console.log(`Payment ${result?.orderStatus}: orderId=${result?.acquiringOrderId}`);
359
+ // TODO: Update your order status in database
360
+ })
361
+ .onRefund((notification) => {
362
+ const result = notification.result;
363
+ console.log(`Refund ${result?.refundStatus}: refundId=${result?.acquiringRefundOrderId}`);
364
+ // TODO: Update your refund status in database
365
+ })
366
+ .onSubscriptionStatus((notification) => {
367
+ const result = notification.result;
368
+ console.log(`Subscription ${result?.subscriptionStatus}: subId=${result?.subscriptionId}`);
369
+ // TODO: Update your subscription status in database
370
+ })
371
+ .onSubscriptionPeriodChanged((notification) => {
372
+ const result = notification.result;
373
+ console.log(`Period changed: subId=${result?.subscriptionId}`);
374
+ // TODO: Record billing period result
375
+ })
376
+ .onSubscriptionChange((notification) => {
377
+ const result = notification.result;
378
+ console.log(`Subscription change ${result?.subscriptionChangeStatus}`);
379
+ // TODO: Handle subscription upgrade/downgrade result
380
+ });
381
+
382
+ const webhookResult = await handler.handleWebhook(body, signature);
383
+
384
+ res.setHeader('X-SIGNATURE', webhookResult.responseSignature);
385
+ res.status(200).send(webhookResult.responseBody);
386
+ }
387
+ );
388
+
389
+ export default router;
390
+ ```
391
+
392
+ ### NestJS
393
+
394
+ ```typescript
395
+ // src/webhooks/waffo-webhook.controller.ts
396
+ import { Controller, Post, Req, Res, RawBodyRequest } from '@nestjs/common';
397
+ import { Request, Response } from 'express';
398
+ import { getWaffo } from '../config/waffo';
399
+
400
+ @Controller('waffo')
401
+ export class WaffoWebhookController {
402
+ @Post('webhook')
403
+ async handleWebhook(
404
+ @Req() req: RawBodyRequest<Request>,
405
+ @Res() res: Response,
406
+ ) {
407
+ const waffo = getWaffo();
408
+ const body = req.rawBody?.toString() ?? '';
409
+ const signature = req.headers['x-signature'] as string;
410
+
411
+ const handler = waffo.webhook()
412
+ .onPayment((notification) => {
413
+ // TODO: Handle payment notification
414
+ })
415
+ .onRefund((notification) => {
416
+ // TODO: Handle refund notification
417
+ });
418
+
419
+ const result = await handler.handleWebhook(body, signature);
420
+
421
+ res.setHeader('X-SIGNATURE', result.responseSignature);
422
+ res.status(200).send(result.responseBody);
423
+ }
424
+ }
425
+ ```
426
+
427
+ ### Fastify
428
+
429
+ ```typescript
430
+ // src/webhooks/waffo-webhook.ts
431
+ import { FastifyInstance } from 'fastify';
432
+ import { getWaffo } from '../config/waffo';
433
+
434
+ export async function waffoWebhookRoute(fastify: FastifyInstance) {
435
+ fastify.addContentTypeParser(
436
+ 'application/json',
437
+ { parseAs: 'string' },
438
+ (req, body, done) => done(null, body),
439
+ );
440
+
441
+ fastify.post('/waffo/webhook', async (request, reply) => {
442
+ const waffo = getWaffo();
443
+ const body = request.body as string;
444
+ const signature = request.headers['x-signature'] as string;
445
+
446
+ const handler = waffo.webhook()
447
+ .onPayment((notification) => {
448
+ // TODO: Handle payment notification
449
+ })
450
+ .onRefund((notification) => {
451
+ // TODO: Handle refund notification
452
+ });
453
+
454
+ const result = await handler.handleWebhook(body, signature);
455
+
456
+ reply.header('X-SIGNATURE', result.responseSignature);
457
+ reply.status(200).send(result.responseBody);
458
+ });
459
+ }
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Test Template (Sandbox Integration)
465
+
466
+ ```typescript
467
+ // tests/payment.test.ts
468
+ import { describe, it, expect } from 'vitest'; // or jest
469
+ import { Waffo, Environment } from '@waffo/waffo-node';
470
+ import { v4 as uuidv4 } from 'uuid';
471
+
472
+ const HAS_CREDENTIALS = !!(
473
+ process.env.WAFFO_API_KEY &&
474
+ process.env.WAFFO_PRIVATE_KEY &&
475
+ process.env.WAFFO_PUBLIC_KEY &&
476
+ process.env.WAFFO_MERCHANT_ID
477
+ );
478
+
479
+ const conditionalIt = HAS_CREDENTIALS ? it : it.skip;
480
+
481
+ function createWaffo(): Waffo {
482
+ return new Waffo({
483
+ apiKey: process.env.WAFFO_API_KEY!,
484
+ privateKey: process.env.WAFFO_PRIVATE_KEY!,
485
+ waffoPublicKey: process.env.WAFFO_PUBLIC_KEY!,
486
+ environment: Environment.SANDBOX,
487
+ merchantId: process.env.WAFFO_MERCHANT_ID!,
488
+ });
489
+ }
490
+
491
+ describe('Waffo Payment Integration', () => {
492
+ conditionalIt('creates a payment order', async () => {
493
+ const waffo = createWaffo();
494
+ const paymentRequestId = uuidv4();
495
+
496
+ const response = await waffo.order().create({
497
+ paymentRequestId,
498
+ merchantOrderId: `test-${Date.now()}`,
499
+ orderCurrency: 'USD',
500
+ orderAmount: '1.00',
501
+ orderDescription: 'Integration test order',
502
+ notifyUrl: 'https://example.com/webhook',
503
+ userInfo: { userId: 'test-user', userEmail: 'test@example.com' },
504
+ paymentInfo: { productName: 'Test' },
505
+ });
506
+
507
+ if (!response.isSuccess()) {
508
+ console.error(`Create order failed: paymentRequestId=${paymentRequestId}, ` +
509
+ `code=${response.getCode()}, message=${response.getMessage()}, ` +
510
+ `data=${JSON.stringify(response.getData())}`);
511
+ }
512
+ expect(response.isSuccess()).toBe(true);
513
+
514
+ const data = response.getData();
515
+ expect(data?.acquiringOrderId).toBeTruthy();
516
+ console.log(`Order created: acquiringOrderId=${data?.acquiringOrderId}`);
517
+ });
518
+
519
+ conditionalIt('queries an order', async () => {
520
+ const waffo = createWaffo();
521
+ const paymentRequestId = uuidv4();
522
+
523
+ // Create first
524
+ await waffo.order().create({
525
+ paymentRequestId,
526
+ merchantOrderId: `test-${Date.now()}`,
527
+ orderCurrency: 'USD',
528
+ orderAmount: '1.00',
529
+ orderDescription: 'Test',
530
+ notifyUrl: 'https://example.com/webhook',
531
+ userInfo: { userId: 'test-user', userEmail: 'test@example.com' },
532
+ paymentInfo: { productName: 'Test' },
533
+ });
534
+
535
+ // Then query
536
+ const response = await waffo.order().inquiry({ paymentRequestId });
537
+
538
+ if (!response.isSuccess()) {
539
+ console.error(`Inquiry failed: paymentRequestId=${paymentRequestId}, ` +
540
+ `code=${response.getCode()}, message=${response.getMessage()}`);
541
+ }
542
+ expect(response.isSuccess()).toBe(true);
543
+ });
544
+ });
545
+ ```
546
+
547
+ ---
548
+
549
+ ## Merchant Config & Payment Method Query
550
+
551
+ ```typescript
552
+ // src/services/config-service.ts
553
+ import { getWaffo } from '../config/waffo';
554
+
555
+ export async function getMerchantConfig() {
556
+ const waffo = getWaffo();
557
+ const response = await waffo.merchantConfig().inquiry({});
558
+
559
+ if (!response.isSuccess()) {
560
+ throw new Error(`Merchant config inquiry failed: ${response.getCode()} - ${response.getMessage()}`);
561
+ }
562
+
563
+ return response.getData();
564
+ }
565
+
566
+ export async function getPaymentMethods() {
567
+ const waffo = getWaffo();
568
+ const response = await waffo.payMethodConfig().inquiry({});
569
+
570
+ if (!response.isSuccess()) {
571
+ throw new Error(`Pay method inquiry failed: ${response.getCode()} - ${response.getMessage()}`);
572
+ }
573
+
574
+ return response.getData();
575
+ }
576
+ ```