@zucker-framework/payment 1.0.0 → 1.0.3
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 +35 -0
- package/dist/index.d.mts +527 -124
- package/dist/index.d.ts +527 -124
- package/dist/index.js +1301 -279
- package/dist/index.mjs +1269 -280
- package/package.json +16 -4
package/dist/index.js
CHANGED
|
@@ -21,20 +21,194 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
DatabaseOrderPersistence: () => DatabaseOrderPersistence,
|
|
25
|
+
ORDER_PERSISTENCE: () => ORDER_PERSISTENCE,
|
|
26
|
+
OrderNotFoundError: () => OrderNotFoundError,
|
|
24
27
|
OrderService: () => OrderService,
|
|
25
28
|
PAYMENT_PROVIDERS: () => PAYMENT_PROVIDERS,
|
|
29
|
+
PAYPAL_SUPPORTED_CURRENCIES: () => PAYPAL_SUPPORTED_CURRENCIES,
|
|
30
|
+
PayPalPaymentTransport: () => PayPalPaymentTransport,
|
|
26
31
|
PaymentEvents: () => PaymentEvents,
|
|
32
|
+
PaymentProviderRegistry: () => PaymentProviderRegistry,
|
|
27
33
|
PaymentRecoveryService: () => PaymentRecoveryService,
|
|
28
|
-
|
|
34
|
+
RECOVERY_ATTEMPT_STORE: () => RECOVERY_ATTEMPT_STORE,
|
|
29
35
|
RECOVERY_MAIL_SENDER: () => RECOVERY_MAIL_SENDER,
|
|
30
36
|
RECOVERY_URL_GENERATOR: () => RECOVERY_URL_GENERATOR,
|
|
31
37
|
RECOVERY_USER_LOOKUP: () => RECOVERY_USER_LOOKUP,
|
|
38
|
+
SETTLEMENT_STATES: () => SETTLEMENT_STATES,
|
|
32
39
|
ZuckerPaymentModule: () => ZuckerPaymentModule,
|
|
33
|
-
|
|
40
|
+
adapterSupportsCurrency: () => adapterSupportsCurrency,
|
|
41
|
+
checkPaymentAdapterContract: () => checkPaymentAdapterContract,
|
|
42
|
+
createFakeAdapter: () => createFakeAdapter,
|
|
43
|
+
createPayPalAdapter: () => createPayPalAdapter,
|
|
44
|
+
createStripeAdapter: () => createStripeAdapter,
|
|
45
|
+
createStripeCheckoutSession: () => createStripeCheckoutSession,
|
|
46
|
+
createStripeClient: () => createStripeClient,
|
|
47
|
+
currencyExponent: () => currencyExponent,
|
|
48
|
+
decodePayPalMoneyReturnedEvent: () => decodePayPalMoneyReturnedEvent,
|
|
49
|
+
decodePayPalWebhookEvent: () => decodePayPalWebhookEvent,
|
|
50
|
+
friendlyErrorReason: () => friendlyErrorReason,
|
|
51
|
+
fromMinorUnits: () => fromMinorUnits,
|
|
52
|
+
isMoneyMoved: () => isMoneyMoved,
|
|
53
|
+
isTerminal: () => isTerminal,
|
|
54
|
+
observePayPalOrder: () => observePayPalOrder,
|
|
55
|
+
observePayPalState: () => observePayPalState,
|
|
56
|
+
observeStripeCheckoutSession: () => observeStripeCheckoutSession,
|
|
57
|
+
observeStripePaymentIntent: () => observeStripePaymentIntent,
|
|
58
|
+
observeStripeState: () => observeStripeState,
|
|
59
|
+
reconcile: () => reconcile,
|
|
60
|
+
retrieveStripeCheckoutSession: () => retrieveStripeCheckoutSession,
|
|
61
|
+
retrieveStripePaymentIntent: () => retrieveStripePaymentIntent,
|
|
62
|
+
stripeResourceId: () => stripeResourceId,
|
|
63
|
+
toMinorUnits: () => toMinorUnits,
|
|
64
|
+
verifyStripeWebhookEvent: () => verifyStripeWebhookEvent
|
|
34
65
|
});
|
|
35
66
|
module.exports = __toCommonJS(index_exports);
|
|
36
67
|
|
|
37
|
-
// src/payment.
|
|
68
|
+
// src/contract/payment-contract.ts
|
|
69
|
+
function adapterSupportsCurrency(adapter, currency) {
|
|
70
|
+
return !adapter.supportedCurrencies || adapter.supportedCurrencies.includes(currency.toUpperCase());
|
|
71
|
+
}
|
|
72
|
+
__name(adapterSupportsCurrency, "adapterSupportsCurrency");
|
|
73
|
+
|
|
74
|
+
// src/contract/settlement.ts
|
|
75
|
+
var SETTLEMENT_STATES = [
|
|
76
|
+
/** 已创建,用户尚未授权 */
|
|
77
|
+
"created",
|
|
78
|
+
/** 用户已授权但尚未扣款(PayPal APPROVED / Stripe requires_capture) */
|
|
79
|
+
"authorized",
|
|
80
|
+
/** 已扣款 */
|
|
81
|
+
"captured",
|
|
82
|
+
/** 已全额退款 */
|
|
83
|
+
"refunded",
|
|
84
|
+
/** 已部分退款 */
|
|
85
|
+
"partially_refunded",
|
|
86
|
+
/** 终态失败 */
|
|
87
|
+
"failed",
|
|
88
|
+
/** 终态取消(用户放弃 / 商户撤单) */
|
|
89
|
+
"canceled",
|
|
90
|
+
/**
|
|
91
|
+
* 无法判定。
|
|
92
|
+
* 不是"失败",也不是一个可持久化的终态——查询异常、供应商返回中间态、
|
|
93
|
+
* 回调迟到都会落到这里,必须保留原状继续观察。
|
|
94
|
+
*/
|
|
95
|
+
"unknown"
|
|
96
|
+
];
|
|
97
|
+
var MONEY_MOVED = /* @__PURE__ */ new Set([
|
|
98
|
+
"captured",
|
|
99
|
+
"refunded",
|
|
100
|
+
"partially_refunded"
|
|
101
|
+
]);
|
|
102
|
+
var TERMINAL = /* @__PURE__ */ new Set([
|
|
103
|
+
"refunded",
|
|
104
|
+
"failed",
|
|
105
|
+
"canceled"
|
|
106
|
+
]);
|
|
107
|
+
function isMoneyMoved(state) {
|
|
108
|
+
return MONEY_MOVED.has(state);
|
|
109
|
+
}
|
|
110
|
+
__name(isMoneyMoved, "isMoneyMoved");
|
|
111
|
+
function isTerminal(state) {
|
|
112
|
+
return TERMINAL.has(state);
|
|
113
|
+
}
|
|
114
|
+
__name(isTerminal, "isTerminal");
|
|
115
|
+
function reconcile(current, observed) {
|
|
116
|
+
if (observed === "unknown") {
|
|
117
|
+
return {
|
|
118
|
+
next: current,
|
|
119
|
+
changed: false,
|
|
120
|
+
rejected: "not-observable"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (observed === current) {
|
|
124
|
+
return {
|
|
125
|
+
next: current,
|
|
126
|
+
changed: false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (isMoneyMoved(current) && (observed === "failed" || observed === "canceled")) {
|
|
130
|
+
return {
|
|
131
|
+
next: current,
|
|
132
|
+
changed: false,
|
|
133
|
+
rejected: "money-already-moved"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (current === "refunded") {
|
|
137
|
+
return {
|
|
138
|
+
next: current,
|
|
139
|
+
changed: false,
|
|
140
|
+
rejected: "already-terminal"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (current === "partially_refunded" && observed === "captured") {
|
|
144
|
+
return {
|
|
145
|
+
next: current,
|
|
146
|
+
changed: false,
|
|
147
|
+
rejected: "already-terminal"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (isTerminal(current) && !isMoneyMoved(observed)) {
|
|
151
|
+
return {
|
|
152
|
+
next: current,
|
|
153
|
+
changed: false,
|
|
154
|
+
rejected: "already-terminal"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
next: observed,
|
|
159
|
+
changed: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
__name(reconcile, "reconcile");
|
|
163
|
+
|
|
164
|
+
// src/contract/money.ts
|
|
165
|
+
var ZERO_DECIMAL = /* @__PURE__ */ new Set([
|
|
166
|
+
"BIF",
|
|
167
|
+
"CLP",
|
|
168
|
+
"DJF",
|
|
169
|
+
"GNF",
|
|
170
|
+
"JPY",
|
|
171
|
+
"KMF",
|
|
172
|
+
"KRW",
|
|
173
|
+
"MGA",
|
|
174
|
+
"PYG",
|
|
175
|
+
"RWF",
|
|
176
|
+
"UGX",
|
|
177
|
+
"VND",
|
|
178
|
+
"VUV",
|
|
179
|
+
"XAF",
|
|
180
|
+
"XOF",
|
|
181
|
+
"XPF"
|
|
182
|
+
]);
|
|
183
|
+
function currencyExponent(currency) {
|
|
184
|
+
return ZERO_DECIMAL.has(currency.toUpperCase()) ? 0 : 2;
|
|
185
|
+
}
|
|
186
|
+
__name(currencyExponent, "currencyExponent");
|
|
187
|
+
function toMinorUnits(amount, currency) {
|
|
188
|
+
const exponent = currencyExponent(currency);
|
|
189
|
+
const match = /^(-?)(\d+)(?:\.(\d*))?$/.exec(amount.trim());
|
|
190
|
+
if (!match) throw new Error(`\u91D1\u989D\u683C\u5F0F\u975E\u6CD5\uFF1A${amount}`);
|
|
191
|
+
const [, sign, whole, fraction = ""] = match;
|
|
192
|
+
if (fraction.length > exponent && /[^0]/.test(fraction.slice(exponent))) {
|
|
193
|
+
throw new Error(`${currency} \u6700\u591A\u652F\u6301 ${exponent} \u4F4D\u5C0F\u6570\uFF0C\u6536\u5230 ${amount}`);
|
|
194
|
+
}
|
|
195
|
+
const padded = (fraction + "0".repeat(exponent)).slice(0, exponent);
|
|
196
|
+
const value = Number(`${whole}${padded}`);
|
|
197
|
+
if (!Number.isSafeInteger(value)) throw new Error(`\u91D1\u989D\u8D85\u51FA\u5B89\u5168\u6574\u6570\u8303\u56F4\uFF1A${amount}`);
|
|
198
|
+
return sign === "-" ? -value : value;
|
|
199
|
+
}
|
|
200
|
+
__name(toMinorUnits, "toMinorUnits");
|
|
201
|
+
function fromMinorUnits(value, currency) {
|
|
202
|
+
if (!Number.isInteger(value)) throw new Error(`\u6700\u5C0F\u5355\u4F4D\u5FC5\u987B\u662F\u6574\u6570\uFF1A${value}`);
|
|
203
|
+
const exponent = currencyExponent(currency);
|
|
204
|
+
const sign = value < 0 ? "-" : "";
|
|
205
|
+
const digits = String(Math.abs(value)).padStart(exponent + 1, "0");
|
|
206
|
+
if (exponent === 0) return `${sign}${digits}`;
|
|
207
|
+
return `${sign}${digits.slice(0, -exponent)}.${digits.slice(-exponent)}`;
|
|
208
|
+
}
|
|
209
|
+
__name(fromMinorUnits, "fromMinorUnits");
|
|
210
|
+
|
|
211
|
+
// src/contract/registry.ts
|
|
38
212
|
var import_common = require("@nestjs/common");
|
|
39
213
|
function _ts_decorate(decorators, target, key, desc) {
|
|
40
214
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -50,76 +224,631 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
50
224
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
51
225
|
}
|
|
52
226
|
__name(_ts_decorate, "_ts_decorate");
|
|
53
|
-
|
|
227
|
+
function _ts_metadata(metadataKey, metadataValue) {
|
|
228
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
229
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
233
|
+
var PaymentProviderRegistry = class _PaymentProviderRegistry {
|
|
54
234
|
static {
|
|
55
|
-
__name(this, "
|
|
56
|
-
}
|
|
57
|
-
logger = new import_common.Logger(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const provider = this.getProvider(providerId);
|
|
90
|
-
this.logger.log(`[${providerId}] \u521B\u5EFA\u652F\u4ED8\u610F\u56FE: ${params.amount} ${params.currency}`);
|
|
91
|
-
return provider.createPaymentIntent(params);
|
|
92
|
-
}
|
|
93
|
-
/** 确认支付 */
|
|
94
|
-
async confirmPayment(providerId, intentId) {
|
|
95
|
-
const provider = this.getProvider(providerId);
|
|
96
|
-
this.logger.log(`[${providerId}] \u786E\u8BA4\u652F\u4ED8: ${intentId}`);
|
|
97
|
-
return provider.confirmPayment(intentId);
|
|
98
|
-
}
|
|
99
|
-
/** 退款 */
|
|
100
|
-
async refund(providerId, paymentId, amount, reason) {
|
|
101
|
-
const provider = this.getProvider(providerId);
|
|
102
|
-
this.logger.log(`[${providerId}] \u9000\u6B3E: ${paymentId}, amount=${amount ?? "full"}`);
|
|
103
|
-
return provider.refund(paymentId, amount, reason);
|
|
104
|
-
}
|
|
105
|
-
/** 查询支付状态 */
|
|
106
|
-
async getPaymentStatus(providerId, intentId) {
|
|
107
|
-
const provider = this.getProvider(providerId);
|
|
108
|
-
return provider.getPaymentStatus(intentId);
|
|
109
|
-
}
|
|
110
|
-
/** 处理 Webhook */
|
|
111
|
-
async handleWebhook(providerId, payload, signature) {
|
|
112
|
-
const provider = this.getProvider(providerId);
|
|
113
|
-
this.logger.log(`[${providerId}] \u5904\u7406 Webhook`);
|
|
114
|
-
return provider.handleWebhook(payload, signature);
|
|
235
|
+
__name(this, "PaymentProviderRegistry");
|
|
236
|
+
}
|
|
237
|
+
logger = new import_common.Logger(_PaymentProviderRegistry.name);
|
|
238
|
+
adapters = /* @__PURE__ */ new Map();
|
|
239
|
+
constructor(adapters = []) {
|
|
240
|
+
for (const adapter of adapters) this.register(adapter);
|
|
241
|
+
}
|
|
242
|
+
register(adapter) {
|
|
243
|
+
if (this.adapters.has(adapter.id)) {
|
|
244
|
+
throw new Error(`\u6E20\u9053 "${adapter.id}" \u5DF2\u6CE8\u518C\uFF1B\u91CD\u590D\u6CE8\u518C\u901A\u5E38\u610F\u5473\u7740\u88C5\u914D\u9519\u8BEF\uFF0C\u4E0D\u4E88\u9759\u9ED8\u8986\u76D6`);
|
|
245
|
+
}
|
|
246
|
+
this.adapters.set(adapter.id, adapter);
|
|
247
|
+
this.logger.log(`\u5DF2\u6CE8\u518C\u652F\u4ED8\u6E20\u9053 "${adapter.id}"`);
|
|
248
|
+
}
|
|
249
|
+
has(id) {
|
|
250
|
+
return this.adapters.has(id);
|
|
251
|
+
}
|
|
252
|
+
get(id) {
|
|
253
|
+
const adapter = this.adapters.get(id);
|
|
254
|
+
if (!adapter) {
|
|
255
|
+
throw new Error(`\u6E20\u9053 "${id}" \u672A\u6CE8\u518C\u3002\u5DF2\u6CE8\u518C\uFF1A[${[
|
|
256
|
+
...this.adapters.keys()
|
|
257
|
+
].join(", ")}]`);
|
|
258
|
+
}
|
|
259
|
+
return adapter;
|
|
260
|
+
}
|
|
261
|
+
list() {
|
|
262
|
+
return [
|
|
263
|
+
...this.adapters.values()
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
/** 能处理该币种的渠道;"本产品卖不卖这个币种"由上层决定,不在这里过滤。 */
|
|
267
|
+
listForCurrency(currency) {
|
|
268
|
+
return this.list().filter((adapter) => adapterSupportsCurrency(adapter, currency));
|
|
115
269
|
}
|
|
116
270
|
};
|
|
117
|
-
|
|
118
|
-
(0, import_common.Injectable)()
|
|
119
|
-
|
|
271
|
+
PaymentProviderRegistry = _ts_decorate([
|
|
272
|
+
(0, import_common.Injectable)(),
|
|
273
|
+
_ts_metadata("design:type", Function),
|
|
274
|
+
_ts_metadata("design:paramtypes", [
|
|
275
|
+
Object
|
|
276
|
+
])
|
|
277
|
+
], PaymentProviderRegistry);
|
|
278
|
+
|
|
279
|
+
// src/contract/adapters/paypal.adapter.ts
|
|
280
|
+
var CAPTURE_INTENT = "CAPTURE";
|
|
281
|
+
var PAY_NOW = "PAY_NOW";
|
|
282
|
+
var NO_SHIPPING = "NO_SHIPPING";
|
|
283
|
+
var PAYPAL_SUPPORTED_CURRENCIES = [
|
|
284
|
+
"AUD",
|
|
285
|
+
"BRL",
|
|
286
|
+
"CAD",
|
|
287
|
+
"CNY",
|
|
288
|
+
"CZK",
|
|
289
|
+
"DKK",
|
|
290
|
+
"EUR",
|
|
291
|
+
"HKD",
|
|
292
|
+
"HUF",
|
|
293
|
+
"ILS",
|
|
294
|
+
"JPY",
|
|
295
|
+
"MYR",
|
|
296
|
+
"MXN",
|
|
297
|
+
"TWD",
|
|
298
|
+
"NZD",
|
|
299
|
+
"NOK",
|
|
300
|
+
"PHP",
|
|
301
|
+
"PLN",
|
|
302
|
+
"GBP",
|
|
303
|
+
"RUB",
|
|
304
|
+
"SGD",
|
|
305
|
+
"SEK",
|
|
306
|
+
"CHF",
|
|
307
|
+
"THB",
|
|
308
|
+
"USD"
|
|
309
|
+
];
|
|
310
|
+
function observePayPalState(order) {
|
|
311
|
+
const captures = order.purchaseUnits?.[0]?.payments?.captures ?? [];
|
|
312
|
+
switch (order.status) {
|
|
313
|
+
case "CREATED":
|
|
314
|
+
case "SAVED":
|
|
315
|
+
case "PAYER_ACTION_REQUIRED":
|
|
316
|
+
return "created";
|
|
317
|
+
case "APPROVED":
|
|
318
|
+
return "authorized";
|
|
319
|
+
case "VOIDED":
|
|
320
|
+
return "canceled";
|
|
321
|
+
case "COMPLETED": {
|
|
322
|
+
if (captures.some((capture) => capture.status === "COMPLETED")) return "captured";
|
|
323
|
+
if (captures.some((c) => c.status === "DECLINED" || c.status === "FAILED")) return "failed";
|
|
324
|
+
return "authorized";
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return "unknown";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
__name(observePayPalState, "observePayPalState");
|
|
331
|
+
function createPayPalAdapter(options) {
|
|
332
|
+
const { transport } = options;
|
|
333
|
+
const toSession = /* @__PURE__ */ __name((order) => {
|
|
334
|
+
const unit = order.purchaseUnits?.[0];
|
|
335
|
+
const approve = order.links?.find((link) => link.rel === "approve")?.href;
|
|
336
|
+
return {
|
|
337
|
+
providerRef: order.id ?? "",
|
|
338
|
+
nextAction: approve ? {
|
|
339
|
+
kind: "redirect",
|
|
340
|
+
url: approve
|
|
341
|
+
} : {
|
|
342
|
+
kind: "none"
|
|
343
|
+
},
|
|
344
|
+
state: observePayPalState(order),
|
|
345
|
+
money: {
|
|
346
|
+
amount: unit?.amount?.value ?? "0",
|
|
347
|
+
currency: unit?.amount?.currencyCode ?? ""
|
|
348
|
+
},
|
|
349
|
+
raw: order
|
|
350
|
+
};
|
|
351
|
+
}, "toSession");
|
|
352
|
+
return {
|
|
353
|
+
id: "paypal",
|
|
354
|
+
supportedCurrencies: PAYPAL_SUPPORTED_CURRENCIES,
|
|
355
|
+
async createPayment(input2) {
|
|
356
|
+
const payment = await transport.createOrder({
|
|
357
|
+
body: {
|
|
358
|
+
intent: CAPTURE_INTENT,
|
|
359
|
+
purchaseUnits: [
|
|
360
|
+
{
|
|
361
|
+
referenceId: input2.reference,
|
|
362
|
+
amount: {
|
|
363
|
+
currencyCode: input2.money.currency,
|
|
364
|
+
// 原样透传十进制字符串,不经过 Number
|
|
365
|
+
value: input2.money.amount
|
|
366
|
+
},
|
|
367
|
+
description: input2.description
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
applicationContext: {
|
|
371
|
+
returnUrl: input2.returnUrl,
|
|
372
|
+
cancelUrl: input2.cancelUrl,
|
|
373
|
+
userAction: PAY_NOW,
|
|
374
|
+
shippingPreference: NO_SHIPPING,
|
|
375
|
+
brandName: options.brandName
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
// 幂等:同一个键重试,PayPal 返回同一笔订单
|
|
379
|
+
paypalRequestId: input2.idempotencyKey,
|
|
380
|
+
prefer: "return=representation"
|
|
381
|
+
});
|
|
382
|
+
return toSession(payment.raw);
|
|
383
|
+
},
|
|
384
|
+
async capture(providerRef, idempotencyKey) {
|
|
385
|
+
const payment = await transport.captureOrder({
|
|
386
|
+
id: providerRef,
|
|
387
|
+
paypalRequestId: idempotencyKey,
|
|
388
|
+
prefer: "return=representation"
|
|
389
|
+
});
|
|
390
|
+
return toSession(payment.raw);
|
|
391
|
+
},
|
|
392
|
+
/** 只读。对账会反复调用它,绝不能在这里触发 captureOrder。 */
|
|
393
|
+
async retrieve(providerRef) {
|
|
394
|
+
const payment = await transport.getOrder({
|
|
395
|
+
id: providerRef
|
|
396
|
+
});
|
|
397
|
+
return toSession(payment.raw);
|
|
398
|
+
},
|
|
399
|
+
async refund(input2) {
|
|
400
|
+
const refund = await transport.refundCapture(input2.providerRef, {
|
|
401
|
+
...input2.money ? {
|
|
402
|
+
amount: {
|
|
403
|
+
value: input2.money.amount,
|
|
404
|
+
currency_code: input2.money.currency
|
|
405
|
+
}
|
|
406
|
+
} : {},
|
|
407
|
+
note_to_payer: input2.reason
|
|
408
|
+
}, input2.idempotencyKey);
|
|
409
|
+
return {
|
|
410
|
+
refundRef: refund.id ?? "",
|
|
411
|
+
state: refund.status === "COMPLETED" ? "refunded" : "unknown"
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
async parseWebhook(request) {
|
|
415
|
+
if (!options.webhookId) {
|
|
416
|
+
throw new Error("PayPal webhookId is not configured; refusing to trust the request");
|
|
417
|
+
}
|
|
418
|
+
const body = typeof request.rawBody === "string" ? request.rawBody : new TextDecoder().decode(request.rawBody);
|
|
419
|
+
const valid = await transport.verifyWebhookSignature(body, request.headers, options.webhookId);
|
|
420
|
+
if (!valid) throw new Error("PayPal webhook signature verification failed");
|
|
421
|
+
const event = JSON.parse(body);
|
|
422
|
+
const type = event.event_type ?? "unknown";
|
|
423
|
+
const providerRef = event.resource?.supplementary_data?.related_ids?.order_id;
|
|
424
|
+
const state = type === "PAYMENT.CAPTURE.COMPLETED" ? "captured" : type === "PAYMENT.CAPTURE.DENIED" || type === "PAYMENT.CAPTURE.DECLINED" ? "failed" : type === "PAYMENT.CAPTURE.REFUNDED" ? "refunded" : type === "PAYMENT.CAPTURE.REVERSED" ? "refunded" : "unknown";
|
|
425
|
+
return {
|
|
426
|
+
type,
|
|
427
|
+
providerRef,
|
|
428
|
+
state,
|
|
429
|
+
raw: event
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
__name(createPayPalAdapter, "createPayPalAdapter");
|
|
435
|
+
|
|
436
|
+
// src/providers/stripe.ts
|
|
437
|
+
var import_node_module = require("module");
|
|
438
|
+
var import_meta = {};
|
|
439
|
+
var requireSdk = (0, import_node_module.createRequire)(typeof __filename === "string" ? __filename : import_meta.url);
|
|
440
|
+
function createStripeClient(secretKey, options) {
|
|
441
|
+
const StripeSdk = requireSdk("stripe");
|
|
442
|
+
return new StripeSdk(secretKey, options);
|
|
443
|
+
}
|
|
444
|
+
__name(createStripeClient, "createStripeClient");
|
|
445
|
+
function verifyStripeWebhookEvent(client, payload, signature, webhookSecret) {
|
|
446
|
+
return client.webhooks.constructEvent(payload, signature, webhookSecret);
|
|
447
|
+
}
|
|
448
|
+
__name(verifyStripeWebhookEvent, "verifyStripeWebhookEvent");
|
|
449
|
+
function stripeResourceId(value) {
|
|
450
|
+
return typeof value === "string" ? value : value?.id;
|
|
451
|
+
}
|
|
452
|
+
__name(stripeResourceId, "stripeResourceId");
|
|
453
|
+
function observeStripeCheckoutSession(raw) {
|
|
454
|
+
return {
|
|
455
|
+
raw,
|
|
456
|
+
id: raw.id,
|
|
457
|
+
url: raw.url || void 0,
|
|
458
|
+
clientReferenceId: raw.client_reference_id || void 0,
|
|
459
|
+
paymentIntentId: stripeResourceId(raw.payment_intent),
|
|
460
|
+
subscriptionId: stripeResourceId(raw.subscription),
|
|
461
|
+
settlement: raw.payment_status === "paid" ? "paid" : raw.status === "expired" ? "unpaid" : "unknown"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
__name(observeStripeCheckoutSession, "observeStripeCheckoutSession");
|
|
465
|
+
async function createStripeCheckoutSession(client, params, options) {
|
|
466
|
+
return observeStripeCheckoutSession(options ? await client.checkout.sessions.create(params, options) : await client.checkout.sessions.create(params));
|
|
467
|
+
}
|
|
468
|
+
__name(createStripeCheckoutSession, "createStripeCheckoutSession");
|
|
469
|
+
async function retrieveStripeCheckoutSession(client, id) {
|
|
470
|
+
return observeStripeCheckoutSession(await client.checkout.sessions.retrieve(id));
|
|
471
|
+
}
|
|
472
|
+
__name(retrieveStripeCheckoutSession, "retrieveStripeCheckoutSession");
|
|
473
|
+
function observeStripePaymentIntent(raw) {
|
|
474
|
+
const charge = typeof raw.latest_charge === "object" ? raw.latest_charge : null;
|
|
475
|
+
return {
|
|
476
|
+
raw,
|
|
477
|
+
id: raw.id,
|
|
478
|
+
chargeId: stripeResourceId(raw.latest_charge) ?? null,
|
|
479
|
+
paymentMethodType: charge?.payment_method_details?.type,
|
|
480
|
+
failureCode: raw.last_payment_error?.code,
|
|
481
|
+
failureMessage: raw.last_payment_error?.message,
|
|
482
|
+
settlement: raw.status === "succeeded" ? "paid" : raw.status === "canceled" ? "unpaid" : "unknown"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
__name(observeStripePaymentIntent, "observeStripePaymentIntent");
|
|
486
|
+
async function retrieveStripePaymentIntent(client, id, params) {
|
|
487
|
+
const raw = params ? await client.paymentIntents.retrieve(id, params) : await client.paymentIntents.retrieve(id);
|
|
488
|
+
return observeStripePaymentIntent(raw);
|
|
489
|
+
}
|
|
490
|
+
__name(retrieveStripePaymentIntent, "retrieveStripePaymentIntent");
|
|
491
|
+
|
|
492
|
+
// src/contract/adapters/stripe.adapter.ts
|
|
493
|
+
function observeStripeState(session) {
|
|
494
|
+
if (session.payment_status === "paid" || session.payment_status === "no_payment_required") {
|
|
495
|
+
return "captured";
|
|
496
|
+
}
|
|
497
|
+
if (session.status === "expired") return "canceled";
|
|
498
|
+
if (session.status === "open") return "created";
|
|
499
|
+
return "unknown";
|
|
500
|
+
}
|
|
501
|
+
__name(observeStripeState, "observeStripeState");
|
|
502
|
+
function createStripeAdapter(options) {
|
|
503
|
+
const { client } = options;
|
|
504
|
+
const toSession = /* @__PURE__ */ __name((raw) => ({
|
|
505
|
+
providerRef: raw.id,
|
|
506
|
+
nextAction: raw.url ? {
|
|
507
|
+
kind: "redirect",
|
|
508
|
+
url: raw.url
|
|
509
|
+
} : {
|
|
510
|
+
kind: "none"
|
|
511
|
+
},
|
|
512
|
+
state: observeStripeState(raw),
|
|
513
|
+
money: {
|
|
514
|
+
amount: fromMinorUnits(raw.amount_total ?? 0, raw.currency ?? "usd"),
|
|
515
|
+
currency: (raw.currency ?? "").toUpperCase()
|
|
516
|
+
},
|
|
517
|
+
raw
|
|
518
|
+
}), "toSession");
|
|
519
|
+
return {
|
|
520
|
+
id: "stripe",
|
|
521
|
+
// Stripe 支持的币种远超本契约会用到的范围,不声明限制;
|
|
522
|
+
// 不受支持的币种由 Stripe 自己拒绝,胡乱穷举一份只会过期。
|
|
523
|
+
supportedCurrencies: void 0,
|
|
524
|
+
async createPayment(input2) {
|
|
525
|
+
const observation = await createStripeCheckoutSession(client, {
|
|
526
|
+
mode: "payment",
|
|
527
|
+
line_items: [
|
|
528
|
+
{
|
|
529
|
+
price_data: {
|
|
530
|
+
currency: input2.money.currency.toLowerCase(),
|
|
531
|
+
product_data: {
|
|
532
|
+
name: input2.description || input2.reference
|
|
533
|
+
},
|
|
534
|
+
unit_amount: toMinorUnits(input2.money.amount, input2.money.currency)
|
|
535
|
+
},
|
|
536
|
+
quantity: 1
|
|
537
|
+
}
|
|
538
|
+
],
|
|
539
|
+
success_url: input2.returnUrl,
|
|
540
|
+
cancel_url: input2.cancelUrl,
|
|
541
|
+
client_reference_id: input2.reference,
|
|
542
|
+
metadata: input2.metadata,
|
|
543
|
+
...options.paymentMethodConfiguration ? {
|
|
544
|
+
payment_method_configuration: options.paymentMethodConfiguration
|
|
545
|
+
} : {}
|
|
546
|
+
}, {
|
|
547
|
+
idempotencyKey: input2.idempotencyKey
|
|
548
|
+
});
|
|
549
|
+
return toSession(observation.raw);
|
|
550
|
+
},
|
|
551
|
+
/**
|
|
552
|
+
* Stripe Checkout 默认自动扣款,没有独立的 capture 步骤。
|
|
553
|
+
* 按契约仍需实现:返回当前真实状态即可,不得伪造成已扣款。
|
|
554
|
+
*/
|
|
555
|
+
async capture(providerRef) {
|
|
556
|
+
const observation = await retrieveStripeCheckoutSession(client, providerRef);
|
|
557
|
+
return toSession(observation.raw);
|
|
558
|
+
},
|
|
559
|
+
async retrieve(providerRef) {
|
|
560
|
+
const observation = await retrieveStripeCheckoutSession(client, providerRef);
|
|
561
|
+
return toSession(observation.raw);
|
|
562
|
+
},
|
|
563
|
+
async refund(input2) {
|
|
564
|
+
const session = await retrieveStripeCheckoutSession(client, input2.providerRef);
|
|
565
|
+
const paymentIntent = session.paymentIntentId;
|
|
566
|
+
if (!paymentIntent) throw new Error("\u8BE5 Checkout \u4F1A\u8BDD\u6CA1\u6709\u53EF\u9000\u6B3E\u7684 PaymentIntent");
|
|
567
|
+
const refund = await client.refunds.create({
|
|
568
|
+
payment_intent: paymentIntent,
|
|
569
|
+
...input2.money ? {
|
|
570
|
+
amount: toMinorUnits(input2.money.amount, input2.money.currency)
|
|
571
|
+
} : {}
|
|
572
|
+
}, {
|
|
573
|
+
idempotencyKey: input2.idempotencyKey
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
refundRef: refund.id,
|
|
577
|
+
state: refund.status === "succeeded" ? "refunded" : "unknown"
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
async parseWebhook(request) {
|
|
581
|
+
if (!options.webhookSecret) {
|
|
582
|
+
throw new Error("Stripe webhookSecret is not configured; refusing to trust the request");
|
|
583
|
+
}
|
|
584
|
+
const signature = request.headers["stripe-signature"];
|
|
585
|
+
if (!signature) throw new Error("Stripe webhook is missing the stripe-signature header");
|
|
586
|
+
const event = verifyStripeWebhookEvent(client, typeof request.rawBody === "string" ? request.rawBody : Buffer.from(request.rawBody), signature, options.webhookSecret);
|
|
587
|
+
const object = event.data.object;
|
|
588
|
+
const state = event.type === "checkout.session.completed" ? observeStripeState(event.data.object) : event.type === "checkout.session.expired" ? "canceled" : event.type === "charge.refunded" ? "refunded" : "unknown";
|
|
589
|
+
return {
|
|
590
|
+
type: event.type,
|
|
591
|
+
providerRef: object.id,
|
|
592
|
+
state,
|
|
593
|
+
raw: event
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
__name(createStripeAdapter, "createStripeAdapter");
|
|
599
|
+
|
|
600
|
+
// src/contract/contract-suite.ts
|
|
601
|
+
var MUTATING = [
|
|
602
|
+
"capture",
|
|
603
|
+
"refund",
|
|
604
|
+
"cancel"
|
|
605
|
+
];
|
|
606
|
+
async function checkPaymentAdapterContract(probe) {
|
|
607
|
+
const checks = [];
|
|
608
|
+
const captureNames = probe.captureCallNames ?? MUTATING;
|
|
609
|
+
const run = /* @__PURE__ */ __name(async (name, fn) => {
|
|
610
|
+
try {
|
|
611
|
+
const detail = await fn();
|
|
612
|
+
checks.push({
|
|
613
|
+
name,
|
|
614
|
+
ok: detail === void 0,
|
|
615
|
+
detail
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
checks.push({
|
|
619
|
+
name,
|
|
620
|
+
ok: false,
|
|
621
|
+
detail: `\u629B\u51FA\u5F02\u5E38\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}, "run");
|
|
625
|
+
await run("retrieve \u5FC5\u987B\u53EA\u8BFB", async () => {
|
|
626
|
+
const { adapter, mutatingCalls } = probe.create();
|
|
627
|
+
const created = await adapter.createPayment(input(probe.supportedCurrency));
|
|
628
|
+
const before = mutatingCalls().filter((c) => captureNames.includes(c)).length;
|
|
629
|
+
await adapter.retrieve(created.providerRef);
|
|
630
|
+
const after = mutatingCalls().filter((c) => captureNames.includes(c)).length;
|
|
631
|
+
return after > before ? `retrieve \u89E6\u53D1\u4E86\u5199\u64CD\u4F5C\uFF1A${mutatingCalls().join(", ")}` : void 0;
|
|
632
|
+
});
|
|
633
|
+
await run("\u76F8\u540C\u5E42\u7B49\u952E\u4E0D\u5F97\u4EA7\u751F\u7B2C\u4E8C\u7B14\u652F\u4ED8", async () => {
|
|
634
|
+
const { adapter } = probe.create();
|
|
635
|
+
const key = "contract-idempotency-key";
|
|
636
|
+
const first = await adapter.createPayment(input(probe.supportedCurrency, key));
|
|
637
|
+
const second = await adapter.createPayment(input(probe.supportedCurrency, key));
|
|
638
|
+
return first.providerRef === second.providerRef ? void 0 : `\u4EA7\u751F\u4E86\u4E24\u7B14\uFF1A${first.providerRef} \u4E0E ${second.providerRef}`;
|
|
639
|
+
});
|
|
640
|
+
await run("\u9A8C\u7B7E\u5931\u8D25\u5FC5\u987B\u629B\u9519\u800C\u975E\u8FD4\u56DE\u672A\u77E5\u4E8B\u4EF6", async () => {
|
|
641
|
+
const { adapter } = probe.create();
|
|
642
|
+
try {
|
|
643
|
+
const observation = await adapter.parseWebhook(probe.invalidWebhook);
|
|
644
|
+
return `\u672A\u629B\u9519\uFF0C\u8FD4\u56DE\u4E86 ${JSON.stringify(observation.type)}`;
|
|
645
|
+
} catch {
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
await run("\u91D1\u989D\u5FC5\u987B\u4EE5\u5341\u8FDB\u5236\u5B57\u7B26\u4E32\u539F\u6837\u900F\u4F20", async () => {
|
|
650
|
+
const { adapter } = probe.create();
|
|
651
|
+
const amount = "29.00";
|
|
652
|
+
const created = await adapter.createPayment({
|
|
653
|
+
...input(probe.supportedCurrency),
|
|
654
|
+
money: {
|
|
655
|
+
amount,
|
|
656
|
+
currency: probe.supportedCurrency
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
return created.money.amount === amount ? void 0 : `\u671F\u671B "${amount}"\uFF0C\u5B9E\u9645 ${JSON.stringify(created.money.amount)}`;
|
|
660
|
+
});
|
|
661
|
+
await run("\u5728\u9014\u652F\u4ED8\u4E0D\u5F97\u88AB\u5224\u4E3A failed", async () => {
|
|
662
|
+
const { adapter } = probe.create();
|
|
663
|
+
const created = await adapter.createPayment(input(probe.supportedCurrency));
|
|
664
|
+
const retrieved = await adapter.retrieve(created.providerRef);
|
|
665
|
+
return retrieved.state === "failed" ? "\u521A\u521B\u5EFA\u3001\u7528\u6237\u5C1A\u672A\u64CD\u4F5C\u7684\u652F\u4ED8\u88AB\u5224\u4E3A failed" : void 0;
|
|
666
|
+
});
|
|
667
|
+
await run("nextAction \u5FC5\u987B\u662F\u5951\u7EA6\u5185\u7684 kind", async () => {
|
|
668
|
+
const { adapter } = probe.create();
|
|
669
|
+
const created = await adapter.createPayment(input(probe.supportedCurrency));
|
|
670
|
+
const known = [
|
|
671
|
+
"redirect",
|
|
672
|
+
"qrcode",
|
|
673
|
+
"form_post",
|
|
674
|
+
"sdk_params",
|
|
675
|
+
"none"
|
|
676
|
+
];
|
|
677
|
+
return known.includes(created.nextAction.kind) ? void 0 : `\u672A\u77E5 kind\uFF1A${created.nextAction.kind}`;
|
|
678
|
+
});
|
|
679
|
+
await run("\u5E01\u79CD\u80FD\u529B\u58F0\u660E\u4E0E\u5B9E\u9645\u4E00\u81F4", async () => {
|
|
680
|
+
const { adapter } = probe.create();
|
|
681
|
+
if (!adapter.supportedCurrencies) return void 0;
|
|
682
|
+
if (!adapterSupportsCurrency(adapter, probe.supportedCurrency)) {
|
|
683
|
+
return `\u58F0\u660E\u4E0D\u652F\u6301 ${probe.supportedCurrency}\uFF0C\u4F46\u63A2\u9488\u79F0\u5176\u652F\u6301`;
|
|
684
|
+
}
|
|
685
|
+
if (probe.unsupportedCurrency && adapterSupportsCurrency(adapter, probe.unsupportedCurrency)) {
|
|
686
|
+
return `\u58F0\u660E\u652F\u6301 ${probe.unsupportedCurrency}\uFF0C\u4F46\u63A2\u9488\u79F0\u5176\u4E0D\u652F\u6301`;
|
|
687
|
+
}
|
|
688
|
+
return void 0;
|
|
689
|
+
});
|
|
690
|
+
return checks;
|
|
691
|
+
}
|
|
692
|
+
__name(checkPaymentAdapterContract, "checkPaymentAdapterContract");
|
|
693
|
+
function input(currency, idempotencyKey = "contract-default-key") {
|
|
694
|
+
return {
|
|
695
|
+
reference: "contract-order",
|
|
696
|
+
money: {
|
|
697
|
+
amount: "10.00",
|
|
698
|
+
currency
|
|
699
|
+
},
|
|
700
|
+
idempotencyKey
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
__name(input, "input");
|
|
704
|
+
|
|
705
|
+
// src/contract/fake-adapter.ts
|
|
706
|
+
function createFakeAdapter(options = {}) {
|
|
707
|
+
const defects = options.defects ?? {};
|
|
708
|
+
const payments = /* @__PURE__ */ new Map();
|
|
709
|
+
const byIdempotencyKey = /* @__PURE__ */ new Map();
|
|
710
|
+
const mutatingCalls = [];
|
|
711
|
+
let counter = 0;
|
|
712
|
+
const adapter = {
|
|
713
|
+
id: options.id ?? "fake",
|
|
714
|
+
supportedCurrencies: options.supportedCurrencies,
|
|
715
|
+
async createPayment(input2) {
|
|
716
|
+
mutatingCalls.push("createPayment");
|
|
717
|
+
const existing = defects.ignoreIdempotency ? void 0 : byIdempotencyKey.get(input2.idempotencyKey);
|
|
718
|
+
if (existing) {
|
|
719
|
+
const payment2 = payments.get(existing);
|
|
720
|
+
return session(payment2);
|
|
721
|
+
}
|
|
722
|
+
const ref = `fake_${++counter}`;
|
|
723
|
+
const amount = defects.mangleAmount ? String(Number(input2.money.amount)) : input2.money.amount;
|
|
724
|
+
const payment = {
|
|
725
|
+
ref,
|
|
726
|
+
state: "created",
|
|
727
|
+
amount,
|
|
728
|
+
currency: input2.money.currency
|
|
729
|
+
};
|
|
730
|
+
payments.set(ref, payment);
|
|
731
|
+
byIdempotencyKey.set(input2.idempotencyKey, ref);
|
|
732
|
+
return session(payment);
|
|
733
|
+
},
|
|
734
|
+
async capture(providerRef) {
|
|
735
|
+
mutatingCalls.push("capture");
|
|
736
|
+
const payment = expect(providerRef);
|
|
737
|
+
payment.state = "captured";
|
|
738
|
+
return session(payment);
|
|
739
|
+
},
|
|
740
|
+
async retrieve(providerRef) {
|
|
741
|
+
if (defects.retrieveCaptures) {
|
|
742
|
+
mutatingCalls.push("capture");
|
|
743
|
+
expect(providerRef).state = "captured";
|
|
744
|
+
}
|
|
745
|
+
const payment = expect(providerRef);
|
|
746
|
+
if (defects.guessesPendingAsFailed && payment.state === "created") {
|
|
747
|
+
return {
|
|
748
|
+
...session(payment),
|
|
749
|
+
state: "failed"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
return session(payment);
|
|
753
|
+
},
|
|
754
|
+
async refund(input2) {
|
|
755
|
+
mutatingCalls.push("refund");
|
|
756
|
+
const payment = expect(input2.providerRef);
|
|
757
|
+
payment.state = input2.money ? "partially_refunded" : "refunded";
|
|
758
|
+
return {
|
|
759
|
+
refundRef: `refund_${payment.ref}`,
|
|
760
|
+
state: payment.state
|
|
761
|
+
};
|
|
762
|
+
},
|
|
763
|
+
async parseWebhook(request) {
|
|
764
|
+
const valid = request.headers["x-fake-signature"] === "valid";
|
|
765
|
+
if (!valid && !defects.acceptsBadSignature) {
|
|
766
|
+
throw new Error("webhook signature verification failed");
|
|
767
|
+
}
|
|
768
|
+
const ref = request.headers["x-fake-ref"];
|
|
769
|
+
return {
|
|
770
|
+
type: "fake.payment.updated",
|
|
771
|
+
providerRef: ref,
|
|
772
|
+
state: ref && payments.has(ref) ? payments.get(ref).state : "unknown",
|
|
773
|
+
raw: {}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
function expect(ref) {
|
|
778
|
+
const payment = payments.get(ref);
|
|
779
|
+
if (!payment) throw new Error(`unknown payment ${ref}`);
|
|
780
|
+
return payment;
|
|
781
|
+
}
|
|
782
|
+
__name(expect, "expect");
|
|
783
|
+
function session(payment) {
|
|
784
|
+
return {
|
|
785
|
+
providerRef: payment.ref,
|
|
786
|
+
nextAction: {
|
|
787
|
+
kind: "redirect",
|
|
788
|
+
url: `https://fake.test/pay/${payment.ref}`
|
|
789
|
+
},
|
|
790
|
+
state: payment.state,
|
|
791
|
+
money: {
|
|
792
|
+
amount: payment.amount,
|
|
793
|
+
currency: payment.currency
|
|
794
|
+
},
|
|
795
|
+
raw: {}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
__name(session, "session");
|
|
799
|
+
return {
|
|
800
|
+
adapter,
|
|
801
|
+
mutatingCalls: /* @__PURE__ */ __name(() => [
|
|
802
|
+
...mutatingCalls
|
|
803
|
+
], "mutatingCalls")
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
__name(createFakeAdapter, "createFakeAdapter");
|
|
120
807
|
|
|
121
808
|
// src/order.service.ts
|
|
122
809
|
var import_common2 = require("@nestjs/common");
|
|
810
|
+
|
|
811
|
+
// src/order-persistence.ts
|
|
812
|
+
var ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
|
|
813
|
+
var DatabaseOrderPersistence = class {
|
|
814
|
+
static {
|
|
815
|
+
__name(this, "DatabaseOrderPersistence");
|
|
816
|
+
}
|
|
817
|
+
model;
|
|
818
|
+
constructor(model) {
|
|
819
|
+
this.model = model;
|
|
820
|
+
}
|
|
821
|
+
create(args) {
|
|
822
|
+
return this.model.create(args);
|
|
823
|
+
}
|
|
824
|
+
findUnique(args) {
|
|
825
|
+
return this.model.findUnique(args);
|
|
826
|
+
}
|
|
827
|
+
findMany(args) {
|
|
828
|
+
return this.model.findMany(args);
|
|
829
|
+
}
|
|
830
|
+
update(args) {
|
|
831
|
+
return this.model.update(args);
|
|
832
|
+
}
|
|
833
|
+
updateMany(args) {
|
|
834
|
+
return this.model.updateMany(args);
|
|
835
|
+
}
|
|
836
|
+
count(args) {
|
|
837
|
+
return this.model.count(args);
|
|
838
|
+
}
|
|
839
|
+
async compareAndSet(id, expected, data) {
|
|
840
|
+
const result = await this.model.updateMany({
|
|
841
|
+
where: {
|
|
842
|
+
...expected,
|
|
843
|
+
id
|
|
844
|
+
},
|
|
845
|
+
data
|
|
846
|
+
});
|
|
847
|
+
return result.count === 1;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/order.service.ts
|
|
123
852
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
124
853
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
125
854
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
@@ -134,138 +863,198 @@ function _ts_decorate2(decorators, target, key, desc) {
|
|
|
134
863
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
135
864
|
}
|
|
136
865
|
__name(_ts_decorate2, "_ts_decorate");
|
|
137
|
-
function
|
|
866
|
+
function _ts_metadata2(metadataKey, metadataValue) {
|
|
138
867
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
139
868
|
return Reflect.metadata(metadataKey, metadataValue);
|
|
140
869
|
}
|
|
141
870
|
}
|
|
142
|
-
__name(
|
|
143
|
-
|
|
871
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
872
|
+
function _ts_param(paramIndex, decorator) {
|
|
873
|
+
return function(target, key) {
|
|
874
|
+
decorator(target, key, paramIndex);
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
__name(_ts_param, "_ts_param");
|
|
878
|
+
var OrderNotFoundError = class extends Error {
|
|
879
|
+
static {
|
|
880
|
+
__name(this, "OrderNotFoundError");
|
|
881
|
+
}
|
|
882
|
+
orderId;
|
|
883
|
+
constructor(orderId) {
|
|
884
|
+
super(`\u8BA2\u5355 "${orderId}" \u4E0D\u5B58\u5728`), this.orderId = orderId;
|
|
885
|
+
this.name = "OrderNotFoundError";
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
var OrderService = class {
|
|
144
889
|
static {
|
|
145
890
|
__name(this, "OrderService");
|
|
146
891
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
orders
|
|
150
|
-
|
|
151
|
-
this.
|
|
892
|
+
providers;
|
|
893
|
+
orders;
|
|
894
|
+
constructor(providers, orders) {
|
|
895
|
+
this.providers = providers;
|
|
896
|
+
this.orders = orders;
|
|
897
|
+
if (!orders) throw new Error("Order persistence must be configured");
|
|
152
898
|
}
|
|
153
|
-
/** 创建订单并发起支付 */
|
|
154
899
|
async createOrder(params) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
900
|
+
if (await this.orders.findUnique({
|
|
901
|
+
where: {
|
|
902
|
+
id: params.orderId
|
|
903
|
+
}
|
|
904
|
+
})) {
|
|
905
|
+
throw new Error(`\u8BA2\u5355 "${params.orderId}" \u5DF2\u5B58\u5728`);
|
|
158
906
|
}
|
|
159
907
|
const now = /* @__PURE__ */ new Date();
|
|
160
|
-
const order = {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
908
|
+
const order = await this.orders.create({
|
|
909
|
+
data: {
|
|
910
|
+
id: params.orderId,
|
|
911
|
+
providerId: params.providerId,
|
|
912
|
+
amount: params.payment.money.amount,
|
|
913
|
+
currency: params.payment.money.currency,
|
|
914
|
+
metadata: params.payment.metadata,
|
|
915
|
+
status: "created",
|
|
916
|
+
createdAt: now,
|
|
917
|
+
updatedAt: now
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return this.initiatePayment(order, params.payment);
|
|
921
|
+
}
|
|
922
|
+
async initiatePayment(order, payment) {
|
|
923
|
+
const session = await this.providers.get(order.providerId).createPayment(payment);
|
|
924
|
+
await this.transition(order.id, [
|
|
925
|
+
"created"
|
|
926
|
+
], "paying", {
|
|
927
|
+
paymentIntentId: session.providerRef
|
|
928
|
+
});
|
|
929
|
+
return this.getOrder(order.id);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 扣款。幂等键由调用方给出——同一笔订单重试必须复用同一个键,否则渠道会认为是新支付。
|
|
933
|
+
* 判定不了的结果保持 confirming,交由只读对账解决,绝不在这里重复扣款。
|
|
934
|
+
*/
|
|
935
|
+
async confirmOrder(orderId, idempotencyKey) {
|
|
936
|
+
const order = await this.getOrder(orderId);
|
|
937
|
+
if (!order.paymentIntentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentIntentId`);
|
|
938
|
+
await this.transition(orderId, [
|
|
939
|
+
"paying"
|
|
940
|
+
], "confirming");
|
|
941
|
+
const session = await this.providers.get(order.providerId).capture(order.paymentIntentId, idempotencyKey);
|
|
942
|
+
await this.applySettlement(orderId, "confirming", session.state, session.providerRef);
|
|
943
|
+
return session;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* 把渠道观察到的结算状态落到订单上。
|
|
947
|
+
* 迁移是否允许由 reconcile() 统一裁决,保证所有渠道遵守同一套安全性质
|
|
948
|
+
* (unknown 不覆盖已知状态、已收款不被迟到的失败回调回退)。
|
|
949
|
+
*/
|
|
950
|
+
async applySettlement(orderId, from, observed, paymentRef) {
|
|
951
|
+
const current = from === "paid" ? "captured" : from === "refunded" ? "refunded" : "created";
|
|
952
|
+
const decision = reconcile(current, observed);
|
|
953
|
+
if (!decision.changed) return;
|
|
954
|
+
if (decision.next === "captured") {
|
|
955
|
+
await this.transition(orderId, [
|
|
956
|
+
from
|
|
957
|
+
], "paid", {
|
|
958
|
+
paymentId: paymentRef,
|
|
959
|
+
paidAt: /* @__PURE__ */ new Date()
|
|
960
|
+
});
|
|
961
|
+
} else if (decision.next === "failed" || decision.next === "canceled") {
|
|
962
|
+
await this.transition(orderId, [
|
|
963
|
+
from
|
|
964
|
+
], "failed");
|
|
965
|
+
} else if (decision.next === "refunded" || decision.next === "partially_refunded") {
|
|
966
|
+
await this.transition(orderId, [
|
|
967
|
+
from
|
|
968
|
+
], "refunded", {
|
|
969
|
+
refundedAt: /* @__PURE__ */ new Date()
|
|
970
|
+
});
|
|
183
971
|
}
|
|
184
|
-
return {
|
|
185
|
-
...order
|
|
186
|
-
};
|
|
187
972
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
973
|
+
async refundOrder(orderId, idempotencyKey, options = {}) {
|
|
974
|
+
const order = await this.getOrder(orderId);
|
|
975
|
+
if (!order.paymentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentId`);
|
|
976
|
+
await this.transition(orderId, [
|
|
977
|
+
"paid"
|
|
978
|
+
], "refunding");
|
|
979
|
+
const result = await this.providers.get(order.providerId).refund({
|
|
980
|
+
providerRef: order.paymentId,
|
|
981
|
+
money: options.money,
|
|
982
|
+
reason: options.reason,
|
|
983
|
+
idempotencyKey
|
|
984
|
+
});
|
|
985
|
+
if (result.state === "refunded") {
|
|
986
|
+
await this.transition(orderId, [
|
|
987
|
+
"refunding"
|
|
988
|
+
], "refunded", {
|
|
989
|
+
refundedAt: /* @__PURE__ */ new Date()
|
|
990
|
+
});
|
|
991
|
+
} else if (result.state === "failed") {
|
|
992
|
+
await this.transition(orderId, [
|
|
993
|
+
"refunding"
|
|
994
|
+
], "paid");
|
|
995
|
+
}
|
|
207
996
|
return result;
|
|
208
997
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
order
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
order.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.logger.log(`\u8BA2\u5355 "${orderId}" \u9000\u6B3E\u7ED3\u679C: ${result.status}`);
|
|
229
|
-
return result;
|
|
998
|
+
/**
|
|
999
|
+
* 重试失败订单。
|
|
1000
|
+
* idempotencyKey 必须与上一次不同:沿用旧键会让渠道原样返回那笔已失败的支付,
|
|
1001
|
+
* 看起来"重试了"其实什么都没发生。
|
|
1002
|
+
*/
|
|
1003
|
+
async retryOrder(orderId, idempotencyKey) {
|
|
1004
|
+
const order = await this.getOrder(orderId);
|
|
1005
|
+
await this.transition(orderId, [
|
|
1006
|
+
"failed"
|
|
1007
|
+
], "created");
|
|
1008
|
+
return this.initiatePayment(order, {
|
|
1009
|
+
reference: order.id,
|
|
1010
|
+
money: {
|
|
1011
|
+
amount: order.amount,
|
|
1012
|
+
currency: order.currency
|
|
1013
|
+
},
|
|
1014
|
+
metadata: order.metadata,
|
|
1015
|
+
idempotencyKey
|
|
1016
|
+
});
|
|
230
1017
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const order = this.getOrder(orderId);
|
|
234
|
-
const cancellableStates = [
|
|
1018
|
+
async cancelOrder(orderId) {
|
|
1019
|
+
await this.transition(orderId, [
|
|
235
1020
|
"created",
|
|
236
1021
|
"paying",
|
|
237
1022
|
"failed"
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
throw new Error(`\u8BA2\u5355 "${orderId}" \u72B6\u6001\u4E3A "${order.status}"\uFF0C\u65E0\u6CD5\u53D6\u6D88`);
|
|
241
|
-
}
|
|
242
|
-
order.status = "cancelled";
|
|
243
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
244
|
-
this.logger.log(`\u8BA2\u5355 "${orderId}" \u5DF2\u53D6\u6D88`);
|
|
245
|
-
return {
|
|
246
|
-
...order
|
|
247
|
-
};
|
|
1023
|
+
], "cancelled");
|
|
1024
|
+
return this.getOrder(orderId);
|
|
248
1025
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
1026
|
+
async getOrder(orderId) {
|
|
1027
|
+
const order = await this.orders.findUnique({
|
|
1028
|
+
where: {
|
|
1029
|
+
id: orderId
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
if (!order) throw new OrderNotFoundError(orderId);
|
|
255
1033
|
return order;
|
|
256
1034
|
}
|
|
257
|
-
/** 列出所有订单 */
|
|
258
1035
|
listOrders() {
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
1036
|
+
return this.orders.findMany();
|
|
1037
|
+
}
|
|
1038
|
+
async transition(id, from, to, data = {}) {
|
|
1039
|
+
const changed = await this.orders.compareAndSet(id, {
|
|
1040
|
+
status: {
|
|
1041
|
+
in: from
|
|
1042
|
+
}
|
|
1043
|
+
}, {
|
|
1044
|
+
...data,
|
|
1045
|
+
status: to,
|
|
1046
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1047
|
+
});
|
|
1048
|
+
if (!changed) throw new Error(`\u8BA2\u5355 "${id}" \u4E0D\u5B58\u5728\u6216\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u65E0\u6CD5\u8F6C\u6362\u4E3A ${to}`);
|
|
262
1049
|
}
|
|
263
1050
|
};
|
|
264
1051
|
OrderService = _ts_decorate2([
|
|
265
1052
|
(0, import_common2.Injectable)(),
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
1053
|
+
_ts_param(1, (0, import_common2.Inject)(ORDER_PERSISTENCE)),
|
|
1054
|
+
_ts_metadata2("design:type", Function),
|
|
1055
|
+
_ts_metadata2("design:paramtypes", [
|
|
1056
|
+
typeof PaymentProviderRegistry === "undefined" ? Object : PaymentProviderRegistry,
|
|
1057
|
+
typeof OrderPersistence === "undefined" ? Object : OrderPersistence
|
|
269
1058
|
])
|
|
270
1059
|
], OrderService);
|
|
271
1060
|
|
|
@@ -298,21 +1087,22 @@ function _ts_decorate3(decorators, target, key, desc) {
|
|
|
298
1087
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
299
1088
|
}
|
|
300
1089
|
__name(_ts_decorate3, "_ts_decorate");
|
|
301
|
-
function
|
|
1090
|
+
function _ts_metadata3(metadataKey, metadataValue) {
|
|
302
1091
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
303
1092
|
return Reflect.metadata(metadataKey, metadataValue);
|
|
304
1093
|
}
|
|
305
1094
|
}
|
|
306
|
-
__name(
|
|
307
|
-
function
|
|
1095
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
1096
|
+
function _ts_param2(paramIndex, decorator) {
|
|
308
1097
|
return function(target, key) {
|
|
309
1098
|
decorator(target, key, paramIndex);
|
|
310
1099
|
};
|
|
311
1100
|
}
|
|
312
|
-
__name(
|
|
1101
|
+
__name(_ts_param2, "_ts_param");
|
|
313
1102
|
var RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
|
|
314
1103
|
var RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
|
|
315
1104
|
var RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
|
|
1105
|
+
var RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
|
|
316
1106
|
var DEFAULT_OPTIONS = {
|
|
317
1107
|
maxAttempts: 3,
|
|
318
1108
|
retryLinkExpireHours: 48,
|
|
@@ -323,18 +1113,16 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
323
1113
|
__name(this, "PaymentRecoveryService");
|
|
324
1114
|
}
|
|
325
1115
|
orderService;
|
|
326
|
-
|
|
1116
|
+
attempts;
|
|
327
1117
|
mailSender;
|
|
328
1118
|
userLookup;
|
|
329
1119
|
urlGenerator;
|
|
330
1120
|
eventEmitter;
|
|
331
1121
|
logger = new import_common3.Logger(_PaymentRecoveryService.name);
|
|
332
1122
|
options;
|
|
333
|
-
|
|
334
|
-
recoveryAttempts = /* @__PURE__ */ new Map();
|
|
335
|
-
constructor(orderService, paymentService, mailSender, userLookup, urlGenerator, eventEmitter) {
|
|
1123
|
+
constructor(orderService, attempts, mailSender, userLookup, urlGenerator, eventEmitter) {
|
|
336
1124
|
this.orderService = orderService;
|
|
337
|
-
this.
|
|
1125
|
+
this.attempts = attempts;
|
|
338
1126
|
this.mailSender = mailSender;
|
|
339
1127
|
this.userLookup = userLookup;
|
|
340
1128
|
this.urlGenerator = urlGenerator;
|
|
@@ -353,8 +1141,9 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
353
1141
|
async handleFailedPayment(orderId, errorCode, errorMessage) {
|
|
354
1142
|
let order;
|
|
355
1143
|
try {
|
|
356
|
-
order = this.orderService.getOrder(orderId);
|
|
357
|
-
} catch {
|
|
1144
|
+
order = await this.orderService.getOrder(orderId);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (!(error instanceof OrderNotFoundError)) throw error;
|
|
358
1147
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
|
|
359
1148
|
return;
|
|
360
1149
|
}
|
|
@@ -362,11 +1151,11 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
362
1151
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u72B6\u6001\u4E3A ${order.status}\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
|
|
363
1152
|
return;
|
|
364
1153
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this.logger.log(`\u8BA2\u5355 ${orderId} \u5DF2\u633D\u56DE ${attempts.count} \u6B21\uFF0C\u4E0D\u518D\u53D1\u9001`);
|
|
1154
|
+
if (!this.mailSender) {
|
|
1155
|
+
this.logger.warn("Recovery mail sender is not configured; no attempt was reserved");
|
|
368
1156
|
return;
|
|
369
1157
|
}
|
|
1158
|
+
if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
|
|
370
1159
|
const userId = order.metadata?.userId;
|
|
371
1160
|
let email;
|
|
372
1161
|
let userName = "";
|
|
@@ -387,32 +1176,20 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
387
1176
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u65E0\u6CD5\u751F\u6210\u91CD\u8BD5\u94FE\u63A5`);
|
|
388
1177
|
return;
|
|
389
1178
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
this.logger.log(`\u633D\u56DE\u90AE\u4EF6\u5DF2\u53D1\u9001: ${email}, \u8BA2\u5355 ${orderId} (\u7B2C ${attemptNumber} \u6B21)`);
|
|
406
|
-
} catch (err) {
|
|
407
|
-
this.logger.error(`\u633D\u56DE\u90AE\u4EF6\u53D1\u9001\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
this.logger.log(`[Recovery:NoMailSender] \u8BA2\u5355 ${orderId} \u9700\u8981\u633D\u56DE, email=${email}, retryUrl=${retryUrl}, reason=${errorReason}`);
|
|
412
|
-
}
|
|
413
|
-
this.recoveryAttempts.set(orderId, {
|
|
414
|
-
count: attemptNumber,
|
|
415
|
-
lastAt: /* @__PURE__ */ new Date()
|
|
1179
|
+
const reservation = await this.attempts.claim(orderId, this.options.maxAttempts, /* @__PURE__ */ new Date());
|
|
1180
|
+
if (!reservation) return;
|
|
1181
|
+
const attemptNumber = reservation.attemptNumber;
|
|
1182
|
+
await this.mailSender.sendRecoveryEmail({
|
|
1183
|
+
to: email,
|
|
1184
|
+
userName: userName || email.split("@")[0],
|
|
1185
|
+
orderId: order.id,
|
|
1186
|
+
orderNumber: order.metadata?.orderNumber,
|
|
1187
|
+
amount: order.amount,
|
|
1188
|
+
currency: order.currency,
|
|
1189
|
+
errorReason: friendlyErrorReason(errorCode, errorMessage),
|
|
1190
|
+
retryUrl,
|
|
1191
|
+
expireHours: this.options.retryLinkExpireHours,
|
|
1192
|
+
idempotencyKey: `payment-recovery:${orderId}:${attemptNumber}`
|
|
416
1193
|
});
|
|
417
1194
|
if (this.eventEmitter) {
|
|
418
1195
|
const event = {
|
|
@@ -426,30 +1203,15 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
426
1203
|
}
|
|
427
1204
|
}
|
|
428
1205
|
/**
|
|
429
|
-
* 重试失败订单 — 将 failed → created
|
|
1206
|
+
* 重试失败订单 — 将 failed → created 并重新发起支付。
|
|
1207
|
+
* 幂等键由调用方给出且必须与上次不同,否则渠道会原样返回那笔已失败的支付。
|
|
430
1208
|
*/
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (order.status !== "failed") {
|
|
434
|
-
throw new Error(`\u8BA2\u5355 ${orderId} \u72B6\u6001\u4E3A ${order.status}\uFF0C\u4EC5 failed \u8BA2\u5355\u53EF\u91CD\u8BD5`);
|
|
435
|
-
}
|
|
436
|
-
const intent = await this.paymentService.createPaymentIntent(order.providerId, {
|
|
437
|
-
amount: order.amount,
|
|
438
|
-
currency: order.currency,
|
|
439
|
-
metadata: order.metadata
|
|
440
|
-
});
|
|
441
|
-
const internalOrder = this.orderService.getOrder(orderId);
|
|
442
|
-
internalOrder.paymentIntentId = intent.id;
|
|
443
|
-
internalOrder.status = "paying";
|
|
444
|
-
internalOrder.updatedAt = /* @__PURE__ */ new Date();
|
|
445
|
-
this.logger.log(`\u8BA2\u5355 ${orderId} \u91CD\u8BD5\u652F\u4ED8, \u65B0 intentId=${intent.id}`);
|
|
446
|
-
return {
|
|
447
|
-
...internalOrder
|
|
448
|
-
};
|
|
1209
|
+
retryOrder(orderId, idempotencyKey) {
|
|
1210
|
+
return this.orderService.retryOrder(orderId, idempotencyKey);
|
|
449
1211
|
}
|
|
450
|
-
/** 获取订单挽回记录 */
|
|
451
1212
|
getRecoveryAttempts(orderId) {
|
|
452
|
-
|
|
1213
|
+
if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
|
|
1214
|
+
return this.attempts.get(orderId);
|
|
453
1215
|
}
|
|
454
1216
|
async generateRetryUrl(order) {
|
|
455
1217
|
if (this.urlGenerator) {
|
|
@@ -460,17 +1222,18 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
460
1222
|
};
|
|
461
1223
|
PaymentRecoveryService = _ts_decorate3([
|
|
462
1224
|
(0, import_common3.Injectable)(),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1225
|
+
_ts_param2(1, (0, import_common3.Inject)(RECOVERY_ATTEMPT_STORE)),
|
|
1226
|
+
_ts_param2(2, (0, import_common3.Optional)()),
|
|
1227
|
+
_ts_param2(2, (0, import_common3.Inject)(RECOVERY_MAIL_SENDER)),
|
|
1228
|
+
_ts_param2(3, (0, import_common3.Optional)()),
|
|
1229
|
+
_ts_param2(3, (0, import_common3.Inject)(RECOVERY_USER_LOOKUP)),
|
|
1230
|
+
_ts_param2(4, (0, import_common3.Optional)()),
|
|
1231
|
+
_ts_param2(4, (0, import_common3.Inject)(RECOVERY_URL_GENERATOR)),
|
|
1232
|
+
_ts_param2(5, (0, import_common3.Optional)()),
|
|
1233
|
+
_ts_metadata3("design:type", Function),
|
|
1234
|
+
_ts_metadata3("design:paramtypes", [
|
|
472
1235
|
typeof OrderService === "undefined" ? Object : OrderService,
|
|
473
|
-
|
|
1236
|
+
Object,
|
|
474
1237
|
typeof RecoveryMailSender === "undefined" ? Object : RecoveryMailSender,
|
|
475
1238
|
typeof RecoveryUserLookup === "undefined" ? Object : RecoveryUserLookup,
|
|
476
1239
|
typeof RecoveryUrlGenerator === "undefined" ? Object : RecoveryUrlGenerator,
|
|
@@ -510,33 +1273,62 @@ function _ts_decorate4(decorators, target, key, desc) {
|
|
|
510
1273
|
}
|
|
511
1274
|
__name(_ts_decorate4, "_ts_decorate");
|
|
512
1275
|
var PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
|
|
1276
|
+
var PAYMENT_OPTIONS = /* @__PURE__ */ Symbol("PAYMENT_OPTIONS");
|
|
1277
|
+
function configuredProviders() {
|
|
1278
|
+
return [
|
|
1279
|
+
{
|
|
1280
|
+
provide: ORDER_PERSISTENCE,
|
|
1281
|
+
inject: [
|
|
1282
|
+
PAYMENT_OPTIONS
|
|
1283
|
+
],
|
|
1284
|
+
useFactory: /* @__PURE__ */ __name((options) => {
|
|
1285
|
+
if (!options.orderPersistence) throw new Error("Order persistence must be configured");
|
|
1286
|
+
return options.orderPersistence;
|
|
1287
|
+
}, "useFactory")
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
provide: RECOVERY_ATTEMPT_STORE,
|
|
1291
|
+
inject: [
|
|
1292
|
+
PAYMENT_OPTIONS
|
|
1293
|
+
],
|
|
1294
|
+
useFactory: /* @__PURE__ */ __name((options) => options.recoveryAttempts ?? null, "useFactory")
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
provide: PaymentProviderRegistry,
|
|
1298
|
+
inject: [
|
|
1299
|
+
PAYMENT_OPTIONS
|
|
1300
|
+
],
|
|
1301
|
+
useFactory: /* @__PURE__ */ __name((options) => new PaymentProviderRegistry(options.providers ?? []), "useFactory")
|
|
1302
|
+
},
|
|
1303
|
+
OrderService,
|
|
1304
|
+
PaymentRecoveryService,
|
|
1305
|
+
{
|
|
1306
|
+
provide: PAYMENT_PROVIDERS,
|
|
1307
|
+
inject: [
|
|
1308
|
+
PaymentProviderRegistry
|
|
1309
|
+
],
|
|
1310
|
+
useFactory: /* @__PURE__ */ __name((registry) => registry.list(), "useFactory")
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
}
|
|
1314
|
+
__name(configuredProviders, "configuredProviders");
|
|
513
1315
|
var ZuckerPaymentModule = class _ZuckerPaymentModule {
|
|
514
1316
|
static {
|
|
515
1317
|
__name(this, "ZuckerPaymentModule");
|
|
516
1318
|
}
|
|
517
|
-
static forRoot(options
|
|
1319
|
+
static forRoot(options) {
|
|
518
1320
|
return {
|
|
519
1321
|
module: _ZuckerPaymentModule,
|
|
520
1322
|
global: true,
|
|
521
1323
|
providers: [
|
|
522
|
-
PaymentService,
|
|
523
|
-
OrderService,
|
|
524
|
-
PaymentRecoveryService,
|
|
525
1324
|
{
|
|
526
|
-
provide:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
return paymentService.listProviders();
|
|
532
|
-
}, "useFactory"),
|
|
533
|
-
inject: [
|
|
534
|
-
PaymentService
|
|
535
|
-
]
|
|
536
|
-
}
|
|
1325
|
+
provide: PAYMENT_OPTIONS,
|
|
1326
|
+
useValue: options
|
|
1327
|
+
},
|
|
1328
|
+
...configuredProviders()
|
|
537
1329
|
],
|
|
538
1330
|
exports: [
|
|
539
|
-
|
|
1331
|
+
PaymentProviderRegistry,
|
|
540
1332
|
OrderService,
|
|
541
1333
|
PaymentRecoveryService
|
|
542
1334
|
]
|
|
@@ -548,27 +1340,15 @@ var ZuckerPaymentModule = class _ZuckerPaymentModule {
|
|
|
548
1340
|
global: true,
|
|
549
1341
|
imports: options.imports ?? [],
|
|
550
1342
|
providers: [
|
|
551
|
-
PaymentService,
|
|
552
|
-
OrderService,
|
|
553
|
-
PaymentRecoveryService,
|
|
554
1343
|
{
|
|
555
|
-
provide:
|
|
556
|
-
useFactory:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
paymentService.register(provider);
|
|
561
|
-
}
|
|
562
|
-
return paymentService.listProviders();
|
|
563
|
-
}, "useFactory"),
|
|
564
|
-
inject: [
|
|
565
|
-
PaymentService,
|
|
566
|
-
...options.inject ?? []
|
|
567
|
-
]
|
|
568
|
-
}
|
|
1344
|
+
provide: PAYMENT_OPTIONS,
|
|
1345
|
+
useFactory: options.useFactory,
|
|
1346
|
+
inject: options.inject ?? []
|
|
1347
|
+
},
|
|
1348
|
+
...configuredProviders()
|
|
569
1349
|
],
|
|
570
1350
|
exports: [
|
|
571
|
-
|
|
1351
|
+
PaymentProviderRegistry,
|
|
572
1352
|
OrderService,
|
|
573
1353
|
PaymentRecoveryService
|
|
574
1354
|
]
|
|
@@ -579,16 +1359,258 @@ ZuckerPaymentModule = _ts_decorate4([
|
|
|
579
1359
|
(0, import_common4.Global)(),
|
|
580
1360
|
(0, import_common4.Module)({})
|
|
581
1361
|
], ZuckerPaymentModule);
|
|
1362
|
+
|
|
1363
|
+
// src/providers/paypal.ts
|
|
1364
|
+
var import_node_module2 = require("module");
|
|
1365
|
+
var import_meta2 = {};
|
|
1366
|
+
var requireSdk2 = (0, import_node_module2.createRequire)(typeof __filename === "string" ? __filename : import_meta2.url);
|
|
1367
|
+
function observePayPalOrder(raw) {
|
|
1368
|
+
return {
|
|
1369
|
+
raw,
|
|
1370
|
+
id: raw.id,
|
|
1371
|
+
approveUrl: raw.links?.find((link) => link.rel === "approve")?.href,
|
|
1372
|
+
captureId: raw.purchaseUnits?.[0]?.payments?.captures?.[0]?.id,
|
|
1373
|
+
settlement: raw.status === "COMPLETED" ? "paid" : raw.status === "VOIDED" ? "unpaid" : "unknown"
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
__name(observePayPalOrder, "observePayPalOrder");
|
|
1377
|
+
function captureLinkId(resource) {
|
|
1378
|
+
if (!Array.isArray(resource.links)) return void 0;
|
|
1379
|
+
for (const value of resource.links) {
|
|
1380
|
+
const link = asRecord(value);
|
|
1381
|
+
if (link?.rel !== "up" || typeof link.href !== "string") continue;
|
|
1382
|
+
try {
|
|
1383
|
+
const url = new URL(link.href);
|
|
1384
|
+
if (url.protocol !== "https:" || !/^api(?:-m)?(?:\.sandbox)?\.paypal\.com$/.test(url.hostname)) continue;
|
|
1385
|
+
const match = /^\/v2\/payments\/captures\/([A-Za-z0-9_-]+)$/.exec(url.pathname);
|
|
1386
|
+
if (match) return match[1];
|
|
1387
|
+
} catch {
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return void 0;
|
|
1391
|
+
}
|
|
1392
|
+
__name(captureLinkId, "captureLinkId");
|
|
1393
|
+
function asRecord(value) {
|
|
1394
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1395
|
+
}
|
|
1396
|
+
__name(asRecord, "asRecord");
|
|
1397
|
+
function decodePayPalWebhookEvent(payload) {
|
|
1398
|
+
const raw = asRecord(JSON.parse(payload));
|
|
1399
|
+
if (!raw || typeof raw.event_type !== "string") throw new Error("Invalid PayPal webhook event");
|
|
1400
|
+
const type = raw.event_type;
|
|
1401
|
+
const kind = type === "PAYMENT.CAPTURE.COMPLETED" ? "capture-succeeded" : type === "PAYMENT.CAPTURE.DENIED" || type === "PAYMENT.CAPTURE.DECLINED" ? "capture-failed" : "unknown";
|
|
1402
|
+
if (kind === "unknown") return {
|
|
1403
|
+
raw,
|
|
1404
|
+
type,
|
|
1405
|
+
kind
|
|
1406
|
+
};
|
|
1407
|
+
const resource = asRecord(raw.resource);
|
|
1408
|
+
if (!resource || typeof resource.id !== "string" || !resource.id) {
|
|
1409
|
+
throw new Error("PayPal capture event is missing its resource ID");
|
|
1410
|
+
}
|
|
1411
|
+
const reason = asRecord(resource.status_details)?.reason;
|
|
1412
|
+
return {
|
|
1413
|
+
raw,
|
|
1414
|
+
type,
|
|
1415
|
+
kind,
|
|
1416
|
+
capture: {
|
|
1417
|
+
raw: resource,
|
|
1418
|
+
id: resource.id,
|
|
1419
|
+
failureReason: typeof reason === "string" ? reason : void 0
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
__name(decodePayPalWebhookEvent, "decodePayPalWebhookEvent");
|
|
1424
|
+
function decodePayPalMoneyReturnedEvent(payload) {
|
|
1425
|
+
const raw = asRecord(JSON.parse(payload));
|
|
1426
|
+
if (!raw || typeof raw.event_type !== "string") throw new Error("Invalid PayPal webhook event");
|
|
1427
|
+
const type = raw.event_type;
|
|
1428
|
+
if (type === "PAYMENT.CAPTURE.REFUNDED" || type === "PAYMENT.CAPTURE.REVERSED") {
|
|
1429
|
+
const resource = asRecord(raw.resource);
|
|
1430
|
+
const amount = asRecord(resource?.amount);
|
|
1431
|
+
const related = asRecord(asRecord(resource?.supplementary_data)?.related_ids);
|
|
1432
|
+
const reversal = type === "PAYMENT.CAPTURE.REVERSED";
|
|
1433
|
+
if (!resource || typeof resource.id !== "string" || !resource.id || typeof amount?.value !== "string" || !/^\d+(?:\.\d{1,2})?$/.test(amount.value) || typeof amount.currency_code !== "string" || !/^[A-Z]{3}$/.test(amount.currency_code)) {
|
|
1434
|
+
throw new Error("Invalid PayPal money-returned event");
|
|
1435
|
+
}
|
|
1436
|
+
const status = reversal ? "COMPLETED" : resource.status;
|
|
1437
|
+
if (status !== "PENDING" && status !== "COMPLETED" && status !== "FAILED" && status !== "CANCELLED") {
|
|
1438
|
+
throw new Error("Unknown PayPal refund status");
|
|
1439
|
+
}
|
|
1440
|
+
const captureId = reversal ? resource.id : typeof related?.capture_id === "string" ? related.capture_id : captureLinkId(resource);
|
|
1441
|
+
const orderId = typeof related?.order_id === "string" ? related.order_id : void 0;
|
|
1442
|
+
if (!captureId && !orderId) throw new Error("PayPal refund event has no capture or order identity");
|
|
1443
|
+
return {
|
|
1444
|
+
raw,
|
|
1445
|
+
type,
|
|
1446
|
+
kind: reversal ? "capture-reversed" : "capture-refunded",
|
|
1447
|
+
returned: {
|
|
1448
|
+
id: reversal ? `reversal:${resource.id}` : resource.id,
|
|
1449
|
+
captureId,
|
|
1450
|
+
orderId,
|
|
1451
|
+
amount: amount.value,
|
|
1452
|
+
currency: amount.currency_code,
|
|
1453
|
+
status
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
__name(decodePayPalMoneyReturnedEvent, "decodePayPalMoneyReturnedEvent");
|
|
1460
|
+
var PayPalPaymentTransport = class {
|
|
1461
|
+
static {
|
|
1462
|
+
__name(this, "PayPalPaymentTransport");
|
|
1463
|
+
}
|
|
1464
|
+
options;
|
|
1465
|
+
orders;
|
|
1466
|
+
baseUrl;
|
|
1467
|
+
fetcher;
|
|
1468
|
+
constructor(options) {
|
|
1469
|
+
this.options = options;
|
|
1470
|
+
const { Client, Environment, LogLevel, OrdersController } = requireSdk2("@paypal/paypal-server-sdk");
|
|
1471
|
+
const client = new Client({
|
|
1472
|
+
clientCredentialsAuthCredentials: {
|
|
1473
|
+
oAuthClientId: options.clientId,
|
|
1474
|
+
oAuthClientSecret: options.clientSecret
|
|
1475
|
+
},
|
|
1476
|
+
timeout: 0,
|
|
1477
|
+
environment: options.mode === "live" ? Environment.Production : Environment.Sandbox,
|
|
1478
|
+
logging: {
|
|
1479
|
+
logLevel: LogLevel.Info,
|
|
1480
|
+
logRequest: {
|
|
1481
|
+
logBody: false
|
|
1482
|
+
},
|
|
1483
|
+
logResponse: {
|
|
1484
|
+
logHeaders: false
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
this.orders = new OrdersController(client);
|
|
1489
|
+
this.baseUrl = options.mode === "live" ? "https://api.paypal.com" : "https://api.sandbox.paypal.com";
|
|
1490
|
+
this.fetcher = options.fetcher ?? fetch;
|
|
1491
|
+
}
|
|
1492
|
+
async createOrder(input2) {
|
|
1493
|
+
const { result } = await this.orders.createOrder(input2);
|
|
1494
|
+
return observePayPalOrder(result);
|
|
1495
|
+
}
|
|
1496
|
+
/** An explicit capture command; never invoked by getOrder or status observation. */
|
|
1497
|
+
async captureOrder(input2) {
|
|
1498
|
+
const { result } = await this.orders.captureOrder(input2);
|
|
1499
|
+
return observePayPalOrder(result);
|
|
1500
|
+
}
|
|
1501
|
+
async getOrder(input2) {
|
|
1502
|
+
const { result } = await this.orders.getOrder(input2);
|
|
1503
|
+
return observePayPalOrder(result);
|
|
1504
|
+
}
|
|
1505
|
+
async getAccessToken() {
|
|
1506
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/oauth2/token`, {
|
|
1507
|
+
method: "POST",
|
|
1508
|
+
headers: {
|
|
1509
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1510
|
+
Authorization: `Basic ${Buffer.from(`${this.options.clientId}:${this.options.clientSecret}`).toString("base64")}`
|
|
1511
|
+
},
|
|
1512
|
+
body: "grant_type=client_credentials"
|
|
1513
|
+
});
|
|
1514
|
+
const data = await response.json();
|
|
1515
|
+
if (!response.ok || typeof data.access_token !== "string" || !data.access_token) {
|
|
1516
|
+
throw new Error("Unable to obtain PayPal access token");
|
|
1517
|
+
}
|
|
1518
|
+
return data.access_token;
|
|
1519
|
+
}
|
|
1520
|
+
async verifyWebhookSignature(payload, headers, webhookId) {
|
|
1521
|
+
const transmissionId = headers["paypal-transmission-id"];
|
|
1522
|
+
const transmissionTime = headers["paypal-transmission-time"];
|
|
1523
|
+
const certUrl = headers["paypal-cert-url"];
|
|
1524
|
+
const authAlgo = headers["paypal-auth-algo"];
|
|
1525
|
+
const transmissionSig = headers["paypal-transmission-sig"];
|
|
1526
|
+
if (!transmissionId || !transmissionTime || !certUrl || !authAlgo || !transmissionSig) {
|
|
1527
|
+
this.options.logger?.warn("PayPal webhook is missing required signature headers");
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
try {
|
|
1531
|
+
const accessToken = await this.getAccessToken();
|
|
1532
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/notifications/verify-webhook-signature`, {
|
|
1533
|
+
method: "POST",
|
|
1534
|
+
headers: {
|
|
1535
|
+
"Content-Type": "application/json",
|
|
1536
|
+
Authorization: `Bearer ${accessToken}`
|
|
1537
|
+
},
|
|
1538
|
+
body: JSON.stringify({
|
|
1539
|
+
auth_algo: authAlgo,
|
|
1540
|
+
cert_url: certUrl,
|
|
1541
|
+
transmission_id: transmissionId,
|
|
1542
|
+
transmission_sig: transmissionSig,
|
|
1543
|
+
transmission_time: transmissionTime,
|
|
1544
|
+
webhook_id: webhookId,
|
|
1545
|
+
webhook_event: JSON.parse(payload)
|
|
1546
|
+
})
|
|
1547
|
+
});
|
|
1548
|
+
const result = await response.json();
|
|
1549
|
+
return response.ok && result.verification_status === "SUCCESS";
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
this.options.logger?.error(`PayPal webhook verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async refundCapture(captureId, request, requestId) {
|
|
1556
|
+
const accessToken = await this.getAccessToken();
|
|
1557
|
+
const response = await this.fetcher(`${this.baseUrl}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
1558
|
+
method: "POST",
|
|
1559
|
+
headers: {
|
|
1560
|
+
"Content-Type": "application/json",
|
|
1561
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1562
|
+
...requestId ? {
|
|
1563
|
+
"PayPal-Request-Id": requestId
|
|
1564
|
+
} : {}
|
|
1565
|
+
},
|
|
1566
|
+
body: JSON.stringify(request)
|
|
1567
|
+
});
|
|
1568
|
+
const result = await response.json();
|
|
1569
|
+
if (!response.ok) throw new Error(`PayPal refund request failed (${response.status})`);
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
582
1573
|
// Annotate the CommonJS export names for ESM import in node:
|
|
583
1574
|
0 && (module.exports = {
|
|
1575
|
+
DatabaseOrderPersistence,
|
|
1576
|
+
ORDER_PERSISTENCE,
|
|
1577
|
+
OrderNotFoundError,
|
|
584
1578
|
OrderService,
|
|
585
1579
|
PAYMENT_PROVIDERS,
|
|
1580
|
+
PAYPAL_SUPPORTED_CURRENCIES,
|
|
1581
|
+
PayPalPaymentTransport,
|
|
586
1582
|
PaymentEvents,
|
|
1583
|
+
PaymentProviderRegistry,
|
|
587
1584
|
PaymentRecoveryService,
|
|
588
|
-
|
|
1585
|
+
RECOVERY_ATTEMPT_STORE,
|
|
589
1586
|
RECOVERY_MAIL_SENDER,
|
|
590
1587
|
RECOVERY_URL_GENERATOR,
|
|
591
1588
|
RECOVERY_USER_LOOKUP,
|
|
1589
|
+
SETTLEMENT_STATES,
|
|
592
1590
|
ZuckerPaymentModule,
|
|
593
|
-
|
|
1591
|
+
adapterSupportsCurrency,
|
|
1592
|
+
checkPaymentAdapterContract,
|
|
1593
|
+
createFakeAdapter,
|
|
1594
|
+
createPayPalAdapter,
|
|
1595
|
+
createStripeAdapter,
|
|
1596
|
+
createStripeCheckoutSession,
|
|
1597
|
+
createStripeClient,
|
|
1598
|
+
currencyExponent,
|
|
1599
|
+
decodePayPalMoneyReturnedEvent,
|
|
1600
|
+
decodePayPalWebhookEvent,
|
|
1601
|
+
friendlyErrorReason,
|
|
1602
|
+
fromMinorUnits,
|
|
1603
|
+
isMoneyMoved,
|
|
1604
|
+
isTerminal,
|
|
1605
|
+
observePayPalOrder,
|
|
1606
|
+
observePayPalState,
|
|
1607
|
+
observeStripeCheckoutSession,
|
|
1608
|
+
observeStripePaymentIntent,
|
|
1609
|
+
observeStripeState,
|
|
1610
|
+
reconcile,
|
|
1611
|
+
retrieveStripeCheckoutSession,
|
|
1612
|
+
retrieveStripePaymentIntent,
|
|
1613
|
+
stripeResourceId,
|
|
1614
|
+
toMinorUnits,
|
|
1615
|
+
verifyStripeWebhookEvent
|
|
594
1616
|
});
|