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