@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.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,
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
orders
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
190
|
-
|
|
244
|
+
await this.transition(orderId, [
|
|
245
|
+
"refunding"
|
|
246
|
+
], "refunded", {
|
|
247
|
+
refundedAt: /* @__PURE__ */ new Date()
|
|
248
|
+
});
|
|
191
249
|
} else if (result.status === "failed") {
|
|
192
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
227
|
-
|
|
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
|
|
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
|
|
345
|
+
function _ts_param2(paramIndex, decorator) {
|
|
275
346
|
return function(target, key) {
|
|
276
347
|
decorator(target, key, paramIndex);
|
|
277
348
|
};
|
|
278
349
|
}
|
|
279
|
-
__name(
|
|
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
|
-
|
|
365
|
+
attempts;
|
|
294
366
|
mailSender;
|
|
295
367
|
userLookup;
|
|
296
368
|
urlGenerator;
|
|
297
369
|
eventEmitter;
|
|
298
|
-
logger = new
|
|
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.
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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:
|
|
523
|
-
useFactory:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
839
|
+
createStripeCheckoutSession,
|
|
840
|
+
createStripeClient,
|
|
841
|
+
decodePayPalWebhookEvent,
|
|
842
|
+
friendlyErrorReason,
|
|
843
|
+
observePayPalOrder,
|
|
844
|
+
observeStripeCheckoutSession,
|
|
845
|
+
observeStripePaymentIntent,
|
|
846
|
+
retrieveStripeCheckoutSession,
|
|
847
|
+
retrieveStripePaymentIntent,
|
|
848
|
+
stripeResourceId,
|
|
849
|
+
verifyStripeWebhookEvent
|
|
560
850
|
};
|