@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.
@@ -0,0 +1,580 @@
1
+ # Waffo PSP Node.js SDK
2
+
3
+ Waffo PSP(支付服务提供商)收单服务的官方 Node.js SDK。本 SDK 提供基于 RSA 签名的安全 API 通信,以及完整的收单操作类型定义支持。
4
+
5
+ **语言**: [English](./README.md) | [中文](./README.zh-CN.md) | [日本語](./README.ja.md)
6
+
7
+ ## 快速开始
8
+
9
+ ```typescript
10
+ import {
11
+ Waffo,
12
+ Environment,
13
+ CurrencyCode,
14
+ ProductName,
15
+ } from '@waffo/waffo-node';
16
+
17
+ // 1. 初始化 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. 创建订单
25
+ const result = await waffo.order.create({
26
+ paymentRequestId: 'REQ_001',
27
+ merchantOrderId: 'ORDER_001',
28
+ orderCurrency: CurrencyCode.IDR,
29
+ orderAmount: '100000',
30
+ orderDescription: '商品购买',
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. 处理响应
45
+ if (result.success) {
46
+ console.log('订单创建成功:', result.data);
47
+ // 引导用户跳转到支付页面
48
+ if (result.data?.orderAction) {
49
+ const action = JSON.parse(result.data.orderAction);
50
+ window.location.href = action.webUrl;
51
+ }
52
+ }
53
+ ```
54
+
55
+ > **提示**:需要生成新的 RSA 密钥对?使用 `Waffo.generateKeyPair()` 来创建:
56
+ > ```typescript
57
+ > const keyPair = Waffo.generateKeyPair();
58
+ > console.log(keyPair.privateKey); // 妥善保管,用于 SDK 初始化
59
+ > console.log(keyPair.publicKey); // 提供给 Waffo
60
+ > ```
61
+
62
+ ## 特性
63
+
64
+ - RSA-2048 请求签名与响应验证
65
+ - 完整的 TypeScript 类型定义支持
66
+ - 零生产依赖(仅使用 Node.js 内置 `crypto` 模块)
67
+ - 支持沙箱和生产环境
68
+ - 双模块格式支持(ESM/CommonJS)
69
+ - 订单管理(创建、查询、取消、退款、捕获)
70
+ - 订阅管理(创建、查询、取消、获取管理链接)
71
+ - 退款状态查询
72
+ - 商户配置查询(交易限额、每日限额)
73
+ - 支付方式配置查询(可用性、维护时段)
74
+ - Webhook 处理器,支持自动签名验证和事件路由
75
+ - Webhook 签名验证工具
76
+ - 直接 HTTP 客户端访问,支持自定义 API 请求
77
+ - 请求时间戳参数自动默认值
78
+
79
+ ## 时间戳参数自动默认值
80
+
81
+ 所有时间戳参数(`orderRequestedAt`、`requestedAt`、`captureRequestedAt`)均为**可选参数**,如不提供将自动使用当前时间(`new Date().toISOString()`):
82
+
83
+ ```typescript
84
+ // 时间戳自动设置为当前时间
85
+ await waffo.order.create({
86
+ paymentRequestId: 'REQ_001',
87
+ merchantOrderId: 'ORDER_001',
88
+ // ... 其他必填字段
89
+ // orderRequestedAt 自动设置
90
+ });
91
+
92
+ // 或显式提供自定义时间戳
93
+ await waffo.order.create({
94
+ paymentRequestId: 'REQ_001',
95
+ merchantOrderId: 'ORDER_001',
96
+ orderRequestedAt: '2025-01-01T00:00:00.000Z', // 自定义时间戳
97
+ // ... 其他必填字段
98
+ });
99
+ ```
100
+
101
+ 此特性适用于:
102
+ - `CreateOrderParams.orderRequestedAt`
103
+ - `CancelOrderParams.orderRequestedAt`
104
+ - `RefundOrderParams.requestedAt`
105
+ - `CaptureOrderParams.captureRequestedAt`
106
+ - `CreateSubscriptionParams.requestedAt`
107
+ - `CancelSubscriptionParams.requestedAt`
108
+
109
+ ## 安装
110
+
111
+ ```bash
112
+ npm install @waffo/waffo-node
113
+ ```
114
+
115
+ ## 使用指南
116
+
117
+ ### 初始化 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, // 或 Environment.PRODUCTION
126
+ });
127
+ ```
128
+
129
+ ### 生成 RSA 密钥对
130
+
131
+ ```typescript
132
+ import { Waffo } from '@waffo/waffo-node';
133
+
134
+ const keyPair = Waffo.generateKeyPair();
135
+ console.log(keyPair.privateKey); // Base64 编码的 PKCS8 私钥
136
+ console.log(keyPair.publicKey); // Base64 编码的 X509 公钥
137
+ ```
138
+
139
+ ### 创建订单
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: '商品购买',
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('订单创建成功:', result.data);
167
+ } else {
168
+ console.error('错误:', result.error);
169
+ }
170
+ ```
171
+
172
+ ### 查询订单状态
173
+
174
+ ```typescript
175
+ const result = await waffo.order.inquiry({
176
+ acquiringOrderId: 'A202512230000001',
177
+ // 或使用 paymentRequestId: 'REQ_001'
178
+ });
179
+
180
+ if (result.success) {
181
+ console.log('订单状态:', result.data?.orderStatus);
182
+ }
183
+ ```
184
+
185
+ ### 取消订单
186
+
187
+ ```typescript
188
+ const result = await waffo.order.cancel({
189
+ acquiringOrderId: 'A202512230000001',
190
+ merchantId: 'your-merchant-id',
191
+ // orderRequestedAt 为可选参数,默认为当前时间
192
+ });
193
+ ```
194
+
195
+ ### 订单退款
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: '用户申请退款',
204
+ refundNotifyUrl: 'https://merchant.com/refund-notify',
205
+ // requestedAt 为可选参数,默认为当前时间
206
+ });
207
+ ```
208
+
209
+ ### 查询退款状态
210
+
211
+ ```typescript
212
+ const result = await waffo.refund.inquiry({
213
+ refundRequestId: 'REFUND_001',
214
+ // 或使用 acquiringRefundOrderId: 'R202512230000001'
215
+ });
216
+
217
+ if (result.success) {
218
+ console.log('退款状态:', result.data?.refundStatus);
219
+ }
220
+ ```
221
+
222
+ ### 捕获预授权支付
223
+
224
+ ```typescript
225
+ const result = await waffo.order.capture({
226
+ acquiringOrderId: 'A202512230000001',
227
+ merchantId: 'your-merchant-id',
228
+ captureAmount: '100000',
229
+ // captureRequestedAt 为可选参数,默认为当前时间
230
+ });
231
+ ```
232
+
233
+ ### 创建订阅
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: '月度订阅',
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: '高级会员',
260
+ },
261
+ notifyUrl: 'https://merchant.com/subscription/notify',
262
+ // requestedAt 为可选参数,默认为当前时间
263
+ });
264
+
265
+ if (result.success) {
266
+ console.log('订阅创建成功:', result.data);
267
+ // 引导用户完成订阅签约
268
+ if (result.data?.subscriptionAction?.webUrl) {
269
+ window.location.href = result.data.subscriptionAction.webUrl;
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### 查询订阅状态
275
+
276
+ ```typescript
277
+ const result = await waffo.subscription.inquiry({
278
+ merchantId: 'your-merchant-id',
279
+ subscriptionId: 'SUB_202512230000001',
280
+ paymentDetails: '1', // 包含支付历史
281
+ });
282
+
283
+ if (result.success) {
284
+ console.log('订阅状态:', result.data?.subscriptionStatus);
285
+ }
286
+ ```
287
+
288
+ ### 取消订阅
289
+
290
+ ```typescript
291
+ const result = await waffo.subscription.cancel({
292
+ merchantId: 'your-merchant-id',
293
+ subscriptionId: 'SUB_202512230000001',
294
+ // requestedAt 为可选参数,默认为当前时间
295
+ });
296
+ ```
297
+
298
+ ### 获取订阅管理链接
299
+
300
+ ```typescript
301
+ const result = await waffo.subscription.manage({
302
+ subscriptionId: 'SUB_202512230000001',
303
+ // 或使用 subscriptionRequest: 'SUB_REQ_001'
304
+ });
305
+
306
+ if (result.success) {
307
+ console.log('管理链接:', result.data?.managementUrl);
308
+ console.log('过期时间:', result.data?.expiresAt);
309
+ // 引导用户管理其订阅
310
+ window.location.href = result.data?.managementUrl;
311
+ }
312
+ ```
313
+
314
+ ### 查询商户配置
315
+
316
+ ```typescript
317
+ const result = await waffo.merchantConfig.inquiry({
318
+ merchantId: 'your-merchant-id',
319
+ });
320
+
321
+ if (result.success) {
322
+ console.log('每日限额:', result.data?.totalDailyLimit);
323
+ console.log('剩余每日限额:', result.data?.remainingDailyLimit);
324
+ console.log('单笔交易限额:', result.data?.transactionLimit);
325
+ }
326
+ ```
327
+
328
+ ### 查询支付方式配置
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' ? '可用' : '不可用'}`);
338
+ if (method.fixedMaintenanceRules) {
339
+ console.log('维护时段:', method.fixedMaintenanceRules);
340
+ }
341
+ });
342
+ }
343
+ ```
344
+
345
+ ### Webhook 处理器
346
+
347
+ SDK 提供了内置的 Webhook 处理器,可自动处理签名验证、事件路由和响应签名:
348
+
349
+ ```typescript
350
+ // 在 Webhook 处理器中
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('支付状态:', notification.result.orderStatus);
358
+ // 处理支付通知
359
+ },
360
+ onRefund: async ({ notification }) => {
361
+ console.log('退款状态:', notification.result.refundStatus);
362
+ // 处理退款通知
363
+ },
364
+ onSubscriptionStatus: async ({ notification }) => {
365
+ console.log('订阅状态:', notification.result.subscriptionStatus);
366
+ // 处理订阅状态变更
367
+ },
368
+ onSubscriptionPayment: async ({ notification }) => {
369
+ console.log('订阅支付:', notification.result.orderStatus);
370
+ // 处理订阅周期性支付
371
+ },
372
+ onError: async (error) => {
373
+ console.error('Webhook 错误:', error.message);
374
+ },
375
+ });
376
+
377
+ return res.json(result.response);
378
+ });
379
+ ```
380
+
381
+ ### 手动 Webhook 签名验证
382
+
383
+ 如需更细粒度的控制,可以使用底层 Webhook 工具函数:
384
+
385
+ ```typescript
386
+ import {
387
+ verifyWebhookSignature,
388
+ buildSuccessResponse,
389
+ buildFailedResponse,
390
+ isPaymentNotification,
391
+ isRefundNotification,
392
+ } from '@waffo/waffo-node';
393
+
394
+ // 在 Webhook 处理器中
395
+ app.post('/webhook', (req, res) => {
396
+ const signature = req.headers['x-signature'];
397
+ const body = JSON.stringify(req.body);
398
+
399
+ // 验证签名
400
+ const verifyResult = verifyWebhookSignature(body, signature, waffoPublicKey);
401
+
402
+ if (!verifyResult.valid) {
403
+ return res.json(buildFailedResponse('签名验证失败', merchantPrivateKey));
404
+ }
405
+
406
+ // 根据类型处理通知
407
+ const notification = verifyResult.notification;
408
+
409
+ if (isPaymentNotification(notification)) {
410
+ // 处理支付通知
411
+ console.log('支付状态:', notification.result.orderStatus);
412
+ } else if (isRefundNotification(notification)) {
413
+ // 处理退款通知
414
+ console.log('退款状态:', notification.result.refundStatus);
415
+ }
416
+
417
+ // 返回成功响应
418
+ return res.json(buildSuccessResponse(merchantPrivateKey));
419
+ });
420
+ ```
421
+
422
+ ### 直接 HTTP 客户端访问
423
+
424
+ 用于 SDK 方法未覆盖的自定义 API 请求:
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
+ ## 配置选项
437
+
438
+ | 选项 | 类型 | 必填 | 默认值 | 描述 |
439
+ |------|------|------|--------|------|
440
+ | `apiKey` | string | 是 | - | Waffo 提供的 API 密钥 |
441
+ | `privateKey` | string | 是 | - | Base64 编码的 PKCS8 私钥 |
442
+ | `waffoPublicKey` | string | 否 | 内置公钥 | 用于响应验证的自定义 Waffo 公钥 |
443
+ | `environment` | Environment | 否 | PRODUCTION | API 环境(SANDBOX 或 PRODUCTION) |
444
+ | `timeout` | number | 否 | 30000 | 请求超时时间(毫秒) |
445
+ | `logger` | Logger | 否 | - | 调试用的日志实例(可使用 `console`) |
446
+
447
+ ## API 响应格式
448
+
449
+ 所有 API 方法返回 `ApiResponse<T>` 对象:
450
+
451
+ ```typescript
452
+ interface ApiResponse<T> {
453
+ success: boolean; // 请求是否成功
454
+ statusCode: number; // HTTP 状态码
455
+ data?: T; // 响应数据(成功时)
456
+ error?: string; // 错误信息(失败时)
457
+ }
458
+ ```
459
+
460
+ ## 类型定义
461
+
462
+ SDK 导出完整的 TypeScript 类型定义,包括:
463
+
464
+ - `Environment` - SDK 环境枚举
465
+ - `CountryCode` - ISO 3166-1 alpha-3 国家代码
466
+ - `CurrencyCode` - ISO 4217 货币代码
467
+ - `ProductName` - 支付产品类型枚举(ONE_TIME_PAYMENT、SUBSCRIPTION)
468
+ - `payMethodType` - 支付方式类别(字符串: "EWALLET"、"CREDITCARD"、"BANKTRANSFER"、"ONLINE_BANKING"、"DIGITAL_BANKING"、"OTC"、"DEBITCARD")
469
+ - `payMethodName` - 具体支付方式(字符串: "OVO"、"DANA"、"GOPAY"、"GCASH"、"CC_VISA"、"CC_MASTERCARD"、"VA_BCA"、"VA_BNI" 等)
470
+ - `OrderStatus` - 订单状态枚举(PAY_IN_PROGRESS、AUTHORIZATION_REQUIRED、PAY_SUCCESS、ORDER_CLOSE 等)
471
+ - `RefundStatus` - 退款状态枚举(REFUND_IN_PROGRESS、ORDER_PARTIALLY_REFUNDED、ORDER_FULLY_REFUNDED、ORDER_REFUND_FAILED)
472
+ - `SubscriptionStatus` - 订阅状态枚举(AUTHORIZATION_REQUIRED、ACTIVE、PAUSED、MERCHANT_CANCELLED 等)
473
+ - `PeriodType` - 订阅周期类型枚举(DAILY、WEEKLY、MONTHLY、YEARLY)
474
+ - `UserTerminalType` - 用户终端类型枚举(WEB、APP、IN_WALLET_APP、IN_MINI_PROGRAM)
475
+ - 所有 API 操作的请求/响应接口
476
+
477
+ ## 开发
478
+
479
+ ```bash
480
+ # 安装依赖
481
+ npm install
482
+
483
+ # 构建(同时生成 ESM 和 CJS)
484
+ npm run build
485
+
486
+ # 运行测试
487
+ npm test
488
+
489
+ # 运行测试并生成覆盖率报告
490
+ npm run test:coverage
491
+
492
+ # 监听模式运行测试
493
+ npm run test:watch
494
+
495
+ # 代码检查
496
+ npm run lint
497
+
498
+ # 代码检查并自动修复
499
+ npm run lint:fix
500
+
501
+ # 格式化代码
502
+ npm run format
503
+
504
+ # 检查代码格式
505
+ npm run format:check
506
+ ```
507
+
508
+ ### 代码质量
509
+
510
+ 本项目使用以下工具:
511
+ - **ESLint** - 代码检查,支持 TypeScript
512
+ - **Prettier** - 代码格式化
513
+ - **Husky** - Git 钩子
514
+ - **lint-staged** - 对暂存文件运行检查
515
+
516
+ 提交代码时,pre-commit 钩子会自动对暂存的 `.ts` 文件运行 ESLint 和 Prettier。
517
+
518
+ ### 运行 E2E 测试
519
+
520
+ E2E 测试需要 Waffo 沙箱环境凭证。SDK 支持多个商户配置,用于不同的测试场景,并基于官方 Waffo 测试用例文档提供全面的测试覆盖。
521
+
522
+ ```bash
523
+ # 复制模板文件并填写凭证
524
+ cp .env.template .env
525
+ # 编辑 .env 文件,填入你的凭证
526
+ ```
527
+
528
+ 环境变量说明:
529
+
530
+ | 变量名 | 必填 | 描述 |
531
+ |--------|------|------|
532
+ | `WAFFO_PUBLIC_KEY` | 否 | 用于签名验证的 Waffo 公钥(通用) |
533
+ | `ACQUIRING_MERCHANT_ID` | 是* | 支付/订单测试用的商户 ID |
534
+ | `ACQUIRING_API_KEY` | 是* | 支付/订单测试用的 API 密钥 |
535
+ | `ACQUIRING_MERCHANT_PRIVATE_KEY` | 是* | 支付/订单测试用的私钥 |
536
+ | `SUBSCRIPTION_MERCHANT_ID` | 是** | 订阅测试用的商户 ID |
537
+ | `SUBSCRIPTION_API_KEY` | 是** | 订阅测试用的 API 密钥 |
538
+ | `SUBSCRIPTION_MERCHANT_PRIVATE_KEY` | 是** | 订阅测试用的私钥 |
539
+
540
+ \* 运行支付/订单 E2E 测试时必填
541
+ \** 运行订阅 E2E 测试时必填
542
+
543
+ **E2E 测试覆盖范围:**
544
+
545
+ | 模块 | 测试用例 |
546
+ |------|----------|
547
+ | 创建订单 | 支付成功/失败、渠道拒绝 (C0005)、幂等错误 (A0011)、系统错误 (C0001)、未知状态 (E0001) |
548
+ | 查询订单 | 支付前/支付后查询 |
549
+ | 取消订单 | 支付前取消、渠道不支持 (A0015)、已支付 (A0013) |
550
+ | 退款订单 | 全额/部分退款、参数校验 (A0003)、退款规则 (A0014) |
551
+ | 创建订阅 | 订阅成功/失败、下期支付模拟 |
552
+ | 取消订阅 | 商户发起取消 |
553
+ | Webhook 通知 | 支付、退款、订阅通知签名验证 |
554
+
555
+ **沙箱环境金额触发规则:**
556
+
557
+ | 金额模式 | 错误码 | 描述 |
558
+ |----------|--------|------|
559
+ | 9, 90, 990, 1990, 19990 | C0005 | 渠道拒绝 |
560
+ | 9.1, 91, 991, 1991, 19991 | C0001 | 系统错误 |
561
+ | 9.2, 92, 992, 1992, 19992 | E0001 | 未知状态 |
562
+ | 9.3, 93, 993, 1993, 19993 | C0001 | 取消系统错误 |
563
+ | 9.4, 94, 994, 1994, 19994 | E0001 | 取消未知状态 |
564
+ | 9.5, 95, 995, 1995, 19995 | C0001 | 退款系统错误 |
565
+ | 9.6, 96, 996, 1996, 199996 | E0001 | 退款未知状态 |
566
+
567
+ ```bash
568
+ # 运行所有测试
569
+ npm test
570
+ ```
571
+
572
+ ## 构建产物
573
+
574
+ SDK 使用 [tsup](https://tsup.egoist.dev/) 构建,输出以下文件:
575
+
576
+ | 文件 | 格式 | 描述 |
577
+ |------|------|------|
578
+ | `dist/index.js` | CommonJS | 用于 `require()` 导入 |
579
+ | `dist/index.mjs` | ESM | 用于 `import` 语句 |
580
+ | `dist/index.d.ts` | TypeScript | 类型声明文件 |