@waffo/waffo-node
v2.0.2
Published
Official Node.js SDK for the Waffo payment platform. Supports order management, refunds, and secure RSA-signed API communication.
Downloads
92
Readme
Waffo PSP Node.js SDK
Waffo PSP(支付服务提供商)收单服务的官方 Node.js SDK。本 SDK 提供基于 RSA 签名的安全 API 通信,以及完整的收单操作类型定义支持。
快速开始
import {
Waffo,
Environment,
CurrencyCode,
ProductName,
} from '@waffo/waffo-node';
// 1. 初始化 SDK
const waffo = new Waffo({
apiKey: 'your-api-key',
privateKey: 'your-base64-encoded-private-key',
environment: Environment.SANDBOX,
});
// 2. 创建订单
const result = await waffo.order.create({
paymentRequestId: 'REQ_001',
merchantOrderId: 'ORDER_001',
orderCurrency: CurrencyCode.IDR,
orderAmount: '100000',
orderDescription: '商品购买',
notifyUrl: 'https://merchant.com/notify',
merchantInfo: { merchantId: 'your-merchant-id' },
userInfo: {
userId: 'user_001',
userEmail: '[email protected]',
},
paymentInfo: {
productName: ProductName.ONE_TIME_PAYMENT,
payMethodType: 'EWALLET',
payMethodName: 'DANA',
},
});
// 3. 处理响应
if (result.success) {
console.log('订单创建成功:', result.data);
// 引导用户跳转到支付页面
if (result.data?.orderAction) {
const action = JSON.parse(result.data.orderAction);
window.location.href = action.webUrl;
}
}提示:需要生成新的 RSA 密钥对?使用
Waffo.generateKeyPair()来创建:const keyPair = Waffo.generateKeyPair(); console.log(keyPair.privateKey); // 妥善保管,用于 SDK 初始化 console.log(keyPair.publicKey); // 提供给 Waffo
特性
- RSA-2048 请求签名与响应验证
- 完整的 TypeScript 类型定义支持
- 零生产依赖(仅使用 Node.js 内置
crypto模块) - 支持沙箱和生产环境
- 双模块格式支持(ESM/CommonJS)
- 订单管理(创建、查询、取消、退款、捕获)
- 订阅管理(创建、查询、取消、获取管理链接)
- 退款状态查询
- 商户配置查询(交易限额、每日限额)
- 支付方式配置查询(可用性、维护时段)
- Webhook 处理器,支持自动签名验证和事件路由
- Webhook 签名验证工具
- 直接 HTTP 客户端访问,支持自定义 API 请求
- 请求时间戳参数自动默认值
时间戳参数自动默认值
所有时间戳参数(orderRequestedAt、requestedAt、captureRequestedAt)均为可选参数,如不提供将自动使用当前时间(new Date().toISOString()):
// 时间戳自动设置为当前时间
await waffo.order.create({
paymentRequestId: 'REQ_001',
merchantOrderId: 'ORDER_001',
// ... 其他必填字段
// orderRequestedAt 自动设置
});
// 或显式提供自定义时间戳
await waffo.order.create({
paymentRequestId: 'REQ_001',
merchantOrderId: 'ORDER_001',
orderRequestedAt: '2025-01-01T00:00:00.000Z', // 自定义时间戳
// ... 其他必填字段
});此特性适用于:
CreateOrderParams.orderRequestedAtCancelOrderParams.orderRequestedAtRefundOrderParams.requestedAtCaptureOrderParams.captureRequestedAtCreateSubscriptionParams.requestedAtCancelSubscriptionParams.requestedAt
安装
npm install @waffo/waffo-node使用指南
初始化 SDK
import { Waffo, Environment } from '@waffo/waffo-node';
const waffo = new Waffo({
apiKey: 'your-api-key',
privateKey: 'your-base64-encoded-private-key',
environment: Environment.SANDBOX, // 或 Environment.PRODUCTION
});生成 RSA 密钥对
import { Waffo } from '@waffo/waffo-node';
const keyPair = Waffo.generateKeyPair();
console.log(keyPair.privateKey); // Base64 编码的 PKCS8 私钥
console.log(keyPair.publicKey); // Base64 编码的 X509 公钥创建订单
const result = await waffo.order.create({
paymentRequestId: 'REQ_001',
merchantOrderId: 'ORDER_001',
orderCurrency: CurrencyCode.IDR,
orderAmount: '100000',
orderDescription: '商品购买',
notifyUrl: 'https://merchant.com/notify',
merchantInfo: {
merchantId: 'your-merchant-id',
},
userInfo: {
userId: 'user_001',
userEmail: '[email protected]',
userPhone: '+62-81234567890',
userTerminal: UserTerminalType.WEB,
},
paymentInfo: {
productName: ProductName.ONE_TIME_PAYMENT,
payMethodType: 'EWALLET',
payMethodName: 'DANA',
},
});
if (result.success) {
console.log('订单创建成功:', result.data);
} else {
console.error('错误:', result.error);
}查询订单状态
const result = await waffo.order.inquiry({
acquiringOrderId: 'A202512230000001',
// 或使用 paymentRequestId: 'REQ_001'
});
if (result.success) {
console.log('订单状态:', result.data?.orderStatus);
}取消订单
const result = await waffo.order.cancel({
acquiringOrderId: 'A202512230000001',
merchantId: 'your-merchant-id',
// orderRequestedAt 为可选参数,默认为当前时间
});订单退款
const result = await waffo.order.refund({
refundRequestId: 'REFUND_001',
acquiringOrderId: 'A202512230000001',
merchantId: 'your-merchant-id',
refundAmount: '50000',
refundReason: '用户申请退款',
refundNotifyUrl: 'https://merchant.com/refund-notify',
// requestedAt 为可选参数,默认为当前时间
});查询退款状态
const result = await waffo.refund.inquiry({
refundRequestId: 'REFUND_001',
// 或使用 acquiringRefundOrderId: 'R202512230000001'
});
if (result.success) {
console.log('退款状态:', result.data?.refundStatus);
}捕获预授权支付
const result = await waffo.order.capture({
acquiringOrderId: 'A202512230000001',
merchantId: 'your-merchant-id',
captureAmount: '100000',
// captureRequestedAt 为可选参数,默认为当前时间
});创建订阅
const result = await waffo.subscription.create({
subscriptionRequest: 'SUB_REQ_001',
merchantSubscriptionId: 'MERCHANT_SUB_001',
currency: CurrencyCode.PHP,
amount: '100',
productInfo: {
periodType: PeriodType.MONTHLY,
periodInterval: '1',
numberOfPeriod: '12',
description: '月度订阅',
},
paymentInfo: {
productName: ProductName.SUBSCRIPTION,
payMethodType: 'EWALLET',
payMethodName: 'GCASH',
},
merchantInfo: { merchantId: 'your-merchant-id' },
userInfo: {
userId: 'user_001',
userEmail: '[email protected]',
},
goodsInfo: {
goodsId: 'GOODS_001',
goodsName: '高级会员',
},
notifyUrl: 'https://merchant.com/subscription/notify',
// requestedAt 为可选参数,默认为当前时间
});
if (result.success) {
console.log('订阅创建成功:', result.data);
// 引导用户完成订阅签约
if (result.data?.subscriptionAction?.webUrl) {
window.location.href = result.data.subscriptionAction.webUrl;
}
}查询订阅状态
const result = await waffo.subscription.inquiry({
merchantId: 'your-merchant-id',
subscriptionId: 'SUB_202512230000001',
paymentDetails: '1', // 包含支付历史
});
if (result.success) {
console.log('订阅状态:', result.data?.subscriptionStatus);
}取消订阅
const result = await waffo.subscription.cancel({
merchantId: 'your-merchant-id',
subscriptionId: 'SUB_202512230000001',
// requestedAt 为可选参数,默认为当前时间
});获取订阅管理链接
const result = await waffo.subscription.manage({
subscriptionId: 'SUB_202512230000001',
// 或使用 subscriptionRequest: 'SUB_REQ_001'
});
if (result.success) {
console.log('管理链接:', result.data?.managementUrl);
console.log('过期时间:', result.data?.expiresAt);
// 引导用户管理其订阅
window.location.href = result.data?.managementUrl;
}查询商户配置
const result = await waffo.merchantConfig.inquiry({
merchantId: 'your-merchant-id',
});
if (result.success) {
console.log('每日限额:', result.data?.totalDailyLimit);
console.log('剩余每日限额:', result.data?.remainingDailyLimit);
console.log('单笔交易限额:', result.data?.transactionLimit);
}查询支付方式配置
const result = await waffo.payMethodConfig.inquiry({
merchantId: 'your-merchant-id',
});
if (result.success) {
result.data?.payMethodDetails.forEach(method => {
console.log(`${method.payMethodName}: ${method.currentStatus === '1' ? '可用' : '不可用'}`);
if (method.fixedMaintenanceRules) {
console.log('维护时段:', method.fixedMaintenanceRules);
}
});
}Webhook 处理器
SDK 提供了内置的 Webhook 处理器,可自动处理签名验证、事件路由和响应签名:
// 在 Webhook 处理器中
app.post('/webhook', async (req, res) => {
const signature = req.headers['x-signature'] as string;
const body = JSON.stringify(req.body);
const result = await waffo.webhook.handle(body, signature, {
onPayment: async ({ notification }) => {
console.log('支付状态:', notification.result.orderStatus);
// 处理支付通知
},
onRefund: async ({ notification }) => {
console.log('退款状态:', notification.result.refundStatus);
// 处理退款通知
},
onSubscriptionStatus: async ({ notification }) => {
console.log('订阅状态:', notification.result.subscriptionStatus);
// 处理订阅状态变更
},
onSubscriptionPayment: async ({ notification }) => {
console.log('订阅支付:', notification.result.orderStatus);
// 处理订阅周期性支付
},
onError: async (error) => {
console.error('Webhook 错误:', error.message);
},
});
return res.json(result.response);
});手动 Webhook 签名验证
如需更细粒度的控制,可以使用底层 Webhook 工具函数:
import {
verifyWebhookSignature,
buildSuccessResponse,
buildFailedResponse,
isPaymentNotification,
isRefundNotification,
} from '@waffo/waffo-node';
// 在 Webhook 处理器中
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const body = JSON.stringify(req.body);
// 验证签名
const verifyResult = verifyWebhookSignature(body, signature, waffoPublicKey);
if (!verifyResult.valid) {
return res.json(buildFailedResponse('签名验证失败', merchantPrivateKey));
}
// 根据类型处理通知
const notification = verifyResult.notification;
if (isPaymentNotification(notification)) {
// 处理支付通知
console.log('支付状态:', notification.result.orderStatus);
} else if (isRefundNotification(notification)) {
// 处理退款通知
console.log('退款状态:', notification.result.refundStatus);
}
// 返回成功响应
return res.json(buildSuccessResponse(merchantPrivateKey));
});直接 HTTP 客户端访问
用于 SDK 方法未覆盖的自定义 API 请求:
const response = await waffo.httpClient.post<CustomResponseType>('/custom/endpoint', {
body: { key: 'value' }
});
if (response.success) {
console.log(response.data);
}配置选项
| 选项 | 类型 | 必填 | 默认值 | 描述 |
|------|------|------|--------|------|
| apiKey | string | 是 | - | Waffo 提供的 API 密钥 |
| privateKey | string | 是 | - | Base64 编码的 PKCS8 私钥 |
| waffoPublicKey | string | 否 | 内置公钥 | 用于响应验证的自定义 Waffo 公钥 |
| environment | Environment | 否 | PRODUCTION | API 环境(SANDBOX 或 PRODUCTION) |
| timeout | number | 否 | 30000 | 请求超时时间(毫秒) |
| logger | Logger | 否 | - | 调试用的日志实例(可使用 console) |
API 响应格式
所有 API 方法返回 ApiResponse<T> 对象:
interface ApiResponse<T> {
success: boolean; // 请求是否成功
statusCode: number; // HTTP 状态码
data?: T; // 响应数据(成功时)
error?: string; // 错误信息(失败时)
}类型定义
SDK 导出完整的 TypeScript 类型定义,包括:
Environment- SDK 环境枚举CountryCode- ISO 3166-1 alpha-3 国家代码CurrencyCode- ISO 4217 货币代码ProductName- 支付产品类型枚举(ONE_TIME_PAYMENT、SUBSCRIPTION)payMethodType- 支付方式类别(字符串: "EWALLET"、"CREDITCARD"、"BANKTRANSFER"、"ONLINE_BANKING"、"DIGITAL_BANKING"、"OTC"、"DEBITCARD")payMethodName- 具体支付方式(字符串: "OVO"、"DANA"、"GOPAY"、"GCASH"、"CC_VISA"、"CC_MASTERCARD"、"VA_BCA"、"VA_BNI" 等)OrderStatus- 订单状态枚举(PAY_IN_PROGRESS、AUTHORIZATION_REQUIRED、PAY_SUCCESS、ORDER_CLOSE 等)RefundStatus- 退款状态枚举(REFUND_IN_PROGRESS、ORDER_PARTIALLY_REFUNDED、ORDER_FULLY_REFUNDED、ORDER_REFUND_FAILED)SubscriptionStatus- 订阅状态枚举(AUTHORIZATION_REQUIRED、ACTIVE、PAUSED、MERCHANT_CANCELLED 等)PeriodType- 订阅周期类型枚举(DAILY、WEEKLY、MONTHLY、YEARLY)UserTerminalType- 用户终端类型枚举(WEB、APP、IN_WALLET_APP、IN_MINI_PROGRAM)- 所有 API 操作的请求/响应接口
开发
# 安装依赖
npm install
# 构建(同时生成 ESM 和 CJS)
npm run build
# 运行测试
npm test
# 运行测试并生成覆盖率报告
npm run test:coverage
# 监听模式运行测试
npm run test:watch
# 代码检查
npm run lint
# 代码检查并自动修复
npm run lint:fix
# 格式化代码
npm run format
# 检查代码格式
npm run format:check代码质量
本项目使用以下工具:
- ESLint - 代码检查,支持 TypeScript
- Prettier - 代码格式化
- Husky - Git 钩子
- lint-staged - 对暂存文件运行检查
提交代码时,pre-commit 钩子会自动对暂存的 .ts 文件运行 ESLint 和 Prettier。
运行 E2E 测试
E2E 测试需要 Waffo 沙箱环境凭证。SDK 支持多个商户配置,用于不同的测试场景,并基于官方 Waffo 测试用例文档提供全面的测试覆盖。
# 复制模板文件并填写凭证
cp .env.template .env
# 编辑 .env 文件,填入你的凭证环境变量说明:
| 变量名 | 必填 | 描述 |
|--------|------|------|
| WAFFO_PUBLIC_KEY | 否 | 用于签名验证的 Waffo 公钥(通用) |
| ACQUIRING_MERCHANT_ID | 是* | 支付/订单测试用的商户 ID |
| ACQUIRING_API_KEY | 是* | 支付/订单测试用的 API 密钥 |
| ACQUIRING_MERCHANT_PRIVATE_KEY | 是* | 支付/订单测试用的私钥 |
| SUBSCRIPTION_MERCHANT_ID | 是** | 订阅测试用的商户 ID |
| SUBSCRIPTION_API_KEY | 是** | 订阅测试用的 API 密钥 |
| SUBSCRIPTION_MERCHANT_PRIVATE_KEY | 是** | 订阅测试用的私钥 |
* 运行支付/订单 E2E 测试时必填 ** 运行订阅 E2E 测试时必填
E2E 测试覆盖范围:
| 模块 | 测试用例 | |------|----------| | 创建订单 | 支付成功/失败、渠道拒绝 (C0005)、幂等错误 (A0011)、系统错误 (C0001)、未知状态 (E0001) | | 查询订单 | 支付前/支付后查询 | | 取消订单 | 支付前取消、渠道不支持 (A0015)、已支付 (A0013) | | 退款订单 | 全额/部分退款、参数校验 (A0003)、退款规则 (A0014) | | 创建订阅 | 订阅成功/失败、下期支付模拟 | | 取消订阅 | 商户发起取消 | | Webhook 通知 | 支付、退款、订阅通知签名验证 |
沙箱环境金额触发规则:
| 金额模式 | 错误码 | 描述 | |----------|--------|------| | 9, 90, 990, 1990, 19990 | C0005 | 渠道拒绝 | | 9.1, 91, 991, 1991, 19991 | C0001 | 系统错误 | | 9.2, 92, 992, 1992, 19992 | E0001 | 未知状态 | | 9.3, 93, 993, 1993, 19993 | C0001 | 取消系统错误 | | 9.4, 94, 994, 1994, 19994 | E0001 | 取消未知状态 | | 9.5, 95, 995, 1995, 19995 | C0001 | 退款系统错误 | | 9.6, 96, 996, 1996, 199996 | E0001 | 退款未知状态 |
# 运行所有测试
npm test构建产物
SDK 使用 tsup 构建,输出以下文件:
| 文件 | 格式 | 描述 |
|------|------|------|
| dist/index.js | CommonJS | 用于 require() 导入 |
| dist/index.mjs | ESM | 用于 import 语句 |
| dist/index.d.ts | TypeScript | 类型声明文件 |
