payment-kit 1.18.56 → 1.19.1
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/.eslintrc.js +6 -0
- package/api/src/crons/index.ts +8 -0
- package/api/src/index.ts +4 -0
- package/api/src/libs/credit-grant.ts +146 -0
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/invoice.ts +4 -3
- package/api/src/libs/notification/template/base.ts +388 -2
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
- package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
- package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
- package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
- package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
- package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
- package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
- package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
- package/api/src/libs/payment.ts +69 -0
- package/api/src/libs/queue/index.ts +3 -2
- package/api/src/libs/session.ts +8 -0
- package/api/src/libs/subscription.ts +74 -3
- package/api/src/libs/ws.ts +23 -1
- package/api/src/locales/en.ts +33 -0
- package/api/src/locales/zh.ts +31 -0
- package/api/src/queues/credit-consume.ts +715 -0
- package/api/src/queues/credit-grant.ts +572 -0
- package/api/src/queues/notification.ts +173 -128
- package/api/src/queues/payment.ts +210 -122
- package/api/src/queues/subscription.ts +179 -0
- package/api/src/routes/checkout-sessions.ts +157 -9
- package/api/src/routes/connect/shared.ts +3 -2
- package/api/src/routes/credit-grants.ts +241 -0
- package/api/src/routes/credit-transactions.ts +208 -0
- package/api/src/routes/index.ts +8 -0
- package/api/src/routes/meter-events.ts +347 -0
- package/api/src/routes/meters.ts +219 -0
- package/api/src/routes/payment-currencies.ts +14 -2
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +14 -2
- package/api/src/routes/prices.ts +43 -0
- package/api/src/routes/pricing-table.ts +13 -7
- package/api/src/routes/products.ts +63 -4
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/subscriptions.ts +4 -0
- package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
- package/api/src/store/models/credit-grant.ts +486 -0
- package/api/src/store/models/credit-transaction.ts +268 -0
- package/api/src/store/models/customer.ts +8 -0
- package/api/src/store/models/index.ts +52 -1
- package/api/src/store/models/meter-event.ts +423 -0
- package/api/src/store/models/meter.ts +176 -0
- package/api/src/store/models/payment-currency.ts +66 -14
- package/api/src/store/models/price.ts +6 -0
- package/api/src/store/models/product.ts +2 -2
- package/api/src/store/models/subscription.ts +24 -0
- package/api/src/store/models/types.ts +28 -2
- package/api/tests/libs/subscription.spec.ts +53 -0
- package/blocklet.yml +9 -1
- package/package.json +57 -58
- package/scripts/sdk.js +233 -1
- package/src/app.tsx +10 -0
- package/src/components/actions.tsx +22 -9
- package/src/components/balance-list.tsx +40 -12
- package/src/components/collapse.tsx +33 -15
- package/src/components/copyable.tsx +8 -7
- package/src/components/currency.tsx +15 -7
- package/src/components/customer/actions.tsx +1 -5
- package/src/components/customer/credit-grant-item-list.tsx +99 -0
- package/src/components/customer/credit-overview.tsx +233 -0
- package/src/components/customer/form.tsx +7 -2
- package/src/components/customer/link.tsx +4 -12
- package/src/components/customer/notification-preference.tsx +18 -9
- package/src/components/customer/overdraft-protection.tsx +112 -41
- package/src/components/drawer-form.tsx +42 -18
- package/src/components/error.tsx +1 -5
- package/src/components/event/list.tsx +9 -10
- package/src/components/filter-toolbar.tsx +20 -19
- package/src/components/info-card.tsx +32 -18
- package/src/components/info-metric.tsx +16 -6
- package/src/components/info-row-group.tsx +1 -7
- package/src/components/info-row.tsx +30 -24
- package/src/components/invoice/action.tsx +1 -7
- package/src/components/invoice/list.tsx +34 -26
- package/src/components/invoice/recharge.tsx +5 -7
- package/src/components/invoice/table.tsx +17 -12
- package/src/components/layout/user.tsx +1 -1
- package/src/components/metadata/form.tsx +290 -94
- package/src/components/metadata/list.tsx +11 -3
- package/src/components/meter/actions.tsx +101 -0
- package/src/components/meter/add-usage-dialog.tsx +239 -0
- package/src/components/meter/events-list.tsx +657 -0
- package/src/components/meter/form.tsx +245 -0
- package/src/components/meter/products.tsx +264 -0
- package/src/components/meter/usage-guide.tsx +174 -0
- package/src/components/passport/actions.tsx +9 -4
- package/src/components/payment-currency/add.tsx +16 -3
- package/src/components/payment-currency/form.tsx +14 -6
- package/src/components/payment-intent/actions.tsx +24 -16
- package/src/components/payment-intent/list.tsx +30 -9
- package/src/components/payment-link/actions.tsx +1 -5
- package/src/components/payment-link/after-pay.tsx +4 -2
- package/src/components/payment-link/before-pay.tsx +14 -4
- package/src/components/payment-link/item.tsx +27 -6
- package/src/components/payment-link/preview.tsx +9 -9
- package/src/components/payment-link/product-select.tsx +69 -15
- package/src/components/payment-method/arcblock.tsx +8 -1
- package/src/components/payment-method/base.tsx +8 -1
- package/src/components/payment-method/bitcoin.tsx +8 -1
- package/src/components/payment-method/ethereum.tsx +8 -1
- package/src/components/payment-method/evm-rpc-input.tsx +11 -7
- package/src/components/payment-method/form.tsx +2 -7
- package/src/components/payment-method/stripe.tsx +2 -0
- package/src/components/payouts/actions.tsx +1 -5
- package/src/components/payouts/list.tsx +30 -10
- package/src/components/payouts/portal/list.tsx +11 -9
- package/src/components/price/currency-select.tsx +63 -32
- package/src/components/price/form.tsx +895 -370
- package/src/components/price/upsell-select.tsx +10 -2
- package/src/components/price/upsell.tsx +7 -2
- package/src/components/pricing-table/actions.tsx +1 -5
- package/src/components/pricing-table/customer-settings.tsx +5 -1
- package/src/components/pricing-table/payment-settings.tsx +14 -4
- package/src/components/pricing-table/preview.tsx +9 -9
- package/src/components/pricing-table/price-item.tsx +6 -1
- package/src/components/pricing-table/product-item.tsx +6 -1
- package/src/components/pricing-table/product-settings.tsx +17 -4
- package/src/components/product/actions.tsx +1 -5
- package/src/components/product/add-price.tsx +9 -7
- package/src/components/product/create.tsx +8 -9
- package/src/components/product/cross-sell-select.tsx +5 -1
- package/src/components/product/cross-sell.tsx +7 -2
- package/src/components/product/edit-price.tsx +21 -12
- package/src/components/product/features.tsx +26 -6
- package/src/components/product/form.tsx +115 -72
- package/src/components/progress-bar.tsx +1 -1
- package/src/components/refund/actions.tsx +1 -7
- package/src/components/refund/list.tsx +31 -18
- package/src/components/section/header.tsx +12 -14
- package/src/components/subscription/actions/cancel.tsx +22 -5
- package/src/components/subscription/actions/index.tsx +9 -10
- package/src/components/subscription/actions/pause.tsx +32 -6
- package/src/components/subscription/actions/slash-stake.tsx +5 -3
- package/src/components/subscription/description.tsx +12 -8
- package/src/components/subscription/items/index.tsx +31 -16
- package/src/components/subscription/items/usage-records.tsx +19 -5
- package/src/components/subscription/list.tsx +5 -7
- package/src/components/subscription/metrics.tsx +62 -15
- package/src/components/subscription/portal/actions.tsx +78 -71
- package/src/components/subscription/portal/cancel.tsx +10 -3
- package/src/components/subscription/portal/list.tsx +48 -26
- package/src/components/uploader.tsx +5 -13
- package/src/components/webhook/attempts.tsx +51 -16
- package/src/components/webhook/request-info.tsx +8 -6
- package/src/contexts/products.tsx +27 -10
- package/src/hooks/subscription.ts +34 -0
- package/src/libs/meter-utils.ts +196 -0
- package/src/libs/util.ts +4 -0
- package/src/locales/en.tsx +385 -4
- package/src/locales/zh.tsx +364 -0
- package/src/pages/admin/billing/index.tsx +61 -33
- package/src/pages/admin/billing/invoices/detail.tsx +49 -13
- package/src/pages/admin/billing/meters/create.tsx +60 -0
- package/src/pages/admin/billing/meters/detail.tsx +435 -0
- package/src/pages/admin/billing/meters/index.tsx +210 -0
- package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
- package/src/pages/admin/customers/customers/detail.tsx +67 -14
- package/src/pages/admin/customers/customers/index.tsx +6 -1
- package/src/pages/admin/customers/index.tsx +5 -0
- package/src/pages/admin/developers/events/detail.tsx +37 -11
- package/src/pages/admin/developers/index.tsx +1 -1
- package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
- package/src/pages/admin/index.tsx +15 -2
- package/src/pages/admin/overview.tsx +107 -19
- package/src/pages/admin/payments/intents/detail.tsx +58 -14
- package/src/pages/admin/payments/payouts/detail.tsx +63 -15
- package/src/pages/admin/payments/refunds/detail.tsx +58 -14
- package/src/pages/admin/products/index.tsx +11 -4
- package/src/pages/admin/products/links/create.tsx +22 -4
- package/src/pages/admin/products/links/detail.tsx +43 -14
- package/src/pages/admin/products/passports/index.tsx +23 -4
- package/src/pages/admin/products/prices/actions.tsx +16 -9
- package/src/pages/admin/products/prices/detail.tsx +73 -14
- package/src/pages/admin/products/prices/list.tsx +15 -3
- package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
- package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
- package/src/pages/admin/products/products/create.tsx +233 -54
- package/src/pages/admin/products/products/detail.tsx +74 -18
- package/src/pages/admin/settings/index.tsx +8 -1
- package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
- package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
- package/src/pages/admin/settings/vault-config/index.tsx +57 -10
- package/src/pages/customer/credit-grant/detail.tsx +308 -0
- package/src/pages/customer/index.tsx +76 -17
- package/src/pages/customer/invoice/detail.tsx +63 -14
- package/src/pages/customer/invoice/past-due.tsx +11 -3
- package/src/pages/customer/payout/detail.tsx +56 -13
- package/src/pages/customer/recharge/account.tsx +78 -18
- package/src/pages/customer/recharge/subscription.tsx +86 -25
- package/src/pages/customer/refund/list.tsx +60 -24
- package/src/pages/customer/subscription/change-payment.tsx +17 -6
- package/src/pages/customer/subscription/change-plan.tsx +34 -7
- package/src/pages/customer/subscription/detail.tsx +134 -34
- package/src/pages/customer/subscription/embed.tsx +25 -5
- package/src/pages/home.tsx +26 -4
- package/src/pages/integrations/donations/edit-form.tsx +25 -9
- package/src/pages/integrations/donations/index.tsx +26 -9
- package/src/pages/integrations/donations/preview.tsx +59 -15
- package/src/pages/integrations/index.tsx +10 -1
- package/src/pages/integrations/overview.tsx +78 -17
- package/vite.config.ts +60 -30
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
SubscriptionTrialStartEmailTemplateOptions,
|
|
39
39
|
} from '../libs/notification/template/subscription-trial-start';
|
|
40
40
|
import {
|
|
41
|
-
|
|
41
|
+
SubscriptionTrialWillEndEmailTemplate,
|
|
42
42
|
SubscriptionTrialWillEndEmailTemplateOptions,
|
|
43
43
|
} from '../libs/notification/template/subscription-trial-will-end';
|
|
44
44
|
import {
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
Refund,
|
|
68
68
|
Subscription,
|
|
69
69
|
Customer,
|
|
70
|
+
CreditGrant,
|
|
70
71
|
} from '../store/models';
|
|
71
72
|
import {
|
|
72
73
|
UsageReportEmptyEmailTemplate,
|
|
@@ -80,6 +81,18 @@ import {
|
|
|
80
81
|
OverdraftProtectionExhaustedEmailTemplate,
|
|
81
82
|
OverdraftProtectionExhaustedEmailTemplateOptions,
|
|
82
83
|
} from '../libs/notification/template/subscription-overdraft-protection-exhausted';
|
|
84
|
+
import {
|
|
85
|
+
CustomerCreditInsufficientEmailTemplate,
|
|
86
|
+
CustomerCreditInsufficientEmailTemplateOptions,
|
|
87
|
+
} from '../libs/notification/template/customer-credit-insufficient';
|
|
88
|
+
import {
|
|
89
|
+
CustomerCreditGrantGrantedEmailTemplate,
|
|
90
|
+
CustomerCreditGrantGrantedEmailTemplateOptions,
|
|
91
|
+
} from '../libs/notification/template/customer-credit-grant-granted';
|
|
92
|
+
import {
|
|
93
|
+
CustomerCreditGrantLowBalanceEmailTemplate,
|
|
94
|
+
CustomerCreditGrantLowBalanceEmailTemplateOptions,
|
|
95
|
+
} from '../libs/notification/template/customer-credit-grant-low-balance';
|
|
83
96
|
import {
|
|
84
97
|
CustomerRevenueSucceededEmailTemplate,
|
|
85
98
|
CustomerRevenueSucceededEmailTemplateOptions,
|
|
@@ -108,7 +121,10 @@ export type NotificationQueueJobType =
|
|
|
108
121
|
| 'customer.reward.succeeded'
|
|
109
122
|
| 'usage.report.empty'
|
|
110
123
|
| 'billing.discrepancy'
|
|
111
|
-
| 'subscription.overdraftProtection.exhausted'
|
|
124
|
+
| 'subscription.overdraftProtection.exhausted'
|
|
125
|
+
| 'customer.credit.insufficient'
|
|
126
|
+
| 'customer.credit_grant.granted'
|
|
127
|
+
| 'customer.credit_grant.low_balance';
|
|
112
128
|
|
|
113
129
|
export type NotificationQueueJob = {
|
|
114
130
|
type: NotificationQueueJobType;
|
|
@@ -200,7 +216,7 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
200
216
|
return new SubscriptionWillRenewEmailTemplate(job.options as SubscriptionWillRenewEmailTemplateOptions);
|
|
201
217
|
}
|
|
202
218
|
if (job.type === 'customer.subscription.trial_will_end') {
|
|
203
|
-
return new
|
|
219
|
+
return new SubscriptionTrialWillEndEmailTemplate(job.options as SubscriptionTrialWillEndEmailTemplateOptions);
|
|
204
220
|
}
|
|
205
221
|
if (job.type === 'customer.subscription.will_canceled') {
|
|
206
222
|
return new SubscriptionWillCanceledEmailTemplate(job.options as SubscriptionWillCanceledEmailTemplateOptions);
|
|
@@ -229,6 +245,20 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
229
245
|
return new CustomerRevenueSucceededEmailTemplate(job.options as CustomerRevenueSucceededEmailTemplateOptions);
|
|
230
246
|
}
|
|
231
247
|
|
|
248
|
+
if (job.type === 'customer.credit.insufficient') {
|
|
249
|
+
return new CustomerCreditInsufficientEmailTemplate(job.options as CustomerCreditInsufficientEmailTemplateOptions);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (job.type === 'customer.credit_grant.granted') {
|
|
253
|
+
return new CustomerCreditGrantGrantedEmailTemplate(job.options as CustomerCreditGrantGrantedEmailTemplateOptions);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (job.type === 'customer.credit_grant.low_balance') {
|
|
257
|
+
return new CustomerCreditGrantLowBalanceEmailTemplate(
|
|
258
|
+
job.options as CustomerCreditGrantLowBalanceEmailTemplateOptions
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
232
262
|
throw new Error(`Unknown job type: ${job.type}`);
|
|
233
263
|
}
|
|
234
264
|
|
|
@@ -252,24 +282,28 @@ interface NotificationItem<T = Record<string, any>> {
|
|
|
252
282
|
data: T;
|
|
253
283
|
}
|
|
254
284
|
|
|
285
|
+
// 内存缓存,记录最近发送的通知
|
|
286
|
+
const notificationCache = new Map<string, number>();
|
|
287
|
+
|
|
288
|
+
// 清理过期缓存的定时器
|
|
289
|
+
setInterval(
|
|
290
|
+
() => {
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
for (const [key, timestamp] of notificationCache.entries()) {
|
|
293
|
+
// 清理超过24小时的缓存记录
|
|
294
|
+
if (now - timestamp > 24 * 60 * 60 * 1000) {
|
|
295
|
+
notificationCache.delete(key);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
60 * 60 * 1000
|
|
300
|
+
); // 每小时清理一次
|
|
301
|
+
|
|
255
302
|
/**
|
|
256
303
|
* Handles immediate notifications by pushing them directly to the notification queue
|
|
257
304
|
*/
|
|
258
305
|
function handleImmediateNotification(type: string, data: Record<string, any>, extraIds: string[] = []) {
|
|
259
|
-
|
|
260
|
-
if (extraIds.length) {
|
|
261
|
-
idParts.push(...extraIds);
|
|
262
|
-
} else {
|
|
263
|
-
idParts.push(Date.now().toString());
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return notificationQueue.push({
|
|
267
|
-
id: idParts.join('.'),
|
|
268
|
-
job: {
|
|
269
|
-
type,
|
|
270
|
-
options: data,
|
|
271
|
-
},
|
|
272
|
-
});
|
|
306
|
+
return addNotificationJob(type as NotificationQueueJobType, data, extraIds);
|
|
273
307
|
}
|
|
274
308
|
|
|
275
309
|
/**
|
|
@@ -423,29 +457,13 @@ export const aggregatedNotificationQueue = createQueue<AggregatedNotificationJob
|
|
|
423
457
|
export async function startNotificationQueue() {
|
|
424
458
|
// 试用期开始
|
|
425
459
|
events.on('customer.subscription.trial_start', (subscription: Subscription) => {
|
|
426
|
-
|
|
427
|
-
id: `customer.subscription.trial_start.${subscription.id}`,
|
|
428
|
-
job: {
|
|
429
|
-
type: 'customer.subscription.trial_start',
|
|
430
|
-
options: {
|
|
431
|
-
subscriptionId: subscription.id,
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
});
|
|
460
|
+
addNotificationJob('customer.subscription.trial_start', { subscriptionId: subscription.id }, [subscription.id]);
|
|
435
461
|
});
|
|
436
462
|
|
|
437
463
|
events.on('customer.subscription.started', (subscription: Subscription) => {
|
|
438
464
|
if (!subscription.trial_start) {
|
|
439
465
|
// 没有试用期的 subscription 通知
|
|
440
|
-
|
|
441
|
-
id: `customer.subscription.started.${subscription.id}`,
|
|
442
|
-
job: {
|
|
443
|
-
type: 'customer.subscription.started',
|
|
444
|
-
options: {
|
|
445
|
-
subscriptionId: subscription.id,
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
});
|
|
466
|
+
addNotificationJob('customer.subscription.started', { subscriptionId: subscription.id }, [subscription.id]);
|
|
449
467
|
}
|
|
450
468
|
});
|
|
451
469
|
|
|
@@ -454,27 +472,13 @@ export async function startNotificationQueue() {
|
|
|
454
472
|
if (checkoutSession.mode === 'payment') {
|
|
455
473
|
const paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
456
474
|
if (paymentLink?.submit_type === 'donate') {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
type: 'customer.reward.succeeded',
|
|
461
|
-
options: {
|
|
462
|
-
checkoutSessionId: checkoutSession.id,
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
});
|
|
475
|
+
addNotificationJob('customer.reward.succeeded', { checkoutSessionId: checkoutSession.id }, [
|
|
476
|
+
checkoutSession.id,
|
|
477
|
+
]);
|
|
466
478
|
return;
|
|
467
479
|
}
|
|
468
480
|
|
|
469
|
-
|
|
470
|
-
id: `checkout.session.completed.${checkoutSession.id}`,
|
|
471
|
-
job: {
|
|
472
|
-
type: 'checkout.session.completed',
|
|
473
|
-
options: {
|
|
474
|
-
checkoutSessionId: checkoutSession.id,
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
});
|
|
481
|
+
addNotificationJob('checkout.session.completed', { checkoutSessionId: checkoutSession.id }, [checkoutSession.id]);
|
|
478
482
|
}
|
|
479
483
|
});
|
|
480
484
|
|
|
@@ -523,106 +527,95 @@ export async function startNotificationQueue() {
|
|
|
523
527
|
});
|
|
524
528
|
|
|
525
529
|
if (invoice && subscription.metadata.renew_failed_reason) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
invoice,
|
|
532
|
-
result: subscription.metadata.renew_failed_reason,
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
});
|
|
530
|
+
addNotificationJob(
|
|
531
|
+
'customer.subscription.renew_failed',
|
|
532
|
+
{ invoice, result: subscription.metadata.renew_failed_reason },
|
|
533
|
+
[subscription.id, invoice.id]
|
|
534
|
+
);
|
|
536
535
|
}
|
|
537
536
|
});
|
|
538
537
|
|
|
539
538
|
events.on('customer.subscription.upgraded', (subscription: Subscription) => {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
subscriptionId: subscription.id,
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
});
|
|
539
|
+
const extraIds = [subscription.id];
|
|
540
|
+
if (subscription.latest_invoice_id) {
|
|
541
|
+
extraIds.push(subscription.latest_invoice_id);
|
|
542
|
+
}
|
|
543
|
+
addNotificationJob('customer.subscription.upgraded', { subscriptionId: subscription.id }, extraIds);
|
|
549
544
|
});
|
|
550
545
|
|
|
551
546
|
events.on('refund.succeeded', (refund: Refund) => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
refundId: refund.id,
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
});
|
|
547
|
+
const extraIds = [refund.id];
|
|
548
|
+
if (refund.subscription_id) {
|
|
549
|
+
extraIds.unshift(refund.subscription_id);
|
|
550
|
+
}
|
|
551
|
+
addNotificationJob('refund.succeeded', { refundId: refund.id }, extraIds);
|
|
561
552
|
});
|
|
562
553
|
|
|
563
554
|
events.on('customer.subscription.deleted', (subscription: Subscription) => {
|
|
564
|
-
|
|
565
|
-
id: `customer.subscription.deleted.${subscription.id}`,
|
|
566
|
-
job: {
|
|
567
|
-
type: 'customer.subscription.deleted',
|
|
568
|
-
options: {
|
|
569
|
-
subscriptionId: subscription.id,
|
|
570
|
-
},
|
|
571
|
-
},
|
|
572
|
-
});
|
|
555
|
+
addNotificationJob('customer.subscription.deleted', { subscriptionId: subscription.id }, [subscription.id]);
|
|
573
556
|
});
|
|
574
557
|
|
|
575
558
|
events.on('usage.report.empty', (subscription: Subscription, { usageReportStart, usageReportEnd }) => {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
type: 'usage.report.empty',
|
|
580
|
-
options: {
|
|
581
|
-
subscriptionId: subscription.id,
|
|
582
|
-
usageReportStart,
|
|
583
|
-
usageReportEnd,
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
});
|
|
559
|
+
addNotificationJob('usage.report.empty', { subscriptionId: subscription.id, usageReportStart, usageReportEnd }, [
|
|
560
|
+
subscription.id,
|
|
561
|
+
]);
|
|
587
562
|
});
|
|
588
563
|
|
|
589
564
|
events.on('billing.discrepancy', (invoice: Invoice) => {
|
|
590
|
-
|
|
591
|
-
id: `billing.discrepancy.${invoice.id}`,
|
|
592
|
-
job: {
|
|
593
|
-
type: 'billing.discrepancy',
|
|
594
|
-
options: {
|
|
595
|
-
invoiceId: invoice.id,
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
});
|
|
565
|
+
addNotificationJob('billing.discrepancy', { invoiceId: invoice.id }, [invoice.id]);
|
|
599
566
|
});
|
|
600
567
|
|
|
601
568
|
events.on('subscription.overdraft_protection.exhausted', (subscription: Subscription) => {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
type: 'subscription.overdraftProtection.exhausted',
|
|
606
|
-
options: {
|
|
607
|
-
subscriptionId: subscription.id,
|
|
608
|
-
},
|
|
609
|
-
},
|
|
610
|
-
});
|
|
569
|
+
addNotificationJob('subscription.overdraftProtection.exhausted', { subscriptionId: subscription.id }, [
|
|
570
|
+
subscription.id,
|
|
571
|
+
]);
|
|
611
572
|
});
|
|
612
573
|
|
|
613
574
|
events.on('payout.paid', (payout: Payout) => {
|
|
614
575
|
if (payout.customer_id) {
|
|
615
|
-
|
|
616
|
-
id: `payout.paid.${payout.id}`,
|
|
617
|
-
job: {
|
|
618
|
-
type: 'payout.paid',
|
|
619
|
-
options: {
|
|
620
|
-
payoutId: payout.id,
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
});
|
|
576
|
+
addNotificationJob('payout.paid', { payoutId: payout.id }, [payout.id]);
|
|
624
577
|
}
|
|
625
578
|
});
|
|
579
|
+
|
|
580
|
+
events.on('customer.credit_grant.granted', (creditGrant: CreditGrant) => {
|
|
581
|
+
addNotificationJob(
|
|
582
|
+
'customer.credit_grant.granted',
|
|
583
|
+
{
|
|
584
|
+
creditGrantId: creditGrant.id,
|
|
585
|
+
},
|
|
586
|
+
[creditGrant.id]
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
events.on('customer.credit_grant.low_balance', (creditGrant: CreditGrant) => {
|
|
591
|
+
addNotificationJob(
|
|
592
|
+
'customer.credit_grant.low_balance',
|
|
593
|
+
{
|
|
594
|
+
creditGrantId: creditGrant.id,
|
|
595
|
+
},
|
|
596
|
+
[creditGrant.id],
|
|
597
|
+
true,
|
|
598
|
+
24 * 3600 // 1 天
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
events.on('customer.credit.insufficient', (customer: Customer, { metadata }: { metadata: any }) => {
|
|
603
|
+
addNotificationJob(
|
|
604
|
+
'customer.credit.insufficient',
|
|
605
|
+
{
|
|
606
|
+
customerId: customer.id,
|
|
607
|
+
currencyId: metadata.currency_id,
|
|
608
|
+
meterEventName: metadata.meter_event_name || 'Service',
|
|
609
|
+
requiredAmount: metadata.required_amount,
|
|
610
|
+
availableAmount: metadata.available_amount,
|
|
611
|
+
pendingAmount: metadata.pending_amount || '0',
|
|
612
|
+
subscriptionId: metadata.subscription_id,
|
|
613
|
+
},
|
|
614
|
+
[customer.id, metadata.currency_id, metadata.subscription_id],
|
|
615
|
+
true,
|
|
616
|
+
600
|
|
617
|
+
);
|
|
618
|
+
});
|
|
626
619
|
}
|
|
627
620
|
|
|
628
621
|
export async function handleNotificationPreferenceChange(
|
|
@@ -705,3 +698,55 @@ export async function handleNotificationPreferenceChange(
|
|
|
705
698
|
/* eslint-enable no-await-in-loop */
|
|
706
699
|
}
|
|
707
700
|
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Add a notification job to the queue
|
|
704
|
+
* @param type Notification type
|
|
705
|
+
* @param options Notification options
|
|
706
|
+
* @param extraIds Extra IDs for building a unique job ID, if not provided, use timestamp
|
|
707
|
+
* @param preventDuplicate Whether to prevent duplicate, if true, will check the cache
|
|
708
|
+
* @param duplicateWindow Duplicate prevention time window (seconds), default 600 seconds (10 minutes)
|
|
709
|
+
*/
|
|
710
|
+
export function addNotificationJob(
|
|
711
|
+
type: NotificationQueueJobType,
|
|
712
|
+
options: NotificationQueueJobOptions,
|
|
713
|
+
extraIds: string[] = [],
|
|
714
|
+
preventDuplicate: boolean = false,
|
|
715
|
+
duplicateWindow: number = 600 // 10分钟
|
|
716
|
+
) {
|
|
717
|
+
const idParts = [type];
|
|
718
|
+
|
|
719
|
+
if (extraIds.length) {
|
|
720
|
+
idParts.push(...extraIds);
|
|
721
|
+
} else {
|
|
722
|
+
idParts.push(Date.now().toString());
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// if prevent duplicate, check the cache
|
|
726
|
+
if (preventDuplicate) {
|
|
727
|
+
const cacheKey = `${type}.${extraIds.join('.')}`;
|
|
728
|
+
const lastSent = notificationCache.get(cacheKey);
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
|
|
731
|
+
if (lastSent && now - lastSent < duplicateWindow * 1000) {
|
|
732
|
+
logger.info('Notification skipped due to duplicate prevention', {
|
|
733
|
+
type,
|
|
734
|
+
cacheKey,
|
|
735
|
+
lastSent: new Date(lastSent),
|
|
736
|
+
duplicateWindow,
|
|
737
|
+
});
|
|
738
|
+
return Promise.resolve(); // 跳过重复通知
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 记录本次发送时间
|
|
742
|
+
notificationCache.set(cacheKey, now);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return notificationQueue.push({
|
|
746
|
+
id: idParts.join('.'),
|
|
747
|
+
job: {
|
|
748
|
+
type,
|
|
749
|
+
options,
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
}
|