@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/README.md +35 -0
- package/dist/index.d.mts +177 -26
- package/dist/index.d.ts +177 -26
- package/dist/index.js +497 -190
- package/dist/index.mjs +482 -192
- package/package.json +16 -4
package/dist/index.js
CHANGED
|
@@ -21,16 +21,31 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
DatabaseOrderPersistence: () => DatabaseOrderPersistence,
|
|
25
|
+
ORDER_PERSISTENCE: () => ORDER_PERSISTENCE,
|
|
26
|
+
OrderNotFoundError: () => OrderNotFoundError,
|
|
24
27
|
OrderService: () => OrderService,
|
|
25
28
|
PAYMENT_PROVIDERS: () => PAYMENT_PROVIDERS,
|
|
29
|
+
PayPalPaymentTransport: () => PayPalPaymentTransport,
|
|
26
30
|
PaymentEvents: () => PaymentEvents,
|
|
27
31
|
PaymentRecoveryService: () => PaymentRecoveryService,
|
|
28
32
|
PaymentService: () => PaymentService,
|
|
33
|
+
RECOVERY_ATTEMPT_STORE: () => RECOVERY_ATTEMPT_STORE,
|
|
29
34
|
RECOVERY_MAIL_SENDER: () => RECOVERY_MAIL_SENDER,
|
|
30
35
|
RECOVERY_URL_GENERATOR: () => RECOVERY_URL_GENERATOR,
|
|
31
36
|
RECOVERY_USER_LOOKUP: () => RECOVERY_USER_LOOKUP,
|
|
32
37
|
ZuckerPaymentModule: () => ZuckerPaymentModule,
|
|
33
|
-
|
|
38
|
+
createStripeCheckoutSession: () => createStripeCheckoutSession,
|
|
39
|
+
createStripeClient: () => createStripeClient,
|
|
40
|
+
decodePayPalWebhookEvent: () => decodePayPalWebhookEvent,
|
|
41
|
+
friendlyErrorReason: () => friendlyErrorReason,
|
|
42
|
+
observePayPalOrder: () => observePayPalOrder,
|
|
43
|
+
observeStripeCheckoutSession: () => observeStripeCheckoutSession,
|
|
44
|
+
observeStripePaymentIntent: () => observeStripePaymentIntent,
|
|
45
|
+
retrieveStripeCheckoutSession: () => retrieveStripeCheckoutSession,
|
|
46
|
+
retrieveStripePaymentIntent: () => retrieveStripePaymentIntent,
|
|
47
|
+
stripeResourceId: () => stripeResourceId,
|
|
48
|
+
verifyStripeWebhookEvent: () => verifyStripeWebhookEvent
|
|
34
49
|
});
|
|
35
50
|
module.exports = __toCommonJS(index_exports);
|
|
36
51
|
|
|
@@ -120,6 +135,48 @@ PaymentService = _ts_decorate([
|
|
|
120
135
|
|
|
121
136
|
// src/order.service.ts
|
|
122
137
|
var import_common2 = require("@nestjs/common");
|
|
138
|
+
|
|
139
|
+
// src/order-persistence.ts
|
|
140
|
+
var ORDER_PERSISTENCE = "PAYMENT_ORDER_PERSISTENCE";
|
|
141
|
+
var DatabaseOrderPersistence = class {
|
|
142
|
+
static {
|
|
143
|
+
__name(this, "DatabaseOrderPersistence");
|
|
144
|
+
}
|
|
145
|
+
model;
|
|
146
|
+
constructor(model) {
|
|
147
|
+
this.model = model;
|
|
148
|
+
}
|
|
149
|
+
create(args) {
|
|
150
|
+
return this.model.create(args);
|
|
151
|
+
}
|
|
152
|
+
findUnique(args) {
|
|
153
|
+
return this.model.findUnique(args);
|
|
154
|
+
}
|
|
155
|
+
findMany(args) {
|
|
156
|
+
return this.model.findMany(args);
|
|
157
|
+
}
|
|
158
|
+
update(args) {
|
|
159
|
+
return this.model.update(args);
|
|
160
|
+
}
|
|
161
|
+
updateMany(args) {
|
|
162
|
+
return this.model.updateMany(args);
|
|
163
|
+
}
|
|
164
|
+
count(args) {
|
|
165
|
+
return this.model.count(args);
|
|
166
|
+
}
|
|
167
|
+
async compareAndSet(id, expected, data) {
|
|
168
|
+
const result = await this.model.updateMany({
|
|
169
|
+
where: {
|
|
170
|
+
...expected,
|
|
171
|
+
id
|
|
172
|
+
},
|
|
173
|
+
data
|
|
174
|
+
});
|
|
175
|
+
return result.count === 1;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/order.service.ts
|
|
123
180
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
124
181
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
125
182
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
@@ -140,132 +197,161 @@ function _ts_metadata(metadataKey, metadataValue) {
|
|
|
140
197
|
}
|
|
141
198
|
}
|
|
142
199
|
__name(_ts_metadata, "_ts_metadata");
|
|
143
|
-
|
|
200
|
+
function _ts_param(paramIndex, decorator) {
|
|
201
|
+
return function(target, key) {
|
|
202
|
+
decorator(target, key, paramIndex);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
__name(_ts_param, "_ts_param");
|
|
206
|
+
var OrderNotFoundError = class extends Error {
|
|
207
|
+
static {
|
|
208
|
+
__name(this, "OrderNotFoundError");
|
|
209
|
+
}
|
|
210
|
+
orderId;
|
|
211
|
+
constructor(orderId) {
|
|
212
|
+
super(`\u8BA2\u5355 "${orderId}" \u4E0D\u5B58\u5728`), this.orderId = orderId;
|
|
213
|
+
this.name = "OrderNotFoundError";
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var OrderService = class {
|
|
144
217
|
static {
|
|
145
218
|
__name(this, "OrderService");
|
|
146
219
|
}
|
|
147
220
|
paymentService;
|
|
148
|
-
|
|
149
|
-
orders
|
|
150
|
-
constructor(paymentService) {
|
|
221
|
+
orders;
|
|
222
|
+
constructor(paymentService, orders) {
|
|
151
223
|
this.paymentService = paymentService;
|
|
224
|
+
this.orders = orders;
|
|
225
|
+
if (!orders) throw new Error("Order persistence must be configured");
|
|
152
226
|
}
|
|
153
|
-
/** 创建订单并发起支付 */
|
|
154
227
|
async createOrder(params) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
228
|
+
if (await this.orders.findUnique({
|
|
229
|
+
where: {
|
|
230
|
+
id: params.orderId
|
|
231
|
+
}
|
|
232
|
+
})) {
|
|
233
|
+
throw new Error(`\u8BA2\u5355 "${params.orderId}" \u5DF2\u5B58\u5728`);
|
|
158
234
|
}
|
|
159
235
|
const now = /* @__PURE__ */ new Date();
|
|
160
|
-
const order = {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
...order
|
|
186
|
-
};
|
|
236
|
+
const order = await this.orders.create({
|
|
237
|
+
data: {
|
|
238
|
+
id: params.orderId,
|
|
239
|
+
providerId: params.providerId,
|
|
240
|
+
amount: params.payment.amount,
|
|
241
|
+
currency: params.payment.currency,
|
|
242
|
+
metadata: params.payment.metadata,
|
|
243
|
+
status: "created",
|
|
244
|
+
createdAt: now,
|
|
245
|
+
updatedAt: now
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return this.initiatePayment(order, params.payment);
|
|
249
|
+
}
|
|
250
|
+
async initiatePayment(order, payment) {
|
|
251
|
+
const intent = await this.paymentService.createPaymentIntent(order.providerId, payment);
|
|
252
|
+
await this.transition(order.id, [
|
|
253
|
+
"created"
|
|
254
|
+
], "paying", {
|
|
255
|
+
paymentIntentId: intent.id
|
|
256
|
+
});
|
|
257
|
+
return this.getOrder(order.id);
|
|
187
258
|
}
|
|
188
|
-
/** 确认订单支付 */
|
|
189
259
|
async confirmOrder(orderId) {
|
|
190
|
-
const order = this.getOrder(orderId);
|
|
191
|
-
if (order.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentIntentId`);
|
|
196
|
-
}
|
|
260
|
+
const order = await this.getOrder(orderId);
|
|
261
|
+
if (!order.paymentIntentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentIntentId`);
|
|
262
|
+
await this.transition(orderId, [
|
|
263
|
+
"paying"
|
|
264
|
+
], "confirming");
|
|
197
265
|
const result = await this.paymentService.confirmPayment(order.providerId, order.paymentIntentId);
|
|
198
266
|
if (result.status === "succeeded") {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
267
|
+
await this.transition(orderId, [
|
|
268
|
+
"confirming"
|
|
269
|
+
], "paid", {
|
|
270
|
+
paymentId: result.id,
|
|
271
|
+
paidAt: result.paidAt ?? /* @__PURE__ */ new Date()
|
|
272
|
+
});
|
|
273
|
+
} else if (result.status === "failed" || result.status === "cancelled") {
|
|
274
|
+
await this.transition(orderId, [
|
|
275
|
+
"confirming"
|
|
276
|
+
], "failed");
|
|
277
|
+
} else if (result.status === "refunded") {
|
|
278
|
+
await this.transition(orderId, [
|
|
279
|
+
"confirming"
|
|
280
|
+
], "refunded");
|
|
204
281
|
}
|
|
205
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
206
|
-
this.logger.log(`\u8BA2\u5355 "${orderId}" \u652F\u4ED8\u7ED3\u679C: ${result.status}`);
|
|
207
282
|
return result;
|
|
208
283
|
}
|
|
209
|
-
/** 订单退款 */
|
|
210
284
|
async refundOrder(orderId, amount, reason) {
|
|
211
|
-
const order = this.getOrder(orderId);
|
|
212
|
-
if (order.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentId`);
|
|
217
|
-
}
|
|
218
|
-
order.status = "refunding";
|
|
219
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
285
|
+
const order = await this.getOrder(orderId);
|
|
286
|
+
if (!order.paymentId) throw new Error(`\u8BA2\u5355 "${orderId}" \u7F3A\u5C11 paymentId`);
|
|
287
|
+
await this.transition(orderId, [
|
|
288
|
+
"paid"
|
|
289
|
+
], "refunding");
|
|
220
290
|
const result = await this.paymentService.refund(order.providerId, order.paymentId, amount, reason);
|
|
221
291
|
if (result.status === "succeeded") {
|
|
222
|
-
|
|
223
|
-
|
|
292
|
+
await this.transition(orderId, [
|
|
293
|
+
"refunding"
|
|
294
|
+
], "refunded", {
|
|
295
|
+
refundedAt: /* @__PURE__ */ new Date()
|
|
296
|
+
});
|
|
224
297
|
} else if (result.status === "failed") {
|
|
225
|
-
|
|
298
|
+
await this.transition(orderId, [
|
|
299
|
+
"refunding"
|
|
300
|
+
], "paid");
|
|
226
301
|
}
|
|
227
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
228
|
-
this.logger.log(`\u8BA2\u5355 "${orderId}" \u9000\u6B3E\u7ED3\u679C: ${result.status}`);
|
|
229
302
|
return result;
|
|
230
303
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
304
|
+
async retryOrder(orderId) {
|
|
305
|
+
const order = await this.getOrder(orderId);
|
|
306
|
+
await this.transition(orderId, [
|
|
307
|
+
"failed"
|
|
308
|
+
], "created");
|
|
309
|
+
return this.initiatePayment(order, {
|
|
310
|
+
amount: order.amount,
|
|
311
|
+
currency: order.currency,
|
|
312
|
+
metadata: order.metadata
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async cancelOrder(orderId) {
|
|
316
|
+
await this.transition(orderId, [
|
|
235
317
|
"created",
|
|
236
318
|
"paying",
|
|
237
319
|
"failed"
|
|
238
|
-
];
|
|
239
|
-
|
|
240
|
-
throw new Error(`\u8BA2\u5355 "${orderId}" \u72B6\u6001\u4E3A "${order.status}"\uFF0C\u65E0\u6CD5\u53D6\u6D88`);
|
|
241
|
-
}
|
|
242
|
-
order.status = "cancelled";
|
|
243
|
-
order.updatedAt = /* @__PURE__ */ new Date();
|
|
244
|
-
this.logger.log(`\u8BA2\u5355 "${orderId}" \u5DF2\u53D6\u6D88`);
|
|
245
|
-
return {
|
|
246
|
-
...order
|
|
247
|
-
};
|
|
320
|
+
], "cancelled");
|
|
321
|
+
return this.getOrder(orderId);
|
|
248
322
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
323
|
+
async getOrder(orderId) {
|
|
324
|
+
const order = await this.orders.findUnique({
|
|
325
|
+
where: {
|
|
326
|
+
id: orderId
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
if (!order) throw new OrderNotFoundError(orderId);
|
|
255
330
|
return order;
|
|
256
331
|
}
|
|
257
|
-
/** 列出所有订单 */
|
|
258
332
|
listOrders() {
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
|
|
333
|
+
return this.orders.findMany();
|
|
334
|
+
}
|
|
335
|
+
async transition(id, from, to, data = {}) {
|
|
336
|
+
const changed = await this.orders.compareAndSet(id, {
|
|
337
|
+
status: {
|
|
338
|
+
in: from
|
|
339
|
+
}
|
|
340
|
+
}, {
|
|
341
|
+
...data,
|
|
342
|
+
status: to,
|
|
343
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
344
|
+
});
|
|
345
|
+
if (!changed) throw new Error(`\u8BA2\u5355 "${id}" \u4E0D\u5B58\u5728\u6216\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u65E0\u6CD5\u8F6C\u6362\u4E3A ${to}`);
|
|
262
346
|
}
|
|
263
347
|
};
|
|
264
348
|
OrderService = _ts_decorate2([
|
|
265
349
|
(0, import_common2.Injectable)(),
|
|
350
|
+
_ts_param(1, (0, import_common2.Inject)(ORDER_PERSISTENCE)),
|
|
266
351
|
_ts_metadata("design:type", Function),
|
|
267
352
|
_ts_metadata("design:paramtypes", [
|
|
268
|
-
typeof PaymentService === "undefined" ? Object : PaymentService
|
|
353
|
+
typeof PaymentService === "undefined" ? Object : PaymentService,
|
|
354
|
+
typeof OrderPersistence === "undefined" ? Object : OrderPersistence
|
|
269
355
|
])
|
|
270
356
|
], OrderService);
|
|
271
357
|
|
|
@@ -304,15 +390,16 @@ function _ts_metadata2(metadataKey, metadataValue) {
|
|
|
304
390
|
}
|
|
305
391
|
}
|
|
306
392
|
__name(_ts_metadata2, "_ts_metadata");
|
|
307
|
-
function
|
|
393
|
+
function _ts_param2(paramIndex, decorator) {
|
|
308
394
|
return function(target, key) {
|
|
309
395
|
decorator(target, key, paramIndex);
|
|
310
396
|
};
|
|
311
397
|
}
|
|
312
|
-
__name(
|
|
398
|
+
__name(_ts_param2, "_ts_param");
|
|
313
399
|
var RECOVERY_MAIL_SENDER = "RECOVERY_MAIL_SENDER";
|
|
314
400
|
var RECOVERY_USER_LOOKUP = "RECOVERY_USER_LOOKUP";
|
|
315
401
|
var RECOVERY_URL_GENERATOR = "RECOVERY_URL_GENERATOR";
|
|
402
|
+
var RECOVERY_ATTEMPT_STORE = "RECOVERY_ATTEMPT_STORE";
|
|
316
403
|
var DEFAULT_OPTIONS = {
|
|
317
404
|
maxAttempts: 3,
|
|
318
405
|
retryLinkExpireHours: 48,
|
|
@@ -323,18 +410,16 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
323
410
|
__name(this, "PaymentRecoveryService");
|
|
324
411
|
}
|
|
325
412
|
orderService;
|
|
326
|
-
|
|
413
|
+
attempts;
|
|
327
414
|
mailSender;
|
|
328
415
|
userLookup;
|
|
329
416
|
urlGenerator;
|
|
330
417
|
eventEmitter;
|
|
331
418
|
logger = new import_common3.Logger(_PaymentRecoveryService.name);
|
|
332
419
|
options;
|
|
333
|
-
|
|
334
|
-
recoveryAttempts = /* @__PURE__ */ new Map();
|
|
335
|
-
constructor(orderService, paymentService, mailSender, userLookup, urlGenerator, eventEmitter) {
|
|
420
|
+
constructor(orderService, attempts, mailSender, userLookup, urlGenerator, eventEmitter) {
|
|
336
421
|
this.orderService = orderService;
|
|
337
|
-
this.
|
|
422
|
+
this.attempts = attempts;
|
|
338
423
|
this.mailSender = mailSender;
|
|
339
424
|
this.userLookup = userLookup;
|
|
340
425
|
this.urlGenerator = urlGenerator;
|
|
@@ -353,8 +438,9 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
353
438
|
async handleFailedPayment(orderId, errorCode, errorMessage) {
|
|
354
439
|
let order;
|
|
355
440
|
try {
|
|
356
|
-
order = this.orderService.getOrder(orderId);
|
|
357
|
-
} catch {
|
|
441
|
+
order = await this.orderService.getOrder(orderId);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (!(error instanceof OrderNotFoundError)) throw error;
|
|
358
444
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
|
|
359
445
|
return;
|
|
360
446
|
}
|
|
@@ -362,11 +448,11 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
362
448
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u72B6\u6001\u4E3A ${order.status}\uFF0C\u8DF3\u8FC7\u633D\u56DE`);
|
|
363
449
|
return;
|
|
364
450
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this.logger.log(`\u8BA2\u5355 ${orderId} \u5DF2\u633D\u56DE ${attempts.count} \u6B21\uFF0C\u4E0D\u518D\u53D1\u9001`);
|
|
451
|
+
if (!this.mailSender) {
|
|
452
|
+
this.logger.warn("Recovery mail sender is not configured; no attempt was reserved");
|
|
368
453
|
return;
|
|
369
454
|
}
|
|
455
|
+
if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
|
|
370
456
|
const userId = order.metadata?.userId;
|
|
371
457
|
let email;
|
|
372
458
|
let userName = "";
|
|
@@ -387,32 +473,20 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
387
473
|
this.logger.warn(`\u8BA2\u5355 ${orderId} \u65E0\u6CD5\u751F\u6210\u91CD\u8BD5\u94FE\u63A5`);
|
|
388
474
|
return;
|
|
389
475
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
this.logger.log(`\u633D\u56DE\u90AE\u4EF6\u5DF2\u53D1\u9001: ${email}, \u8BA2\u5355 ${orderId} (\u7B2C ${attemptNumber} \u6B21)`);
|
|
406
|
-
} catch (err) {
|
|
407
|
-
this.logger.error(`\u633D\u56DE\u90AE\u4EF6\u53D1\u9001\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
this.logger.log(`[Recovery:NoMailSender] \u8BA2\u5355 ${orderId} \u9700\u8981\u633D\u56DE, email=${email}, retryUrl=${retryUrl}, reason=${errorReason}`);
|
|
412
|
-
}
|
|
413
|
-
this.recoveryAttempts.set(orderId, {
|
|
414
|
-
count: attemptNumber,
|
|
415
|
-
lastAt: /* @__PURE__ */ new Date()
|
|
476
|
+
const reservation = await this.attempts.claim(orderId, this.options.maxAttempts, /* @__PURE__ */ new Date());
|
|
477
|
+
if (!reservation) return;
|
|
478
|
+
const attemptNumber = reservation.attemptNumber;
|
|
479
|
+
await this.mailSender.sendRecoveryEmail({
|
|
480
|
+
to: email,
|
|
481
|
+
userName: userName || email.split("@")[0],
|
|
482
|
+
orderId: order.id,
|
|
483
|
+
orderNumber: order.metadata?.orderNumber,
|
|
484
|
+
amount: order.amount,
|
|
485
|
+
currency: order.currency,
|
|
486
|
+
errorReason: friendlyErrorReason(errorCode, errorMessage),
|
|
487
|
+
retryUrl,
|
|
488
|
+
expireHours: this.options.retryLinkExpireHours,
|
|
489
|
+
idempotencyKey: `payment-recovery:${orderId}:${attemptNumber}`
|
|
416
490
|
});
|
|
417
491
|
if (this.eventEmitter) {
|
|
418
492
|
const event = {
|
|
@@ -428,28 +502,12 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
428
502
|
/**
|
|
429
503
|
* 重试失败订单 — 将 failed → created 并重新发起支付
|
|
430
504
|
*/
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (order.status !== "failed") {
|
|
434
|
-
throw new Error(`\u8BA2\u5355 ${orderId} \u72B6\u6001\u4E3A ${order.status}\uFF0C\u4EC5 failed \u8BA2\u5355\u53EF\u91CD\u8BD5`);
|
|
435
|
-
}
|
|
436
|
-
const intent = await this.paymentService.createPaymentIntent(order.providerId, {
|
|
437
|
-
amount: order.amount,
|
|
438
|
-
currency: order.currency,
|
|
439
|
-
metadata: order.metadata
|
|
440
|
-
});
|
|
441
|
-
const internalOrder = this.orderService.getOrder(orderId);
|
|
442
|
-
internalOrder.paymentIntentId = intent.id;
|
|
443
|
-
internalOrder.status = "paying";
|
|
444
|
-
internalOrder.updatedAt = /* @__PURE__ */ new Date();
|
|
445
|
-
this.logger.log(`\u8BA2\u5355 ${orderId} \u91CD\u8BD5\u652F\u4ED8, \u65B0 intentId=${intent.id}`);
|
|
446
|
-
return {
|
|
447
|
-
...internalOrder
|
|
448
|
-
};
|
|
505
|
+
retryOrder(orderId) {
|
|
506
|
+
return this.orderService.retryOrder(orderId);
|
|
449
507
|
}
|
|
450
|
-
/** 获取订单挽回记录 */
|
|
451
508
|
getRecoveryAttempts(orderId) {
|
|
452
|
-
|
|
509
|
+
if (!this.attempts) throw new Error("Recovery attempt persistence must be configured");
|
|
510
|
+
return this.attempts.get(orderId);
|
|
453
511
|
}
|
|
454
512
|
async generateRetryUrl(order) {
|
|
455
513
|
if (this.urlGenerator) {
|
|
@@ -460,17 +518,18 @@ var PaymentRecoveryService = class _PaymentRecoveryService {
|
|
|
460
518
|
};
|
|
461
519
|
PaymentRecoveryService = _ts_decorate3([
|
|
462
520
|
(0, import_common3.Injectable)(),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
521
|
+
_ts_param2(1, (0, import_common3.Inject)(RECOVERY_ATTEMPT_STORE)),
|
|
522
|
+
_ts_param2(2, (0, import_common3.Optional)()),
|
|
523
|
+
_ts_param2(2, (0, import_common3.Inject)(RECOVERY_MAIL_SENDER)),
|
|
524
|
+
_ts_param2(3, (0, import_common3.Optional)()),
|
|
525
|
+
_ts_param2(3, (0, import_common3.Inject)(RECOVERY_USER_LOOKUP)),
|
|
526
|
+
_ts_param2(4, (0, import_common3.Optional)()),
|
|
527
|
+
_ts_param2(4, (0, import_common3.Inject)(RECOVERY_URL_GENERATOR)),
|
|
528
|
+
_ts_param2(5, (0, import_common3.Optional)()),
|
|
470
529
|
_ts_metadata2("design:type", Function),
|
|
471
530
|
_ts_metadata2("design:paramtypes", [
|
|
472
531
|
typeof OrderService === "undefined" ? Object : OrderService,
|
|
473
|
-
|
|
532
|
+
Object,
|
|
474
533
|
typeof RecoveryMailSender === "undefined" ? Object : RecoveryMailSender,
|
|
475
534
|
typeof RecoveryUserLookup === "undefined" ? Object : RecoveryUserLookup,
|
|
476
535
|
typeof RecoveryUrlGenerator === "undefined" ? Object : RecoveryUrlGenerator,
|
|
@@ -510,30 +569,63 @@ function _ts_decorate4(decorators, target, key, desc) {
|
|
|
510
569
|
}
|
|
511
570
|
__name(_ts_decorate4, "_ts_decorate");
|
|
512
571
|
var PAYMENT_PROVIDERS = "PAYMENT_PROVIDERS";
|
|
572
|
+
var PAYMENT_OPTIONS = /* @__PURE__ */ Symbol("PAYMENT_OPTIONS");
|
|
573
|
+
function configuredProviders() {
|
|
574
|
+
return [
|
|
575
|
+
{
|
|
576
|
+
provide: ORDER_PERSISTENCE,
|
|
577
|
+
inject: [
|
|
578
|
+
PAYMENT_OPTIONS
|
|
579
|
+
],
|
|
580
|
+
useFactory: /* @__PURE__ */ __name((options) => {
|
|
581
|
+
if (!options.orderPersistence) throw new Error("Order persistence must be configured");
|
|
582
|
+
return options.orderPersistence;
|
|
583
|
+
}, "useFactory")
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
provide: RECOVERY_ATTEMPT_STORE,
|
|
587
|
+
inject: [
|
|
588
|
+
PAYMENT_OPTIONS
|
|
589
|
+
],
|
|
590
|
+
useFactory: /* @__PURE__ */ __name((options) => options.recoveryAttempts ?? null, "useFactory")
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
provide: PaymentService,
|
|
594
|
+
inject: [
|
|
595
|
+
PAYMENT_OPTIONS
|
|
596
|
+
],
|
|
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")
|
|
602
|
+
},
|
|
603
|
+
OrderService,
|
|
604
|
+
PaymentRecoveryService,
|
|
605
|
+
{
|
|
606
|
+
provide: PAYMENT_PROVIDERS,
|
|
607
|
+
inject: [
|
|
608
|
+
PaymentService
|
|
609
|
+
],
|
|
610
|
+
useFactory: /* @__PURE__ */ __name((service) => service.listProviders(), "useFactory")
|
|
611
|
+
}
|
|
612
|
+
];
|
|
613
|
+
}
|
|
614
|
+
__name(configuredProviders, "configuredProviders");
|
|
513
615
|
var ZuckerPaymentModule = class _ZuckerPaymentModule {
|
|
514
616
|
static {
|
|
515
617
|
__name(this, "ZuckerPaymentModule");
|
|
516
618
|
}
|
|
517
|
-
static forRoot(options
|
|
619
|
+
static forRoot(options) {
|
|
518
620
|
return {
|
|
519
621
|
module: _ZuckerPaymentModule,
|
|
520
622
|
global: true,
|
|
521
623
|
providers: [
|
|
522
|
-
PaymentService,
|
|
523
|
-
OrderService,
|
|
524
|
-
PaymentRecoveryService,
|
|
525
624
|
{
|
|
526
|
-
provide:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
return paymentService.listProviders();
|
|
532
|
-
}, "useFactory"),
|
|
533
|
-
inject: [
|
|
534
|
-
PaymentService
|
|
535
|
-
]
|
|
536
|
-
}
|
|
625
|
+
provide: PAYMENT_OPTIONS,
|
|
626
|
+
useValue: options
|
|
627
|
+
},
|
|
628
|
+
...configuredProviders()
|
|
537
629
|
],
|
|
538
630
|
exports: [
|
|
539
631
|
PaymentService,
|
|
@@ -548,24 +640,12 @@ var ZuckerPaymentModule = class _ZuckerPaymentModule {
|
|
|
548
640
|
global: true,
|
|
549
641
|
imports: options.imports ?? [],
|
|
550
642
|
providers: [
|
|
551
|
-
PaymentService,
|
|
552
|
-
OrderService,
|
|
553
|
-
PaymentRecoveryService,
|
|
554
643
|
{
|
|
555
|
-
provide:
|
|
556
|
-
useFactory:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
paymentService.register(provider);
|
|
561
|
-
}
|
|
562
|
-
return paymentService.listProviders();
|
|
563
|
-
}, "useFactory"),
|
|
564
|
-
inject: [
|
|
565
|
-
PaymentService,
|
|
566
|
-
...options.inject ?? []
|
|
567
|
-
]
|
|
568
|
-
}
|
|
644
|
+
provide: PAYMENT_OPTIONS,
|
|
645
|
+
useFactory: options.useFactory,
|
|
646
|
+
inject: options.inject ?? []
|
|
647
|
+
},
|
|
648
|
+
...configuredProviders()
|
|
569
649
|
],
|
|
570
650
|
exports: [
|
|
571
651
|
PaymentService,
|
|
@@ -579,16 +659,243 @@ ZuckerPaymentModule = _ts_decorate4([
|
|
|
579
659
|
(0, import_common4.Global)(),
|
|
580
660
|
(0, import_common4.Module)({})
|
|
581
661
|
], ZuckerPaymentModule);
|
|
662
|
+
|
|
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
|
+
// src/providers/paypal.ts
|
|
720
|
+
var import_node_module2 = require("module");
|
|
721
|
+
var import_meta2 = {};
|
|
722
|
+
var requireSdk2 = (0, import_node_module2.createRequire)(typeof __filename === "string" ? __filename : import_meta2.url);
|
|
723
|
+
function observePayPalOrder(raw) {
|
|
724
|
+
return {
|
|
725
|
+
raw,
|
|
726
|
+
id: raw.id,
|
|
727
|
+
approveUrl: raw.links?.find((link) => link.rel === "approve")?.href,
|
|
728
|
+
captureId: raw.purchaseUnits?.[0]?.payments?.captures?.[0]?.id,
|
|
729
|
+
settlement: raw.status === "COMPLETED" ? "paid" : raw.status === "VOIDED" ? "unpaid" : "unknown"
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
__name(observePayPalOrder, "observePayPalOrder");
|
|
733
|
+
function asRecord(value) {
|
|
734
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
735
|
+
}
|
|
736
|
+
__name(asRecord, "asRecord");
|
|
737
|
+
function decodePayPalWebhookEvent(payload) {
|
|
738
|
+
const raw = asRecord(JSON.parse(payload));
|
|
739
|
+
if (!raw || typeof raw.event_type !== "string") throw new Error("Invalid PayPal webhook event");
|
|
740
|
+
const type = raw.event_type;
|
|
741
|
+
const kind = type === "PAYMENT.CAPTURE.COMPLETED" ? "capture-succeeded" : type === "PAYMENT.CAPTURE.DENIED" || type === "PAYMENT.CAPTURE.DECLINED" ? "capture-failed" : "unknown";
|
|
742
|
+
if (kind === "unknown") return {
|
|
743
|
+
raw,
|
|
744
|
+
type,
|
|
745
|
+
kind
|
|
746
|
+
};
|
|
747
|
+
const resource = asRecord(raw.resource);
|
|
748
|
+
if (!resource || typeof resource.id !== "string" || !resource.id) {
|
|
749
|
+
throw new Error("PayPal capture event is missing its resource ID");
|
|
750
|
+
}
|
|
751
|
+
const reason = asRecord(resource.status_details)?.reason;
|
|
752
|
+
return {
|
|
753
|
+
raw,
|
|
754
|
+
type,
|
|
755
|
+
kind,
|
|
756
|
+
capture: {
|
|
757
|
+
raw: resource,
|
|
758
|
+
id: resource.id,
|
|
759
|
+
failureReason: typeof reason === "string" ? reason : void 0
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
__name(decodePayPalWebhookEvent, "decodePayPalWebhookEvent");
|
|
764
|
+
var PayPalPaymentTransport = class {
|
|
765
|
+
static {
|
|
766
|
+
__name(this, "PayPalPaymentTransport");
|
|
767
|
+
}
|
|
768
|
+
options;
|
|
769
|
+
orders;
|
|
770
|
+
baseUrl;
|
|
771
|
+
fetcher;
|
|
772
|
+
constructor(options) {
|
|
773
|
+
this.options = options;
|
|
774
|
+
const { Client, Environment, LogLevel, OrdersController } = requireSdk2("@paypal/paypal-server-sdk");
|
|
775
|
+
const client = new Client({
|
|
776
|
+
clientCredentialsAuthCredentials: {
|
|
777
|
+
oAuthClientId: options.clientId,
|
|
778
|
+
oAuthClientSecret: options.clientSecret
|
|
779
|
+
},
|
|
780
|
+
timeout: 0,
|
|
781
|
+
environment: options.mode === "live" ? Environment.Production : Environment.Sandbox,
|
|
782
|
+
logging: {
|
|
783
|
+
logLevel: LogLevel.Info,
|
|
784
|
+
logRequest: {
|
|
785
|
+
logBody: false
|
|
786
|
+
},
|
|
787
|
+
logResponse: {
|
|
788
|
+
logHeaders: false
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
this.orders = new OrdersController(client);
|
|
793
|
+
this.baseUrl = options.mode === "live" ? "https://api.paypal.com" : "https://api.sandbox.paypal.com";
|
|
794
|
+
this.fetcher = options.fetcher ?? fetch;
|
|
795
|
+
}
|
|
796
|
+
async createOrder(input) {
|
|
797
|
+
const { result } = await this.orders.createOrder(input);
|
|
798
|
+
return observePayPalOrder(result);
|
|
799
|
+
}
|
|
800
|
+
/** An explicit capture command; never invoked by getOrder or status observation. */
|
|
801
|
+
async captureOrder(input) {
|
|
802
|
+
const { result } = await this.orders.captureOrder(input);
|
|
803
|
+
return observePayPalOrder(result);
|
|
804
|
+
}
|
|
805
|
+
async getOrder(input) {
|
|
806
|
+
const { result } = await this.orders.getOrder(input);
|
|
807
|
+
return observePayPalOrder(result);
|
|
808
|
+
}
|
|
809
|
+
async getAccessToken() {
|
|
810
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/oauth2/token`, {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: {
|
|
813
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
814
|
+
Authorization: `Basic ${Buffer.from(`${this.options.clientId}:${this.options.clientSecret}`).toString("base64")}`
|
|
815
|
+
},
|
|
816
|
+
body: "grant_type=client_credentials"
|
|
817
|
+
});
|
|
818
|
+
const data = await response.json();
|
|
819
|
+
if (!response.ok || typeof data.access_token !== "string" || !data.access_token) {
|
|
820
|
+
throw new Error("Unable to obtain PayPal access token");
|
|
821
|
+
}
|
|
822
|
+
return data.access_token;
|
|
823
|
+
}
|
|
824
|
+
async verifyWebhookSignature(payload, headers, webhookId) {
|
|
825
|
+
const transmissionId = headers["paypal-transmission-id"];
|
|
826
|
+
const transmissionTime = headers["paypal-transmission-time"];
|
|
827
|
+
const certUrl = headers["paypal-cert-url"];
|
|
828
|
+
const authAlgo = headers["paypal-auth-algo"];
|
|
829
|
+
const transmissionSig = headers["paypal-transmission-sig"];
|
|
830
|
+
if (!transmissionId || !transmissionTime || !certUrl || !authAlgo || !transmissionSig) {
|
|
831
|
+
this.options.logger?.warn("PayPal webhook is missing required signature headers");
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
const accessToken = await this.getAccessToken();
|
|
836
|
+
const response = await this.fetcher(`${this.baseUrl}/v1/notifications/verify-webhook-signature`, {
|
|
837
|
+
method: "POST",
|
|
838
|
+
headers: {
|
|
839
|
+
"Content-Type": "application/json",
|
|
840
|
+
Authorization: `Bearer ${accessToken}`
|
|
841
|
+
},
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
auth_algo: authAlgo,
|
|
844
|
+
cert_url: certUrl,
|
|
845
|
+
transmission_id: transmissionId,
|
|
846
|
+
transmission_sig: transmissionSig,
|
|
847
|
+
transmission_time: transmissionTime,
|
|
848
|
+
webhook_id: webhookId,
|
|
849
|
+
webhook_event: JSON.parse(payload)
|
|
850
|
+
})
|
|
851
|
+
});
|
|
852
|
+
const result = await response.json();
|
|
853
|
+
return response.ok && result.verification_status === "SUCCESS";
|
|
854
|
+
} catch (error) {
|
|
855
|
+
this.options.logger?.error(`PayPal webhook verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async refundCapture(captureId, request) {
|
|
860
|
+
const accessToken = await this.getAccessToken();
|
|
861
|
+
const response = await this.fetcher(`${this.baseUrl}/v2/payments/captures/${encodeURIComponent(captureId)}/refund`, {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: {
|
|
864
|
+
"Content-Type": "application/json",
|
|
865
|
+
Authorization: `Bearer ${accessToken}`
|
|
866
|
+
},
|
|
867
|
+
body: JSON.stringify(request)
|
|
868
|
+
});
|
|
869
|
+
const result = await response.json();
|
|
870
|
+
if (!response.ok && result.status === "COMPLETED") throw new Error(`PayPal refund request failed (${response.status})`);
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
582
874
|
// Annotate the CommonJS export names for ESM import in node:
|
|
583
875
|
0 && (module.exports = {
|
|
876
|
+
DatabaseOrderPersistence,
|
|
877
|
+
ORDER_PERSISTENCE,
|
|
878
|
+
OrderNotFoundError,
|
|
584
879
|
OrderService,
|
|
585
880
|
PAYMENT_PROVIDERS,
|
|
881
|
+
PayPalPaymentTransport,
|
|
586
882
|
PaymentEvents,
|
|
587
883
|
PaymentRecoveryService,
|
|
588
884
|
PaymentService,
|
|
885
|
+
RECOVERY_ATTEMPT_STORE,
|
|
589
886
|
RECOVERY_MAIL_SENDER,
|
|
590
887
|
RECOVERY_URL_GENERATOR,
|
|
591
888
|
RECOVERY_USER_LOOKUP,
|
|
592
889
|
ZuckerPaymentModule,
|
|
593
|
-
|
|
890
|
+
createStripeCheckoutSession,
|
|
891
|
+
createStripeClient,
|
|
892
|
+
decodePayPalWebhookEvent,
|
|
893
|
+
friendlyErrorReason,
|
|
894
|
+
observePayPalOrder,
|
|
895
|
+
observeStripeCheckoutSession,
|
|
896
|
+
observeStripePaymentIntent,
|
|
897
|
+
retrieveStripeCheckoutSession,
|
|
898
|
+
retrieveStripePaymentIntent,
|
|
899
|
+
stripeResourceId,
|
|
900
|
+
verifyStripeWebhookEvent
|
|
594
901
|
});
|