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
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import type { ManipulateType } from 'dayjs';
|
|
4
|
-
import prettyMsI18n from 'pretty-ms-i18n';
|
|
5
4
|
import type { LiteralUnion } from 'type-fest';
|
|
6
5
|
|
|
7
6
|
import { fromUnitToToken } from '@ocap/util';
|
|
8
7
|
import dayjs from '../../dayjs';
|
|
9
8
|
import { getTokenByAddress } from '../../../integrations/arcblock/stake';
|
|
10
|
-
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
11
9
|
import { translate } from '../../../locales';
|
|
12
|
-
import {
|
|
13
|
-
Customer,
|
|
14
|
-
Invoice,
|
|
15
|
-
PaymentMethod,
|
|
16
|
-
Price,
|
|
17
|
-
Subscription,
|
|
18
|
-
SubscriptionItem,
|
|
19
|
-
PaymentCurrency,
|
|
20
|
-
} from '../../../store/models';
|
|
10
|
+
import { Invoice, Price, SubscriptionItem } from '../../../store/models';
|
|
21
11
|
import type { PaymentDetail } from '../../payment';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
getPaymentAmountForCycleSubscription,
|
|
27
|
-
} from '../../subscription';
|
|
28
|
-
import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
|
|
29
|
-
import { formatCurrencyInfo, getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
30
|
-
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
12
|
+
import { getSubscriptionPaymentAddress, getPaymentAmountForCycleSubscription } from '../../subscription';
|
|
13
|
+
import { formatTime, getSimplifyDuration } from '../../time';
|
|
14
|
+
import { formatCurrencyInfo, getCustomerRechargeLink } from '../../util';
|
|
15
|
+
import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
|
|
31
16
|
|
|
32
17
|
export interface SubscriptionWillRenewEmailTemplateOptions {
|
|
33
18
|
subscriptionId: string;
|
|
@@ -43,110 +28,109 @@ interface SubscriptionWillRenewEmailTemplateContext {
|
|
|
43
28
|
willRenewDuration: string;
|
|
44
29
|
paymentDetail: PaymentDetail;
|
|
45
30
|
paidType: string;
|
|
46
|
-
|
|
47
31
|
userDid: string;
|
|
48
32
|
paymentInfo: string;
|
|
49
33
|
currentPeriodStart: string;
|
|
50
34
|
currentPeriodEnd: string;
|
|
51
35
|
duration: string;
|
|
52
|
-
|
|
53
36
|
viewSubscriptionLink: string;
|
|
54
37
|
addFundsLink: string;
|
|
55
|
-
paymentMethod: PaymentMethod | null;
|
|
56
38
|
customActions: any[];
|
|
39
|
+
isCreditSubscription: boolean;
|
|
40
|
+
isStripe: boolean;
|
|
57
41
|
}
|
|
58
42
|
|
|
59
|
-
export class SubscriptionWillRenewEmailTemplate
|
|
60
|
-
implements BaseEmailTemplate<SubscriptionWillRenewEmailTemplateContext>
|
|
61
|
-
{
|
|
43
|
+
export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionWillRenewEmailTemplateContext> {
|
|
62
44
|
options: SubscriptionWillRenewEmailTemplateOptions;
|
|
63
45
|
|
|
64
46
|
constructor(options: SubscriptionWillRenewEmailTemplateOptions) {
|
|
47
|
+
super();
|
|
65
48
|
this.options = options;
|
|
66
49
|
}
|
|
67
50
|
|
|
68
51
|
async getContext(): Promise<SubscriptionWillRenewEmailTemplateContext> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
// 获取基础订阅数据
|
|
53
|
+
const basicData = await this.getSubscriptionBasicData(this.options.subscriptionId);
|
|
54
|
+
const { subscription, userDid, locale, productName, paymentCurrency, isCreditSubscription } = basicData;
|
|
55
|
+
|
|
73
56
|
if (subscription.isActive() === false) {
|
|
74
57
|
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
75
58
|
}
|
|
76
59
|
if (subscription.isScheduledToCancel()) {
|
|
77
60
|
throw new Error(`Subscription is scheduled to cancel: ${this.options.subscriptionId}`);
|
|
78
61
|
}
|
|
79
|
-
if (
|
|
62
|
+
if (this.subscriptionIsRenewed(subscription)) {
|
|
80
63
|
throw new Error(`The subscription(${this.options.subscriptionId}) has been renewed`);
|
|
81
64
|
}
|
|
82
65
|
|
|
83
|
-
const
|
|
84
|
-
if (!customer) {
|
|
85
|
-
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
86
|
-
}
|
|
66
|
+
const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
87
67
|
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
68
|
+
// 获取支付信息
|
|
69
|
+
const paymentInfoResult = await this.getPaymentInfo(
|
|
70
|
+
subscription,
|
|
71
|
+
paymentCurrency,
|
|
72
|
+
isCreditSubscription,
|
|
73
|
+
invoice?.id
|
|
74
|
+
);
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
// 获取链接
|
|
77
|
+
const links = this.generateSubscriptionLinks(
|
|
78
|
+
subscription,
|
|
79
|
+
locale,
|
|
80
|
+
userDid,
|
|
81
|
+
'customer.subscription.will_renew',
|
|
82
|
+
invoice?.id,
|
|
83
|
+
isCreditSubscription
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 计算时间和支付详情 - 使用invoice的period_end,如果不存在则使用subscription的current_period_end
|
|
87
|
+
const periodEnd = invoice?.period_end || subscription.current_period_end;
|
|
88
|
+
const at = formatTime(periodEnd * 1000);
|
|
89
|
+
const willRenewDuration = getSimplifyDuration((periodEnd - dayjs().unix()) * 1000, locale);
|
|
100
90
|
|
|
101
91
|
const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
102
92
|
const paymentDetail = { price: paymentAmount, balance: 0, symbol: paymentCurrency.symbol, balanceFormatted: '0' };
|
|
103
93
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
// 获取余额信息(非 Stripe 支付方式)
|
|
95
|
+
const isStripe = paymentInfoResult.paymentMethod?.type === 'stripe';
|
|
96
|
+
if (!isStripe && !isCreditSubscription) {
|
|
97
|
+
const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentInfoResult.paymentMethod!.type);
|
|
98
|
+
const balance = await getTokenByAddress(paymentAddress, paymentInfoResult.paymentMethod!, paymentCurrency);
|
|
99
|
+
paymentDetail.balanceFormatted = fromUnitToToken(balance, paymentCurrency.decimal);
|
|
100
|
+
paymentDetail.balance = +paymentDetail.balanceFormatted;
|
|
107
101
|
}
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
paymentDetail.balance = +paymentDetail.balanceFormatted;
|
|
113
|
-
|
|
114
|
-
const { isPrePaid, interval } = await this.getPaymentCategory({
|
|
115
|
-
subscriptionId: subscription.id,
|
|
116
|
-
});
|
|
117
|
-
const paidType: string = isPrePaid
|
|
103
|
+
// 获取支付类型和周期信息
|
|
104
|
+
const { isPrePaid, interval } = await this.getPaymentCategory(subscription.id);
|
|
105
|
+
const paidType = isPrePaid
|
|
118
106
|
? translate('notification.common.prepaid', locale)
|
|
119
107
|
: translate('notification.common.postpaid', locale);
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
: formatTime(invoice.period_end * 1000);
|
|
127
|
-
const duration: string = prettyMsI18n(
|
|
128
|
-
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
129
|
-
{
|
|
130
|
-
locale: getPrettyMsI18nLocale(locale),
|
|
131
|
-
}
|
|
108
|
+
|
|
109
|
+
const paymentInfo = formatCurrencyInfo(
|
|
110
|
+
paymentDetail?.price || '0',
|
|
111
|
+
paymentCurrency,
|
|
112
|
+
paymentInfoResult.paymentMethod,
|
|
113
|
+
true
|
|
132
114
|
);
|
|
133
115
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
116
|
+
// 计算周期时间 - 使用安全的回退机制
|
|
117
|
+
const periodStart = invoice?.period_start || subscription.current_period_start;
|
|
118
|
+
const periodInfo = this.formatSubscriptionPeriod(
|
|
119
|
+
isPrePaid ? periodEnd : periodStart,
|
|
120
|
+
isPrePaid
|
|
121
|
+
? dayjs(periodEnd * 1000)
|
|
122
|
+
.add(1, interval as ManipulateType)
|
|
123
|
+
.unix()
|
|
124
|
+
: periodEnd,
|
|
125
|
+
locale
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const addFundsLink = getCustomerRechargeLink({
|
|
140
129
|
locale,
|
|
141
130
|
userDid,
|
|
142
131
|
subscriptionId: subscription.id,
|
|
143
132
|
});
|
|
144
133
|
|
|
145
|
-
const customActions = getSubscriptionNotificationCustomActions(
|
|
146
|
-
subscription,
|
|
147
|
-
'customer.subscription.will_renew',
|
|
148
|
-
locale
|
|
149
|
-
);
|
|
150
134
|
return {
|
|
151
135
|
locale,
|
|
152
136
|
productName,
|
|
@@ -154,62 +138,49 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
154
138
|
willRenewDuration,
|
|
155
139
|
paymentDetail,
|
|
156
140
|
paidType,
|
|
157
|
-
|
|
158
141
|
userDid,
|
|
159
142
|
paymentInfo,
|
|
160
|
-
currentPeriodStart,
|
|
161
|
-
currentPeriodEnd,
|
|
162
|
-
duration,
|
|
163
|
-
|
|
164
|
-
viewSubscriptionLink,
|
|
143
|
+
currentPeriodStart: periodInfo.currentPeriodStart,
|
|
144
|
+
currentPeriodEnd: periodInfo.currentPeriodEnd,
|
|
145
|
+
duration: periodInfo.duration,
|
|
146
|
+
viewSubscriptionLink: links.viewSubscriptionLink,
|
|
165
147
|
addFundsLink,
|
|
166
|
-
|
|
167
|
-
|
|
148
|
+
customActions: links.customActions,
|
|
149
|
+
isCreditSubscription,
|
|
150
|
+
isStripe,
|
|
168
151
|
};
|
|
169
152
|
}
|
|
170
|
-
|
|
153
|
+
|
|
154
|
+
private async getPaymentCategory(subscriptionId: string): Promise<{
|
|
171
155
|
isPrePaid: boolean;
|
|
172
156
|
interval: LiteralUnion<'hour' | 'day' | 'week' | 'month' | 'year', string> | undefined;
|
|
173
157
|
}> {
|
|
174
158
|
const subscriptionItems = await SubscriptionItem.findAll({
|
|
175
|
-
where: {
|
|
176
|
-
subscription_id: subscriptionId,
|
|
177
|
-
},
|
|
159
|
+
where: { subscription_id: subscriptionId },
|
|
178
160
|
});
|
|
179
161
|
|
|
180
162
|
const lineItemExpanded = await Price.expand(subscriptionItems);
|
|
181
163
|
const metered = lineItemExpanded.find((lineItem) => lineItem.price.recurring?.usage_type === 'metered');
|
|
182
164
|
const recurringItems = lineItemExpanded.filter((lineItem) => lineItem.price.type === 'recurring');
|
|
183
165
|
|
|
184
|
-
const isPrePaid = !metered;
|
|
185
|
-
const interval = recurringItems?.[0]?.price.recurring?.interval;
|
|
186
|
-
|
|
187
166
|
return {
|
|
188
|
-
isPrePaid,
|
|
189
|
-
interval,
|
|
167
|
+
isPrePaid: !metered,
|
|
168
|
+
interval: recurringItems?.[0]?.price.recurring?.interval,
|
|
190
169
|
};
|
|
191
170
|
}
|
|
192
171
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* @description 该订阅已经在指定期限内续费过了吗?
|
|
196
|
-
* @param {Subscription} subscription
|
|
197
|
-
* @return {*} {Promise<void>}
|
|
198
|
-
* @memberof SubscriptionWillRenewEmailTemplate
|
|
199
|
-
*/
|
|
200
|
-
// eslint-disable-next-line require-await
|
|
201
|
-
async subscriptionIsRenewed(subscription: Subscription): Promise<boolean> {
|
|
202
|
-
// @note: 为了最精确的验证,这里的期限 this.options.willRenewValue 还要 + 1,如果没有订阅没有续费,那么 expectedCurrentPeriodEnd 一定是大于 currentPeriodEnd
|
|
203
|
-
const expectedCurrentPeriodEnd: number = dayjs()
|
|
172
|
+
private subscriptionIsRenewed(subscription: any): boolean {
|
|
173
|
+
const expectedCurrentPeriodEnd = dayjs()
|
|
204
174
|
.add(this.options.willRenewValue + 1, this.options.willRenewUnit)
|
|
205
175
|
.toDate()
|
|
206
176
|
.getTime();
|
|
207
|
-
const currentPeriodEnd
|
|
177
|
+
const currentPeriodEnd = subscription.current_period_end * 1000;
|
|
208
178
|
|
|
209
179
|
return currentPeriodEnd > expectedCurrentPeriodEnd;
|
|
210
180
|
}
|
|
211
181
|
|
|
212
182
|
async getTemplate(): Promise<BaseEmailTemplateType | null> {
|
|
183
|
+
const context = await this.getContext();
|
|
213
184
|
const {
|
|
214
185
|
locale,
|
|
215
186
|
productName,
|
|
@@ -217,195 +188,199 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
217
188
|
willRenewDuration,
|
|
218
189
|
paymentDetail,
|
|
219
190
|
paidType,
|
|
220
|
-
|
|
221
191
|
userDid,
|
|
222
192
|
paymentInfo,
|
|
223
193
|
currentPeriodStart,
|
|
224
194
|
currentPeriodEnd,
|
|
225
195
|
duration,
|
|
226
|
-
|
|
227
196
|
viewSubscriptionLink,
|
|
228
197
|
addFundsLink,
|
|
229
|
-
paymentMethod,
|
|
230
198
|
customActions,
|
|
231
|
-
|
|
199
|
+
isCreditSubscription,
|
|
200
|
+
isStripe,
|
|
201
|
+
} = context;
|
|
232
202
|
|
|
233
203
|
// 如果当前时间大于预计扣费时间,那么不发送通知
|
|
234
204
|
if (dayjs().utc().isAfter(dayjs.utc(at))) {
|
|
235
205
|
return null;
|
|
236
206
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
207
|
+
|
|
208
|
+
const canPay = paymentDetail.balance >= paymentDetail.price;
|
|
209
|
+
if (canPay && !this.options.required) {
|
|
240
210
|
return null;
|
|
241
211
|
}
|
|
212
|
+
|
|
242
213
|
if (!paymentDetail.price && paymentDetail.symbol !== 'USD') {
|
|
243
|
-
// 如果预估的价格是 0 并且货币不是 USD,那么直接不发送
|
|
244
214
|
return null;
|
|
245
215
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
: `${translate('notification.subscriptionWillRenew.unableToPayBody', locale, {
|
|
261
|
-
at,
|
|
262
|
-
productName,
|
|
263
|
-
willRenewDuration,
|
|
264
|
-
})}`,
|
|
265
|
-
// @ts-expect-error
|
|
266
|
-
attachments: [
|
|
267
|
-
{
|
|
268
|
-
type: 'section',
|
|
269
|
-
fields: [
|
|
270
|
-
{
|
|
271
|
-
type: 'text',
|
|
272
|
-
data: {
|
|
273
|
-
type: 'plain',
|
|
274
|
-
color: '#9397A1',
|
|
275
|
-
text: translate('notification.common.account', locale),
|
|
276
|
-
},
|
|
216
|
+
|
|
217
|
+
// 构建字段
|
|
218
|
+
const commonFields = this.buildCommonFields(userDid, productName, locale);
|
|
219
|
+
|
|
220
|
+
// 续费金额字段
|
|
221
|
+
const renewAmountFields = isCreditSubscription
|
|
222
|
+
? []
|
|
223
|
+
: [
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
data: {
|
|
227
|
+
type: 'plain',
|
|
228
|
+
color: '#9397A1',
|
|
229
|
+
text: translate('notification.subscriptionWillRenew.renewAmount', locale),
|
|
277
230
|
},
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: 'text',
|
|
234
|
+
data: {
|
|
235
|
+
type: 'plain',
|
|
236
|
+
text: paymentInfo,
|
|
284
237
|
},
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// 当前余额字段(非 Stripe 时显示)
|
|
242
|
+
const balanceFields =
|
|
243
|
+
!isStripe && !isCreditSubscription
|
|
244
|
+
? [
|
|
285
245
|
{
|
|
286
246
|
type: 'text',
|
|
287
247
|
data: {
|
|
288
248
|
type: 'plain',
|
|
289
249
|
color: '#9397A1',
|
|
290
|
-
text: translate('notification.common.
|
|
250
|
+
text: translate('notification.common.currentBalance', locale),
|
|
291
251
|
},
|
|
292
252
|
},
|
|
293
253
|
{
|
|
294
254
|
type: 'text',
|
|
295
255
|
data: {
|
|
296
256
|
type: 'plain',
|
|
297
|
-
|
|
257
|
+
...(!canPay && { color: '#FF0000' }),
|
|
258
|
+
text: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
|
|
298
259
|
},
|
|
299
260
|
},
|
|
261
|
+
]
|
|
262
|
+
: [];
|
|
263
|
+
|
|
264
|
+
// 余额不足提醒字段
|
|
265
|
+
const insufficientBalanceFields =
|
|
266
|
+
!canPay && !isStripe && !isCreditSubscription
|
|
267
|
+
? [
|
|
300
268
|
{
|
|
301
269
|
type: 'text',
|
|
302
270
|
data: {
|
|
303
271
|
type: 'plain',
|
|
304
272
|
color: '#9397A1',
|
|
305
|
-
text: translate('notification.
|
|
273
|
+
text: translate('notification.common.balanceReminder', locale),
|
|
306
274
|
},
|
|
307
275
|
},
|
|
308
276
|
{
|
|
309
277
|
type: 'text',
|
|
310
278
|
data: {
|
|
311
279
|
type: 'plain',
|
|
312
|
-
|
|
280
|
+
color: '#FF0000',
|
|
281
|
+
text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
|
|
282
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
283
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
284
|
+
}),
|
|
313
285
|
},
|
|
314
286
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
type: 'plain',
|
|
329
|
-
...(!canPay && {
|
|
330
|
-
color: '#FF0000',
|
|
331
|
-
}),
|
|
332
|
-
text: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
]
|
|
336
|
-
: []),
|
|
337
|
-
...(!canPay && !isStripe
|
|
338
|
-
? [
|
|
339
|
-
{
|
|
340
|
-
type: 'text',
|
|
341
|
-
data: {
|
|
342
|
-
type: 'plain',
|
|
343
|
-
color: '#9397A1',
|
|
344
|
-
text: translate('notification.common.balanceReminder', locale),
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
type: 'text',
|
|
349
|
-
data: {
|
|
350
|
-
type: 'plain',
|
|
351
|
-
color: '#FF0000',
|
|
352
|
-
text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
|
|
353
|
-
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
354
|
-
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
355
|
-
}),
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
]
|
|
359
|
-
: []),
|
|
360
|
-
{
|
|
361
|
-
type: 'text',
|
|
362
|
-
data: {
|
|
363
|
-
type: 'plain',
|
|
364
|
-
color: '#9397A1',
|
|
365
|
-
text: translate('notification.common.paidType', locale),
|
|
366
|
-
},
|
|
287
|
+
]
|
|
288
|
+
: [];
|
|
289
|
+
|
|
290
|
+
// 支付类型字段
|
|
291
|
+
const paidTypeFields = isCreditSubscription
|
|
292
|
+
? []
|
|
293
|
+
: [
|
|
294
|
+
{
|
|
295
|
+
type: 'text',
|
|
296
|
+
data: {
|
|
297
|
+
type: 'plain',
|
|
298
|
+
color: '#9397A1',
|
|
299
|
+
text: translate('notification.common.paidType', locale),
|
|
367
300
|
},
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
data: {
|
|
305
|
+
type: 'plain',
|
|
306
|
+
text: paidType,
|
|
374
307
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
// 支付周期字段
|
|
312
|
+
const periodFields = isCreditSubscription
|
|
313
|
+
? []
|
|
314
|
+
: [
|
|
315
|
+
{
|
|
316
|
+
type: 'text',
|
|
317
|
+
data: {
|
|
318
|
+
type: 'plain',
|
|
319
|
+
color: '#9397A1',
|
|
320
|
+
text: translate('notification.common.paymentPeriod', locale),
|
|
382
321
|
},
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: 'text',
|
|
325
|
+
data: {
|
|
326
|
+
type: 'plain',
|
|
327
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
389
328
|
},
|
|
390
|
-
].filter(Boolean),
|
|
391
|
-
},
|
|
392
|
-
].filter(Boolean),
|
|
393
|
-
// @ts-ignore
|
|
394
|
-
actions: [
|
|
395
|
-
!canPay &&
|
|
396
|
-
!isStripe &&
|
|
397
|
-
addFundsLink && {
|
|
398
|
-
name: translate('notification.common.addFunds', locale),
|
|
399
|
-
title: translate('notification.common.addFunds', locale),
|
|
400
|
-
link: addFundsLink,
|
|
401
329
|
},
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
// 构建操作按钮
|
|
333
|
+
const actions = [
|
|
334
|
+
!canPay &&
|
|
335
|
+
!isStripe &&
|
|
336
|
+
!isCreditSubscription &&
|
|
337
|
+
addFundsLink && {
|
|
338
|
+
name: translate('notification.common.addFunds', locale),
|
|
339
|
+
title: translate('notification.common.addFunds', locale),
|
|
340
|
+
link: addFundsLink,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: translate('notification.common.viewSubscription', locale),
|
|
344
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
345
|
+
link: viewSubscriptionLink,
|
|
346
|
+
},
|
|
347
|
+
...customActions,
|
|
348
|
+
].filter(Boolean);
|
|
349
|
+
|
|
350
|
+
const template: BaseEmailTemplateType = {
|
|
351
|
+
title: translate('notification.subscriptionWillRenew.title', locale, {
|
|
352
|
+
productName,
|
|
353
|
+
willRenewDuration,
|
|
354
|
+
}),
|
|
355
|
+
body:
|
|
356
|
+
canPay || isStripe
|
|
357
|
+
? translate('notification.subscriptionWillRenew.body', locale, {
|
|
358
|
+
at,
|
|
359
|
+
productName,
|
|
360
|
+
willRenewDuration,
|
|
361
|
+
balance: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
|
|
362
|
+
})
|
|
363
|
+
: translate('notification.subscriptionWillRenew.unableToPayBody', locale, {
|
|
364
|
+
at,
|
|
365
|
+
productName,
|
|
366
|
+
willRenewDuration,
|
|
367
|
+
}),
|
|
368
|
+
// @ts-expect-error
|
|
369
|
+
attachments: [
|
|
402
370
|
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
371
|
+
type: 'section',
|
|
372
|
+
fields: [
|
|
373
|
+
...commonFields,
|
|
374
|
+
...renewAmountFields,
|
|
375
|
+
...balanceFields,
|
|
376
|
+
...insufficientBalanceFields,
|
|
377
|
+
...paidTypeFields,
|
|
378
|
+
...periodFields,
|
|
379
|
+
].filter(Boolean),
|
|
406
380
|
},
|
|
407
|
-
...customActions,
|
|
408
381
|
].filter(Boolean),
|
|
382
|
+
// @ts-ignore
|
|
383
|
+
actions,
|
|
409
384
|
};
|
|
410
385
|
|
|
411
386
|
return template;
|