@downcity/services 0.1.50 → 0.1.55
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/README.md +11 -12
- package/bin/index.d.ts +5 -8
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +3 -4
- package/bin/index.js.map +1 -1
- package/bin/payment/index.d.ts +17 -9
- package/bin/payment/index.d.ts.map +1 -1
- package/bin/payment/index.js +841 -33
- package/bin/payment/index.js.map +1 -1
- package/bin/payment/redirect.d.ts +39 -0
- package/bin/payment/redirect.d.ts.map +1 -0
- package/bin/payment/redirect.js +41 -0
- package/bin/payment/redirect.js.map +1 -0
- package/bin/payment/schema.d.ts +426 -0
- package/bin/payment/schema.d.ts.map +1 -0
- package/bin/payment/schema.js +41 -0
- package/bin/payment/schema.js.map +1 -0
- package/bin/payment/types.d.ts +300 -91
- package/bin/payment/types.d.ts.map +1 -1
- package/bin/payment/types.js +4 -4
- package/bin/payment-creem/index.d.ts.map +1 -1
- package/bin/payment-creem/index.js +8 -63
- package/bin/payment-creem/index.js.map +1 -1
- package/bin/payment-creem/types.d.ts +0 -19
- package/bin/payment-creem/types.d.ts.map +1 -1
- package/bin/payment-dodo/dodo.d.ts +119 -0
- package/bin/payment-dodo/dodo.d.ts.map +1 -0
- package/bin/payment-dodo/dodo.js +93 -0
- package/bin/payment-dodo/dodo.js.map +1 -0
- package/bin/payment-dodo/index.d.ts +17 -0
- package/bin/payment-dodo/index.d.ts.map +1 -0
- package/bin/payment-dodo/index.js +549 -0
- package/bin/payment-dodo/index.js.map +1 -0
- package/bin/payment-dodo/schema.d.ts +378 -0
- package/bin/payment-dodo/schema.d.ts.map +1 -0
- package/bin/payment-dodo/schema.js +47 -0
- package/bin/payment-dodo/schema.js.map +1 -0
- package/bin/payment-dodo/types.d.ts +261 -0
- package/bin/payment-dodo/types.d.ts.map +1 -0
- package/bin/payment-dodo/types.js +10 -0
- package/bin/payment-dodo/types.js.map +1 -0
- package/bin/payment-stripe/index.d.ts.map +1 -1
- package/bin/payment-stripe/index.js +10 -73
- package/bin/payment-stripe/index.js.map +1 -1
- package/bin/payment-stripe/types.d.ts +0 -24
- package/bin/payment-stripe/types.d.ts.map +1 -1
- package/bin/payment-waffo/index.d.ts +17 -0
- package/bin/payment-waffo/index.d.ts.map +1 -0
- package/bin/payment-waffo/index.js +550 -0
- package/bin/payment-waffo/index.js.map +1 -0
- package/bin/payment-waffo/schema.d.ts +388 -0
- package/bin/payment-waffo/schema.d.ts.map +1 -0
- package/bin/payment-waffo/schema.js +39 -0
- package/bin/payment-waffo/schema.js.map +1 -0
- package/bin/payment-waffo/types.d.ts +289 -0
- package/bin/payment-waffo/types.d.ts.map +1 -0
- package/bin/payment-waffo/types.js +10 -0
- package/bin/payment-waffo/types.js.map +1 -0
- package/bin/payment-waffo/waffo.d.ts +111 -0
- package/bin/payment-waffo/waffo.d.ts.map +1 -0
- package/bin/payment-waffo/waffo.js +84 -0
- package/bin/payment-waffo/waffo.js.map +1 -0
- package/package.json +8 -4
package/bin/payment/index.js
CHANGED
|
@@ -1,86 +1,894 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Downcity 官方 Payment
|
|
2
|
+
* Downcity 官方 Payment 统一服务。
|
|
3
3
|
*
|
|
4
4
|
* 关键说明(中文)
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* - payment 是唯一支付服务,Stripe / Creem / Dodo / Waffo 都只是 provider。
|
|
6
|
+
* - 统一负责 checkout、本地支付记录、webhook 幂等、状态同步和 balance 入账。
|
|
7
|
+
* - 所有 provider 共用 `/v1/payment/*` 路由和统一 payments/events 表。
|
|
8
8
|
*/
|
|
9
|
+
import { createCreemCheckoutSession, normalizeCreemApiBaseURL, parseCreemWebhookEvent, readMetadata as readCreemMetadata, verifyCreemSignature } from "../payment-creem/creem.js";
|
|
10
|
+
import { createDodoCheckoutSession, createDodoClient, normalizeDodoEnvironment, parseDodoWebhookEvent, readMetadata as readDodoMetadata } from "../payment-dodo/dodo.js";
|
|
11
|
+
import { createStripeCheckoutSession, normalizeStripeApiBaseURL, parseStripeWebhookEvent, readMetadata as readStripeMetadata, verifyStripeSignature } from "../payment-stripe/stripe.js";
|
|
12
|
+
import { createWaffoCheckoutSession, createWaffoClient, normalizeWaffoEnvironment, parseWaffoWebhookEvent, readMetadata as readWaffoMetadata } from "../payment-waffo/waffo.js";
|
|
13
|
+
import { paymentEvents, paymentPayments } from "./schema.js";
|
|
14
|
+
import { resolvePaymentRedirectURL } from "./redirect.js";
|
|
15
|
+
/**
|
|
16
|
+
* Payment 服务自身 env。
|
|
17
|
+
*/
|
|
18
|
+
const paymentEnv = [
|
|
19
|
+
{
|
|
20
|
+
key: "DOWNCITY_CITY_BASE_URL",
|
|
21
|
+
description: "City 对外访问地址;用于自动生成统一 payment 结果页地址",
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
9
25
|
/**
|
|
10
26
|
* 创建统一 Payment 服务。
|
|
11
27
|
*/
|
|
12
28
|
export function paymentService(options) {
|
|
29
|
+
const providers = normalizeProviders(options.providers);
|
|
30
|
+
const env = mergeEnvRequirements([
|
|
31
|
+
...paymentEnv,
|
|
32
|
+
...providers.flatMap((provider) => provider.env),
|
|
33
|
+
]);
|
|
13
34
|
return {
|
|
14
35
|
id: "payment",
|
|
15
36
|
name: "Payment",
|
|
16
|
-
version: "0.
|
|
37
|
+
version: "0.2.0",
|
|
38
|
+
env,
|
|
39
|
+
schema: {
|
|
40
|
+
payments: paymentPayments,
|
|
41
|
+
events: paymentEvents,
|
|
42
|
+
},
|
|
17
43
|
instruction: [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
44
|
+
"统一支付服务。Stripe、Creem、Dodo、Waffo 都作为 provider 挂载。",
|
|
45
|
+
"前端先读取 /methods,再通过 /checkout/create 创建对应 provider 的 checkout。",
|
|
46
|
+
"所有 provider 共用 /webhook、/payments、/events 和统一 payment 表。",
|
|
21
47
|
].join("\n"),
|
|
22
48
|
install(ctx) {
|
|
49
|
+
const payments = ctx.table("payments");
|
|
50
|
+
const events = ctx.table("events");
|
|
23
51
|
ctx.route({
|
|
24
52
|
method: "GET",
|
|
25
53
|
path: "/methods",
|
|
26
54
|
auth: [],
|
|
27
|
-
|
|
55
|
+
handler(requestCtx) {
|
|
28
56
|
return requestCtx.jsonResponse({
|
|
29
|
-
items:
|
|
57
|
+
items: providers.map((provider) => provider.method(ctx)),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
ctx.route({
|
|
62
|
+
method: "POST",
|
|
63
|
+
path: "/checkout/create",
|
|
64
|
+
auth: ["user"],
|
|
65
|
+
async handler(requestCtx) {
|
|
66
|
+
const body = await requestCtx.json();
|
|
67
|
+
const provider = readProvider(providers, body.method_id || body.provider);
|
|
68
|
+
const method = provider.method(ctx);
|
|
69
|
+
if (!method.enabled) {
|
|
70
|
+
const reason = method.reason ? `: ${method.reason}` : "";
|
|
71
|
+
return requestCtx.jsonResponse({ error: `Payment provider ${provider.id} is disabled${reason}` }, 400);
|
|
72
|
+
}
|
|
73
|
+
const userId = normalizeRequired(requestCtx.user?.user_id, "user_id");
|
|
74
|
+
const topup = await options.balance.readTopup(normalizeRequired(body.topup_id, "topup_id"));
|
|
75
|
+
if (topup.user_id !== userId) {
|
|
76
|
+
return requestCtx.jsonResponse({ error: "Topup does not belong to current user" }, 403);
|
|
77
|
+
}
|
|
78
|
+
if (topup.status !== "pending") {
|
|
79
|
+
return requestCtx.jsonResponse({ error: `Topup is already ${topup.status}` }, 409);
|
|
80
|
+
}
|
|
81
|
+
const existing = await findActivePaymentByTopup(payments, provider.id, topup.topup_id);
|
|
82
|
+
if (existing)
|
|
83
|
+
return requestCtx.jsonResponse(toCheckoutResult(existing));
|
|
84
|
+
const paymentId = `pay_${randomId()}`;
|
|
85
|
+
const successURL = resolvePaymentRedirectURL({
|
|
86
|
+
path: "/v1/payment/redirect/success",
|
|
87
|
+
ctx,
|
|
88
|
+
request: requestCtx.request,
|
|
30
89
|
});
|
|
90
|
+
const cancelURL = resolvePaymentRedirectURL({
|
|
91
|
+
path: "/v1/payment/redirect/cancel",
|
|
92
|
+
ctx,
|
|
93
|
+
request: requestCtx.request,
|
|
94
|
+
});
|
|
95
|
+
const created = await provider.createCheckout({
|
|
96
|
+
payment_id: paymentId,
|
|
97
|
+
topup,
|
|
98
|
+
request: requestCtx.request,
|
|
99
|
+
ctx,
|
|
100
|
+
success_url: successURL,
|
|
101
|
+
cancel_url: cancelURL,
|
|
102
|
+
});
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
const row = {
|
|
105
|
+
payment_id: paymentId,
|
|
106
|
+
provider: provider.id,
|
|
107
|
+
topup_id: topup.topup_id,
|
|
108
|
+
user_id: topup.user_id,
|
|
109
|
+
provider_session_id: normalizeOptionalText(created.provider_session_id),
|
|
110
|
+
provider_payment_id: normalizeOptionalText(created.provider_payment_id),
|
|
111
|
+
provider_order_id: normalizeOptionalText(created.provider_order_id),
|
|
112
|
+
amount: topup.amount,
|
|
113
|
+
currency: method.currency,
|
|
114
|
+
status: "pending",
|
|
115
|
+
checkout_url: created.checkout_url,
|
|
116
|
+
metadata_json: JSON.stringify({
|
|
117
|
+
unit: topup.unit,
|
|
118
|
+
note: topup.note,
|
|
119
|
+
provider: provider.id,
|
|
120
|
+
...(created.metadata ?? {}),
|
|
121
|
+
}),
|
|
122
|
+
created_at: now,
|
|
123
|
+
updated_at: now,
|
|
124
|
+
};
|
|
125
|
+
await payments.insert(row);
|
|
126
|
+
return requestCtx.jsonResponse(toCheckoutResult(row));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
ctx.route({
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: "/payments/me",
|
|
132
|
+
auth: ["user"],
|
|
133
|
+
async handler(requestCtx) {
|
|
134
|
+
const userId = normalizeRequired(requestCtx.user?.user_id, "user_id");
|
|
135
|
+
return requestCtx.jsonResponse({ items: sortPayments(await payments.select({ user_id: userId })) });
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
ctx.route({
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: "/payments",
|
|
141
|
+
auth: ["admin"],
|
|
142
|
+
async handler(requestCtx) {
|
|
143
|
+
return requestCtx.jsonResponse({ items: sortPayments(await payments.select()) });
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
ctx.route({
|
|
147
|
+
method: "GET",
|
|
148
|
+
path: "/events",
|
|
149
|
+
auth: ["admin"],
|
|
150
|
+
async handler(requestCtx) {
|
|
151
|
+
return requestCtx.jsonResponse({ items: sortEvents(await events.select()) });
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
ctx.route({
|
|
155
|
+
method: "POST",
|
|
156
|
+
path: "/webhook",
|
|
157
|
+
auth: [],
|
|
158
|
+
async handler(requestCtx) {
|
|
159
|
+
const raw = await requestCtx.text();
|
|
160
|
+
const provider = readWebhookProvider(providers, requestCtx.request);
|
|
161
|
+
let webhookEvent;
|
|
162
|
+
try {
|
|
163
|
+
webhookEvent = provider
|
|
164
|
+
? await provider.parseWebhook({ raw, request: requestCtx.request, ctx })
|
|
165
|
+
: await autoParseWebhook(providers, { raw, request: requestCtx.request, ctx });
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return requestCtx.jsonResponse({ error: errorMessage(error) }, 400);
|
|
169
|
+
}
|
|
170
|
+
const eventProvider = provider ?? readProvider(providers, webhookEvent.meta?.provider);
|
|
171
|
+
const eventId = `${eventProvider.id}:${normalizeRequired(webhookEvent.event_id, "payment event id")}`;
|
|
172
|
+
const existing = (await events.select({ event_id: eventId }))[0];
|
|
173
|
+
if (existing) {
|
|
174
|
+
return requestCtx.jsonResponse({
|
|
175
|
+
received: true,
|
|
176
|
+
event_id: eventId,
|
|
177
|
+
provider: eventProvider.id,
|
|
178
|
+
sync_status: existing.sync_status,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
await events.insert({
|
|
182
|
+
event_id: eventId,
|
|
183
|
+
provider: eventProvider.id,
|
|
184
|
+
type: webhookEvent.type,
|
|
185
|
+
payload_json: JSON.stringify(webhookEvent.payload),
|
|
186
|
+
sync_status: "pending",
|
|
187
|
+
sync_error: "",
|
|
188
|
+
created_at: new Date().toISOString(),
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
const syncStatus = await syncPaymentEvent({
|
|
192
|
+
provider: eventProvider,
|
|
193
|
+
event: webhookEvent,
|
|
194
|
+
payments,
|
|
195
|
+
balance: options.balance,
|
|
196
|
+
});
|
|
197
|
+
await updateEvent(events, eventId, syncStatus, "");
|
|
198
|
+
return requestCtx.jsonResponse({
|
|
199
|
+
received: true,
|
|
200
|
+
event_id: eventId,
|
|
201
|
+
provider: eventProvider.id,
|
|
202
|
+
sync_status: syncStatus,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const message = errorMessage(error);
|
|
207
|
+
await updateEvent(events, eventId, "failed", message);
|
|
208
|
+
return requestCtx.jsonResponse({
|
|
209
|
+
received: true,
|
|
210
|
+
event_id: eventId,
|
|
211
|
+
provider: eventProvider.id,
|
|
212
|
+
sync_status: "failed",
|
|
213
|
+
error: message,
|
|
214
|
+
}, 500);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
ctx.route({
|
|
219
|
+
method: "GET",
|
|
220
|
+
path: "/redirect/success",
|
|
221
|
+
auth: [],
|
|
222
|
+
handler(requestCtx) {
|
|
223
|
+
return htmlResponse(renderRedirectPage({
|
|
224
|
+
title: "Payment successful",
|
|
225
|
+
heading: "Payment completed",
|
|
226
|
+
description: "Your payment has been accepted. If the balance view has not refreshed yet, close this page and return to your app.",
|
|
227
|
+
request: requestCtx.request,
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
ctx.route({
|
|
232
|
+
method: "GET",
|
|
233
|
+
path: "/redirect/cancel",
|
|
234
|
+
auth: [],
|
|
235
|
+
handler(requestCtx) {
|
|
236
|
+
return htmlResponse(renderRedirectPage({
|
|
237
|
+
title: "Payment canceled",
|
|
238
|
+
heading: "Payment canceled",
|
|
239
|
+
description: "No charge was completed. You can close this page and return to your app to try again later.",
|
|
240
|
+
request: requestCtx.request,
|
|
241
|
+
}));
|
|
31
242
|
},
|
|
32
243
|
});
|
|
33
244
|
},
|
|
34
245
|
};
|
|
35
246
|
}
|
|
36
247
|
/**
|
|
37
|
-
*
|
|
248
|
+
* 创建 Stripe payment provider。
|
|
38
249
|
*/
|
|
39
|
-
export function
|
|
250
|
+
export function stripePaymentProvider(options = {}) {
|
|
40
251
|
return {
|
|
41
|
-
|
|
252
|
+
id: "stripe",
|
|
253
|
+
label: options.label?.trim() || "Stripe",
|
|
254
|
+
env: [
|
|
255
|
+
{ key: "STRIPE_SECRET_KEY", description: "Stripe secret key,用于创建 Checkout Session", required: true },
|
|
256
|
+
{ key: "STRIPE_WEBHOOK_SECRET", description: "Stripe webhook signing secret,用于校验 stripe-signature", required: false },
|
|
257
|
+
{ key: "STRIPE_CURRENCY", description: "默认结算币种,例如 usd", required: false },
|
|
258
|
+
{ key: "STRIPE_ITEM_NAME", description: "Stripe Checkout 展示的默认商品名", required: false },
|
|
259
|
+
{ key: "STRIPE_API_BASE_URL", description: "可选的 Stripe API 基础地址覆写,通常只用于测试环境", required: false },
|
|
260
|
+
],
|
|
261
|
+
method(ctx) {
|
|
42
262
|
const enabled = Boolean(options.secret_key || ctx.env("STRIPE_SECRET_KEY"));
|
|
43
|
-
return {
|
|
263
|
+
return paymentMethodItem({
|
|
44
264
|
id: "stripe",
|
|
45
|
-
type: "checkout",
|
|
46
265
|
enabled,
|
|
47
266
|
label: options.label?.trim() || "Stripe",
|
|
48
|
-
service: "payment.stripe",
|
|
49
|
-
action: "checkout/create",
|
|
50
|
-
requires_user: true,
|
|
51
267
|
currency: normalizeCurrency(ctx.env("STRIPE_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
52
|
-
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
async createCheckout(input) {
|
|
271
|
+
const secretKey = options.secret_key ?? input.ctx.env("STRIPE_SECRET_KEY");
|
|
272
|
+
if (!secretKey)
|
|
273
|
+
throw new Error("Stripe secret key is not configured");
|
|
274
|
+
const created = await createStripeCheckoutSession(secretKey, normalizeStripeApiBaseURL(input.ctx.env("STRIPE_API_BASE_URL") || options.api_base_url), {
|
|
275
|
+
payment_id: input.payment_id,
|
|
276
|
+
topup: input.topup,
|
|
277
|
+
currency: normalizeCurrency(input.ctx.env("STRIPE_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
278
|
+
success_url: input.success_url,
|
|
279
|
+
cancel_url: input.cancel_url,
|
|
280
|
+
item_name: normalizeOptionalText(input.ctx.env("STRIPE_ITEM_NAME")) || normalizeOptionalText(options.item_name) || "Downcity Topup",
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
provider_session_id: created.session_id,
|
|
284
|
+
provider_payment_id: created.payment_intent_id,
|
|
285
|
+
checkout_url: created.checkout_url,
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
async parseWebhook(input) {
|
|
289
|
+
const webhookSecret = options.webhook_secret ?? input.ctx.env("STRIPE_WEBHOOK_SECRET");
|
|
290
|
+
if (webhookSecret) {
|
|
291
|
+
const valid = await verifyStripeSignature(input.raw, input.request.headers.get("stripe-signature"), webhookSecret);
|
|
292
|
+
if (!valid)
|
|
293
|
+
throw new Error("Invalid Stripe signature");
|
|
294
|
+
}
|
|
295
|
+
const event = parseStripeWebhookEvent(input.raw);
|
|
296
|
+
const object = readStripeMetadata(event.data?.object);
|
|
297
|
+
const metadata = readStripeMetadata(object.metadata);
|
|
298
|
+
const type = normalizeOptionalText(event.type) || "unknown";
|
|
299
|
+
const status = type === "checkout.session.completed"
|
|
300
|
+
? "paid"
|
|
301
|
+
: type === "checkout.session.expired"
|
|
302
|
+
? "expired"
|
|
303
|
+
: type === "payment_intent.payment_failed"
|
|
304
|
+
? "failed"
|
|
305
|
+
: "ignored";
|
|
306
|
+
const isPaymentIntent = type === "payment_intent.payment_failed";
|
|
307
|
+
return {
|
|
308
|
+
event_id: normalizeRequired(event.id, "stripe event id"),
|
|
309
|
+
type,
|
|
310
|
+
payload: event,
|
|
311
|
+
status,
|
|
312
|
+
payment_id: normalizeOptionalText(metadata.payment_id),
|
|
313
|
+
topup_id: normalizeOptionalText(metadata.topup_id) || normalizeOptionalText(object.client_reference_id),
|
|
314
|
+
provider_session_id: isPaymentIntent ? undefined : normalizeOptionalText(object.id),
|
|
315
|
+
provider_payment_id: isPaymentIntent
|
|
316
|
+
? normalizeOptionalText(object.id)
|
|
317
|
+
: normalizeOptionalText(object.payment_intent),
|
|
318
|
+
ref: normalizeOptionalText(object.id),
|
|
319
|
+
meta: {
|
|
320
|
+
provider: "stripe",
|
|
321
|
+
stripe_event_id: normalizeOptionalText(event.id),
|
|
322
|
+
stripe_checkout_session_id: isPaymentIntent ? undefined : normalizeOptionalText(object.id),
|
|
323
|
+
stripe_payment_intent_id: isPaymentIntent ? normalizeOptionalText(object.id) : normalizeOptionalText(object.payment_intent),
|
|
324
|
+
},
|
|
53
325
|
};
|
|
54
326
|
},
|
|
55
327
|
};
|
|
56
328
|
}
|
|
57
329
|
/**
|
|
58
|
-
*
|
|
330
|
+
* 创建 Creem payment provider。
|
|
59
331
|
*/
|
|
60
|
-
export function
|
|
332
|
+
export function creemPaymentProvider(options = {}) {
|
|
61
333
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
334
|
+
id: "creem",
|
|
335
|
+
label: options.label?.trim() || "Creem",
|
|
336
|
+
env: [
|
|
337
|
+
{ key: "CREEM_API_KEY", description: "Creem API key,用于创建 Checkout Session", required: true },
|
|
338
|
+
{ key: "CREEM_PRODUCT_ID", description: "Creem product_id,用于创建 Checkout Session", required: true },
|
|
339
|
+
{ key: "CREEM_WEBHOOK_SECRET", description: "Creem webhook signing secret,用于校验 creem-signature", required: false },
|
|
340
|
+
{ key: "CREEM_CURRENCY", description: "默认结算币种,例如 usd;仅用于支付目录展示和本地记录", required: false },
|
|
341
|
+
{ key: "CREEM_API_BASE_URL", description: "可选的 Creem API 基础地址覆写,通常只用于测试环境", required: false },
|
|
342
|
+
],
|
|
343
|
+
method(ctx) {
|
|
344
|
+
const enabled = Boolean((options.api_key || ctx.env("CREEM_API_KEY")) && (options.product_id || ctx.env("CREEM_PRODUCT_ID")));
|
|
345
|
+
return paymentMethodItem({
|
|
67
346
|
id: "creem",
|
|
68
|
-
type: "checkout",
|
|
69
347
|
enabled,
|
|
70
348
|
label: options.label?.trim() || "Creem",
|
|
71
|
-
service: "payment.creem",
|
|
72
|
-
action: "checkout/create",
|
|
73
|
-
requires_user: true,
|
|
74
349
|
currency: normalizeCurrency(ctx.env("CREEM_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
75
|
-
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
async createCheckout(input) {
|
|
353
|
+
const apiKey = options.api_key ?? input.ctx.env("CREEM_API_KEY");
|
|
354
|
+
const productId = options.product_id ?? input.ctx.env("CREEM_PRODUCT_ID");
|
|
355
|
+
if (!apiKey)
|
|
356
|
+
throw new Error("Creem API key is not configured");
|
|
357
|
+
if (!productId)
|
|
358
|
+
throw new Error("Creem product id is not configured");
|
|
359
|
+
const created = await createCreemCheckoutSession(apiKey, normalizeCreemApiBaseURL(input.ctx.env("CREEM_API_BASE_URL") || options.api_base_url), {
|
|
360
|
+
payment_id: input.payment_id,
|
|
361
|
+
topup: input.topup,
|
|
362
|
+
product_id: productId,
|
|
363
|
+
success_url: input.success_url,
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
provider_session_id: created.checkout_id,
|
|
367
|
+
checkout_url: created.checkout_url,
|
|
368
|
+
metadata: { product_id: productId },
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
async parseWebhook(input) {
|
|
372
|
+
const webhookSecret = options.webhook_secret ?? input.ctx.env("CREEM_WEBHOOK_SECRET");
|
|
373
|
+
if (webhookSecret) {
|
|
374
|
+
const valid = await verifyCreemSignature(input.raw, input.request.headers.get("creem-signature"), webhookSecret);
|
|
375
|
+
if (!valid)
|
|
376
|
+
throw new Error("Invalid Creem signature");
|
|
377
|
+
}
|
|
378
|
+
const event = parseCreemWebhookEvent(input.raw);
|
|
379
|
+
const object = readCreemEventObject(event);
|
|
380
|
+
const metadata = readCreemMetadata(object.metadata);
|
|
381
|
+
const type = normalizeOptionalText(event.eventType) || normalizeOptionalText(event.type) || "unknown";
|
|
382
|
+
const order = readCreemMetadata(object.order);
|
|
383
|
+
const orderId = readObjectId(order) || normalizeOptionalText(object.order_id);
|
|
384
|
+
const checkoutId = readObjectId(object) || normalizeOptionalText(object.checkout_id);
|
|
385
|
+
return {
|
|
386
|
+
event_id: normalizeRequired(event.id, "creem event id"),
|
|
387
|
+
type,
|
|
388
|
+
payload: event,
|
|
389
|
+
status: type === "checkout.completed"
|
|
390
|
+
? "paid"
|
|
391
|
+
: type === "checkout.expired"
|
|
392
|
+
? "expired"
|
|
393
|
+
: type === "checkout.failed" || type === "payment.failed"
|
|
394
|
+
? "failed"
|
|
395
|
+
: "ignored",
|
|
396
|
+
payment_id: normalizeOptionalText(metadata.payment_id) || normalizeOptionalText(object.request_id),
|
|
397
|
+
topup_id: normalizeOptionalText(metadata.topup_id),
|
|
398
|
+
provider_session_id: checkoutId,
|
|
399
|
+
provider_order_id: orderId,
|
|
400
|
+
ref: orderId || checkoutId,
|
|
401
|
+
meta: {
|
|
402
|
+
provider: "creem",
|
|
403
|
+
creem_event_id: normalizeOptionalText(event.id),
|
|
404
|
+
creem_checkout_id: checkoutId,
|
|
405
|
+
creem_order_id: orderId,
|
|
406
|
+
},
|
|
76
407
|
};
|
|
77
408
|
},
|
|
78
409
|
};
|
|
79
410
|
}
|
|
80
411
|
/**
|
|
81
|
-
*
|
|
412
|
+
* 创建 Dodo Payments provider。
|
|
82
413
|
*/
|
|
414
|
+
export function dodoPaymentProvider(options = {}) {
|
|
415
|
+
return {
|
|
416
|
+
id: "dodo",
|
|
417
|
+
label: options.label?.trim() || "Dodo Payments",
|
|
418
|
+
env: [
|
|
419
|
+
{ key: "DODO_PAYMENTS_API_KEY", description: "Dodo Payments API key,用于创建 Checkout Session", required: true },
|
|
420
|
+
{ key: "DODO_PRODUCT_ID", description: "Dodo product_id,用于创建 Checkout Session", required: true },
|
|
421
|
+
{ key: "DODO_WEBHOOK_KEY", description: "Dodo webhook signing key,用于校验 webhook", required: false },
|
|
422
|
+
{ key: "DODO_ENVIRONMENT", description: "Dodo SDK 环境:test_mode 或 live_mode;默认 test_mode", required: false },
|
|
423
|
+
{ key: "DODO_CURRENCY", description: "默认结算币种,例如 usd", required: false },
|
|
424
|
+
{ key: "DODO_API_BASE_URL", description: "可选的 Dodo API 基础地址覆写,通常只用于测试环境", required: false },
|
|
425
|
+
],
|
|
426
|
+
method(ctx) {
|
|
427
|
+
const enabled = Boolean((options.api_key || ctx.env("DODO_PAYMENTS_API_KEY")) && (options.product_id || ctx.env("DODO_PRODUCT_ID")));
|
|
428
|
+
return paymentMethodItem({
|
|
429
|
+
id: "dodo",
|
|
430
|
+
enabled,
|
|
431
|
+
label: options.label?.trim() || "Dodo Payments",
|
|
432
|
+
currency: normalizeCurrency(ctx.env("DODO_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
async createCheckout(input) {
|
|
436
|
+
const apiKey = options.api_key ?? input.ctx.env("DODO_PAYMENTS_API_KEY");
|
|
437
|
+
const productId = options.product_id ?? input.ctx.env("DODO_PRODUCT_ID");
|
|
438
|
+
if (!apiKey)
|
|
439
|
+
throw new Error("Dodo API key is not configured");
|
|
440
|
+
if (!productId)
|
|
441
|
+
throw new Error("Dodo product id is not configured");
|
|
442
|
+
const client = createDodoClient({
|
|
443
|
+
api_key: apiKey,
|
|
444
|
+
webhook_key: options.webhook_key ?? input.ctx.env("DODO_WEBHOOK_KEY"),
|
|
445
|
+
environment: normalizeDodoEnvironment(input.ctx.env("DODO_ENVIRONMENT") || options.environment),
|
|
446
|
+
api_base_url: options.api_base_url ?? input.ctx.env("DODO_API_BASE_URL"),
|
|
447
|
+
});
|
|
448
|
+
const created = await createDodoCheckoutSession(client, {
|
|
449
|
+
payment_id: input.payment_id,
|
|
450
|
+
topup: input.topup,
|
|
451
|
+
product_id: productId,
|
|
452
|
+
currency: normalizeCurrency(input.ctx.env("DODO_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
453
|
+
return_url: input.success_url,
|
|
454
|
+
cancel_url: input.cancel_url,
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
provider_session_id: created.checkout_session_id,
|
|
458
|
+
provider_payment_id: created.dodo_payment_id,
|
|
459
|
+
checkout_url: created.checkout_url,
|
|
460
|
+
metadata: { product_id: productId },
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
async parseWebhook(input) {
|
|
464
|
+
const webhookKey = options.webhook_key ?? input.ctx.env("DODO_WEBHOOK_KEY");
|
|
465
|
+
const client = createDodoClient({
|
|
466
|
+
api_key: options.api_key ?? input.ctx.env("DODO_PAYMENTS_API_KEY") ?? "webhook_only",
|
|
467
|
+
webhook_key: webhookKey,
|
|
468
|
+
environment: normalizeDodoEnvironment(input.ctx.env("DODO_ENVIRONMENT") || options.environment),
|
|
469
|
+
api_base_url: options.api_base_url ?? input.ctx.env("DODO_API_BASE_URL"),
|
|
470
|
+
});
|
|
471
|
+
const event = parseDodoWebhookEvent({
|
|
472
|
+
client,
|
|
473
|
+
raw: input.raw,
|
|
474
|
+
headers: input.request.headers,
|
|
475
|
+
verify: Boolean(webhookKey),
|
|
476
|
+
});
|
|
477
|
+
const object = readDodoMetadata(event.data || event.object);
|
|
478
|
+
const metadata = readDodoMetadata(object.metadata);
|
|
479
|
+
const type = normalizeOptionalText(event.type) || normalizeOptionalText(event.eventType) || "unknown";
|
|
480
|
+
const providerPaymentId = normalizeOptionalText(object.payment_id) || normalizeOptionalText(object.id);
|
|
481
|
+
const checkoutSessionId = normalizeOptionalText(object.checkout_session_id);
|
|
482
|
+
return {
|
|
483
|
+
event_id: normalizeRequired(event.id || event.event_id || providerPaymentId || `evt_${randomId()}`, "dodo event id"),
|
|
484
|
+
type,
|
|
485
|
+
payload: event,
|
|
486
|
+
status: type === "payment.succeeded"
|
|
487
|
+
? "paid"
|
|
488
|
+
: type === "payment.failed"
|
|
489
|
+
? "failed"
|
|
490
|
+
: type === "payment.cancelled" || type === "payment.canceled"
|
|
491
|
+
? "canceled"
|
|
492
|
+
: "ignored",
|
|
493
|
+
payment_id: normalizeOptionalText(metadata.payment_id),
|
|
494
|
+
topup_id: normalizeOptionalText(metadata.topup_id),
|
|
495
|
+
provider_session_id: checkoutSessionId,
|
|
496
|
+
provider_payment_id: providerPaymentId,
|
|
497
|
+
ref: providerPaymentId || checkoutSessionId,
|
|
498
|
+
meta: {
|
|
499
|
+
provider: "dodo",
|
|
500
|
+
dodo_event_id: normalizeOptionalText(event.id || event.event_id),
|
|
501
|
+
dodo_checkout_session_id: checkoutSessionId,
|
|
502
|
+
dodo_payment_id: providerPaymentId,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 创建 Waffo Pancake provider。
|
|
510
|
+
*/
|
|
511
|
+
export function waffoPaymentProvider(options = {}) {
|
|
512
|
+
return {
|
|
513
|
+
id: "waffo",
|
|
514
|
+
label: options.label?.trim() || "Waffo Pancake",
|
|
515
|
+
env: [
|
|
516
|
+
{ key: "WAFFO_MERCHANT_ID", description: "Waffo Merchant ID,例如 MER_xxx", required: true },
|
|
517
|
+
{ key: "WAFFO_PRIVATE_KEY", description: "Waffo API private key,用于 SDK 请求签名", required: true },
|
|
518
|
+
{ key: "WAFFO_PRODUCT_ID", description: "Waffo product_id,用于创建 Checkout Session", required: true },
|
|
519
|
+
{ key: "WAFFO_WEBHOOK_PUBLIC_KEY", description: "Waffo webhook public key,用于校验 x-waffo-signature", required: false },
|
|
520
|
+
{ key: "WAFFO_ENVIRONMENT", description: "Waffo 环境:test 或 prod;默认 test", required: false },
|
|
521
|
+
{ key: "WAFFO_CURRENCY", description: "默认结算币种,例如 usd", required: false },
|
|
522
|
+
{ key: "WAFFO_API_BASE_URL", description: "可选的 Waffo API 基础地址覆写,通常只用于测试环境", required: false },
|
|
523
|
+
],
|
|
524
|
+
method(ctx) {
|
|
525
|
+
const enabled = Boolean((options.merchant_id || ctx.env("WAFFO_MERCHANT_ID")) &&
|
|
526
|
+
(options.private_key || ctx.env("WAFFO_PRIVATE_KEY")) &&
|
|
527
|
+
(options.product_id || ctx.env("WAFFO_PRODUCT_ID")));
|
|
528
|
+
return paymentMethodItem({
|
|
529
|
+
id: "waffo",
|
|
530
|
+
enabled,
|
|
531
|
+
label: options.label?.trim() || "Waffo Pancake",
|
|
532
|
+
currency: normalizeCurrency(ctx.env("WAFFO_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
533
|
+
});
|
|
534
|
+
},
|
|
535
|
+
async createCheckout(input) {
|
|
536
|
+
const merchantId = options.merchant_id ?? input.ctx.env("WAFFO_MERCHANT_ID");
|
|
537
|
+
const privateKey = options.private_key ?? input.ctx.env("WAFFO_PRIVATE_KEY");
|
|
538
|
+
const productId = options.product_id ?? input.ctx.env("WAFFO_PRODUCT_ID");
|
|
539
|
+
if (!merchantId)
|
|
540
|
+
throw new Error("Waffo merchant id is not configured");
|
|
541
|
+
if (!privateKey)
|
|
542
|
+
throw new Error("Waffo private key is not configured");
|
|
543
|
+
if (!productId)
|
|
544
|
+
throw new Error("Waffo product id is not configured");
|
|
545
|
+
const client = createWaffoClient({
|
|
546
|
+
merchant_id: merchantId,
|
|
547
|
+
private_key: privateKey,
|
|
548
|
+
webhook_public_key: options.webhook_public_key ?? input.ctx.env("WAFFO_WEBHOOK_PUBLIC_KEY"),
|
|
549
|
+
api_base_url: options.api_base_url ?? input.ctx.env("WAFFO_API_BASE_URL"),
|
|
550
|
+
});
|
|
551
|
+
const created = await createWaffoCheckoutSession(client, {
|
|
552
|
+
payment_id: input.payment_id,
|
|
553
|
+
topup: input.topup,
|
|
554
|
+
product_id: productId,
|
|
555
|
+
currency: normalizeCurrency(input.ctx.env("WAFFO_CURRENCY")) || normalizeCurrency(options.currency) || "usd",
|
|
556
|
+
success_url: input.success_url,
|
|
557
|
+
});
|
|
558
|
+
return {
|
|
559
|
+
provider_session_id: created.session_id,
|
|
560
|
+
checkout_url: created.checkout_url,
|
|
561
|
+
metadata: { product_id: productId },
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
async parseWebhook(input) {
|
|
565
|
+
const client = createWaffoClient({
|
|
566
|
+
merchant_id: options.merchant_id ?? input.ctx.env("WAFFO_MERCHANT_ID") ?? "MER_webhook",
|
|
567
|
+
private_key: options.private_key ?? input.ctx.env("WAFFO_PRIVATE_KEY") ?? fallbackWaffoPrivateKey(),
|
|
568
|
+
webhook_public_key: options.webhook_public_key ?? input.ctx.env("WAFFO_WEBHOOK_PUBLIC_KEY"),
|
|
569
|
+
api_base_url: options.api_base_url ?? input.ctx.env("WAFFO_API_BASE_URL"),
|
|
570
|
+
});
|
|
571
|
+
const event = parseWaffoWebhookEvent({
|
|
572
|
+
client,
|
|
573
|
+
raw: input.raw,
|
|
574
|
+
signature: input.request.headers.get("x-waffo-signature"),
|
|
575
|
+
environment: normalizeWaffoEnvironment(input.ctx.env("WAFFO_ENVIRONMENT") || options.environment),
|
|
576
|
+
});
|
|
577
|
+
const data = readWaffoMetadata(event.data);
|
|
578
|
+
const metadata = readWaffoMetadata(data.orderMetadata);
|
|
579
|
+
const orderId = normalizeOptionalText(data.orderId);
|
|
580
|
+
const paymentId = normalizeOptionalText(data.paymentId) || normalizeOptionalText(event.eventId);
|
|
581
|
+
const type = normalizeOptionalText(event.eventType) || "unknown";
|
|
582
|
+
return {
|
|
583
|
+
event_id: normalizeRequired(event.id || event.eventId || `evt_${randomId()}`, "waffo event id"),
|
|
584
|
+
type,
|
|
585
|
+
payload: event,
|
|
586
|
+
status: type === "order.completed" ? "paid" : "ignored",
|
|
587
|
+
payment_id: normalizeOptionalText(data.orderMerchantExternalId),
|
|
588
|
+
topup_id: normalizeOptionalText(metadata.topup_id),
|
|
589
|
+
provider_payment_id: paymentId,
|
|
590
|
+
provider_order_id: orderId,
|
|
591
|
+
ref: paymentId || orderId,
|
|
592
|
+
meta: {
|
|
593
|
+
provider: "waffo",
|
|
594
|
+
waffo_event_id: normalizeOptionalText(event.id || event.eventId),
|
|
595
|
+
waffo_order_id: orderId,
|
|
596
|
+
waffo_payment_id: paymentId,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function syncPaymentEvent(input) {
|
|
603
|
+
const { provider, event, payments, balance } = input;
|
|
604
|
+
if (event.status === "ignored")
|
|
605
|
+
return "ignored";
|
|
606
|
+
const payment = await findPaymentByWebhookEvent(payments, provider.id, event);
|
|
607
|
+
if (!payment)
|
|
608
|
+
return "ignored";
|
|
609
|
+
if (event.status === "paid" && payment.status === "paid")
|
|
610
|
+
return "applied";
|
|
611
|
+
if (payment.status !== "pending" && event.status !== "paid")
|
|
612
|
+
return "ignored";
|
|
613
|
+
if (event.status === "paid") {
|
|
614
|
+
const topup = await balance.readTopup(payment.topup_id);
|
|
615
|
+
if (topup.status === "pending") {
|
|
616
|
+
await balance.finishTopup(payment.topup_id, {
|
|
617
|
+
note: `${provider.id} topup`,
|
|
618
|
+
ref: event.ref || event.provider_payment_id || event.provider_order_id || event.provider_session_id || payment.provider_session_id,
|
|
619
|
+
meta: {
|
|
620
|
+
provider: provider.id,
|
|
621
|
+
payment_id: payment.payment_id,
|
|
622
|
+
provider_session_id: event.provider_session_id || payment.provider_session_id,
|
|
623
|
+
provider_payment_id: event.provider_payment_id || payment.provider_payment_id,
|
|
624
|
+
provider_order_id: event.provider_order_id || payment.provider_order_id,
|
|
625
|
+
...(event.meta ?? {}),
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
await updatePayment(payments, payment.payment_id, {
|
|
631
|
+
status: event.status,
|
|
632
|
+
provider_session_id: event.provider_session_id || payment.provider_session_id,
|
|
633
|
+
provider_payment_id: event.provider_payment_id || payment.provider_payment_id,
|
|
634
|
+
provider_order_id: event.provider_order_id || payment.provider_order_id,
|
|
635
|
+
});
|
|
636
|
+
return "applied";
|
|
637
|
+
}
|
|
638
|
+
async function findPaymentByWebhookEvent(payments, provider, event) {
|
|
639
|
+
if (event.payment_id) {
|
|
640
|
+
const record = (await payments.select({ payment_id: event.payment_id, provider }))[0];
|
|
641
|
+
if (record)
|
|
642
|
+
return record;
|
|
643
|
+
}
|
|
644
|
+
if (event.provider_session_id) {
|
|
645
|
+
const record = (await payments.select({ provider, provider_session_id: event.provider_session_id }))[0];
|
|
646
|
+
if (record)
|
|
647
|
+
return record;
|
|
648
|
+
}
|
|
649
|
+
if (event.provider_payment_id) {
|
|
650
|
+
const record = (await payments.select({ provider, provider_payment_id: event.provider_payment_id }))[0];
|
|
651
|
+
if (record)
|
|
652
|
+
return record;
|
|
653
|
+
}
|
|
654
|
+
if (event.provider_order_id) {
|
|
655
|
+
const record = (await payments.select({ provider, provider_order_id: event.provider_order_id }))[0];
|
|
656
|
+
if (record)
|
|
657
|
+
return record;
|
|
658
|
+
}
|
|
659
|
+
if (event.topup_id)
|
|
660
|
+
return await findActivePaymentByTopup(payments, provider, event.topup_id);
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
async function findActivePaymentByTopup(payments, provider, topupId) {
|
|
664
|
+
const rows = sortPayments(await payments.select({ provider, topup_id: topupId }));
|
|
665
|
+
return rows.find((row) => row.status === "pending");
|
|
666
|
+
}
|
|
667
|
+
async function updatePayment(payments, paymentId, input) {
|
|
668
|
+
await payments.update({
|
|
669
|
+
where: { payment_id: paymentId },
|
|
670
|
+
values: {
|
|
671
|
+
status: input.status,
|
|
672
|
+
provider_session_id: normalizeOptionalText(input.provider_session_id),
|
|
673
|
+
provider_payment_id: normalizeOptionalText(input.provider_payment_id),
|
|
674
|
+
provider_order_id: normalizeOptionalText(input.provider_order_id),
|
|
675
|
+
updated_at: new Date().toISOString(),
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
async function updateEvent(events, eventId, syncStatus, syncError) {
|
|
680
|
+
await events.update({
|
|
681
|
+
where: { event_id: eventId },
|
|
682
|
+
values: {
|
|
683
|
+
sync_status: syncStatus,
|
|
684
|
+
sync_error: syncError.trim(),
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function toCheckoutResult(row) {
|
|
689
|
+
return {
|
|
690
|
+
payment_id: row.payment_id,
|
|
691
|
+
provider: row.provider,
|
|
692
|
+
topup_id: row.topup_id,
|
|
693
|
+
provider_session_id: row.provider_session_id,
|
|
694
|
+
provider_payment_id: row.provider_payment_id,
|
|
695
|
+
provider_order_id: row.provider_order_id,
|
|
696
|
+
checkout_url: row.checkout_url,
|
|
697
|
+
status: row.status,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function readProvider(providers, value) {
|
|
701
|
+
const id = normalizeOptionalText(value);
|
|
702
|
+
if (!id)
|
|
703
|
+
throw new TypeError("payment method_id is required");
|
|
704
|
+
const provider = providers.find((item) => item.id === id);
|
|
705
|
+
if (!provider)
|
|
706
|
+
throw new Error(`Payment provider ${id} is not available`);
|
|
707
|
+
return provider;
|
|
708
|
+
}
|
|
709
|
+
function readWebhookProvider(providers, request) {
|
|
710
|
+
const url = new URL(request.url);
|
|
711
|
+
const explicit = normalizeOptionalText(url.searchParams.get("provider"));
|
|
712
|
+
if (explicit)
|
|
713
|
+
return readProvider(providers, explicit);
|
|
714
|
+
if (request.headers.has("stripe-signature"))
|
|
715
|
+
return providers.find((provider) => provider.id === "stripe");
|
|
716
|
+
if (request.headers.has("creem-signature"))
|
|
717
|
+
return providers.find((provider) => provider.id === "creem");
|
|
718
|
+
if (request.headers.has("x-waffo-signature"))
|
|
719
|
+
return providers.find((provider) => provider.id === "waffo");
|
|
720
|
+
if (request.headers.has("webhook-signature") || request.headers.has("svix-signature")) {
|
|
721
|
+
return providers.find((provider) => provider.id === "dodo");
|
|
722
|
+
}
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
async function autoParseWebhook(providers, input) {
|
|
726
|
+
for (const provider of providers) {
|
|
727
|
+
try {
|
|
728
|
+
const event = await provider.parseWebhook(input);
|
|
729
|
+
return {
|
|
730
|
+
...event,
|
|
731
|
+
meta: {
|
|
732
|
+
...(event.meta ?? {}),
|
|
733
|
+
provider: provider.id,
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// 关键点(中文):自动识别只是兜底,单个 provider 解析失败继续尝试下一个。
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
throw new Error("Payment webhook provider is required");
|
|
742
|
+
}
|
|
743
|
+
function normalizeProviders(providers) {
|
|
744
|
+
const normalized = [];
|
|
745
|
+
for (const provider of providers) {
|
|
746
|
+
if (!provider?.id?.trim())
|
|
747
|
+
throw new TypeError("payment provider id is required");
|
|
748
|
+
if (normalized.find((item) => item.id === provider.id)) {
|
|
749
|
+
throw new TypeError(`Duplicate payment provider: ${provider.id}`);
|
|
750
|
+
}
|
|
751
|
+
normalized.push(provider);
|
|
752
|
+
}
|
|
753
|
+
return normalized;
|
|
754
|
+
}
|
|
755
|
+
function mergeEnvRequirements(items) {
|
|
756
|
+
const result = [];
|
|
757
|
+
for (const item of items) {
|
|
758
|
+
if (result.find((existing) => existing.key === item.key))
|
|
759
|
+
continue;
|
|
760
|
+
result.push(item);
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
function paymentMethodItem(input) {
|
|
765
|
+
return {
|
|
766
|
+
id: input.id,
|
|
767
|
+
type: "checkout",
|
|
768
|
+
enabled: input.enabled,
|
|
769
|
+
label: input.label,
|
|
770
|
+
service: "payment",
|
|
771
|
+
action: "checkout/create",
|
|
772
|
+
requires_user: true,
|
|
773
|
+
currency: input.currency,
|
|
774
|
+
reason: input.enabled ? undefined : "not_configured",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function readCreemEventObject(event) {
|
|
778
|
+
const directObject = readCreemMetadata(event.object);
|
|
779
|
+
if (Object.keys(directObject).length > 0)
|
|
780
|
+
return directObject;
|
|
781
|
+
return readCreemMetadata(event.data?.object);
|
|
782
|
+
}
|
|
783
|
+
function readObjectId(object) {
|
|
784
|
+
return normalizeOptionalText(object.id);
|
|
785
|
+
}
|
|
786
|
+
function sortPayments(rows) {
|
|
787
|
+
return [...rows].sort((left, right) => {
|
|
788
|
+
if (left.updated_at === right.updated_at)
|
|
789
|
+
return right.created_at.localeCompare(left.created_at);
|
|
790
|
+
return right.updated_at.localeCompare(left.updated_at);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
function sortEvents(rows) {
|
|
794
|
+
return [...rows].sort((left, right) => right.created_at.localeCompare(left.created_at));
|
|
795
|
+
}
|
|
796
|
+
function normalizeRequired(value, label) {
|
|
797
|
+
const normalized = String(value ?? "").trim();
|
|
798
|
+
if (!normalized)
|
|
799
|
+
throw new TypeError(`${label} is required`);
|
|
800
|
+
return normalized;
|
|
801
|
+
}
|
|
802
|
+
function normalizeOptionalText(value) {
|
|
803
|
+
return typeof value === "string" ? value.trim() : "";
|
|
804
|
+
}
|
|
83
805
|
function normalizeCurrency(value) {
|
|
84
|
-
return
|
|
806
|
+
return normalizeOptionalText(value).toLowerCase();
|
|
807
|
+
}
|
|
808
|
+
function errorMessage(error) {
|
|
809
|
+
return error instanceof Error ? error.message : String(error);
|
|
810
|
+
}
|
|
811
|
+
function htmlResponse(html) {
|
|
812
|
+
return new Response(html, {
|
|
813
|
+
status: 200,
|
|
814
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
function renderRedirectPage(input) {
|
|
818
|
+
const homeURL = escapeHTML(new URL("/", input.request.url).toString());
|
|
819
|
+
const title = escapeHTML(input.title);
|
|
820
|
+
const heading = escapeHTML(input.heading);
|
|
821
|
+
const description = escapeHTML(input.description);
|
|
822
|
+
return `<!doctype html>
|
|
823
|
+
<html lang="en">
|
|
824
|
+
<head>
|
|
825
|
+
<meta charset="utf-8" />
|
|
826
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
827
|
+
<title>${title}</title>
|
|
828
|
+
<style>
|
|
829
|
+
:root { color-scheme: light; --bg: #f5f7fb; --card: #fff; --text: #142033; --muted: #5a6a85; --border: #d9e2f1; --accent: #1f6feb; }
|
|
830
|
+
* { box-sizing: border-box; }
|
|
831
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; background: linear-gradient(180deg, #f8fbff 0%, var(--bg) 100%); color: var(--text); font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
832
|
+
main { width: min(100%, 560px); padding: 32px; border: 1px solid var(--border); border-radius: 20px; background: var(--card); box-shadow: 0 18px 60px rgba(16, 24, 40, 0.08); }
|
|
833
|
+
h1 { margin: 0 0 12px; font-size: 28px; line-height: 1.2; }
|
|
834
|
+
p { margin: 0; color: var(--muted); }
|
|
835
|
+
a { display: inline-block; margin-top: 24px; color: #fff; background: var(--accent); text-decoration: none; padding: 12px 16px; border-radius: 999px; font-weight: 600; }
|
|
836
|
+
</style>
|
|
837
|
+
</head>
|
|
838
|
+
<body>
|
|
839
|
+
<main>
|
|
840
|
+
<h1>${heading}</h1>
|
|
841
|
+
<p>${description}</p>
|
|
842
|
+
<a href="${homeURL}">Return to Downcity</a>
|
|
843
|
+
</main>
|
|
844
|
+
</body>
|
|
845
|
+
</html>`;
|
|
846
|
+
}
|
|
847
|
+
function escapeHTML(value) {
|
|
848
|
+
return value
|
|
849
|
+
.replaceAll("&", "&")
|
|
850
|
+
.replaceAll("<", "<")
|
|
851
|
+
.replaceAll(">", ">")
|
|
852
|
+
.replaceAll('"', """)
|
|
853
|
+
.replaceAll("'", "'");
|
|
854
|
+
}
|
|
855
|
+
function randomId() {
|
|
856
|
+
const buffer = new Uint8Array(12);
|
|
857
|
+
crypto.getRandomValues(buffer);
|
|
858
|
+
return btoa(String.fromCharCode(...buffer))
|
|
859
|
+
.replace(/\+/g, "-")
|
|
860
|
+
.replace(/\//g, "_")
|
|
861
|
+
.replace(/=+$/u, "");
|
|
862
|
+
}
|
|
863
|
+
function fallbackWaffoPrivateKey() {
|
|
864
|
+
return [
|
|
865
|
+
"-----BEGIN RSA PRIVATE KEY-----",
|
|
866
|
+
"MIIEpAIBAAKCAQEAtAqPA17F1sr9kylJy8LbcaHrim/UocR/ur3z5Vu7QQTUhuPO",
|
|
867
|
+
"pbsYnu1oFGwCsCjFhBBapI8/Huy9ARP3/Oxsp4kna9gEPLRSTMfUK5a0nPUnkHXv",
|
|
868
|
+
"DFgzIFYn1Yac/3FiA4zIA5BH+0ZUBu8cFZ1MvaPu7YFQvr165hOmwLnoJsypcY5V",
|
|
869
|
+
"Kjb8p+HjMXyiCy3gUId2DCJuUhjbtUo84gI3v4YAI6YD07pNUSm5wFDFKsYeymdV",
|
|
870
|
+
"QdtBhgPUx0RJDyhOKU1Vq8mge2b03ZiQK3O3gUmQgP0sAUpyyUrHaMjs/nAw9dqy",
|
|
871
|
+
"Wl0VyglAIcl79sRVUXTHgEHGf+j0pNcyVjIF2QIDAQABAoIBAAM1bPcSaVQ6qepF",
|
|
872
|
+
"ghsvjdmomRoOhCud5OjfGcmsqNmvzFnbFYO+oeGzOXejtSiOkXaZFAR6yRU0AupS",
|
|
873
|
+
"AMlxLT6PIzS41NqAHDdiGFXuiamCdQIOGASQTdj1sCAOFh43VxfZGnd1ytKfnj/B",
|
|
874
|
+
"Yy6/bu6yTT/OXjIIDnirQP2OUqTeWTHdf1xgQGkb+y20Oo3JgJ637j67HuLPrpzs",
|
|
875
|
+
"jIluqhRszKwVWhGlyTYJdK8nOv2sn+Kn8cEFybmbcytWk9Sxf128+IjTgsEFgEcy",
|
|
876
|
+
"RS6/UofjKfh9WY+4DK/L7mQgdYwawiRX4y4MkExCusmQxT72mDVGXnSbTYNp2qZl",
|
|
877
|
+
"WEgFeQECgYEA8cgKGrySiDFxJb900I5O/CL+1pIYXy6K4gDLk70luwjRSWU+ufZo",
|
|
878
|
+
"g/IRntgk0b0eC4vzP6539hQsh8rrO2swv0vBDG/cvyjP7ljHTx0pFtAjb/yJ9PXN",
|
|
879
|
+
"ca08G8qx6BX5mqdD46T56V4g5PtLT6cwn5Hth2/H+4MZ/5osP3CbU0UCgYEAvqEJ",
|
|
880
|
+
"OEOlKMRhxdfb7RnKMwiXUN2vZ0NuGgK0slqEC9FAv1E/XV8azcs40uINrDdieRmr",
|
|
881
|
+
"Oi+LBXC08FHiAKZOVuxg1DzKx8gDc/R6YGT5sKVhVhKgWsF9ysmCyyqdqnuvFMAd",
|
|
882
|
+
"7PgS4Nf01rO9jo+cqKBHTzFny0TWZTTAR6NzZ4UCgYEAzqcKs+V/XPbdXcUxg9xO",
|
|
883
|
+
"eEU1CZLfT+NJA3hoiAMAD8eukgv+PBX3KOeq1diqR7ZbysS4iTKHCAYgNYRj4Gpy",
|
|
884
|
+
"xN5rx0SJKb4pUvAAkoc7CmumDl6MT5oUGdhWau6pdtPpfpz+csEcdbFlbjG3IgKl",
|
|
885
|
+
"lY21tq/8/uUEQKq2rRaDO/0CgYAUgC0FqACzCauaI0S7kvJz2pCrWavrZw0ILxJP",
|
|
886
|
+
"u/xHaRGVgZ9W40t2pkxOIZFm2+3zKBeKAmLpCt3qmmO7vibeoj0nlgIYyiHU7o3a",
|
|
887
|
+
"oAFaRe7Z2tbz66sji9hNESAznWmOybpuKZ+eHptuG5ZfJoKqf9IrahzHd3e3Gp0z",
|
|
888
|
+
"FxjqIQKBgQDbHuXgZL8A7pQe+YFUVbXYjhzb+sTlVQD7C4KF0sdiKNKXcXhvcCxL",
|
|
889
|
+
"AfJu59vnIlhD4MUcxt5lWDBLX08dmcJOX2AryDMmNVC7knNu92IehJeboSaM1+t3",
|
|
890
|
+
"1V/8pTMTtzg5ANHzZICj6vn46UXCL78XUC3sqZdAm/+9kS+oAnWaIw==",
|
|
891
|
+
"-----END RSA PRIVATE KEY-----",
|
|
892
|
+
].join("\n");
|
|
85
893
|
}
|
|
86
894
|
//# sourceMappingURL=index.js.map
|