@zucker-framework/payment 1.0.0 → 1.0.2

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
@@ -86,7 +86,49 @@ PaymentService = _ts_decorate([
86
86
  ], PaymentService);
87
87
 
88
88
  // src/order.service.ts
89
- import { Injectable as Injectable2, Logger as Logger2 } from "@nestjs/common";
89
+ import { Injectable as Injectable2, Inject } from "@nestjs/common";
90
+
91
+ // src/order-persistence.ts
92
+ var ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
93
+ var DatabaseOrderPersistence = class {
94
+ static {
95
+ __name(this, "DatabaseOrderPersistence");
96
+ }
97
+ model;
98
+ constructor(model) {
99
+ this.model = model;
100
+ }
101
+ create(args) {
102
+ return this.model.create(args);
103
+ }
104
+ findUnique(args) {
105
+ return this.model.findUnique(args);
106
+ }
107
+ findMany(args) {
108
+ return this.model.findMany(args);
109
+ }
110
+ update(args) {
111
+ return this.model.update(args);
112
+ }
113
+ updateMany(args) {
114
+ return this.model.updateMany(args);
115
+ }
116
+ count(args) {
117
+ return this.model.count(args);
118
+ }
119
+ async compareAndSet(id, expected, data) {
120
+ const result = await this.model.updateMany({
121
+ where: {
122
+ ...expected,
123
+ id
124
+ },
125
+ data
126
+ });
127
+ return result.count === 1;
128
+ }
129
+ };
130
+
131
+ // src/order.service.ts
90
132
  function _ts_decorate2(decorators, target, key, desc) {
91
133
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
92
134
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -107,137 +149,166 @@ function _ts_metadata(metadataKey, metadataValue) {
107
149
  }
108
150
  }
109
151
  __name(_ts_metadata, "_ts_metadata");
110
- var OrderService = class _OrderService {
152
+ function _ts_param(paramIndex, decorator) {
153
+ return function(target, key) {
154
+ decorator(target, key, paramIndex);
155
+ };
156
+ }
157
+ __name(_ts_param, "_ts_param");
158
+ var OrderNotFoundError = class extends Error {
159
+ static {
160
+ __name(this, "OrderNotFoundError");
161
+ }
162
+ orderId;
163
+ constructor(orderId) {
164
+ super(`\u8BA2\u5355 "${orderId}" \u4E0D\u5B58\u5728`), this.orderId = orderId;
165
+ this.name = "OrderNotFoundError";
166
+ }
167
+ };
168
+ var OrderService = class {
111
169
  static {
112
170
  __name(this, "OrderService");
113
171
  }
114
172
  paymentService;
115
- logger = new Logger2(_OrderService.name);
116
- orders = /* @__PURE__ */ new Map();
117
- constructor(paymentService) {
173
+ orders;
174
+ constructor(paymentService, orders) {
118
175
  this.paymentService = paymentService;
176
+ this.orders = orders;
177
+ if (!orders) throw new Error("Order persistence must be configured");
119
178
  }
120
- /** 创建订单并发起支付 */
121
179
  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`);
180
+ if (await this.orders.findUnique({
181
+ where: {
182
+ id: params.orderId
183
+ }
184
+ })) {
185
+ throw new Error(`\u8BA2\u5355 "${params.orderId}" \u5DF2\u5B58\u5728`);
125
186
  }
126
187
  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;
150
- }
151
- return {
152
- ...order
153
- };
188
+ const order = await this.orders.create({
189
+ data: {
190
+ id: params.orderId,
191
+ providerId: params.providerId,
192
+ amount: params.payment.amount,
193
+ currency: params.payment.currency,
194
+ metadata: params.payment.metadata,
195
+ status: "created",
196
+ createdAt: now,
197
+ updatedAt: now
198
+ }
199
+ });
200
+ return this.initiatePayment(order, params.payment);
201
+ }
202
+ async initiatePayment(order, payment) {
203
+ const intent = await this.paymentService.createPaymentIntent(order.providerId, payment);
204
+ await this.transition(order.id, [
205
+ "created"
206
+ ], "paying", {
207
+ paymentIntentId: intent.id
208
+ });
209
+ return this.getOrder(order.id);
154
210
  }
155
- /** 确认订单支付 */
156
211
  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
- }
212
+ const order = await this.getOrder(orderId);
213
+ if (!order.paymentIntentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentIntentId`);
214
+ await this.transition(orderId, [
215
+ "paying"
216
+ ], "confirming");
164
217
  const result = await this.paymentService.confirmPayment(order.providerId, order.paymentIntentId);
165
218
  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";
219
+ await this.transition(orderId, [
220
+ "confirming"
221
+ ], "paid", {
222
+ paymentId: result.id,
223
+ paidAt: result.paidAt ?? /* @__PURE__ */ new Date()
224
+ });
225
+ } else if (result.status === "failed" || result.status === "cancelled") {
226
+ await this.transition(orderId, [
227
+ "confirming"
228
+ ], "failed");
229
+ } else if (result.status === "refunded") {
230
+ await this.transition(orderId, [
231
+ "confirming"
232
+ ], "refunded");
171
233
  }
172
- order.updatedAt = /* @__PURE__ */ new Date();
173
- this.logger.log(`\u8BA2\u5355 "${orderId}" \u652F\u4ED8\u7ED3\u679C: ${result.status}`);
174
234
  return result;
175
235
  }
176
- /** 订单退款 */
177
236
  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();
237
+ const order = await this.getOrder(orderId);
238
+ if (!order.paymentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentId`);
239
+ await this.transition(orderId, [
240
+ "paid"
241
+ ], "refunding");
187
242
  const result = await this.paymentService.refund(order.providerId, order.paymentId, amount, reason);
188
243
  if (result.status === "succeeded") {
189
- order.status = "refunded";
190
- order.refundedAt = /* @__PURE__ */ new Date();
244
+ await this.transition(orderId, [
245
+ "refunding"
246
+ ], "refunded", {
247
+ refundedAt: /* @__PURE__ */ new Date()
248
+ });
191
249
  } else if (result.status === "failed") {
192
- order.status = "paid";
250
+ await this.transition(orderId, [
251
+ "refunding"
252
+ ], "paid");
193
253
  }
194
- order.updatedAt = /* @__PURE__ */ new Date();
195
- this.logger.log(`\u8BA2\u5355 "${orderId}" \u9000\u6B3E\u7ED3\u679C: ${result.status}`);
196
254
  return result;
197
255
  }
198
- /** 取消订单 */
199
- cancelOrder(orderId) {
200
- const order = this.getOrder(orderId);
201
- const cancellableStates = [
256
+ async retryOrder(orderId) {
257
+ const order = await this.getOrder(orderId);
258
+ await this.transition(orderId, [
259
+ "failed"
260
+ ], "created");
261
+ return this.initiatePayment(order, {
262
+ amount: order.amount,
263
+ currency: order.currency,
264
+ metadata: order.metadata
265
+ });
266
+ }
267
+ async cancelOrder(orderId) {
268
+ await this.transition(orderId, [
202
269
  "created",
203
270
  "paying",
204
271
  "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
- };
272
+ ], "cancelled");
273
+ return this.getOrder(orderId);
215
274
  }
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
- }
275
+ async getOrder(orderId) {
276
+ const order = await this.orders.findUnique({
277
+ where: {
278
+ id: orderId
279
+ }
280
+ });
281
+ if (!order) throw new OrderNotFoundError(orderId);
222
282
  return order;
223
283
  }
224
- /** 列出所有订单 */
225
284
  listOrders() {
226
- return Array.from(this.orders.values()).map((o) => ({
227
- ...o
228
- }));
285
+ return this.orders.findMany();
286
+ }
287
+ async transition(id, from, to, data = {}) {
288
+ const changed = await this.orders.compareAndSet(id, {
289
+ status: {
290
+ in: from
291
+ }
292
+ }, {
293
+ ...data,
294
+ status: to,
295
+ updatedAt: /* @__PURE__ */ new Date()
296
+ });
297
+ 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
298
  }
230
299
  };
231
300
  OrderService = _ts_decorate2([
232
301
  Injectable2(),
302
+ _ts_param(1, Inject(ORDER_PERSISTENCE)),
233
303
  _ts_metadata("design:type", Function),
234
304
  _ts_metadata("design:paramtypes", [
235
- typeof PaymentService === "undefined" ? Object : PaymentService
305
+ typeof PaymentService === "undefined" ? Object : PaymentService,
306
+ typeof OrderPersistence === "undefined" ? Object : OrderPersistence
236
307
  ])
237
308
  ], OrderService);
238
309
 
239
310
  // src/recovery.service.ts
240
- import { Injectable as Injectable3, Logger as Logger3, Optional, Inject } from "@nestjs/common";
311
+ import { Injectable as Injectable3, Logger as Logger2, Optional, Inject as Inject2 } from "@nestjs/common";
241
312
  import { EventEmitter2 } from "@nestjs/event-emitter";
242
313
 
243
314
  // src/payment.events.ts
@@ -271,15 +342,16 @@ function _ts_metadata2(metadataKey, metadataValue) {
271
342
  }
272
343
  }
273
344
  __name(_ts_metadata2, "_ts_metadata");
274
- function _ts_param(paramIndex, decorator) {
345
+ function _ts_param2(paramIndex, decorator) {
275
346
  return function(target, key) {
276
347
  decorator(target, key, paramIndex);
277
348
  };
278
349
  }
279
- __name(_ts_param, "_ts_param");
350
+ __name(_ts_param2, "_ts_param");
280
351
  var RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
281
352
  var RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
282
353
  var RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
354
+ var RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
283
355
  var DEFAULT_OPTIONS = {
284
356
  maxAttempts: 3,
285
357
  retryLinkExpireHours: 48,
@@ -290,18 +362,16 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
290
362
  __name(this, "PaymentRecoveryService");
291
363
  }
292
364
  orderService;
293
- paymentService;
365
+ attempts;
294
366
  mailSender;
295
367
  userLookup;
296
368
  urlGenerator;
297
369
  eventEmitter;
298
- logger = new Logger3(_PaymentRecoveryService.name);
370
+ logger = new Logger2(_PaymentRecoveryService.name);
299
371
  options;
300
- /** 内存挽回计数(生产环境建议由业务项目持久化) */
301
- recoveryAttempts = /* @__PURE__ */ new Map();
302
- constructor(orderService, paymentService, mailSender, userLookup, urlGenerator, eventEmitter) {
372
+ constructor(orderService, attempts, mailSender, userLookup, urlGenerator, eventEmitter) {
303
373
  this.orderService = orderService;
304
- this.paymentService = paymentService;
374
+ this.attempts = attempts;
305
375
  this.mailSender = mailSender;
306
376
  this.userLookup = userLookup;
307
377
  this.urlGenerator = urlGenerator;
@@ -320,8 +390,9 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
320
390
  async handleFailedPayment(orderId, errorCode, errorMessage) {
321
391
  let order;
322
392
  try {
323
- order = this.orderService.getOrder(orderId);
324
- } catch {
393
+ order = await this.orderService.getOrder(orderId);
394
+ } catch (error) {
395
+ if (!(error instanceof OrderNotFoundError)) throw error;
325
396
  this.logger.warn(`\u8BA2\u5355 ${orderId} \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
326
397
  return;
327
398
  }
@@ -329,11 +400,11 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
329
400
  this.logger.warn(`\u8BA2\u5355 ${orderId} \u72B6\u6001\u4E3A ${order.status}\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
330
401
  return;
331
402
  }
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`);
403
+ if (!this.mailSender) {
404
+ this.logger.warn("Recovery mail sender is not configured; no attempt was reserved");
335
405
  return;
336
406
  }
407
+ if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
337
408
  const userId = order.metadata?.userId;
338
409
  let email;
339
410
  let userName = "";
@@ -354,32 +425,20 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
354
425
  this.logger.warn(`\u8BA2\u5355 ${orderId} \u65E0\u6CD5\u751F\u6210\u91CD\u8BD5\u94FE\u63A5`);
355
426
  return;
356
427
  }
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()
428
+ const reservation = await this.attempts.claim(orderId, this.options.maxAttempts, /* @__PURE__ */ new Date());
429
+ if (!reservation) return;
430
+ const attemptNumber = reservation.attemptNumber;
431
+ await this.mailSender.sendRecoveryEmail({
432
+ to: email,
433
+ userName: userName || email.split("@")[0],
434
+ orderId: order.id,
435
+ orderNumber: order.metadata?.orderNumber,
436
+ amount: order.amount,
437
+ currency: order.currency,
438
+ errorReason: friendlyErrorReason(errorCode, errorMessage),
439
+ retryUrl,
440
+ expireHours: this.options.retryLinkExpireHours,
441
+ idempotencyKey: `payment-recovery:${orderId}:${attemptNumber}`
383
442
  });
384
443
  if (this.eventEmitter) {
385
444
  const event = {
@@ -395,28 +454,12 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
395
454
  /**
396
455
  * 重试失败订单 — 将 failed → created 并重新发起支付
397
456
  */
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
- };
457
+ retryOrder(orderId) {
458
+ return this.orderService.retryOrder(orderId);
416
459
  }
417
- /** 获取订单挽回记录 */
418
460
  getRecoveryAttempts(orderId) {
419
- return this.recoveryAttempts.get(orderId);
461
+ if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
462
+ return this.attempts.get(orderId);
420
463
  }
421
464
  async generateRetryUrl(order) {
422
465
  if (this.urlGenerator) {
@@ -427,17 +470,18 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
427
470
  };
428
471
  PaymentRecoveryService = _ts_decorate3([
429
472
  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()),
473
+ _ts_param2(1, Inject2(RECOVERY_ATTEMPT_STORE)),
474
+ _ts_param2(2, Optional()),
475
+ _ts_param2(2, Inject2(RECOVERY_MAIL_SENDER)),
476
+ _ts_param2(3, Optional()),
477
+ _ts_param2(3, Inject2(RECOVERY_USER_LOOKUP)),
478
+ _ts_param2(4, Optional()),
479
+ _ts_param2(4, Inject2(RECOVERY_URL_GENERATOR)),
480
+ _ts_param2(5, Optional()),
437
481
  _ts_metadata2("design:type", Function),
438
482
  _ts_metadata2("design:paramtypes", [
439
483
  typeof OrderService === "undefined" ? Object : OrderService,
440
- typeof PaymentService === "undefined" ? Object : PaymentService,
484
+ Object,
441
485
  typeof RecoveryMailSender === "undefined" ? Object : RecoveryMailSender,
442
486
  typeof RecoveryUserLookup === "undefined" ? Object : RecoveryUserLookup,
443
487
  typeof RecoveryUrlGenerator === "undefined" ? Object : RecoveryUrlGenerator,
@@ -477,30 +521,63 @@ function _ts_decorate4(decorators, target, key, desc) {
477
521
  }
478
522
  __name(_ts_decorate4, "_ts_decorate");
479
523
  var PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
524
+ var PAYMENT_OPTIONS = /* @__PURE__ */ Symbol("PAYMENT_OPTIONS");
525
+ function configuredProviders() {
526
+ return [
527
+ {
528
+ provide: ORDER_PERSISTENCE,
529
+ inject: [
530
+ PAYMENT_OPTIONS
531
+ ],
532
+ useFactory: /* @__PURE__ */ __name((options) => {
533
+ if (!options.orderPersistence) throw new Error("Order persistence must be configured");
534
+ return options.orderPersistence;
535
+ }, "useFactory")
536
+ },
537
+ {
538
+ provide: RECOVERY_ATTEMPT_STORE,
539
+ inject: [
540
+ PAYMENT_OPTIONS
541
+ ],
542
+ useFactory: /* @__PURE__ */ __name((options) => options.recoveryAttempts ?? null, "useFactory")
543
+ },
544
+ {
545
+ provide: PaymentService,
546
+ inject: [
547
+ PAYMENT_OPTIONS
548
+ ],
549
+ useFactory: /* @__PURE__ */ __name((options) => {
550
+ const service = new PaymentService();
551
+ for (const provider of options.providers ?? []) service.register(provider);
552
+ return service;
553
+ }, "useFactory")
554
+ },
555
+ OrderService,
556
+ PaymentRecoveryService,
557
+ {
558
+ provide: PAYMENT_PROVIDERS,
559
+ inject: [
560
+ PaymentService
561
+ ],
562
+ useFactory: /* @__PURE__ */ __name((service) => service.listProviders(), "useFactory")
563
+ }
564
+ ];
565
+ }
566
+ __name(configuredProviders, "configuredProviders");
480
567
  var ZuckerPaymentModule = class _ZuckerPaymentModule {
481
568
  static {
482
569
  __name(this, "ZuckerPaymentModule");
483
570
  }
484
- static forRoot(options = {}) {
571
+ static forRoot(options) {
485
572
  return {
486
573
  module: _ZuckerPaymentModule,
487
574
  global: true,
488
575
  providers: [
489
- PaymentService,
490
- OrderService,
491
- PaymentRecoveryService,
492
576
  {
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
- }
577
+ provide: PAYMENT_OPTIONS,
578
+ useValue: options
579
+ },
580
+ ...configuredProviders()
504
581
  ],
505
582
  exports: [
506
583
  PaymentService,
@@ -515,24 +592,12 @@ var ZuckerPaymentModule = class _ZuckerPaymentModule {
515
592
  global: true,
516
593
  imports: options.imports ?? [],
517
594
  providers: [
518
- PaymentService,
519
- OrderService,
520
- PaymentRecoveryService,
521
595
  {
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
- }
596
+ provide: PAYMENT_OPTIONS,
597
+ useFactory: options.useFactory,
598
+ inject: options.inject ?? []
599
+ },
600
+ ...configuredProviders()
536
601
  ],
537
602
  exports: [
538
603
  PaymentService,
@@ -546,15 +611,240 @@ ZuckerPaymentModule = _ts_decorate4([
546
611
  Global(),
547
612
  Module({})
548
613
  ], ZuckerPaymentModule);
614
+
615
+ // src/providers/stripe.ts
616
+ import { createRequire } from "module";
617
+ var requireSdk = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
618
+ function createStripeClient(secretKey, options) {
619
+ const StripeSdk = requireSdk("stripe");
620
+ return new StripeSdk(secretKey, options);
621
+ }
622
+ __name(createStripeClient, "createStripeClient");
623
+ function verifyStripeWebhookEvent(client, payload, signature, webhookSecret) {
624
+ return client.webhooks.constructEvent(payload, signature, webhookSecret);
625
+ }
626
+ __name(verifyStripeWebhookEvent, "verifyStripeWebhookEvent");
627
+ function stripeResourceId(value) {
628
+ return typeof value === "string" ? value : value?.id;
629
+ }
630
+ __name(stripeResourceId, "stripeResourceId");
631
+ function observeStripeCheckoutSession(raw) {
632
+ return {
633
+ raw,
634
+ id: raw.id,
635
+ url: raw.url || void 0,
636
+ clientReferenceId: raw.client_reference_id || void 0,
637
+ paymentIntentId: stripeResourceId(raw.payment_intent),
638
+ subscriptionId: stripeResourceId(raw.subscription),
639
+ settlement: raw.payment_status === "paid" ? "paid" : raw.status === "expired" ? "unpaid" : "unknown"
640
+ };
641
+ }
642
+ __name(observeStripeCheckoutSession, "observeStripeCheckoutSession");
643
+ async function createStripeCheckoutSession(client, params) {
644
+ return observeStripeCheckoutSession(await client.checkout.sessions.create(params));
645
+ }
646
+ __name(createStripeCheckoutSession, "createStripeCheckoutSession");
647
+ async function retrieveStripeCheckoutSession(client, id) {
648
+ return observeStripeCheckoutSession(await client.checkout.sessions.retrieve(id));
649
+ }
650
+ __name(retrieveStripeCheckoutSession, "retrieveStripeCheckoutSession");
651
+ function observeStripePaymentIntent(raw) {
652
+ const charge = typeof raw.latest_charge === "object" ? raw.latest_charge : null;
653
+ return {
654
+ raw,
655
+ id: raw.id,
656
+ chargeId: stripeResourceId(raw.latest_charge) ?? null,
657
+ paymentMethodType: charge?.payment_method_details?.type,
658
+ failureCode: raw.last_payment_error?.code,
659
+ failureMessage: raw.last_payment_error?.message,
660
+ settlement: raw.status === "succeeded" ? "paid" : raw.status === "canceled" ? "unpaid" : "unknown"
661
+ };
662
+ }
663
+ __name(observeStripePaymentIntent, "observeStripePaymentIntent");
664
+ async function retrieveStripePaymentIntent(client, id, params) {
665
+ const raw = params ? await client.paymentIntents.retrieve(id, params) : await client.paymentIntents.retrieve(id);
666
+ return observeStripePaymentIntent(raw);
667
+ }
668
+ __name(retrieveStripePaymentIntent, "retrieveStripePaymentIntent");
669
+
670
+ // src/providers/paypal.ts
671
+ import { createRequire as createRequire2 } from "module";
672
+ var requireSdk2 = createRequire2(typeof __filename === "string" ? __filename : import.meta.url);
673
+ function observePayPalOrder(raw) {
674
+ return {
675
+ raw,
676
+ id: raw.id,
677
+ approveUrl: raw.links?.find((link) => link.rel === "approve")?.href,
678
+ captureId: raw.purchaseUnits?.[0]?.payments?.captures?.[0]?.id,
679
+ settlement: raw.status === "COMPLETED" ? "paid" : raw.status === "VOIDED" ? "unpaid" : "unknown"
680
+ };
681
+ }
682
+ __name(observePayPalOrder, "observePayPalOrder");
683
+ function asRecord(value) {
684
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
685
+ }
686
+ __name(asRecord, "asRecord");
687
+ function decodePayPalWebhookEvent(payload) {
688
+ const raw = asRecord(JSON.parse(payload));
689
+ if (!raw || typeof raw.event_type !== "string") throw new Error("Invalid PayPal webhook event");
690
+ const type = raw.event_type;
691
+ const kind = type === "PAYMENT.CAPTURE.COMPLETED" ? "capture-succeeded" : type === "PAYMENT.CAPTURE.DENIED" || type === "PAYMENT.CAPTURE.DECLINED" ? "capture-failed" : "unknown";
692
+ if (kind === "unknown") return {
693
+ raw,
694
+ type,
695
+ kind
696
+ };
697
+ const resource = asRecord(raw.resource);
698
+ if (!resource || typeof resource.id !== "string" || !resource.id) {
699
+ throw new Error("PayPal capture event is missing its resource ID");
700
+ }
701
+ const reason = asRecord(resource.status_details)?.reason;
702
+ return {
703
+ raw,
704
+ type,
705
+ kind,
706
+ capture: {
707
+ raw: resource,
708
+ id: resource.id,
709
+ failureReason: typeof reason === "string" ? reason : void 0
710
+ }
711
+ };
712
+ }
713
+ __name(decodePayPalWebhookEvent, "decodePayPalWebhookEvent");
714
+ var PayPalPaymentTransport = class {
715
+ static {
716
+ __name(this, "PayPalPaymentTransport");
717
+ }
718
+ options;
719
+ orders;
720
+ baseUrl;
721
+ fetcher;
722
+ constructor(options) {
723
+ this.options = options;
724
+ const { Client, Environment, LogLevel, OrdersController } = requireSdk2("@paypal/paypal-server-sdk");
725
+ const client = new Client({
726
+ clientCredentialsAuthCredentials: {
727
+ oAuthClientId: options.clientId,
728
+ oAuthClientSecret: options.clientSecret
729
+ },
730
+ timeout: 0,
731
+ environment: options.mode === "live" ? Environment.Production : Environment.Sandbox,
732
+ logging: {
733
+ logLevel: LogLevel.Info,
734
+ logRequest: {
735
+ logBody: false
736
+ },
737
+ logResponse: {
738
+ logHeaders: false
739
+ }
740
+ }
741
+ });
742
+ this.orders = new OrdersController(client);
743
+ this.baseUrl = options.mode === "live" ? "https://api.paypal.com" : "https://api.sandbox.paypal.com";
744
+ this.fetcher = options.fetcher ?? fetch;
745
+ }
746
+ async createOrder(input) {
747
+ const { result } = await this.orders.createOrder(input);
748
+ return observePayPalOrder(result);
749
+ }
750
+ /** An explicit capture command; never invoked by getOrder or status observation. */
751
+ async captureOrder(input) {
752
+ const { result } = await this.orders.captureOrder(input);
753
+ return observePayPalOrder(result);
754
+ }
755
+ async getOrder(input) {
756
+ const { result } = await this.orders.getOrder(input);
757
+ return observePayPalOrder(result);
758
+ }
759
+ async getAccessToken() {
760
+ const response = await this.fetcher(`${this.baseUrl}/v1/oauth2/token`, {
761
+ method: "POST",
762
+ headers: {
763
+ "Content-Type": "application/x-www-form-urlencoded",
764
+ Authorization: `Basic ${Buffer.from(`${this.options.clientId}:${this.options.clientSecret}`).toString("base64")}`
765
+ },
766
+ body: "grant_type=client_credentials"
767
+ });
768
+ const data = await response.json();
769
+ if (!response.ok || typeof data.access_token !== "string" || !data.access_token) {
770
+ throw new Error("Unable to obtain PayPal access token");
771
+ }
772
+ return data.access_token;
773
+ }
774
+ async verifyWebhookSignature(payload, headers, webhookId) {
775
+ const transmissionId = headers["paypal-transmission-id"];
776
+ const transmissionTime = headers["paypal-transmission-time"];
777
+ const certUrl = headers["paypal-cert-url"];
778
+ const authAlgo = headers["paypal-auth-algo"];
779
+ const transmissionSig = headers["paypal-transmission-sig"];
780
+ if (!transmissionId || !transmissionTime || !certUrl || !authAlgo || !transmissionSig) {
781
+ this.options.logger?.warn("PayPal webhook is missing required signature headers");
782
+ return false;
783
+ }
784
+ try {
785
+ const accessToken = await this.getAccessToken();
786
+ const response = await this.fetcher(`${this.baseUrl}/v1/notifications/verify-webhook-signature`, {
787
+ method: "POST",
788
+ headers: {
789
+ "Content-Type": "application/json",
790
+ Authorization: `Bearer ${accessToken}`
791
+ },
792
+ body: JSON.stringify({
793
+ auth_algo: authAlgo,
794
+ cert_url: certUrl,
795
+ transmission_id: transmissionId,
796
+ transmission_sig: transmissionSig,
797
+ transmission_time: transmissionTime,
798
+ webhook_id: webhookId,
799
+ webhook_event: JSON.parse(payload)
800
+ })
801
+ });
802
+ const result = await response.json();
803
+ return response.ok && result.verification_status === "SUCCESS";
804
+ } catch (error) {
805
+ this.options.logger?.error(`PayPal webhook verification failed: ${error instanceof Error ? error.message : String(error)}`);
806
+ return false;
807
+ }
808
+ }
809
+ async refundCapture(captureId, request) {
810
+ const accessToken = await this.getAccessToken();
811
+ const response = await this.fetcher(`${this.baseUrl}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
812
+ method: "POST",
813
+ headers: {
814
+ "Content-Type": "application/json",
815
+ Authorization: `Bearer ${accessToken}`
816
+ },
817
+ body: JSON.stringify(request)
818
+ });
819
+ const result = await response.json();
820
+ if (!response.ok && result.status === "COMPLETED") throw new Error(`PayPal refund request failed (${response.status})`);
821
+ return result;
822
+ }
823
+ };
549
824
  export {
825
+ DatabaseOrderPersistence,
826
+ ORDER_PERSISTENCE,
827
+ OrderNotFoundError,
550
828
  OrderService,
551
829
  PAYMENT_PROVIDERS,
830
+ PayPalPaymentTransport,
552
831
  PaymentEvents,
553
832
  PaymentRecoveryService,
554
833
  PaymentService,
834
+ RECOVERY_ATTEMPT_STORE,
555
835
  RECOVERY_MAIL_SENDER,
556
836
  RECOVERY_URL_GENERATOR,
557
837
  RECOVERY_USER_LOOKUP,
558
838
  ZuckerPaymentModule,
559
- friendlyErrorReason
839
+ createStripeCheckoutSession,
840
+ createStripeClient,
841
+ decodePayPalWebhookEvent,
842
+ friendlyErrorReason,
843
+ observePayPalOrder,
844
+ observeStripeCheckoutSession,
845
+ observeStripePaymentIntent,
846
+ retrieveStripeCheckoutSession,
847
+ retrieveStripePaymentIntent,
848
+ stripeResourceId,
849
+ verifyStripeWebhookEvent
560
850
  };