@h-ai/payment 0.1.0-alpha5
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 +202 -0
- package/README.md +137 -0
- package/dist/api/index.d.ts +211 -0
- package/dist/api/index.js +96 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-4IUARFWH.js +67 -0
- package/dist/chunk-4IUARFWH.js.map +1 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +90 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +854 -0
- package/dist/index.js.map +1 -0
- package/dist/payment-types-D1GvLKHg.d.ts +209 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
import { paymentM, HaiPaymentError } from './chunk-4IUARFWH.js';
|
|
2
|
+
export { HaiPaymentError } from './chunk-4IUARFWH.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { core, err, ok } from '@h-ai/core';
|
|
5
|
+
import { audit } from '@h-ai/audit';
|
|
6
|
+
import { createVerify, createDecipheriv, randomBytes, createSign, createHmac } from 'crypto';
|
|
7
|
+
import { Buffer } from 'buffer';
|
|
8
|
+
|
|
9
|
+
var WechatPayConfigSchema = z.object({
|
|
10
|
+
mchId: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
11
|
+
apiV3Key: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
12
|
+
serialNo: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
13
|
+
privateKey: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
14
|
+
platformCert: z.string().optional(),
|
|
15
|
+
appId: z.string().min(1, paymentM("payment_configFieldRequired"))
|
|
16
|
+
});
|
|
17
|
+
var AlipayConfigSchema = z.object({
|
|
18
|
+
appId: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
19
|
+
privateKey: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
20
|
+
alipayPublicKey: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
21
|
+
signType: z.enum(["RSA2", "RSA"]).default("RSA2"),
|
|
22
|
+
sandbox: z.boolean().default(false)
|
|
23
|
+
});
|
|
24
|
+
var StripeConfigSchema = z.object({
|
|
25
|
+
secretKey: z.string().min(1, paymentM("payment_configFieldRequired")),
|
|
26
|
+
webhookSecret: z.string().min(1, paymentM("payment_configFieldRequired"))
|
|
27
|
+
});
|
|
28
|
+
var PaymentConfigSchema = z.object({
|
|
29
|
+
wechat: WechatPayConfigSchema.optional(),
|
|
30
|
+
alipay: AlipayConfigSchema.optional(),
|
|
31
|
+
stripe: StripeConfigSchema.optional()
|
|
32
|
+
});
|
|
33
|
+
var logger = core.logger.child({ module: "payment", scope: "functions" });
|
|
34
|
+
var providers = /* @__PURE__ */ new Map();
|
|
35
|
+
async function auditLog(input) {
|
|
36
|
+
const result = await audit.log(input);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
logger.warn("Failed to write payment audit log", { action: input.action, error: result.error.message });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function registerProvider(provider) {
|
|
42
|
+
providers.set(provider.name, provider);
|
|
43
|
+
}
|
|
44
|
+
function getProvider(name) {
|
|
45
|
+
return providers.get(name);
|
|
46
|
+
}
|
|
47
|
+
function requireProvider(name) {
|
|
48
|
+
const provider = providers.get(name);
|
|
49
|
+
if (!provider) {
|
|
50
|
+
return err(
|
|
51
|
+
HaiPaymentError.PROVIDER_NOT_FOUND,
|
|
52
|
+
paymentM("payment_providerNotFound")
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return ok(provider);
|
|
56
|
+
}
|
|
57
|
+
async function createOrder(providerName, input) {
|
|
58
|
+
const result = requireProvider(providerName);
|
|
59
|
+
if (!result.success)
|
|
60
|
+
return result;
|
|
61
|
+
const orderResult = await result.data.createOrder(input);
|
|
62
|
+
if (orderResult.success) {
|
|
63
|
+
await auditLog({
|
|
64
|
+
action: "create_order",
|
|
65
|
+
resource: "payment",
|
|
66
|
+
resourceId: input.orderNo,
|
|
67
|
+
details: { provider: providerName, amount: input.amount, tradeType: input.tradeType }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return orderResult;
|
|
71
|
+
}
|
|
72
|
+
async function handleNotify(providerName, request) {
|
|
73
|
+
const result = requireProvider(providerName);
|
|
74
|
+
if (!result.success)
|
|
75
|
+
return result;
|
|
76
|
+
const notifyResult = await result.data.handleNotify(request);
|
|
77
|
+
if (notifyResult.success) {
|
|
78
|
+
await auditLog({
|
|
79
|
+
action: "payment_notify",
|
|
80
|
+
resource: "payment",
|
|
81
|
+
resourceId: notifyResult.data.orderNo,
|
|
82
|
+
details: { provider: providerName, transactionId: notifyResult.data.transactionId, status: notifyResult.data.status, amount: notifyResult.data.amount }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return notifyResult;
|
|
86
|
+
}
|
|
87
|
+
async function queryOrder(providerName, orderNo) {
|
|
88
|
+
const result = requireProvider(providerName);
|
|
89
|
+
if (!result.success)
|
|
90
|
+
return result;
|
|
91
|
+
return result.data.queryOrder(orderNo);
|
|
92
|
+
}
|
|
93
|
+
async function refund(providerName, input) {
|
|
94
|
+
const result = requireProvider(providerName);
|
|
95
|
+
if (!result.success)
|
|
96
|
+
return result;
|
|
97
|
+
const refundResult = await result.data.refund(input);
|
|
98
|
+
if (refundResult.success) {
|
|
99
|
+
await auditLog({
|
|
100
|
+
action: "refund",
|
|
101
|
+
resource: "payment",
|
|
102
|
+
resourceId: input.orderNo,
|
|
103
|
+
details: { provider: providerName, refundNo: input.refundNo, amount: input.amount }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return refundResult;
|
|
107
|
+
}
|
|
108
|
+
async function closeOrder(providerName, orderNo) {
|
|
109
|
+
const result = requireProvider(providerName);
|
|
110
|
+
if (!result.success)
|
|
111
|
+
return result;
|
|
112
|
+
const closeResult = await result.data.closeOrder(orderNo);
|
|
113
|
+
if (closeResult.success) {
|
|
114
|
+
await auditLog({
|
|
115
|
+
action: "close_order",
|
|
116
|
+
resource: "payment",
|
|
117
|
+
resourceId: orderNo,
|
|
118
|
+
details: { provider: providerName }
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return closeResult;
|
|
122
|
+
}
|
|
123
|
+
function clearProviders() {
|
|
124
|
+
providers.clear();
|
|
125
|
+
}
|
|
126
|
+
function signAlipayParams(params, privateKey, signType = "RSA2") {
|
|
127
|
+
const sorted = Object.keys(params).filter((k) => params[k] !== void 0 && params[k] !== "").sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
128
|
+
const algorithm = signType === "RSA2" ? "RSA-SHA256" : "RSA-SHA1";
|
|
129
|
+
const sign = createSign(algorithm);
|
|
130
|
+
sign.update(sorted);
|
|
131
|
+
return sign.sign(privateKey, "base64");
|
|
132
|
+
}
|
|
133
|
+
function verifyAlipayNotify(params, alipayPublicKey) {
|
|
134
|
+
const sign = params.sign ?? "";
|
|
135
|
+
const signType = params.sign_type ?? "RSA2";
|
|
136
|
+
const sorted = Object.keys(params).filter((k) => k !== "sign" && k !== "sign_type" && params[k] !== void 0 && params[k] !== "").sort().map((k) => `${k}=${params[k]}`).join("&");
|
|
137
|
+
const algorithm = signType === "RSA2" ? "RSA-SHA256" : "RSA-SHA1";
|
|
138
|
+
const verify = createVerify(algorithm);
|
|
139
|
+
verify.update(sorted);
|
|
140
|
+
return verify.verify(alipayPublicKey, sign, "base64");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/providers/alipay/alipay-provider.ts
|
|
144
|
+
var ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do";
|
|
145
|
+
var ALIPAY_SANDBOX_GATEWAY = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
|
|
146
|
+
function createAlipayProvider(config) {
|
|
147
|
+
const gateway = config.sandbox ? ALIPAY_SANDBOX_GATEWAY : ALIPAY_GATEWAY;
|
|
148
|
+
const signType = config.signType ?? "RSA2";
|
|
149
|
+
function buildCommonParams(method, notifyUrl) {
|
|
150
|
+
return {
|
|
151
|
+
app_id: config.appId,
|
|
152
|
+
method,
|
|
153
|
+
charset: "utf-8",
|
|
154
|
+
sign_type: signType,
|
|
155
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19),
|
|
156
|
+
version: "1.0",
|
|
157
|
+
notify_url: notifyUrl
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function getTradeConfig(tradeType) {
|
|
161
|
+
const map = {
|
|
162
|
+
jsapi: { method: "alipay.trade.create", productCode: "JSAPI_PAY" },
|
|
163
|
+
h5: { method: "alipay.trade.wap.pay", productCode: "QUICK_WAP_WAY" },
|
|
164
|
+
app: { method: "alipay.trade.app.pay", productCode: "QUICK_MSECURITY_PAY" },
|
|
165
|
+
native: { method: "alipay.trade.precreate", productCode: "FACE_TO_FACE_PAYMENT" }
|
|
166
|
+
};
|
|
167
|
+
return map[tradeType] ?? map.h5;
|
|
168
|
+
}
|
|
169
|
+
async function alipayRequest(params) {
|
|
170
|
+
const sign = signAlipayParams(params, config.privateKey, signType);
|
|
171
|
+
const allParams = { ...params, sign };
|
|
172
|
+
const queryString = Object.entries(allParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
173
|
+
const response = await fetch(`${gateway}?${queryString}`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Alipay API failed: ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
return response.json();
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
name: "alipay",
|
|
184
|
+
async createOrder(input) {
|
|
185
|
+
try {
|
|
186
|
+
const { method, productCode } = getTradeConfig(input.tradeType);
|
|
187
|
+
const bizContent = JSON.stringify({
|
|
188
|
+
out_trade_no: input.orderNo,
|
|
189
|
+
total_amount: (input.amount / 100).toFixed(2),
|
|
190
|
+
// 分 → 元
|
|
191
|
+
subject: input.description,
|
|
192
|
+
product_code: productCode,
|
|
193
|
+
passback_params: input.metadata ? JSON.stringify(input.metadata) : void 0
|
|
194
|
+
});
|
|
195
|
+
const params = {
|
|
196
|
+
...buildCommonParams(method, input.notifyUrl),
|
|
197
|
+
biz_content: bizContent
|
|
198
|
+
};
|
|
199
|
+
const response = await alipayRequest(params);
|
|
200
|
+
const key = `${method.replace(/\./g, "_")}_response`;
|
|
201
|
+
const data = response[key];
|
|
202
|
+
return ok({
|
|
203
|
+
provider: "alipay",
|
|
204
|
+
tradeType: input.tradeType,
|
|
205
|
+
clientParams: data ?? {},
|
|
206
|
+
prepayId: data?.trade_no
|
|
207
|
+
});
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
return err(
|
|
210
|
+
HaiPaymentError.CREATE_ORDER_FAILED,
|
|
211
|
+
paymentM("payment_createOrderFailed"),
|
|
212
|
+
cause
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async handleNotify(request) {
|
|
217
|
+
try {
|
|
218
|
+
const params = {};
|
|
219
|
+
const pairs = request.body.split("&");
|
|
220
|
+
for (const pair of pairs) {
|
|
221
|
+
const [key, ...rest] = pair.split("=");
|
|
222
|
+
params[decodeURIComponent(key)] = decodeURIComponent(rest.join("="));
|
|
223
|
+
}
|
|
224
|
+
const valid = verifyAlipayNotify(params, config.alipayPublicKey);
|
|
225
|
+
if (!valid) {
|
|
226
|
+
return err(
|
|
227
|
+
HaiPaymentError.NOTIFY_VERIFY_FAILED,
|
|
228
|
+
paymentM("payment_notifyVerifyFailed")
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const statusMap = {
|
|
232
|
+
TRADE_SUCCESS: "paid",
|
|
233
|
+
TRADE_CLOSED: "closed",
|
|
234
|
+
TRADE_FINISHED: "paid",
|
|
235
|
+
WAIT_BUYER_PAY: "pending"
|
|
236
|
+
};
|
|
237
|
+
return ok({
|
|
238
|
+
orderNo: params.out_trade_no,
|
|
239
|
+
transactionId: params.trade_no,
|
|
240
|
+
amount: Math.round(Number.parseFloat(params.total_amount) * 100),
|
|
241
|
+
// 元 → 分
|
|
242
|
+
status: statusMap[params.trade_status] ?? "pending",
|
|
243
|
+
paidAt: params.gmt_payment ? new Date(params.gmt_payment) : void 0,
|
|
244
|
+
raw: params
|
|
245
|
+
});
|
|
246
|
+
} catch (cause) {
|
|
247
|
+
return err(
|
|
248
|
+
HaiPaymentError.NOTIFY_PARSE_FAILED,
|
|
249
|
+
paymentM("payment_notifyParseFailed"),
|
|
250
|
+
cause
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
async queryOrder(orderNo) {
|
|
255
|
+
try {
|
|
256
|
+
const method = "alipay.trade.query";
|
|
257
|
+
const params = {
|
|
258
|
+
...buildCommonParams(method, ""),
|
|
259
|
+
biz_content: JSON.stringify({ out_trade_no: orderNo })
|
|
260
|
+
};
|
|
261
|
+
const response = await alipayRequest(params);
|
|
262
|
+
const data = response.alipay_trade_query_response;
|
|
263
|
+
const statusMap = {
|
|
264
|
+
TRADE_SUCCESS: "paid",
|
|
265
|
+
TRADE_CLOSED: "closed",
|
|
266
|
+
TRADE_FINISHED: "paid",
|
|
267
|
+
WAIT_BUYER_PAY: "pending"
|
|
268
|
+
};
|
|
269
|
+
return ok({
|
|
270
|
+
orderNo,
|
|
271
|
+
transactionId: data?.trade_no,
|
|
272
|
+
status: statusMap[data?.trade_status] ?? "pending",
|
|
273
|
+
amount: Math.round(Number.parseFloat(data?.total_amount ?? "0") * 100),
|
|
274
|
+
paidAt: data?.send_pay_date ? new Date(data.send_pay_date) : void 0
|
|
275
|
+
});
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
return err(
|
|
278
|
+
HaiPaymentError.QUERY_ORDER_FAILED,
|
|
279
|
+
paymentM("payment_queryOrderFailed"),
|
|
280
|
+
cause
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
async refund(input) {
|
|
285
|
+
try {
|
|
286
|
+
const method = "alipay.trade.refund";
|
|
287
|
+
const params = {
|
|
288
|
+
...buildCommonParams(method, ""),
|
|
289
|
+
biz_content: JSON.stringify({
|
|
290
|
+
out_trade_no: input.orderNo,
|
|
291
|
+
out_request_no: input.refundNo,
|
|
292
|
+
refund_amount: (input.amount / 100).toFixed(2),
|
|
293
|
+
refund_reason: input.reason
|
|
294
|
+
})
|
|
295
|
+
};
|
|
296
|
+
const response = await alipayRequest(params);
|
|
297
|
+
const data = response.alipay_trade_refund_response;
|
|
298
|
+
return ok({
|
|
299
|
+
refundNo: input.refundNo,
|
|
300
|
+
refundId: data?.trade_no ?? "",
|
|
301
|
+
status: data?.fund_change === "Y" ? "success" : "processing"
|
|
302
|
+
});
|
|
303
|
+
} catch (cause) {
|
|
304
|
+
return err(
|
|
305
|
+
HaiPaymentError.REFUND_FAILED,
|
|
306
|
+
paymentM("payment_refundFailed"),
|
|
307
|
+
cause
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
async closeOrder(orderNo) {
|
|
312
|
+
try {
|
|
313
|
+
const method = "alipay.trade.close";
|
|
314
|
+
const params = {
|
|
315
|
+
...buildCommonParams(method, ""),
|
|
316
|
+
biz_content: JSON.stringify({ out_trade_no: orderNo })
|
|
317
|
+
};
|
|
318
|
+
await alipayRequest(params);
|
|
319
|
+
return ok(void 0);
|
|
320
|
+
} catch (cause) {
|
|
321
|
+
return err(
|
|
322
|
+
HaiPaymentError.CLOSE_ORDER_FAILED,
|
|
323
|
+
paymentM("payment_closeOrderFailed"),
|
|
324
|
+
cause
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
var STRIPE_API_BASE = "https://api.stripe.com/v1";
|
|
331
|
+
function createStripeProvider(config) {
|
|
332
|
+
async function stripeRequest(method, path, body) {
|
|
333
|
+
const response = await fetch(`${STRIPE_API_BASE}${path}`, {
|
|
334
|
+
method,
|
|
335
|
+
headers: {
|
|
336
|
+
"Authorization": `Bearer ${config.secretKey}`,
|
|
337
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
338
|
+
},
|
|
339
|
+
body: body ? Object.entries(body).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : void 0
|
|
340
|
+
});
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
const errorText = await response.text();
|
|
343
|
+
throw new Error(`Stripe API ${method} ${path} failed: ${response.status} ${errorText}`);
|
|
344
|
+
}
|
|
345
|
+
return response.json();
|
|
346
|
+
}
|
|
347
|
+
const TIMESTAMP_TOLERANCE = 300;
|
|
348
|
+
function verifyWebhookSignature(payload, signature) {
|
|
349
|
+
const parts = signature.split(",");
|
|
350
|
+
const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2) ?? "";
|
|
351
|
+
const v1 = parts.find((p) => p.startsWith("v1="))?.slice(3) ?? "";
|
|
352
|
+
const ts = Number.parseInt(timestamp, 10);
|
|
353
|
+
if (Number.isNaN(ts) || Math.abs(Math.floor(Date.now() / 1e3) - ts) > TIMESTAMP_TOLERANCE) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const signedPayload = `${timestamp}.${payload}`;
|
|
357
|
+
const expected = createHmac("sha256", config.webhookSecret).update(signedPayload).digest("hex");
|
|
358
|
+
try {
|
|
359
|
+
return core.string.constantTimeEqual(v1, expected);
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
name: "stripe",
|
|
366
|
+
async createOrder(input) {
|
|
367
|
+
try {
|
|
368
|
+
const params = {
|
|
369
|
+
"mode": "payment",
|
|
370
|
+
"line_items[0][price_data][currency]": input.currency?.toLowerCase() ?? "usd",
|
|
371
|
+
"line_items[0][price_data][product_data][name]": input.description,
|
|
372
|
+
"line_items[0][price_data][unit_amount]": input.amount.toString(),
|
|
373
|
+
"line_items[0][quantity]": "1"
|
|
374
|
+
};
|
|
375
|
+
params["metadata[orderNo]"] = input.orderNo;
|
|
376
|
+
if (input.metadata) {
|
|
377
|
+
for (const [k, v] of Object.entries(input.metadata)) {
|
|
378
|
+
params[`metadata[${k}]`] = v;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const session = await stripeRequest("POST", "/checkout/sessions", params);
|
|
382
|
+
return ok({
|
|
383
|
+
provider: "stripe",
|
|
384
|
+
tradeType: input.tradeType,
|
|
385
|
+
clientParams: {
|
|
386
|
+
sessionId: session.id,
|
|
387
|
+
checkoutUrl: session.url
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
} catch (cause) {
|
|
391
|
+
return err(
|
|
392
|
+
HaiPaymentError.CREATE_ORDER_FAILED,
|
|
393
|
+
paymentM("payment_createOrderFailed"),
|
|
394
|
+
cause
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
async handleNotify(request) {
|
|
399
|
+
try {
|
|
400
|
+
const signature = request.headers["stripe-signature"] ?? "";
|
|
401
|
+
const valid = verifyWebhookSignature(request.body, signature);
|
|
402
|
+
if (!valid) {
|
|
403
|
+
return err(
|
|
404
|
+
HaiPaymentError.NOTIFY_VERIFY_FAILED,
|
|
405
|
+
paymentM("payment_notifyVerifyFailed")
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const event = JSON.parse(request.body);
|
|
409
|
+
const obj = event.data.object;
|
|
410
|
+
const orderNo = obj.metadata?.orderNo ?? "";
|
|
411
|
+
const statusMap = {
|
|
412
|
+
"checkout.session.completed": "paid",
|
|
413
|
+
"payment_intent.payment_failed": "failed"
|
|
414
|
+
};
|
|
415
|
+
return ok({
|
|
416
|
+
orderNo,
|
|
417
|
+
transactionId: obj.id,
|
|
418
|
+
amount: obj.amount_total ?? 0,
|
|
419
|
+
status: statusMap[event.type] ?? "pending",
|
|
420
|
+
paidAt: /* @__PURE__ */ new Date(),
|
|
421
|
+
raw: event
|
|
422
|
+
});
|
|
423
|
+
} catch (cause) {
|
|
424
|
+
return err(
|
|
425
|
+
HaiPaymentError.NOTIFY_PARSE_FAILED,
|
|
426
|
+
paymentM("payment_notifyParseFailed"),
|
|
427
|
+
cause
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
async queryOrder(orderNo) {
|
|
432
|
+
try {
|
|
433
|
+
const sessions = await stripeRequest("GET", `/checkout/sessions?limit=1&metadata[orderNo]=${encodeURIComponent(orderNo)}`);
|
|
434
|
+
const session = sessions.data[0];
|
|
435
|
+
if (!session) {
|
|
436
|
+
return ok({
|
|
437
|
+
orderNo,
|
|
438
|
+
status: "pending",
|
|
439
|
+
amount: 0
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const statusMap = {
|
|
443
|
+
paid: "paid",
|
|
444
|
+
unpaid: "pending",
|
|
445
|
+
no_payment_required: "paid"
|
|
446
|
+
};
|
|
447
|
+
return ok({
|
|
448
|
+
orderNo,
|
|
449
|
+
transactionId: session.id,
|
|
450
|
+
status: statusMap[session.payment_status] ?? "pending",
|
|
451
|
+
amount: session.amount_total
|
|
452
|
+
});
|
|
453
|
+
} catch (cause) {
|
|
454
|
+
return err(
|
|
455
|
+
HaiPaymentError.QUERY_ORDER_FAILED,
|
|
456
|
+
paymentM("payment_queryOrderFailed"),
|
|
457
|
+
cause
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
async refund(input) {
|
|
462
|
+
try {
|
|
463
|
+
const sessions = await stripeRequest("GET", `/checkout/sessions?limit=1&metadata[orderNo]=${encodeURIComponent(input.orderNo)}`);
|
|
464
|
+
const paymentIntent = sessions.data[0]?.payment_intent;
|
|
465
|
+
if (!paymentIntent) {
|
|
466
|
+
return err(
|
|
467
|
+
HaiPaymentError.REFUND_FAILED,
|
|
468
|
+
paymentM("payment_refundFailed")
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const refund2 = await stripeRequest("POST", "/refunds", {
|
|
472
|
+
payment_intent: paymentIntent,
|
|
473
|
+
amount: input.amount.toString(),
|
|
474
|
+
reason: input.reason ?? "requested_by_customer"
|
|
475
|
+
});
|
|
476
|
+
return ok({
|
|
477
|
+
refundNo: input.refundNo,
|
|
478
|
+
refundId: refund2.id,
|
|
479
|
+
status: refund2.status === "succeeded" ? "success" : "processing"
|
|
480
|
+
});
|
|
481
|
+
} catch (cause) {
|
|
482
|
+
return err(
|
|
483
|
+
HaiPaymentError.REFUND_FAILED,
|
|
484
|
+
paymentM("payment_refundFailed"),
|
|
485
|
+
cause
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
async closeOrder(_orderNo) {
|
|
490
|
+
return ok(void 0);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function generateNonce(length = 32) {
|
|
495
|
+
return randomBytes(length).toString("hex").slice(0, length);
|
|
496
|
+
}
|
|
497
|
+
function getTimestamp() {
|
|
498
|
+
return Math.floor(Date.now() / 1e3).toString();
|
|
499
|
+
}
|
|
500
|
+
function signRequest(method, url, timestamp, nonce, body, privateKey) {
|
|
501
|
+
const message = `${method}
|
|
502
|
+
${url}
|
|
503
|
+
${timestamp}
|
|
504
|
+
${nonce}
|
|
505
|
+
${body}
|
|
506
|
+
`;
|
|
507
|
+
const sign = createSign("RSA-SHA256");
|
|
508
|
+
sign.update(message);
|
|
509
|
+
return sign.sign(privateKey, "base64");
|
|
510
|
+
}
|
|
511
|
+
function signJsapi(appId, timestamp, nonce, prepayId, privateKey) {
|
|
512
|
+
const message = `${appId}
|
|
513
|
+
${timestamp}
|
|
514
|
+
${nonce}
|
|
515
|
+
prepay_id=${prepayId}
|
|
516
|
+
`;
|
|
517
|
+
const sign = createSign("RSA-SHA256");
|
|
518
|
+
sign.update(message);
|
|
519
|
+
return sign.sign(privateKey, "base64");
|
|
520
|
+
}
|
|
521
|
+
function verifyNotifySignature(timestamp, nonce, body, signature, platformCert) {
|
|
522
|
+
const message = `${timestamp}
|
|
523
|
+
${nonce}
|
|
524
|
+
${body}
|
|
525
|
+
`;
|
|
526
|
+
const verify = createVerify("RSA-SHA256");
|
|
527
|
+
verify.update(message);
|
|
528
|
+
return verify.verify(platformCert, signature, "base64");
|
|
529
|
+
}
|
|
530
|
+
function decryptResource(ciphertext, nonce, associatedData, apiV3Key) {
|
|
531
|
+
const buf = Buffer.from(ciphertext, "base64");
|
|
532
|
+
const authTag = buf.subarray(buf.length - 16);
|
|
533
|
+
const data = buf.subarray(0, buf.length - 16);
|
|
534
|
+
const decipher = createDecipheriv("aes-256-gcm", apiV3Key, nonce);
|
|
535
|
+
decipher.setAuthTag(authTag);
|
|
536
|
+
decipher.setAAD(Buffer.from(associatedData));
|
|
537
|
+
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
538
|
+
return decrypted.toString("utf-8");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/providers/wechat/wechat-pay-provider.ts
|
|
542
|
+
var WECHAT_API_BASE = "https://api.mch.weixin.qq.com";
|
|
543
|
+
function createWechatPayProvider(config) {
|
|
544
|
+
async function wechatRequest(method, path, body) {
|
|
545
|
+
const timestamp = getTimestamp();
|
|
546
|
+
const nonce = generateNonce();
|
|
547
|
+
const bodyStr = body ? JSON.stringify(body) : "";
|
|
548
|
+
const signature = signRequest(method, path, timestamp, nonce, bodyStr, config.privateKey);
|
|
549
|
+
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${config.mchId}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${config.serialNo}",signature="${signature}"`;
|
|
550
|
+
const response = await fetch(`${WECHAT_API_BASE}${path}`, {
|
|
551
|
+
method,
|
|
552
|
+
headers: {
|
|
553
|
+
"Content-Type": "application/json",
|
|
554
|
+
"Authorization": authorization,
|
|
555
|
+
"Accept": "application/json"
|
|
556
|
+
},
|
|
557
|
+
body: bodyStr || void 0
|
|
558
|
+
});
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
const errorText = await response.text();
|
|
561
|
+
throw new Error(`Wechat API ${method} ${path} failed: ${response.status} ${errorText}`);
|
|
562
|
+
}
|
|
563
|
+
return response.json();
|
|
564
|
+
}
|
|
565
|
+
function getOrderPath(tradeType) {
|
|
566
|
+
const map = {
|
|
567
|
+
jsapi: "/v3/pay/transactions/jsapi",
|
|
568
|
+
h5: "/v3/pay/transactions/h5",
|
|
569
|
+
app: "/v3/pay/transactions/app",
|
|
570
|
+
native: "/v3/pay/transactions/native"
|
|
571
|
+
};
|
|
572
|
+
return map[tradeType] ?? "/v3/pay/transactions/jsapi";
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
name: "wechat",
|
|
576
|
+
async createOrder(input) {
|
|
577
|
+
try {
|
|
578
|
+
const requestBody = {
|
|
579
|
+
appid: config.appId,
|
|
580
|
+
mchid: config.mchId,
|
|
581
|
+
description: input.description,
|
|
582
|
+
out_trade_no: input.orderNo,
|
|
583
|
+
notify_url: input.notifyUrl,
|
|
584
|
+
amount: {
|
|
585
|
+
total: input.amount,
|
|
586
|
+
currency: input.currency ?? "CNY"
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
if (input.tradeType === "jsapi" && input.userId) {
|
|
590
|
+
requestBody.payer = { openid: input.userId };
|
|
591
|
+
}
|
|
592
|
+
if (input.metadata) {
|
|
593
|
+
requestBody.attach = JSON.stringify(input.metadata);
|
|
594
|
+
}
|
|
595
|
+
const path = getOrderPath(input.tradeType);
|
|
596
|
+
const response = await wechatRequest("POST", path, requestBody);
|
|
597
|
+
let clientParams = {};
|
|
598
|
+
if (input.tradeType === "jsapi") {
|
|
599
|
+
const timestamp = getTimestamp();
|
|
600
|
+
const nonce = generateNonce();
|
|
601
|
+
const paySign = signJsapi(config.appId, timestamp, nonce, response.prepay_id, config.privateKey);
|
|
602
|
+
clientParams = {
|
|
603
|
+
appId: config.appId,
|
|
604
|
+
timeStamp: timestamp,
|
|
605
|
+
nonceStr: nonce,
|
|
606
|
+
package: `prepay_id=${response.prepay_id}`,
|
|
607
|
+
signType: "RSA",
|
|
608
|
+
paySign
|
|
609
|
+
};
|
|
610
|
+
} else if (input.tradeType === "h5") {
|
|
611
|
+
clientParams = { h5Url: response.h5_url };
|
|
612
|
+
} else if (input.tradeType === "native") {
|
|
613
|
+
clientParams = { codeUrl: response.code_url };
|
|
614
|
+
}
|
|
615
|
+
return ok({
|
|
616
|
+
provider: "wechat",
|
|
617
|
+
tradeType: input.tradeType,
|
|
618
|
+
clientParams,
|
|
619
|
+
prepayId: response.prepay_id
|
|
620
|
+
});
|
|
621
|
+
} catch (cause) {
|
|
622
|
+
return err(
|
|
623
|
+
HaiPaymentError.CREATE_ORDER_FAILED,
|
|
624
|
+
paymentM("payment_createOrderFailed"),
|
|
625
|
+
cause
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async handleNotify(request) {
|
|
630
|
+
try {
|
|
631
|
+
const timestamp = request.headers["wechatpay-timestamp"] ?? "";
|
|
632
|
+
const nonce = request.headers["wechatpay-nonce"] ?? "";
|
|
633
|
+
const signature = request.headers["wechatpay-signature"] ?? "";
|
|
634
|
+
if (!config.platformCert) {
|
|
635
|
+
return err(
|
|
636
|
+
HaiPaymentError.NOTIFY_VERIFY_FAILED,
|
|
637
|
+
paymentM("payment_notifyVerifyFailed")
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const valid = verifyNotifySignature(timestamp, nonce, request.body, signature, config.platformCert);
|
|
641
|
+
if (!valid) {
|
|
642
|
+
return err(
|
|
643
|
+
HaiPaymentError.NOTIFY_VERIFY_FAILED,
|
|
644
|
+
paymentM("payment_notifyVerifyFailed")
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const body = JSON.parse(request.body);
|
|
648
|
+
const decrypted = decryptResource(
|
|
649
|
+
body.resource.ciphertext,
|
|
650
|
+
body.resource.nonce,
|
|
651
|
+
body.resource.associated_data,
|
|
652
|
+
config.apiV3Key
|
|
653
|
+
);
|
|
654
|
+
const resource = JSON.parse(decrypted);
|
|
655
|
+
return ok({
|
|
656
|
+
orderNo: resource.out_trade_no,
|
|
657
|
+
transactionId: resource.transaction_id,
|
|
658
|
+
amount: resource.amount.total,
|
|
659
|
+
status: resource.trade_state === "SUCCESS" ? "paid" : "failed",
|
|
660
|
+
paidAt: resource.success_time ? new Date(resource.success_time) : void 0,
|
|
661
|
+
raw: resource
|
|
662
|
+
});
|
|
663
|
+
} catch (cause) {
|
|
664
|
+
return err(
|
|
665
|
+
HaiPaymentError.NOTIFY_PARSE_FAILED,
|
|
666
|
+
paymentM("payment_notifyParseFailed"),
|
|
667
|
+
cause
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
async queryOrder(orderNo) {
|
|
672
|
+
try {
|
|
673
|
+
const path = `/v3/pay/transactions/out-trade-no/${encodeURIComponent(orderNo)}?mchid=${encodeURIComponent(config.mchId)}`;
|
|
674
|
+
const response = await wechatRequest("GET", path);
|
|
675
|
+
const statusMap = {
|
|
676
|
+
SUCCESS: "paid",
|
|
677
|
+
CLOSED: "closed",
|
|
678
|
+
REFUND: "refunded",
|
|
679
|
+
NOTPAY: "pending",
|
|
680
|
+
PAYERROR: "failed"
|
|
681
|
+
};
|
|
682
|
+
return ok({
|
|
683
|
+
orderNo: response.out_trade_no,
|
|
684
|
+
transactionId: response.transaction_id,
|
|
685
|
+
status: statusMap[response.trade_state] ?? "pending",
|
|
686
|
+
amount: response.amount.total,
|
|
687
|
+
paidAt: response.success_time ? new Date(response.success_time) : void 0
|
|
688
|
+
});
|
|
689
|
+
} catch (cause) {
|
|
690
|
+
return err(
|
|
691
|
+
HaiPaymentError.QUERY_ORDER_FAILED,
|
|
692
|
+
paymentM("payment_queryOrderFailed"),
|
|
693
|
+
cause
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
async refund(input) {
|
|
698
|
+
try {
|
|
699
|
+
const response = await wechatRequest("POST", "/v3/refund/domestic/refunds", {
|
|
700
|
+
out_trade_no: input.orderNo,
|
|
701
|
+
out_refund_no: input.refundNo,
|
|
702
|
+
amount: {
|
|
703
|
+
refund: input.amount,
|
|
704
|
+
total: input.totalAmount ?? input.amount,
|
|
705
|
+
currency: "CNY"
|
|
706
|
+
},
|
|
707
|
+
reason: input.reason
|
|
708
|
+
});
|
|
709
|
+
const statusMap = {
|
|
710
|
+
SUCCESS: "success",
|
|
711
|
+
PROCESSING: "processing",
|
|
712
|
+
ABNORMAL: "failed"
|
|
713
|
+
};
|
|
714
|
+
return ok({
|
|
715
|
+
refundNo: input.refundNo,
|
|
716
|
+
refundId: response.refund_id,
|
|
717
|
+
status: statusMap[response.status] ?? "processing"
|
|
718
|
+
});
|
|
719
|
+
} catch (cause) {
|
|
720
|
+
return err(
|
|
721
|
+
HaiPaymentError.REFUND_FAILED,
|
|
722
|
+
paymentM("payment_refundFailed"),
|
|
723
|
+
cause
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
async closeOrder(orderNo) {
|
|
728
|
+
try {
|
|
729
|
+
await wechatRequest("POST", `/v3/pay/transactions/out-trade-no/${encodeURIComponent(orderNo)}/close`, {
|
|
730
|
+
mchid: config.mchId
|
|
731
|
+
});
|
|
732
|
+
return ok(void 0);
|
|
733
|
+
} catch (cause) {
|
|
734
|
+
return err(
|
|
735
|
+
HaiPaymentError.CLOSE_ORDER_FAILED,
|
|
736
|
+
paymentM("payment_closeOrderFailed"),
|
|
737
|
+
cause
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/payment-main.ts
|
|
745
|
+
var logger2 = core.logger.child({ module: "payment", scope: "main" });
|
|
746
|
+
var currentConfig = null;
|
|
747
|
+
var initInProgress = false;
|
|
748
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
749
|
+
HaiPaymentError.NOT_INITIALIZED,
|
|
750
|
+
() => paymentM("payment_notInitialized")
|
|
751
|
+
);
|
|
752
|
+
var payment = {
|
|
753
|
+
/**
|
|
754
|
+
* 初始化支付模块
|
|
755
|
+
*
|
|
756
|
+
* 根据提供的配置自动注册对应的 Provider。
|
|
757
|
+
*
|
|
758
|
+
* @param config - 支付配置
|
|
759
|
+
*/
|
|
760
|
+
async init(config) {
|
|
761
|
+
if (initInProgress) {
|
|
762
|
+
logger2.warn("Payment module init already in progress, skipping");
|
|
763
|
+
return err(
|
|
764
|
+
HaiPaymentError.CONFIG_ERROR,
|
|
765
|
+
paymentM("payment_configError", { params: { error: "init already in progress" } })
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
initInProgress = true;
|
|
769
|
+
try {
|
|
770
|
+
if (currentConfig !== null) {
|
|
771
|
+
logger2.warn("Payment module is already initialized, reinitializing");
|
|
772
|
+
await payment.close();
|
|
773
|
+
}
|
|
774
|
+
logger2.info("Initializing payment module");
|
|
775
|
+
const parseResult = PaymentConfigSchema.safeParse(config);
|
|
776
|
+
if (!parseResult.success) {
|
|
777
|
+
logger2.error("Payment config validation failed", { error: parseResult.error.message });
|
|
778
|
+
return err(
|
|
779
|
+
HaiPaymentError.CONFIG_ERROR,
|
|
780
|
+
paymentM("payment_configError", { params: { error: parseResult.error.message } }),
|
|
781
|
+
parseResult.error
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const parsed = parseResult.data;
|
|
785
|
+
clearProviders();
|
|
786
|
+
if (parsed.wechat) {
|
|
787
|
+
registerProvider(createWechatPayProvider(parsed.wechat));
|
|
788
|
+
}
|
|
789
|
+
if (parsed.alipay) {
|
|
790
|
+
registerProvider(createAlipayProvider(parsed.alipay));
|
|
791
|
+
}
|
|
792
|
+
if (parsed.stripe) {
|
|
793
|
+
registerProvider(createStripeProvider(parsed.stripe));
|
|
794
|
+
}
|
|
795
|
+
currentConfig = parsed;
|
|
796
|
+
logger2.info("Payment module initialized", {
|
|
797
|
+
providers: [
|
|
798
|
+
parsed.wechat ? "wechat" : null,
|
|
799
|
+
parsed.alipay ? "alipay" : null,
|
|
800
|
+
parsed.stripe ? "stripe" : null
|
|
801
|
+
].filter(Boolean)
|
|
802
|
+
});
|
|
803
|
+
return ok(void 0);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
logger2.error("Payment module initialization failed", { error });
|
|
806
|
+
return err(
|
|
807
|
+
HaiPaymentError.CONFIG_ERROR,
|
|
808
|
+
paymentM("payment_initFailed", {
|
|
809
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
810
|
+
}),
|
|
811
|
+
error
|
|
812
|
+
);
|
|
813
|
+
} finally {
|
|
814
|
+
initInProgress = false;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
/**
|
|
818
|
+
* 关闭模块、清除所有 Provider
|
|
819
|
+
*/
|
|
820
|
+
async close() {
|
|
821
|
+
if (!currentConfig) {
|
|
822
|
+
logger2.info("Payment module already closed, skipping");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
logger2.info("Closing payment module");
|
|
826
|
+
clearProviders();
|
|
827
|
+
currentConfig = null;
|
|
828
|
+
logger2.info("Payment module closed");
|
|
829
|
+
},
|
|
830
|
+
get config() {
|
|
831
|
+
return currentConfig;
|
|
832
|
+
},
|
|
833
|
+
get isInitialized() {
|
|
834
|
+
return currentConfig !== null;
|
|
835
|
+
},
|
|
836
|
+
/** 创建支付订单 */
|
|
837
|
+
createOrder: (...args) => currentConfig ? createOrder(...args) : Promise.resolve(notInitialized.result()),
|
|
838
|
+
/** 处理异步回调通知 */
|
|
839
|
+
handleNotify: (...args) => currentConfig ? handleNotify(...args) : Promise.resolve(notInitialized.result()),
|
|
840
|
+
/** 查询订单状态 */
|
|
841
|
+
queryOrder: (...args) => currentConfig ? queryOrder(...args) : Promise.resolve(notInitialized.result()),
|
|
842
|
+
/** 发起退款 */
|
|
843
|
+
refund: (...args) => currentConfig ? refund(...args) : Promise.resolve(notInitialized.result()),
|
|
844
|
+
/** 关闭订单 */
|
|
845
|
+
closeOrder: (...args) => currentConfig ? closeOrder(...args) : Promise.resolve(notInitialized.result()),
|
|
846
|
+
/** 获取已注册的 Provider */
|
|
847
|
+
getProvider,
|
|
848
|
+
/** 手动注册 Provider(自定义渠道) */
|
|
849
|
+
registerProvider
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
export { AlipayConfigSchema, PaymentConfigSchema, StripeConfigSchema, WechatPayConfigSchema, payment };
|
|
853
|
+
//# sourceMappingURL=index.js.map
|
|
854
|
+
//# sourceMappingURL=index.js.map
|