@vobs/payment 0.3.0 → 1.2.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.
- package/LICENSE +1 -1
- package/README.md +182 -0
- package/dist/alipay/client.cjs +112 -0
- package/dist/alipay/client.cjs.map +1 -0
- package/dist/alipay/client.d.cts +36 -0
- package/dist/alipay/client.d.ts +36 -0
- package/dist/alipay/client.js +110 -0
- package/dist/alipay/client.js.map +1 -0
- package/dist/alipay/config.cjs +67 -0
- package/dist/alipay/config.cjs.map +1 -0
- package/dist/alipay/config.d.cts +30 -0
- package/dist/alipay/config.d.ts +30 -0
- package/dist/alipay/config.js +65 -0
- package/dist/alipay/config.js.map +1 -0
- package/dist/alipay/index.cjs +592 -0
- package/dist/alipay/index.cjs.map +1 -0
- package/dist/alipay/index.d.cts +12 -0
- package/dist/alipay/index.d.ts +12 -0
- package/dist/alipay/index.js +580 -0
- package/dist/alipay/index.js.map +1 -0
- package/dist/alipay/notify.cjs +70 -0
- package/dist/alipay/notify.cjs.map +1 -0
- package/dist/alipay/notify.d.cts +52 -0
- package/dist/alipay/notify.d.ts +52 -0
- package/dist/alipay/notify.js +68 -0
- package/dist/alipay/notify.js.map +1 -0
- package/dist/alipay/payment.cjs +49 -0
- package/dist/alipay/payment.cjs.map +1 -0
- package/dist/alipay/payment.d.cts +24 -0
- package/dist/alipay/payment.d.ts +24 -0
- package/dist/alipay/payment.js +47 -0
- package/dist/alipay/payment.js.map +1 -0
- package/dist/alipay/plugin.cjs +300 -0
- package/dist/alipay/plugin.cjs.map +1 -0
- package/dist/alipay/plugin.d.cts +43 -0
- package/dist/alipay/plugin.d.ts +43 -0
- package/dist/alipay/plugin.js +296 -0
- package/dist/alipay/plugin.js.map +1 -0
- package/dist/alipay/query.cjs +80 -0
- package/dist/alipay/query.cjs.map +1 -0
- package/dist/alipay/query.d.cts +26 -0
- package/dist/alipay/query.d.ts +26 -0
- package/dist/alipay/query.js +78 -0
- package/dist/alipay/query.js.map +1 -0
- package/dist/alipay/refund.cjs +104 -0
- package/dist/alipay/refund.cjs.map +1 -0
- package/dist/alipay/refund.d.cts +32 -0
- package/dist/alipay/refund.d.ts +32 -0
- package/dist/alipay/refund.js +102 -0
- package/dist/alipay/refund.js.map +1 -0
- package/dist/alipay/response.cjs +38 -0
- package/dist/alipay/response.cjs.map +1 -0
- package/dist/alipay/response.d.cts +19 -0
- package/dist/alipay/response.d.ts +19 -0
- package/dist/alipay/response.js +35 -0
- package/dist/alipay/response.js.map +1 -0
- package/dist/alipay/types.cjs +4 -0
- package/dist/alipay/types.cjs.map +1 -0
- package/dist/alipay/types.d.cts +242 -0
- package/dist/alipay/types.d.ts +242 -0
- package/dist/alipay/types.js +3 -0
- package/dist/alipay/types.js.map +1 -0
- package/dist/alipay/wap.cjs +50 -0
- package/dist/alipay/wap.cjs.map +1 -0
- package/dist/alipay/wap.d.cts +24 -0
- package/dist/alipay/wap.d.ts +24 -0
- package/dist/alipay/wap.js +48 -0
- package/dist/alipay/wap.js.map +1 -0
- package/dist/index-B1aUR7Vt.d.ts +49 -0
- package/dist/index-B8pzZxGq.d.cts +44 -0
- package/dist/index-BWnIrwRi.d.ts +44 -0
- package/dist/index-Dyrkl86r.d.cts +49 -0
- package/dist/index.cjs +1071 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -37
- package/dist/index.js +1052 -37
- package/dist/index.js.map +1 -0
- package/dist/wechat/client.cjs +119 -0
- package/dist/wechat/client.cjs.map +1 -0
- package/dist/wechat/client.d.cts +39 -0
- package/dist/wechat/client.d.ts +39 -0
- package/dist/wechat/client.js +117 -0
- package/dist/wechat/client.js.map +1 -0
- package/dist/wechat/config.cjs +76 -0
- package/dist/wechat/config.cjs.map +1 -0
- package/dist/wechat/config.d.cts +29 -0
- package/dist/wechat/config.d.ts +29 -0
- package/dist/wechat/config.js +74 -0
- package/dist/wechat/config.js.map +1 -0
- package/dist/wechat/index.cjs +612 -0
- package/dist/wechat/index.cjs.map +1 -0
- package/dist/wechat/index.d.cts +10 -0
- package/dist/wechat/index.d.ts +10 -0
- package/dist/wechat/index.js +602 -0
- package/dist/wechat/index.js.map +1 -0
- package/dist/wechat/notify.cjs +73 -0
- package/dist/wechat/notify.cjs.map +1 -0
- package/dist/wechat/notify.d.cts +53 -0
- package/dist/wechat/notify.d.ts +53 -0
- package/dist/wechat/notify.js +71 -0
- package/dist/wechat/notify.js.map +1 -0
- package/dist/wechat/payment.cjs +186 -0
- package/dist/wechat/payment.cjs.map +1 -0
- package/dist/wechat/payment.d.cts +50 -0
- package/dist/wechat/payment.d.ts +50 -0
- package/dist/wechat/payment.js +184 -0
- package/dist/wechat/payment.js.map +1 -0
- package/dist/wechat/plugin.cjs +305 -0
- package/dist/wechat/plugin.cjs.map +1 -0
- package/dist/wechat/plugin.d.cts +40 -0
- package/dist/wechat/plugin.d.ts +40 -0
- package/dist/wechat/plugin.js +301 -0
- package/dist/wechat/plugin.js.map +1 -0
- package/dist/wechat/refund.cjs +112 -0
- package/dist/wechat/refund.cjs.map +1 -0
- package/dist/wechat/refund.d.cts +26 -0
- package/dist/wechat/refund.d.ts +26 -0
- package/dist/wechat/refund.js +110 -0
- package/dist/wechat/refund.js.map +1 -0
- package/dist/wechat/response.cjs +50 -0
- package/dist/wechat/response.cjs.map +1 -0
- package/dist/wechat/response.d.cts +20 -0
- package/dist/wechat/response.d.ts +20 -0
- package/dist/wechat/response.js +46 -0
- package/dist/wechat/response.js.map +1 -0
- package/dist/wechat/types.cjs +4 -0
- package/dist/wechat/types.cjs.map +1 -0
- package/dist/wechat/types.d.cts +200 -0
- package/dist/wechat/types.d.ts +200 -0
- package/dist/wechat/types.js +3 -0
- package/dist/wechat/types.js.map +1 -0
- package/package.json +37 -28
- package/src/alipay/alipay.test.ts +231 -0
- package/src/alipay/client.ts +67 -0
- package/src/alipay/config.ts +72 -0
- package/src/alipay/index.ts +32 -0
- package/src/alipay/notify.ts +92 -0
- package/src/alipay/payment.ts +60 -0
- package/src/alipay/plugin.ts +64 -0
- package/src/alipay/query.ts +52 -0
- package/src/alipay/refund.ts +85 -0
- package/src/alipay/response.ts +40 -0
- package/src/alipay/types.ts +271 -0
- package/src/alipay/wap.ts +55 -0
- package/src/index.ts +78 -0
- package/src/wechat/client.ts +68 -0
- package/src/wechat/config.ts +81 -0
- package/src/wechat/index.ts +30 -0
- package/src/wechat/notify.ts +93 -0
- package/src/wechat/payment.ts +177 -0
- package/src/wechat/plugin.ts +59 -0
- package/src/wechat/refund.ts +80 -0
- package/src/wechat/response.ts +54 -0
- package/src/wechat/types.ts +223 -0
- package/src/wechat/wechat.test.ts +393 -0
- package/dist/alipay-official.d.ts +0 -34
- package/dist/alipay-official.js +0 -206
- package/dist/handlers.d.ts +0 -45
- package/dist/handlers.js +0 -105
- package/dist/jeepay.d.ts +0 -39
- package/dist/jeepay.js +0 -206
- package/dist/lakala.d.ts +0 -41
- package/dist/lakala.js +0 -243
- package/dist/manager.d.ts +0 -67
- package/dist/manager.js +0 -538
- package/dist/mock-driver.d.ts +0 -22
- package/dist/mock-driver.js +0 -59
- package/dist/state-machine.d.ts +0 -41
- package/dist/state-machine.js +0 -60
- package/dist/store.d.ts +0 -33
- package/dist/store.js +0 -76
- package/dist/types.d.ts +0 -168
- package/dist/types.js +0 -11
- package/dist/verify.d.ts +0 -34
- package/dist/verify.js +0 -55
- package/dist/wechat-official.d.ts +0 -40
- package/dist/wechat-official.js +0 -208
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { generateKeyPairSync } from 'node:crypto'
|
|
3
|
+
import { Aes, Formatter, Rsa } from 'wechatpay-axios-plugin'
|
|
4
|
+
import { WechatConfig } from './config'
|
|
5
|
+
import { WechatApiError, toWechatApiError } from './response'
|
|
6
|
+
import { WechatClient } from './client'
|
|
7
|
+
import { createPayment } from './payment'
|
|
8
|
+
import { createNotifyHandler } from './notify'
|
|
9
|
+
import { createRefund } from './refund'
|
|
10
|
+
import { wechatPlugin, WECHAT_KEY } from './plugin'
|
|
11
|
+
import type { WechatClient as WechatClientType } from './client'
|
|
12
|
+
import type { WechatClientOptions, WechatNotifyTransaction } from './types'
|
|
13
|
+
import type { VobsContext } from '@vobs/vobs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 在模块边界 mock Wechatpay 类:其构造函数返回的是"魔法链 Proxy",
|
|
17
|
+
* 原型注入不可行;链式方法与 request 全部落在假实现的 per-instance spy 上。
|
|
18
|
+
* Rsa / Aes / Formatter 保持真实现,测试内完成真实签名与 AES-GCM 往返。
|
|
19
|
+
*/
|
|
20
|
+
vi.mock('wechatpay-axios-plugin', async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import('wechatpay-axios-plugin')>()
|
|
22
|
+
|
|
23
|
+
class FakeWechatpay {
|
|
24
|
+
options: Record<string, any>
|
|
25
|
+
spies: {
|
|
26
|
+
jsapiPost: ReturnType<typeof vi.fn>
|
|
27
|
+
appPost: ReturnType<typeof vi.fn>
|
|
28
|
+
nativePost: ReturnType<typeof vi.fn>
|
|
29
|
+
h5Post: ReturnType<typeof vi.fn>
|
|
30
|
+
refundPost: ReturnType<typeof vi.fn>
|
|
31
|
+
request: ReturnType<typeof vi.fn>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(options: Record<string, any>) {
|
|
35
|
+
this.options = options
|
|
36
|
+
this.spies = {
|
|
37
|
+
jsapiPost: vi.fn(),
|
|
38
|
+
appPost: vi.fn(),
|
|
39
|
+
nativePost: vi.fn(),
|
|
40
|
+
h5Post: vi.fn(),
|
|
41
|
+
refundPost: vi.fn(),
|
|
42
|
+
request: vi.fn()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get client() {
|
|
47
|
+
return { request: this.spies.request }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get v3() {
|
|
51
|
+
const spies = this.spies
|
|
52
|
+
return {
|
|
53
|
+
pay: {
|
|
54
|
+
transactions: {
|
|
55
|
+
jsapi: { post: spies.jsapiPost },
|
|
56
|
+
app: { post: spies.appPost },
|
|
57
|
+
native: { post: spies.nativePost },
|
|
58
|
+
h5: { post: spies.h5Post }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
refund: { domestic: { refunds: { post: spies.refundPost } } }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { ...actual, Wechatpay: FakeWechatpay }
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
interface FakeSdk {
|
|
70
|
+
spies: {
|
|
71
|
+
jsapiPost: ReturnType<typeof vi.fn>
|
|
72
|
+
appPost: ReturnType<typeof vi.fn>
|
|
73
|
+
nativePost: ReturnType<typeof vi.fn>
|
|
74
|
+
h5Post: ReturnType<typeof vi.fn>
|
|
75
|
+
refundPost: ReturnType<typeof vi.fn>
|
|
76
|
+
request: ReturnType<typeof vi.fn>
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sdkOf(client: WechatClientType): FakeSdk {
|
|
81
|
+
return client.sdk as unknown as FakeSdk
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeTestClient() {
|
|
85
|
+
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
86
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
|
|
87
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString()
|
|
88
|
+
|
|
89
|
+
const client = new WechatClient({
|
|
90
|
+
mchid: '1900000000',
|
|
91
|
+
serial: 'MCH_CERT_SERIAL_1',
|
|
92
|
+
privateKey: privateKeyPem,
|
|
93
|
+
certs: { PUB_KEY_ID_TEST_1: publicKeyPem },
|
|
94
|
+
appid: 'wx1234567890',
|
|
95
|
+
apiv3Key: '0123456789abcdef0123456789abcdef',
|
|
96
|
+
notifyUrl: 'https://merchant.example.com/wechat/notify'
|
|
97
|
+
})
|
|
98
|
+
return { client, privateKeyPem, publicKeyPem }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function axiosFailure(status: number, code: string, message: string) {
|
|
102
|
+
const error = new Error(message) as any
|
|
103
|
+
error.response = { status, data: { code, message } }
|
|
104
|
+
return error
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const BASE_OPTIONS: WechatClientOptions = {
|
|
108
|
+
mchid: '1900000000',
|
|
109
|
+
serial: 'S1',
|
|
110
|
+
privateKey: '',
|
|
111
|
+
certs: {},
|
|
112
|
+
appid: 'wx1234567890',
|
|
113
|
+
apiv3Key: '0123456789abcdef0123456789abcdef',
|
|
114
|
+
notifyUrl: 'https://merchant.example.com/wechat/notify'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('wechat client config validation', () => {
|
|
118
|
+
it('缺少 certs 或 apiv3Key 长度错误时拒绝构建', () => {
|
|
119
|
+
expect(() => new WechatConfig({ ...BASE_OPTIONS, privateKey: 'K', certs: {} })).toThrow('certs')
|
|
120
|
+
expect(() => new WechatConfig({
|
|
121
|
+
...BASE_OPTIONS,
|
|
122
|
+
privateKey: 'K',
|
|
123
|
+
certs: { P: 'K' },
|
|
124
|
+
apiv3Key: 'short'
|
|
125
|
+
})).toThrow('apiv3Key')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('wechat payment', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
vi.clearAllMocks()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('jsapiPay 返回可验真的调起签名参数', async () => {
|
|
135
|
+
const { client } = makeTestClient()
|
|
136
|
+
sdkOf(client).spies.jsapiPost.mockResolvedValue({ data: { prepay_id: 'wx29prepay' }, status: 200 })
|
|
137
|
+
|
|
138
|
+
const result = await createPayment(client).jsapiPay({
|
|
139
|
+
outTradeNo: 'ORDER-1',
|
|
140
|
+
description: '会员',
|
|
141
|
+
amount: { total: 29900 },
|
|
142
|
+
openid: 'oX-openid'
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(result.appId).toBe('wx1234567890')
|
|
146
|
+
expect(result.package).toBe('prepay_id=wx29prepay')
|
|
147
|
+
expect(result.signType).toBe('RSA')
|
|
148
|
+
|
|
149
|
+
const message = `${result.appId}\n${result.timeStamp}\n${result.nonceStr}\n${result.package}\n`
|
|
150
|
+
expect(Rsa.verify(message, result.paySign, client.config.options.certs['PUB_KEY_ID_TEST_1'])).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('jsapiPay 下单请求体包含 openid、金额与回调地址', async () => {
|
|
154
|
+
const { client } = makeTestClient()
|
|
155
|
+
sdkOf(client).spies.jsapiPost.mockResolvedValue({ data: { prepay_id: 'p1' }, status: 200 })
|
|
156
|
+
|
|
157
|
+
await createPayment(client).jsapiPay({
|
|
158
|
+
outTradeNo: 'ORDER-1',
|
|
159
|
+
description: '会员',
|
|
160
|
+
amount: { total: 29900 },
|
|
161
|
+
openid: 'oX-openid',
|
|
162
|
+
attach: 'attach-1'
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const body = sdkOf(client).spies.jsapiPost.mock.calls[0]![0] as Record<string, any>
|
|
166
|
+
expect(body.appid).toBe('wx1234567890')
|
|
167
|
+
expect(body.mchid).toBe('1900000000')
|
|
168
|
+
expect(body.out_trade_no).toBe('ORDER-1')
|
|
169
|
+
expect(body.attach).toBe('attach-1')
|
|
170
|
+
expect(body.notify_url).toBe('https://merchant.example.com/wechat/notify')
|
|
171
|
+
expect(body.amount).toEqual({ total: 29900, currency: 'CNY' })
|
|
172
|
+
expect(body.payer).toEqual({ openid: 'oX-openid' })
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('nativePay 映射 code_url,h5Pay 携带 payer_client_ip', async () => {
|
|
176
|
+
const { client } = makeTestClient()
|
|
177
|
+
const spies = sdkOf(client).spies
|
|
178
|
+
spies.nativePost.mockResolvedValue({
|
|
179
|
+
data: { code_url: 'weixin://wxpay/bizpayurl?pr=abc' },
|
|
180
|
+
status: 200
|
|
181
|
+
})
|
|
182
|
+
spies.h5Post.mockResolvedValue({
|
|
183
|
+
data: { h5_url: 'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=p' },
|
|
184
|
+
status: 200
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const native = await createPayment(client).nativePay({
|
|
188
|
+
outTradeNo: 'ORDER-2',
|
|
189
|
+
description: '会员',
|
|
190
|
+
amount: { total: 100 }
|
|
191
|
+
})
|
|
192
|
+
expect(native.codeUrl).toContain('weixin://')
|
|
193
|
+
|
|
194
|
+
const h5 = await createPayment(client).h5Pay({
|
|
195
|
+
outTradeNo: 'ORDER-3',
|
|
196
|
+
description: '会员',
|
|
197
|
+
amount: { total: 100 },
|
|
198
|
+
payerClientIp: '1.2.3.4'
|
|
199
|
+
})
|
|
200
|
+
expect(h5.h5Url).toContain('checkmweb')
|
|
201
|
+
const h5Body = spies.h5Post.mock.calls[0]![0] as Record<string, any>
|
|
202
|
+
expect(h5Body.scene_info.payer_client_ip).toBe('1.2.3.4')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('query 按商户订单号请求并映射响应', async () => {
|
|
206
|
+
const { client } = makeTestClient()
|
|
207
|
+
sdkOf(client).spies.request.mockResolvedValue({
|
|
208
|
+
data: {
|
|
209
|
+
appid: 'wx1234567890',
|
|
210
|
+
mchid: '1900000000',
|
|
211
|
+
out_trade_no: 'ORDER-1',
|
|
212
|
+
transaction_id: '4200001',
|
|
213
|
+
trade_state: 'SUCCESS',
|
|
214
|
+
trade_state_desc: '支付成功',
|
|
215
|
+
amount: { total: 29900, payer_total: 29800 }
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const result = await createPayment(client).query({ outTradeNo: 'ORDER-1' })
|
|
220
|
+
expect(sdkOf(client).spies.request.mock.calls[0]![0]).toBe('/v3/pay/transactions/out-trade-no/ORDER-1')
|
|
221
|
+
expect(result.tradeState).toBe('SUCCESS')
|
|
222
|
+
expect(result.payerTotal).toBe(29800)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('接口失败时抛出 WechatApiError 并携带状态码与业务码', async () => {
|
|
226
|
+
const { client } = makeTestClient()
|
|
227
|
+
sdkOf(client).spies.jsapiPost.mockRejectedValue(
|
|
228
|
+
axiosFailure(400, 'PARAM_ERROR', 'openid 与 appid 不匹配')
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
await expect(
|
|
232
|
+
createPayment(client).jsapiPay({
|
|
233
|
+
outTradeNo: 'ORDER-1',
|
|
234
|
+
description: 'x',
|
|
235
|
+
amount: { total: 1 },
|
|
236
|
+
openid: 'bad'
|
|
237
|
+
})
|
|
238
|
+
).rejects.toMatchObject({
|
|
239
|
+
name: 'WechatApiError',
|
|
240
|
+
status: 400,
|
|
241
|
+
code: 'PARAM_ERROR'
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('toWechatApiError 归一化网络层错误', () => {
|
|
246
|
+
const error = new Error('connect ECONNREFUSED') as any
|
|
247
|
+
error.code = 'ECONNREFUSED'
|
|
248
|
+
const apiError = toWechatApiError(error)
|
|
249
|
+
expect(apiError).toBeInstanceOf(WechatApiError)
|
|
250
|
+
expect(apiError.status).toBeUndefined()
|
|
251
|
+
expect(apiError.code).toBe('ECONNREFUSED')
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('wechat refund', () => {
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
vi.clearAllMocks()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('refund 提交并映射退款单', async () => {
|
|
261
|
+
const { client } = makeTestClient()
|
|
262
|
+
sdkOf(client).spies.refundPost.mockResolvedValue({
|
|
263
|
+
data: {
|
|
264
|
+
refund_id: 're-1',
|
|
265
|
+
out_refund_no: 'RF-1',
|
|
266
|
+
out_trade_no: 'ORDER-1',
|
|
267
|
+
transaction_id: '4200001',
|
|
268
|
+
status: 'PROCESSING',
|
|
269
|
+
amount: { refund: 100, total: 29900, payer_total: 29900, user_received_amount: 100 }
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const result = await createRefund(client).refund({
|
|
274
|
+
outTradeNo: 'ORDER-1',
|
|
275
|
+
outRefundNo: 'RF-1',
|
|
276
|
+
amount: { refund: 100 },
|
|
277
|
+
reason: '七天无理由'
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const body = sdkOf(client).spies.refundPost.mock.calls[0]![0] as Record<string, any>
|
|
281
|
+
expect(body.out_trade_no).toBe('ORDER-1')
|
|
282
|
+
expect(body.amount.refund).toBe(100)
|
|
283
|
+
expect(result.status).toBe('PROCESSING')
|
|
284
|
+
expect(result.userReceivedAmount).toBe(100)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('queryRefund 对路径参数做 URL 编码', async () => {
|
|
288
|
+
const { client } = makeTestClient()
|
|
289
|
+
sdkOf(client).spies.request.mockResolvedValue({ data: { refund_id: 're 1', status: 'SUCCESS', amount: { refund: 1 } } })
|
|
290
|
+
|
|
291
|
+
await createRefund(client).queryRefund({ outRefundNo: 'RF 1' })
|
|
292
|
+
expect(sdkOf(client).spies.request.mock.calls[0]![0]).toBe('/v3/refund/domestic/refunds/RF%201')
|
|
293
|
+
expect(sdkOf(client).spies.request.mock.calls[0]![1]).toBe('GET')
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('wechat notify', () => {
|
|
298
|
+
it('verify 用对应序列号的公钥验签原文', () => {
|
|
299
|
+
const { client, privateKeyPem } = makeTestClient()
|
|
300
|
+
const handler = createNotifyHandler(client)
|
|
301
|
+
const body = JSON.stringify({ id: 'evt-1', resource: {} })
|
|
302
|
+
const headers = {
|
|
303
|
+
timestamp: `${Formatter.timestamp()}`,
|
|
304
|
+
nonce: Formatter.nonce(16),
|
|
305
|
+
serial: 'PUB_KEY_ID_TEST_1',
|
|
306
|
+
signature: ''
|
|
307
|
+
}
|
|
308
|
+
headers.signature = Rsa.sign(Formatter.response(headers.timestamp, headers.nonce, body), privateKeyPem)
|
|
309
|
+
|
|
310
|
+
expect(handler.verify(headers, body)).toBe(true)
|
|
311
|
+
expect(handler.verify({ ...headers, signature: 'broken' }, body)).toBe(false)
|
|
312
|
+
expect(handler.verify({ ...headers, serial: 'UNKNOWN' }, body)).toBe(false)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('decrypt 走真实 AES-256-GCM 解密', () => {
|
|
316
|
+
const { client } = makeTestClient()
|
|
317
|
+
const handler = createNotifyHandler(client)
|
|
318
|
+
const transaction: WechatNotifyTransaction = {
|
|
319
|
+
mchid: '1900000000',
|
|
320
|
+
appid: 'wx1234567890',
|
|
321
|
+
outTradeNo: 'ORDER-1',
|
|
322
|
+
transactionId: '4200001',
|
|
323
|
+
tradeState: 'SUCCESS',
|
|
324
|
+
amount: { total: 29900, payerTotal: 29900 }
|
|
325
|
+
}
|
|
326
|
+
const ciphertext = Aes.AesGcm.encrypt(
|
|
327
|
+
JSON.stringify(transaction),
|
|
328
|
+
client.config.apiv3Key,
|
|
329
|
+
'nonce1234',
|
|
330
|
+
'transaction'
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const payload = handler.decrypt({
|
|
334
|
+
ciphertext,
|
|
335
|
+
nonce: 'nonce1234',
|
|
336
|
+
associatedData: 'transaction'
|
|
337
|
+
})
|
|
338
|
+
expect(payload.outTradeNo).toBe('ORDER-1')
|
|
339
|
+
expect((payload as WechatNotifyTransaction).amount.total).toBe(29900)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('validate 比对商户号、订单号与金额', () => {
|
|
343
|
+
const { client } = makeTestClient()
|
|
344
|
+
const handler = createNotifyHandler(client)
|
|
345
|
+
const payload = {
|
|
346
|
+
mchid: '1900000000',
|
|
347
|
+
outTradeNo: 'ORDER-1',
|
|
348
|
+
amount: { total: 29900 }
|
|
349
|
+
} as WechatNotifyTransaction
|
|
350
|
+
|
|
351
|
+
expect(handler.validate(payload, { outTradeNo: 'ORDER-1', total: 29900 }).valid).toBe(true)
|
|
352
|
+
expect(handler.validate(payload, { outTradeNo: 'ORDER-2' }).valid).toBe(false)
|
|
353
|
+
expect(handler.validate({ ...payload, mchid: 'other' } as WechatNotifyTransaction, { total: 29900 }).errors[0]).toContain('mchid')
|
|
354
|
+
expect(handler.validate(payload, { total: 1 }).errors[0]).toContain('amount.total')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('应答体符合微信支付约定', () => {
|
|
358
|
+
const { client } = makeTestClient()
|
|
359
|
+
const handler = createNotifyHandler(client)
|
|
360
|
+
expect(handler.successResponse()).toEqual({ code: 'SUCCESS' })
|
|
361
|
+
expect(handler.failResponse()).toEqual({ code: 'FAIL', message: '处理失败' })
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('wechat plugin lifecycle', () => {
|
|
366
|
+
it('安装自建客户端并在卸载时销毁', () => {
|
|
367
|
+
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
368
|
+
const provide = vi.fn()
|
|
369
|
+
const plugin = wechatPlugin({
|
|
370
|
+
config: {
|
|
371
|
+
...BASE_OPTIONS,
|
|
372
|
+
privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
373
|
+
certs: { PUB_KEY_ID_TEST: publicKey.export({ type: 'spki', format: 'pem' }).toString() }
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
const cleanup = plugin.install?.({ provide } as unknown as VobsContext)
|
|
377
|
+
const client = provide.mock.calls[0]![1] as WechatClientType
|
|
378
|
+
expect(provide).toHaveBeenCalledWith(WECHAT_KEY, client)
|
|
379
|
+
expect(typeof cleanup).toBe('function')
|
|
380
|
+
cleanup!()
|
|
381
|
+
expect(() => client.sdk).toThrow('客户端已销毁')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('外部传入的客户端在卸载时不被销毁', () => {
|
|
385
|
+
const { client } = makeTestClient()
|
|
386
|
+
const provide = vi.fn()
|
|
387
|
+
const plugin = wechatPlugin({ client })
|
|
388
|
+
const cleanup = plugin.install?.({ provide } as unknown as VobsContext)
|
|
389
|
+
expect(provide).toHaveBeenCalledWith(WECHAT_KEY, client)
|
|
390
|
+
expect(cleanup).toBeUndefined()
|
|
391
|
+
expect(() => client.sdk).not.toThrow()
|
|
392
|
+
})
|
|
393
|
+
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Alipay official driver (Open Platform API) — face-to-face (PC scan) / PC web / mobile web (H5).
|
|
7
|
-
*
|
|
8
|
-
* Covered scenes (web):
|
|
9
|
-
* - `pc` → alipay.trade.precreate (face-to-face) → `{kind:'qr', qrUrl: qr_code}`
|
|
10
|
-
* - `h5` → alipay.trade.wap.pay (mobile web) → `{kind:'redirect', payUrl}`
|
|
11
|
-
* - `miniapp` → same as h5 (Alipay miniapp webview uses H5 payment; native my.tradePay out of scope)
|
|
12
|
-
*
|
|
13
|
-
* Security:
|
|
14
|
-
* - Request signing: app private key RSA2 (SHA256withRSA), params joined in lexicographic order
|
|
15
|
-
* - Async notify: verify with Alipay public key (excluding sign/sign_type) + notify_id anti-replay + state-machine idempotency
|
|
16
|
-
* - Channel self-verification: callbacks bypass the framework's generic HMAC protocol; this driver verifies itself
|
|
17
|
-
*
|
|
18
|
-
* Dependencies: node:crypto (Node built-in, zero third-party). Sandbox: set baseUrl to openapi-sandbox.dl.alipaydev.com.
|
|
19
|
-
*/
|
|
20
|
-
import type { PaymentDriver } from './types.js';
|
|
21
|
-
import type { PaymentStore } from './store.js';
|
|
22
|
-
export interface AlipayOfficialOptions {
|
|
23
|
-
readonly appId: string;
|
|
24
|
-
/** App private key (PEM, used for request signing). */
|
|
25
|
-
readonly privateKeyPem: string;
|
|
26
|
-
/** Alipay public key (PEM, used to verify async-notify/response signatures). */
|
|
27
|
-
readonly alipayPublicKeyPem: string;
|
|
28
|
-
/** Gateway URL. Production defaults to openapi.alipay.com, sandbox to openapi-sandbox.dl.alipaydev.com. */
|
|
29
|
-
readonly baseUrl?: string;
|
|
30
|
-
/** Page-jump gateway (GET redirects for PC web / mobile web). Defaults to baseUrl. */
|
|
31
|
-
readonly pageBaseUrl?: string;
|
|
32
|
-
readonly fetchImpl?: typeof fetch;
|
|
33
|
-
}
|
|
34
|
-
export declare function createAlipayOfficialDriver(store: PaymentStore, options: AlipayOfficialOptions): PaymentDriver;
|
package/dist/alipay-official.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Alipay official driver (Open Platform API) — face-to-face (PC scan) / PC web / mobile web (H5).
|
|
7
|
-
*
|
|
8
|
-
* Covered scenes (web):
|
|
9
|
-
* - `pc` → alipay.trade.precreate (face-to-face) → `{kind:'qr', qrUrl: qr_code}`
|
|
10
|
-
* - `h5` → alipay.trade.wap.pay (mobile web) → `{kind:'redirect', payUrl}`
|
|
11
|
-
* - `miniapp` → same as h5 (Alipay miniapp webview uses H5 payment; native my.tradePay out of scope)
|
|
12
|
-
*
|
|
13
|
-
* Security:
|
|
14
|
-
* - Request signing: app private key RSA2 (SHA256withRSA), params joined in lexicographic order
|
|
15
|
-
* - Async notify: verify with Alipay public key (excluding sign/sign_type) + notify_id anti-replay + state-machine idempotency
|
|
16
|
-
* - Channel self-verification: callbacks bypass the framework's generic HMAC protocol; this driver verifies itself
|
|
17
|
-
*
|
|
18
|
-
* Dependencies: node:crypto (Node built-in, zero third-party). Sandbox: set baseUrl to openapi-sandbox.dl.alipaydev.com.
|
|
19
|
-
*/
|
|
20
|
-
import { createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
|
|
21
|
-
const DEFAULT_GATEWAY = 'https://openapi.alipay.com/gateway.do';
|
|
22
|
-
/** Yuan (string) → fen (integer): '99.90' → 9990. */
|
|
23
|
-
function yuanToFen(v) {
|
|
24
|
-
return Math.round(Number(v) * 100);
|
|
25
|
-
}
|
|
26
|
-
/** Fen → yuan string (2 decimals): 9990 → '99.90'. */
|
|
27
|
-
function fenToYuan(amount) {
|
|
28
|
-
return (amount / 100).toFixed(2);
|
|
29
|
-
}
|
|
30
|
-
function nowTimestamp() {
|
|
31
|
-
const d = new Date();
|
|
32
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
33
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
34
|
-
}
|
|
35
|
-
export function createAlipayOfficialDriver(store, options) {
|
|
36
|
-
const baseUrl = options.baseUrl ?? DEFAULT_GATEWAY;
|
|
37
|
-
const pageBaseUrl = options.pageBaseUrl ?? options.baseUrl ?? DEFAULT_GATEWAY;
|
|
38
|
-
const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
39
|
-
const privateKey = createPrivateKey(options.privateKeyPem);
|
|
40
|
-
const alipayPublicKey = createPublicKey(options.alipayPublicKeyPem);
|
|
41
|
-
/** RSA2 signature (SHA256withRSA), base64-encoded. */
|
|
42
|
-
function rsaSign(data) {
|
|
43
|
-
return sign('RSA-SHA256', Buffer.from(data), privateKey).toString('base64');
|
|
44
|
-
}
|
|
45
|
-
/** RSA2 verify (Alipay public key). */
|
|
46
|
-
function rsaVerify(data, signature) {
|
|
47
|
-
try {
|
|
48
|
-
return verify('RSA-SHA256', Buffer.from(data), alipayPublicKey, Buffer.from(signature, 'base64'));
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/** Join params (excluding sign/sign_type) in lexicographic order → k=v&k=v. */
|
|
55
|
-
function sortParams(params) {
|
|
56
|
-
return Object.keys(params)
|
|
57
|
-
.filter((k) => k !== 'sign' && k !== 'sign_type')
|
|
58
|
-
.sort()
|
|
59
|
-
.map((k) => `${k}=${params[k]}`)
|
|
60
|
-
.join('&');
|
|
61
|
-
}
|
|
62
|
-
async function gateway(method, bizContent, notifyUrl = '') {
|
|
63
|
-
const params = {
|
|
64
|
-
app_id: options.appId,
|
|
65
|
-
method,
|
|
66
|
-
charset: 'utf-8',
|
|
67
|
-
sign_type: 'RSA2',
|
|
68
|
-
timestamp: nowTimestamp(),
|
|
69
|
-
version: '1.0',
|
|
70
|
-
biz_content: JSON.stringify(bizContent),
|
|
71
|
-
...(notifyUrl ? { notify_url: notifyUrl } : {}),
|
|
72
|
-
};
|
|
73
|
-
params['sign'] = rsaSign(sortParams(params));
|
|
74
|
-
const res = await fetchImpl(baseUrl, {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
|
77
|
-
body: new URLSearchParams(params).toString(),
|
|
78
|
-
});
|
|
79
|
-
const body = (await res.json());
|
|
80
|
-
if (!res.ok) {
|
|
81
|
-
throw new Error(`alipay_api_error:${res.status}`);
|
|
82
|
-
}
|
|
83
|
-
return body;
|
|
84
|
-
}
|
|
85
|
-
function buildPageUrl(method, bizContent, notifyUrl) {
|
|
86
|
-
const params = {
|
|
87
|
-
app_id: options.appId,
|
|
88
|
-
method,
|
|
89
|
-
charset: 'utf-8',
|
|
90
|
-
sign_type: 'RSA2',
|
|
91
|
-
timestamp: nowTimestamp(),
|
|
92
|
-
version: '1.0',
|
|
93
|
-
biz_content: JSON.stringify(bizContent),
|
|
94
|
-
return_url: '',
|
|
95
|
-
notify_url: notifyUrl,
|
|
96
|
-
};
|
|
97
|
-
params['sign'] = rsaSign(sortParams(params));
|
|
98
|
-
const qs = Object.keys(params)
|
|
99
|
-
.sort()
|
|
100
|
-
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k] ?? '')}`)
|
|
101
|
-
.join('&');
|
|
102
|
-
return `${pageBaseUrl}?${qs}`;
|
|
103
|
-
}
|
|
104
|
-
async function createOrder(input) {
|
|
105
|
-
const biz = {
|
|
106
|
-
out_trade_no: input.orderId,
|
|
107
|
-
total_amount: fenToYuan(input.amount),
|
|
108
|
-
subject: input.subject,
|
|
109
|
-
};
|
|
110
|
-
if (input.scene === 'pc') {
|
|
111
|
-
// Face-to-face (scan) payment
|
|
112
|
-
const body = await gateway('alipay.trade.precreate', biz, input.notifyUrl);
|
|
113
|
-
const resp = body.alipay_trade_precreate_response;
|
|
114
|
-
if (resp?.code !== '10000' || !resp.qr_code) {
|
|
115
|
-
throw new Error(`alipay_precreate_failed:${resp?.sub_code ?? resp?.msg ?? 'unknown'}`);
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
kind: 'qr',
|
|
119
|
-
qrUrl: resp.qr_code,
|
|
120
|
-
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
// h5 / miniapp: mobile web payment (redirect to cashier)
|
|
124
|
-
return { kind: 'redirect', payUrl: buildPageUrl('alipay.trade.wap.pay', biz, input.notifyUrl) };
|
|
125
|
-
}
|
|
126
|
-
function verifyCallback(payload, _headers) {
|
|
127
|
-
// Async notify is a form-encoded string: a=1&b=2&sign=... (URL-decoded)
|
|
128
|
-
let params;
|
|
129
|
-
if (typeof payload === 'string') {
|
|
130
|
-
params = {};
|
|
131
|
-
try {
|
|
132
|
-
for (const pair of payload.split('&')) {
|
|
133
|
-
const eq = pair.indexOf('=');
|
|
134
|
-
if (eq > 0) {
|
|
135
|
-
const decode = (value) => decodeURIComponent(value.replace(/\+/gu, ' '));
|
|
136
|
-
const k = decode(pair.slice(0, eq));
|
|
137
|
-
const v = decode(pair.slice(eq + 1));
|
|
138
|
-
params[k] = v;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return { ok: false, reason: 'bad_payload' };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
const obj = payload;
|
|
148
|
-
params = Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, String(v)]));
|
|
149
|
-
}
|
|
150
|
-
const signature = params['sign'] ?? '';
|
|
151
|
-
if (!rsaVerify(sortParams(params), signature)) {
|
|
152
|
-
return { ok: false, reason: 'bad_alipay_signature' };
|
|
153
|
-
}
|
|
154
|
-
// notify_id anti-replay (channel self-verification path)
|
|
155
|
-
const notifyId = params['notify_id'] ?? '';
|
|
156
|
-
if (notifyId.length === 0) {
|
|
157
|
-
return { ok: false, reason: 'bad_payload' };
|
|
158
|
-
}
|
|
159
|
-
if (!store.addNonce(`ali:${notifyId}`)) {
|
|
160
|
-
return { ok: false, reason: 'nonce_reused' };
|
|
161
|
-
}
|
|
162
|
-
// Only successful states are booked (TRADE_SUCCESS / TRADE_FINISHED)
|
|
163
|
-
const tradeStatus = params['trade_status'] ?? '';
|
|
164
|
-
if (tradeStatus !== 'TRADE_SUCCESS' && tradeStatus !== 'TRADE_FINISHED') {
|
|
165
|
-
return { ok: false, reason: `trade_status:${tradeStatus}` };
|
|
166
|
-
}
|
|
167
|
-
const orderId = params['out_trade_no'];
|
|
168
|
-
if (!orderId || params['total_amount'] === undefined) {
|
|
169
|
-
return { ok: false, reason: 'bad_payload' };
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
ok: true,
|
|
173
|
-
orderId,
|
|
174
|
-
paidAmount: yuanToFen(params['total_amount']),
|
|
175
|
-
...(params['trade_no'] ? { channelTxnId: params['trade_no'] } : {}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
async function queryOrder(orderId) {
|
|
179
|
-
try {
|
|
180
|
-
const body = await gateway('alipay.trade.query', {
|
|
181
|
-
out_trade_no: orderId,
|
|
182
|
-
});
|
|
183
|
-
const resp = body.alipay_trade_query_response;
|
|
184
|
-
const status = resp?.trade_status;
|
|
185
|
-
if (status === 'TRADE_SUCCESS' || status === 'TRADE_FINISHED') {
|
|
186
|
-
return { state: 'paid', ...(resp.send_pay_date ? { paidAt: resp.send_pay_date } : {}) };
|
|
187
|
-
}
|
|
188
|
-
if (status === 'TRADE_CLOSED') {
|
|
189
|
-
return { state: 'closed' };
|
|
190
|
-
}
|
|
191
|
-
return { state: 'created' };
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return { state: 'unknown', raw: { error: 'query_failed' } };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
name: 'alipay_official',
|
|
199
|
-
scenes: ['pc', 'h5', 'miniapp'],
|
|
200
|
-
callbackVerification: 'driver',
|
|
201
|
-
createOrder,
|
|
202
|
-
verifyCallback,
|
|
203
|
-
queryOrder,
|
|
204
|
-
successResponseText: 'success',
|
|
205
|
-
};
|
|
206
|
-
}
|
package/dist/handlers.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/** @license MIT
|
|
2
|
-
* Copyright (c) 2026 vobsjs
|
|
3
|
-
* @vobs/payment
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Framework-agnostic HTTP handler layer — `paymentHttpDispatch` returns standard responses that
|
|
7
|
-
* the business wraps in one line inside its own server (Hono/Express/Fastify). This package does
|
|
8
|
-
* not bind to any server.
|
|
9
|
-
*
|
|
10
|
-
* Route conventions (per design doc §3.3/§3.5):
|
|
11
|
-
* GET /payment/available-methods available payment methods
|
|
12
|
-
* POST /api/payment/orders create order (server-side pricing + limit + idempotency + duplicate guard)
|
|
13
|
-
* GET /api/payment/orders/:id/status query status (frontend polling)
|
|
14
|
-
* POST /api/payment/orders/:id/close close on timeout/cancel (example helper)
|
|
15
|
-
* POST /api/payment/orders/:id/confirm manual release for large-amount review (admin; business-side auth required)
|
|
16
|
-
* POST /payment/callback/:driver channel callback (verify + anti-replay + state machine)
|
|
17
|
-
*/
|
|
18
|
-
import type { PaymentManager } from './manager.js';
|
|
19
|
-
export interface HttpResponse {
|
|
20
|
-
readonly status: number;
|
|
21
|
-
readonly body: unknown;
|
|
22
|
-
}
|
|
23
|
-
export interface HttpRequestContext {
|
|
24
|
-
readonly method: string;
|
|
25
|
-
/** Relative path (e.g. `/api/payment/orders` or `/payment/callback/mock`). */
|
|
26
|
-
readonly path: string;
|
|
27
|
-
readonly headers?: Readonly<Record<string, string | undefined>>;
|
|
28
|
-
readonly body?: string;
|
|
29
|
-
}
|
|
30
|
-
export type PaymentProtectedRoute = 'status' | 'close' | 'confirm';
|
|
31
|
-
export interface PaymentHttpDispatchOptions {
|
|
32
|
-
/**
|
|
33
|
-
* Authorizes order reads and mutations. The dispatcher denies these routes when omitted;
|
|
34
|
-
* the host owns authentication, CSRF protection, and order ownership checks.
|
|
35
|
-
*/
|
|
36
|
-
readonly authorize?: (context: {
|
|
37
|
-
readonly route: PaymentProtectedRoute;
|
|
38
|
-
readonly orderId: string;
|
|
39
|
-
readonly request: HttpRequestContext;
|
|
40
|
-
}) => boolean | Promise<boolean>;
|
|
41
|
-
}
|
|
42
|
-
type RouteResult = HttpResponse;
|
|
43
|
-
/** Create a dispatch function (pure logic, no framework dependency). */
|
|
44
|
-
export declare function paymentHttpDispatch(manager: PaymentManager, options?: PaymentHttpDispatchOptions): (ctx: HttpRequestContext) => Promise<RouteResult>;
|
|
45
|
-
export {};
|