@zucker-framework/payment 1.0.2 → 1.0.5

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.js CHANGED
@@ -26,30 +26,189 @@ __export(index_exports, {
26
26
  OrderNotFoundError: () => OrderNotFoundError,
27
27
  OrderService: () => OrderService,
28
28
  PAYMENT_PROVIDERS: () => PAYMENT_PROVIDERS,
29
+ PAYPAL_SUPPORTED_CURRENCIES: () => PAYPAL_SUPPORTED_CURRENCIES,
29
30
  PayPalPaymentTransport: () => PayPalPaymentTransport,
30
31
  PaymentEvents: () => PaymentEvents,
32
+ PaymentProviderRegistry: () => PaymentProviderRegistry,
31
33
  PaymentRecoveryService: () => PaymentRecoveryService,
32
- PaymentService: () => PaymentService,
33
34
  RECOVERY_ATTEMPT_STORE: () => RECOVERY_ATTEMPT_STORE,
34
35
  RECOVERY_MAIL_SENDER: () => RECOVERY_MAIL_SENDER,
35
36
  RECOVERY_URL_GENERATOR: () => RECOVERY_URL_GENERATOR,
36
37
  RECOVERY_USER_LOOKUP: () => RECOVERY_USER_LOOKUP,
38
+ SETTLEMENT_STATES: () => SETTLEMENT_STATES,
37
39
  ZuckerPaymentModule: () => ZuckerPaymentModule,
40
+ adapterSupportsCurrency: () => adapterSupportsCurrency,
41
+ checkPaymentAdapterContract: () => checkPaymentAdapterContract,
42
+ createFakeAdapter: () => createFakeAdapter,
43
+ createPayPalAdapter: () => createPayPalAdapter,
44
+ createStripeAdapter: () => createStripeAdapter,
38
45
  createStripeCheckoutSession: () => createStripeCheckoutSession,
39
46
  createStripeClient: () => createStripeClient,
47
+ currencyExponent: () => currencyExponent,
48
+ decodePayPalMoneyReturnedEvent: () => decodePayPalMoneyReturnedEvent,
40
49
  decodePayPalWebhookEvent: () => decodePayPalWebhookEvent,
41
50
  friendlyErrorReason: () => friendlyErrorReason,
51
+ fromMinorUnits: () => fromMinorUnits,
52
+ isMoneyMoved: () => isMoneyMoved,
53
+ isTerminal: () => isTerminal,
42
54
  observePayPalOrder: () => observePayPalOrder,
55
+ observePayPalState: () => observePayPalState,
43
56
  observeStripeCheckoutSession: () => observeStripeCheckoutSession,
44
57
  observeStripePaymentIntent: () => observeStripePaymentIntent,
58
+ observeStripeState: () => observeStripeState,
59
+ reconcile: () => reconcile,
45
60
  retrieveStripeCheckoutSession: () => retrieveStripeCheckoutSession,
46
61
  retrieveStripePaymentIntent: () => retrieveStripePaymentIntent,
47
62
  stripeResourceId: () => stripeResourceId,
63
+ toMinorUnits: () => toMinorUnits,
48
64
  verifyStripeWebhookEvent: () => verifyStripeWebhookEvent
49
65
  });
50
66
  module.exports = __toCommonJS(index_exports);
51
67
 
52
- // src/payment.service.ts
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
53
212
  var import_common = require("@nestjs/common");
54
213
  function _ts_decorate(decorators, target, key, desc) {
55
214
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -65,73 +224,586 @@ function _ts_decorate(decorators, target, key, desc) {
65
224
  return c > 3 && r && Object.defineProperty(target, key, r), r;
66
225
  }
67
226
  __name(_ts_decorate, "_ts_decorate");
68
- var PaymentService = class _PaymentService {
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 {
69
234
  static {
70
- __name(this, "PaymentService");
71
- }
72
- logger = new import_common.Logger(_PaymentService.name);
73
- providers = /* @__PURE__ */ new Map();
74
- /** 注册支付提供商 */
75
- register(provider) {
76
- if (this.providers.has(provider.id)) {
77
- this.logger.warn(`PaymentProvider "${provider.id}" \u5DF2\u6CE8\u518C\uFF0C\u5C06\u88AB\u8986\u76D6`);
78
- }
79
- this.providers.set(provider.id, provider);
80
- this.logger.log(`PaymentProvider "${provider.id}" (${provider.name}) \u5DF2\u6CE8\u518C`);
81
- }
82
- /** 注销支付提供商 */
83
- unregister(providerId) {
84
- const removed = this.providers.delete(providerId);
85
- if (removed) {
86
- this.logger.log(`PaymentProvider "${providerId}" \u5DF2\u6CE8\u9500`);
87
- }
88
- return removed;
89
- }
90
- /** 获取已注册的提供商列表 */
91
- listProviders() {
92
- return Array.from(this.providers.values());
93
- }
94
- /** 获取指定提供商 */
95
- getProvider(providerId) {
96
- const provider = this.providers.get(providerId);
97
- if (!provider) {
98
- throw new Error(`PaymentProvider "${providerId}" \u672A\u6CE8\u518C\u3002\u5DF2\u6CE8\u518C: [${Array.from(this.providers.keys()).join(", ")}]`);
99
- }
100
- return provider;
101
- }
102
- /** 创建支付意图 */
103
- async createPaymentIntent(providerId, params) {
104
- const provider = this.getProvider(providerId);
105
- this.logger.log(`[${providerId}] \u521B\u5EFA\u652F\u4ED8\u610F\u56FE: ${params.amount} ${params.currency}`);
106
- return provider.createPaymentIntent(params);
107
- }
108
- /** 确认支付 */
109
- async confirmPayment(providerId, intentId) {
110
- const provider = this.getProvider(providerId);
111
- this.logger.log(`[${providerId}] \u786E\u8BA4\u652F\u4ED8: ${intentId}`);
112
- return provider.confirmPayment(intentId);
113
- }
114
- /** 退款 */
115
- async refund(providerId, paymentId, amount, reason) {
116
- const provider = this.getProvider(providerId);
117
- this.logger.log(`[${providerId}] \u9000\u6B3E: ${paymentId}, amount=${amount ?? "full"}`);
118
- return provider.refund(paymentId, amount, reason);
119
- }
120
- /** 查询支付状态 */
121
- async getPaymentStatus(providerId, intentId) {
122
- const provider = this.getProvider(providerId);
123
- return provider.getPaymentStatus(intentId);
124
- }
125
- /** 处理 Webhook */
126
- async handleWebhook(providerId, payload, signature) {
127
- const provider = this.getProvider(providerId);
128
- this.logger.log(`[${providerId}] \u5904\u7406 Webhook`);
129
- 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));
130
269
  }
131
270
  };
132
- PaymentService = _ts_decorate([
133
- (0, import_common.Injectable)()
134
- ], PaymentService);
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");
135
807
 
136
808
  // src/order.service.ts
137
809
  var import_common2 = require("@nestjs/common");
@@ -191,12 +863,12 @@ function _ts_decorate2(decorators, target, key, desc) {
191
863
  return c > 3 && r && Object.defineProperty(target, key, r), r;
192
864
  }
193
865
  __name(_ts_decorate2, "_ts_decorate");
194
- function _ts_metadata(metadataKey, metadataValue) {
866
+ function _ts_metadata2(metadataKey, metadataValue) {
195
867
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
196
868
  return Reflect.metadata(metadataKey, metadataValue);
197
869
  }
198
870
  }
199
- __name(_ts_metadata, "_ts_metadata");
871
+ __name(_ts_metadata2, "_ts_metadata");
200
872
  function _ts_param(paramIndex, decorator) {
201
873
  return function(target, key) {
202
874
  decorator(target, key, paramIndex);
@@ -217,10 +889,10 @@ var OrderService = class {
217
889
  static {
218
890
  __name(this, "OrderService");
219
891
  }
220
- paymentService;
892
+ providers;
221
893
  orders;
222
- constructor(paymentService, orders) {
223
- this.paymentService = paymentService;
894
+ constructor(providers, orders) {
895
+ this.providers = providers;
224
896
  this.orders = orders;
225
897
  if (!orders) throw new Error("Order persistence must be configured");
226
898
  }
@@ -237,8 +909,8 @@ var OrderService = class {
237
909
  data: {
238
910
  id: params.orderId,
239
911
  providerId: params.providerId,
240
- amount: params.payment.amount,
241
- currency: params.payment.currency,
912
+ amount: params.payment.money.amount,
913
+ currency: params.payment.money.currency,
242
914
  metadata: params.payment.metadata,
243
915
  status: "created",
244
916
  createdAt: now,
@@ -248,68 +920,99 @@ var OrderService = class {
248
920
  return this.initiatePayment(order, params.payment);
249
921
  }
250
922
  async initiatePayment(order, payment) {
251
- const intent = await this.paymentService.createPaymentIntent(order.providerId, payment);
923
+ const session = await this.providers.get(order.providerId).createPayment(payment);
252
924
  await this.transition(order.id, [
253
925
  "created"
254
926
  ], "paying", {
255
- paymentIntentId: intent.id
927
+ paymentIntentId: session.providerRef
256
928
  });
257
929
  return this.getOrder(order.id);
258
930
  }
259
- async confirmOrder(orderId) {
931
+ /**
932
+ * 扣款。幂等键由调用方给出——同一笔订单重试必须复用同一个键,否则渠道会认为是新支付。
933
+ * 判定不了的结果保持 confirming,交由只读对账解决,绝不在这里重复扣款。
934
+ */
935
+ async confirmOrder(orderId, idempotencyKey) {
260
936
  const order = await this.getOrder(orderId);
261
937
  if (!order.paymentIntentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentIntentId`);
262
938
  await this.transition(orderId, [
263
939
  "paying"
264
940
  ], "confirming");
265
- const result = await this.paymentService.confirmPayment(order.providerId, order.paymentIntentId);
266
- if (result.status === "succeeded") {
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") {
267
955
  await this.transition(orderId, [
268
- "confirming"
956
+ from
269
957
  ], "paid", {
270
- paymentId: result.id,
271
- paidAt: result.paidAt ?? /* @__PURE__ */ new Date()
958
+ paymentId: paymentRef,
959
+ paidAt: /* @__PURE__ */ new Date()
272
960
  });
273
- } else if (result.status === "failed" || result.status === "cancelled") {
961
+ } else if (decision.next === "failed" || decision.next === "canceled") {
274
962
  await this.transition(orderId, [
275
- "confirming"
963
+ from
276
964
  ], "failed");
277
- } else if (result.status === "refunded") {
965
+ } else if (decision.next === "refunded" || decision.next === "partially_refunded") {
278
966
  await this.transition(orderId, [
279
- "confirming"
280
- ], "refunded");
967
+ from
968
+ ], "refunded", {
969
+ refundedAt: /* @__PURE__ */ new Date()
970
+ });
281
971
  }
282
- return result;
283
972
  }
284
- async refundOrder(orderId, amount, reason) {
973
+ async refundOrder(orderId, idempotencyKey, options = {}) {
285
974
  const order = await this.getOrder(orderId);
286
975
  if (!order.paymentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentId`);
287
976
  await this.transition(orderId, [
288
977
  "paid"
289
978
  ], "refunding");
290
- const result = await this.paymentService.refund(order.providerId, order.paymentId, amount, reason);
291
- if (result.status === "succeeded") {
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") {
292
986
  await this.transition(orderId, [
293
987
  "refunding"
294
988
  ], "refunded", {
295
989
  refundedAt: /* @__PURE__ */ new Date()
296
990
  });
297
- } else if (result.status === "failed") {
991
+ } else if (result.state === "failed") {
298
992
  await this.transition(orderId, [
299
993
  "refunding"
300
994
  ], "paid");
301
995
  }
302
996
  return result;
303
997
  }
304
- async retryOrder(orderId) {
998
+ /**
999
+ * 重试失败订单。
1000
+ * idempotencyKey 必须与上一次不同:沿用旧键会让渠道原样返回那笔已失败的支付,
1001
+ * 看起来"重试了"其实什么都没发生。
1002
+ */
1003
+ async retryOrder(orderId, idempotencyKey) {
305
1004
  const order = await this.getOrder(orderId);
306
1005
  await this.transition(orderId, [
307
1006
  "failed"
308
1007
  ], "created");
309
1008
  return this.initiatePayment(order, {
310
- amount: order.amount,
311
- currency: order.currency,
312
- metadata: order.metadata
1009
+ reference: order.id,
1010
+ money: {
1011
+ amount: order.amount,
1012
+ currency: order.currency
1013
+ },
1014
+ metadata: order.metadata,
1015
+ idempotencyKey
313
1016
  });
314
1017
  }
315
1018
  async cancelOrder(orderId) {
@@ -348,9 +1051,9 @@ var OrderService = class {
348
1051
  OrderService = _ts_decorate2([
349
1052
  (0, import_common2.Injectable)(),
350
1053
  _ts_param(1, (0, import_common2.Inject)(ORDER_PERSISTENCE)),
351
- _ts_metadata("design:type", Function),
352
- _ts_metadata("design:paramtypes", [
353
- typeof PaymentService === "undefined" ? Object : PaymentService,
1054
+ _ts_metadata2("design:type", Function),
1055
+ _ts_metadata2("design:paramtypes", [
1056
+ typeof PaymentProviderRegistry === "undefined" ? Object : PaymentProviderRegistry,
354
1057
  typeof OrderPersistence === "undefined" ? Object : OrderPersistence
355
1058
  ])
356
1059
  ], OrderService);
@@ -384,12 +1087,12 @@ function _ts_decorate3(decorators, target, key, desc) {
384
1087
  return c > 3 && r && Object.defineProperty(target, key, r), r;
385
1088
  }
386
1089
  __name(_ts_decorate3, "_ts_decorate");
387
- function _ts_metadata2(metadataKey, metadataValue) {
1090
+ function _ts_metadata3(metadataKey, metadataValue) {
388
1091
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
389
1092
  return Reflect.metadata(metadataKey, metadataValue);
390
1093
  }
391
1094
  }
392
- __name(_ts_metadata2, "_ts_metadata");
1095
+ __name(_ts_metadata3, "_ts_metadata");
393
1096
  function _ts_param2(paramIndex, decorator) {
394
1097
  return function(target, key) {
395
1098
  decorator(target, key, paramIndex);
@@ -500,10 +1203,11 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
500
1203
  }
501
1204
  }
502
1205
  /**
503
- * 重试失败订单 — 将 failed → created 并重新发起支付
1206
+ * 重试失败订单 — 将 failed → created 并重新发起支付。
1207
+ * 幂等键由调用方给出且必须与上次不同,否则渠道会原样返回那笔已失败的支付。
504
1208
  */
505
- retryOrder(orderId) {
506
- return this.orderService.retryOrder(orderId);
1209
+ retryOrder(orderId, idempotencyKey) {
1210
+ return this.orderService.retryOrder(orderId, idempotencyKey);
507
1211
  }
508
1212
  getRecoveryAttempts(orderId) {
509
1213
  if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
@@ -526,8 +1230,8 @@ PaymentRecoveryService = _ts_decorate3([
526
1230
  _ts_param2(4, (0, import_common3.Optional)()),
527
1231
  _ts_param2(4, (0, import_common3.Inject)(RECOVERY_URL_GENERATOR)),
528
1232
  _ts_param2(5, (0, import_common3.Optional)()),
529
- _ts_metadata2("design:type", Function),
530
- _ts_metadata2("design:paramtypes", [
1233
+ _ts_metadata3("design:type", Function),
1234
+ _ts_metadata3("design:paramtypes", [
531
1235
  typeof OrderService === "undefined" ? Object : OrderService,
532
1236
  Object,
533
1237
  typeof RecoveryMailSender === "undefined" ? Object : RecoveryMailSender,
@@ -590,24 +1294,20 @@ function configuredProviders() {
590
1294
  useFactory: /* @__PURE__ */ __name((options) => options.recoveryAttempts ?? null, "useFactory")
591
1295
  },
592
1296
  {
593
- provide: PaymentService,
1297
+ provide: PaymentProviderRegistry,
594
1298
  inject: [
595
1299
  PAYMENT_OPTIONS
596
1300
  ],
597
- useFactory: /* @__PURE__ */ __name((options) => {
598
- const service = new PaymentService();
599
- for (const provider of options.providers ?? []) service.register(provider);
600
- return service;
601
- }, "useFactory")
1301
+ useFactory: /* @__PURE__ */ __name((options) => new PaymentProviderRegistry(options.providers ?? []), "useFactory")
602
1302
  },
603
1303
  OrderService,
604
1304
  PaymentRecoveryService,
605
1305
  {
606
1306
  provide: PAYMENT_PROVIDERS,
607
1307
  inject: [
608
- PaymentService
1308
+ PaymentProviderRegistry
609
1309
  ],
610
- useFactory: /* @__PURE__ */ __name((service) => service.listProviders(), "useFactory")
1310
+ useFactory: /* @__PURE__ */ __name((registry) => registry.list(), "useFactory")
611
1311
  }
612
1312
  ];
613
1313
  }
@@ -628,7 +1328,7 @@ var ZuckerPaymentModule = class _ZuckerPaymentModule {
628
1328
  ...configuredProviders()
629
1329
  ],
630
1330
  exports: [
631
- PaymentService,
1331
+ PaymentProviderRegistry,
632
1332
  OrderService,
633
1333
  PaymentRecoveryService
634
1334
  ]
@@ -648,7 +1348,7 @@ var ZuckerPaymentModule = class _ZuckerPaymentModule {
648
1348
  ...configuredProviders()
649
1349
  ],
650
1350
  exports: [
651
- PaymentService,
1351
+ PaymentProviderRegistry,
652
1352
  OrderService,
653
1353
  PaymentRecoveryService
654
1354
  ]
@@ -660,62 +1360,6 @@ ZuckerPaymentModule = _ts_decorate4([
660
1360
  (0, import_common4.Module)({})
661
1361
  ], ZuckerPaymentModule);
662
1362
 
663
- // src/providers/stripe.ts
664
- var import_node_module = require("module");
665
- var import_meta = {};
666
- var requireSdk = (0, import_node_module.createRequire)(typeof __filename === "string" ? __filename : import_meta.url);
667
- function createStripeClient(secretKey, options) {
668
- const StripeSdk = requireSdk("stripe");
669
- return new StripeSdk(secretKey, options);
670
- }
671
- __name(createStripeClient, "createStripeClient");
672
- function verifyStripeWebhookEvent(client, payload, signature, webhookSecret) {
673
- return client.webhooks.constructEvent(payload, signature, webhookSecret);
674
- }
675
- __name(verifyStripeWebhookEvent, "verifyStripeWebhookEvent");
676
- function stripeResourceId(value) {
677
- return typeof value === "string" ? value : value?.id;
678
- }
679
- __name(stripeResourceId, "stripeResourceId");
680
- function observeStripeCheckoutSession(raw) {
681
- return {
682
- raw,
683
- id: raw.id,
684
- url: raw.url || void 0,
685
- clientReferenceId: raw.client_reference_id || void 0,
686
- paymentIntentId: stripeResourceId(raw.payment_intent),
687
- subscriptionId: stripeResourceId(raw.subscription),
688
- settlement: raw.payment_status === "paid" ? "paid" : raw.status === "expired" ? "unpaid" : "unknown"
689
- };
690
- }
691
- __name(observeStripeCheckoutSession, "observeStripeCheckoutSession");
692
- async function createStripeCheckoutSession(client, params) {
693
- return observeStripeCheckoutSession(await client.checkout.sessions.create(params));
694
- }
695
- __name(createStripeCheckoutSession, "createStripeCheckoutSession");
696
- async function retrieveStripeCheckoutSession(client, id) {
697
- return observeStripeCheckoutSession(await client.checkout.sessions.retrieve(id));
698
- }
699
- __name(retrieveStripeCheckoutSession, "retrieveStripeCheckoutSession");
700
- function observeStripePaymentIntent(raw) {
701
- const charge = typeof raw.latest_charge === "object" ? raw.latest_charge : null;
702
- return {
703
- raw,
704
- id: raw.id,
705
- chargeId: stripeResourceId(raw.latest_charge) ?? null,
706
- paymentMethodType: charge?.payment_method_details?.type,
707
- failureCode: raw.last_payment_error?.code,
708
- failureMessage: raw.last_payment_error?.message,
709
- settlement: raw.status === "succeeded" ? "paid" : raw.status === "canceled" ? "unpaid" : "unknown"
710
- };
711
- }
712
- __name(observeStripePaymentIntent, "observeStripePaymentIntent");
713
- async function retrieveStripePaymentIntent(client, id, params) {
714
- const raw = params ? await client.paymentIntents.retrieve(id, params) : await client.paymentIntents.retrieve(id);
715
- return observeStripePaymentIntent(raw);
716
- }
717
- __name(retrieveStripePaymentIntent, "retrieveStripePaymentIntent");
718
-
719
1363
  // src/providers/paypal.ts
720
1364
  var import_node_module2 = require("module");
721
1365
  var import_meta2 = {};
@@ -730,6 +1374,22 @@ function observePayPalOrder(raw) {
730
1374
  };
731
1375
  }
732
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");
733
1393
  function asRecord(value) {
734
1394
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
735
1395
  }
@@ -761,6 +1421,42 @@ function decodePayPalWebhookEvent(payload) {
761
1421
  };
762
1422
  }
763
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");
764
1460
  var PayPalPaymentTransport = class {
765
1461
  static {
766
1462
  __name(this, "PayPalPaymentTransport");
@@ -793,17 +1489,17 @@ var PayPalPaymentTransport = class {
793
1489
  this.baseUrl = options.mode === "live" ? "https://api.paypal.com" : "https://api.sandbox.paypal.com";
794
1490
  this.fetcher = options.fetcher ?? fetch;
795
1491
  }
796
- async createOrder(input) {
797
- const { result } = await this.orders.createOrder(input);
1492
+ async createOrder(input2) {
1493
+ const { result } = await this.orders.createOrder(input2);
798
1494
  return observePayPalOrder(result);
799
1495
  }
800
1496
  /** An explicit capture command; never invoked by getOrder or status observation. */
801
- async captureOrder(input) {
802
- const { result } = await this.orders.captureOrder(input);
1497
+ async captureOrder(input2) {
1498
+ const { result } = await this.orders.captureOrder(input2);
803
1499
  return observePayPalOrder(result);
804
1500
  }
805
- async getOrder(input) {
806
- const { result } = await this.orders.getOrder(input);
1501
+ async getOrder(input2) {
1502
+ const { result } = await this.orders.getOrder(input2);
807
1503
  return observePayPalOrder(result);
808
1504
  }
809
1505
  async getAccessToken() {
@@ -856,18 +1552,21 @@ var PayPalPaymentTransport = class {
856
1552
  return false;
857
1553
  }
858
1554
  }
859
- async refundCapture(captureId, request) {
1555
+ async refundCapture(captureId, request, requestId) {
860
1556
  const accessToken = await this.getAccessToken();
861
1557
  const response = await this.fetcher(`${this.baseUrl}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
862
1558
  method: "POST",
863
1559
  headers: {
864
1560
  "Content-Type": "application/json",
865
- Authorization: `Bearer ${accessToken}`
1561
+ Authorization: `Bearer ${accessToken}`,
1562
+ ...requestId ? {
1563
+ "PayPal-Request-Id": requestId
1564
+ } : {}
866
1565
  },
867
1566
  body: JSON.stringify(request)
868
1567
  });
869
1568
  const result = await response.json();
870
- if (!response.ok && result.status === "COMPLETED") throw new Error(`PayPal refund request failed (${response.status})`);
1569
+ if (!response.ok) throw new Error(`PayPal refund request failed (${response.status})`);
871
1570
  return result;
872
1571
  }
873
1572
  };
@@ -878,24 +1577,40 @@ var PayPalPaymentTransport = class {
878
1577
  OrderNotFoundError,
879
1578
  OrderService,
880
1579
  PAYMENT_PROVIDERS,
1580
+ PAYPAL_SUPPORTED_CURRENCIES,
881
1581
  PayPalPaymentTransport,
882
1582
  PaymentEvents,
1583
+ PaymentProviderRegistry,
883
1584
  PaymentRecoveryService,
884
- PaymentService,
885
1585
  RECOVERY_ATTEMPT_STORE,
886
1586
  RECOVERY_MAIL_SENDER,
887
1587
  RECOVERY_URL_GENERATOR,
888
1588
  RECOVERY_USER_LOOKUP,
1589
+ SETTLEMENT_STATES,
889
1590
  ZuckerPaymentModule,
1591
+ adapterSupportsCurrency,
1592
+ checkPaymentAdapterContract,
1593
+ createFakeAdapter,
1594
+ createPayPalAdapter,
1595
+ createStripeAdapter,
890
1596
  createStripeCheckoutSession,
891
1597
  createStripeClient,
1598
+ currencyExponent,
1599
+ decodePayPalMoneyReturnedEvent,
892
1600
  decodePayPalWebhookEvent,
893
1601
  friendlyErrorReason,
1602
+ fromMinorUnits,
1603
+ isMoneyMoved,
1604
+ isTerminal,
894
1605
  observePayPalOrder,
1606
+ observePayPalState,
895
1607
  observeStripeCheckoutSession,
896
1608
  observeStripePaymentIntent,
1609
+ observeStripeState,
1610
+ reconcile,
897
1611
  retrieveStripeCheckoutSession,
898
1612
  retrieveStripePaymentIntent,
899
1613
  stripeResourceId,
1614
+ toMinorUnits,
900
1615
  verifyStripeWebhookEvent
901
1616
  });