payment-kit 1.15.16 → 1.15.18
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/api/src/integrations/stripe/handlers/invoice.ts +20 -0
- package/api/src/integrations/stripe/resource.ts +2 -2
- package/api/src/libs/audit.ts +1 -1
- package/api/src/libs/invoice.ts +81 -1
- package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
- package/api/src/libs/notification/template/subscription-renewed.ts +11 -3
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +17 -0
- package/api/src/libs/notification/template/subscription-upgraded.ts +51 -26
- package/api/src/libs/notification/template/subscription-will-canceled.ts +16 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +15 -3
- package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
- package/api/src/libs/queue/index.ts +69 -19
- package/api/src/libs/queue/store.ts +28 -5
- package/api/src/libs/subscription.ts +129 -19
- package/api/src/libs/util.ts +30 -0
- package/api/src/locales/en.ts +13 -0
- package/api/src/locales/zh.ts +13 -0
- package/api/src/queues/invoice.ts +58 -20
- package/api/src/queues/notification.ts +43 -1
- package/api/src/queues/payment.ts +5 -1
- package/api/src/queues/subscription.ts +64 -15
- package/api/src/routes/checkout-sessions.ts +26 -0
- package/api/src/routes/invoices.ts +11 -31
- package/api/src/routes/subscriptions.ts +43 -7
- package/api/src/store/models/checkout-session.ts +2 -0
- package/api/src/store/models/job.ts +4 -0
- package/api/src/store/models/types.ts +22 -4
- package/api/src/store/models/usage-record.ts +5 -1
- package/api/tests/libs/subscription.spec.ts +154 -0
- package/api/tests/libs/util.spec.ts +135 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/scripts/sdk.js +37 -3
- package/src/components/invoice/list.tsx +0 -1
- package/src/components/invoice/table.tsx +7 -2
- package/src/components/subscription/items/index.tsx +26 -7
- package/src/components/subscription/items/usage-records.tsx +21 -10
- package/src/components/subscription/portal/actions.tsx +16 -14
- package/src/libs/util.ts +51 -0
- package/src/locales/en.tsx +2 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -1
- package/src/pages/customer/subscription/embed.tsx +16 -14
- package/vite-server.config.ts +8 -0
|
@@ -21,7 +21,7 @@ import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice'
|
|
|
21
21
|
import { getMainProductName } from '../../product';
|
|
22
22
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
23
23
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
24
|
-
import { getExplorerLink } from '../../util';
|
|
24
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
25
25
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
26
26
|
|
|
27
27
|
export interface SubscriptionSucceededEmailTemplateOptions {
|
|
@@ -49,6 +49,7 @@ interface SubscriptionSucceededEmailTemplateContext {
|
|
|
49
49
|
viewInvoiceLink: string;
|
|
50
50
|
viewTxHashLink: string | undefined;
|
|
51
51
|
oneTimeProductInfo?: Array<OneTimeProductInfo>;
|
|
52
|
+
customActions: any[];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export class SubscriptionSucceededEmailTemplate
|
|
@@ -159,6 +160,12 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
159
160
|
chainHost,
|
|
160
161
|
});
|
|
161
162
|
|
|
163
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
164
|
+
subscription,
|
|
165
|
+
'customer.subscription.started',
|
|
166
|
+
locale
|
|
167
|
+
);
|
|
168
|
+
|
|
162
169
|
return {
|
|
163
170
|
locale,
|
|
164
171
|
productName,
|
|
@@ -176,6 +183,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
176
183
|
viewInvoiceLink,
|
|
177
184
|
viewTxHashLink,
|
|
178
185
|
oneTimeProductInfo,
|
|
186
|
+
customActions,
|
|
179
187
|
};
|
|
180
188
|
}
|
|
181
189
|
|
|
@@ -261,6 +269,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
261
269
|
viewInvoiceLink,
|
|
262
270
|
viewTxHashLink,
|
|
263
271
|
oneTimeProductInfo,
|
|
272
|
+
customActions,
|
|
264
273
|
} = await this.getContext();
|
|
265
274
|
const hasOneTimeProduct = !isEmpty(oneTimeProductInfo);
|
|
266
275
|
|
|
@@ -375,6 +384,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
375
384
|
title: translate('notification.common.viewTxHash', locale),
|
|
376
385
|
link: viewTxHashLink as string,
|
|
377
386
|
},
|
|
387
|
+
...customActions,
|
|
378
388
|
].filter(Boolean),
|
|
379
389
|
};
|
|
380
390
|
|
|
@@ -14,6 +14,7 @@ import { getMainProductName } from '../../product';
|
|
|
14
14
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
15
15
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
16
16
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
17
|
+
import { getSubscriptionNotificationCustomActions } from '../../util';
|
|
17
18
|
|
|
18
19
|
export interface SubscriptionTrialStartEmailTemplateOptions {
|
|
19
20
|
subscriptionId: string;
|
|
@@ -41,6 +42,7 @@ interface SubscriptionTrialStartEmailTemplateContext {
|
|
|
41
42
|
viewSubscriptionLink: string;
|
|
42
43
|
viewInvoiceLink: string;
|
|
43
44
|
oneTimeProductInfo?: Array<OneTimeProductInfo>;
|
|
45
|
+
customActions: any[];
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export class SubscriptionTrialStartEmailTemplate
|
|
@@ -130,6 +132,12 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
130
132
|
locale,
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
136
|
+
subscription,
|
|
137
|
+
'customer.subscription.trial_start',
|
|
138
|
+
locale
|
|
139
|
+
);
|
|
140
|
+
|
|
133
141
|
return {
|
|
134
142
|
locale,
|
|
135
143
|
productName,
|
|
@@ -146,6 +154,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
146
154
|
viewSubscriptionLink,
|
|
147
155
|
viewInvoiceLink,
|
|
148
156
|
oneTimeProductInfo,
|
|
157
|
+
customActions,
|
|
149
158
|
};
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -232,6 +241,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
232
241
|
viewSubscriptionLink,
|
|
233
242
|
viewInvoiceLink,
|
|
234
243
|
oneTimeProductInfo,
|
|
244
|
+
customActions,
|
|
235
245
|
} = await this.getContext();
|
|
236
246
|
|
|
237
247
|
const hasOneTimeProduct = !isEmpty(oneTimeProductInfo);
|
|
@@ -342,6 +352,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
342
352
|
title: translate('notification.common.viewInvoice', locale),
|
|
343
353
|
link: viewInvoiceLink,
|
|
344
354
|
},
|
|
355
|
+
...customActions,
|
|
345
356
|
].filter(Boolean),
|
|
346
357
|
};
|
|
347
358
|
|
|
@@ -13,6 +13,8 @@ import { getMainProductName } from '../../product';
|
|
|
13
13
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
14
14
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
15
15
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
16
|
+
import dayjs from '../../dayjs';
|
|
17
|
+
import { getSubscriptionNotificationCustomActions } from '../../util';
|
|
16
18
|
|
|
17
19
|
export interface SubscriptionTrialWillEndEmailTemplateOptions {
|
|
18
20
|
subscriptionId: string;
|
|
@@ -36,6 +38,7 @@ interface SubscriptionTrialWilEndEmailTemplateContext {
|
|
|
36
38
|
|
|
37
39
|
viewSubscriptionLink: string;
|
|
38
40
|
paymentMethod: PaymentMethod | null;
|
|
41
|
+
customActions: any[];
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export class SubscriptionTrialWilEndEmailTemplate
|
|
@@ -104,6 +107,12 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
104
107
|
userDid,
|
|
105
108
|
});
|
|
106
109
|
|
|
110
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
111
|
+
subscription,
|
|
112
|
+
'customer.subscription.trial_will_end',
|
|
113
|
+
locale
|
|
114
|
+
);
|
|
115
|
+
|
|
107
116
|
return {
|
|
108
117
|
locale,
|
|
109
118
|
productName,
|
|
@@ -119,6 +128,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
119
128
|
|
|
120
129
|
viewSubscriptionLink,
|
|
121
130
|
paymentMethod,
|
|
131
|
+
customActions,
|
|
122
132
|
};
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -162,8 +172,14 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
162
172
|
duration,
|
|
163
173
|
paymentMethod,
|
|
164
174
|
viewSubscriptionLink,
|
|
175
|
+
customActions,
|
|
165
176
|
} = await this.getContext();
|
|
166
177
|
|
|
178
|
+
// 如果当前时间大于试用结束时间,那么不发送通知
|
|
179
|
+
if (dayjs().utc().isAfter(dayjs.utc(at))) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
|
|
168
184
|
if (canPay && !this.options.required) {
|
|
169
185
|
// 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
|
|
@@ -289,6 +305,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
289
305
|
title: translate('notification.common.viewSubscription', locale),
|
|
290
306
|
link: viewSubscriptionLink,
|
|
291
307
|
},
|
|
308
|
+
...customActions,
|
|
292
309
|
].filter(Boolean),
|
|
293
310
|
};
|
|
294
311
|
|
|
@@ -19,7 +19,7 @@ import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
|
19
19
|
import { getMainProductName } from '../../product';
|
|
20
20
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
21
21
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
22
|
-
import { getExplorerLink } from '../../util';
|
|
22
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
23
23
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
24
24
|
|
|
25
25
|
export interface SubscriptionUpgradedEmailTemplateOptions {
|
|
@@ -40,6 +40,8 @@ interface SubscriptionUpgradedEmailTemplateContext {
|
|
|
40
40
|
viewSubscriptionLink: string;
|
|
41
41
|
viewInvoiceLink: string;
|
|
42
42
|
viewTxHashLink: string | undefined;
|
|
43
|
+
skipInvoice: boolean;
|
|
44
|
+
customActions: any[];
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<SubscriptionUpgradedEmailTemplateContext> {
|
|
@@ -54,7 +56,7 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
54
56
|
if (!subscription) {
|
|
55
57
|
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
56
58
|
}
|
|
57
|
-
if (subscription.
|
|
59
|
+
if (!subscription.isActive()) {
|
|
58
60
|
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -63,7 +65,10 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
63
65
|
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
// invoice应该是最新的subscription_update invoice
|
|
69
|
+
const invoiceId = subscription.pending_update?.updates?.latest_invoice_id || subscription.latest_invoice_id;
|
|
70
|
+
const invoice = (await Invoice.findByPk(invoiceId)) as Invoice;
|
|
71
|
+
const skipInvoice = subscription.status === 'trialing' || invoice.billing_reason !== 'subscription_update';
|
|
67
72
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
68
73
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
69
74
|
where: {
|
|
@@ -90,8 +95,12 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
90
95
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
91
96
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
|
|
92
97
|
: undefined;
|
|
93
|
-
const currentPeriodStart: string = formatTime(
|
|
94
|
-
|
|
98
|
+
const currentPeriodStart: string = formatTime(
|
|
99
|
+
skipInvoice ? subscription.current_period_start * 1000 : invoice.period_start * 1000
|
|
100
|
+
);
|
|
101
|
+
const currentPeriodEnd: string = formatTime(
|
|
102
|
+
skipInvoice ? subscription.current_period_end * 1000 : invoice.period_end * 1000
|
|
103
|
+
);
|
|
95
104
|
const duration: string = prettyMsI18n(
|
|
96
105
|
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
97
106
|
{
|
|
@@ -123,6 +132,12 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
123
132
|
chainHost,
|
|
124
133
|
});
|
|
125
134
|
|
|
135
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
136
|
+
subscription,
|
|
137
|
+
'customer.subscription.upgraded',
|
|
138
|
+
locale
|
|
139
|
+
);
|
|
140
|
+
|
|
126
141
|
return {
|
|
127
142
|
locale,
|
|
128
143
|
productName,
|
|
@@ -139,6 +154,8 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
139
154
|
viewSubscriptionLink,
|
|
140
155
|
viewInvoiceLink,
|
|
141
156
|
viewTxHashLink,
|
|
157
|
+
skipInvoice,
|
|
158
|
+
customActions,
|
|
142
159
|
};
|
|
143
160
|
}
|
|
144
161
|
|
|
@@ -157,6 +174,8 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
157
174
|
viewSubscriptionLink,
|
|
158
175
|
viewInvoiceLink,
|
|
159
176
|
viewTxHashLink,
|
|
177
|
+
skipInvoice,
|
|
178
|
+
customActions,
|
|
160
179
|
} = await this.getContext();
|
|
161
180
|
|
|
162
181
|
const template: BaseEmailTemplateType = {
|
|
@@ -210,21 +229,25 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
210
229
|
text: productName,
|
|
211
230
|
},
|
|
212
231
|
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
...(skipInvoice
|
|
233
|
+
? []
|
|
234
|
+
: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
data: {
|
|
238
|
+
type: 'plain',
|
|
239
|
+
color: '#9397A1',
|
|
240
|
+
text: translate('notification.common.paymentAmount', locale),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'text',
|
|
245
|
+
data: {
|
|
246
|
+
type: 'plain',
|
|
247
|
+
text: paymentInfo,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
]),
|
|
228
251
|
{
|
|
229
252
|
type: 'text',
|
|
230
253
|
data: {
|
|
@@ -250,16 +273,18 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
250
273
|
title: translate('notification.common.viewSubscription', locale),
|
|
251
274
|
link: viewSubscriptionLink,
|
|
252
275
|
},
|
|
253
|
-
{
|
|
276
|
+
!skipInvoice && {
|
|
254
277
|
name: translate('notification.common.viewInvoice', locale),
|
|
255
278
|
title: translate('notification.common.viewInvoice', locale),
|
|
256
279
|
link: viewInvoiceLink,
|
|
257
280
|
},
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
!skipInvoice &&
|
|
282
|
+
viewTxHashLink && {
|
|
283
|
+
name: translate('notification.common.viewTxHash', locale),
|
|
284
|
+
title: translate('notification.common.viewTxHash', locale),
|
|
285
|
+
link: viewTxHashLink,
|
|
286
|
+
},
|
|
287
|
+
...customActions,
|
|
263
288
|
].filter(Boolean),
|
|
264
289
|
};
|
|
265
290
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { fromUnitToToken } from '@ocap/util';
|
|
4
4
|
import type { ManipulateType } from 'dayjs';
|
|
5
5
|
|
|
6
|
+
import dayjs from '../../dayjs';
|
|
6
7
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
8
|
import { translate } from '../../../locales';
|
|
8
9
|
import { Customer, Invoice, Subscription } from '../../../store/models';
|
|
@@ -13,6 +14,7 @@ import { getMainProductName } from '../../product';
|
|
|
13
14
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
14
15
|
import { formatTime } from '../../time';
|
|
15
16
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
17
|
+
import { getSubscriptionNotificationCustomActions } from '../../util';
|
|
16
18
|
|
|
17
19
|
export interface SubscriptionWillCanceledEmailTemplateOptions {
|
|
18
20
|
subscriptionId: string;
|
|
@@ -32,6 +34,7 @@ interface SubscriptionWillCanceledEmailTemplateContext {
|
|
|
32
34
|
|
|
33
35
|
viewSubscriptionLink: string;
|
|
34
36
|
viewInvoiceLink: string;
|
|
37
|
+
customActions: any[];
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export class SubscriptionWillCanceledEmailTemplate
|
|
@@ -90,6 +93,11 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
90
93
|
action: 'pay',
|
|
91
94
|
});
|
|
92
95
|
|
|
96
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
97
|
+
subscription,
|
|
98
|
+
'customer.subscription.will_canceled',
|
|
99
|
+
locale
|
|
100
|
+
);
|
|
93
101
|
return {
|
|
94
102
|
locale,
|
|
95
103
|
productName,
|
|
@@ -101,6 +109,7 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
101
109
|
|
|
102
110
|
viewSubscriptionLink,
|
|
103
111
|
viewInvoiceLink,
|
|
112
|
+
customActions,
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
|
|
@@ -141,8 +150,14 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
141
150
|
|
|
142
151
|
viewSubscriptionLink,
|
|
143
152
|
viewInvoiceLink,
|
|
153
|
+
customActions,
|
|
144
154
|
} = await this.getContext();
|
|
145
155
|
|
|
156
|
+
// 如果当前时间大于订阅终止时间,那么不发送通知
|
|
157
|
+
if (dayjs().utc().isAfter(dayjs.utc(at))) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
const template: BaseEmailTemplateType = {
|
|
147
162
|
title: `${translate('notification.subscriptWillCanceled.title', locale, {
|
|
148
163
|
productName,
|
|
@@ -217,6 +232,7 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
217
232
|
title: translate('notification.common.renewNow', locale),
|
|
218
233
|
link: viewInvoiceLink,
|
|
219
234
|
},
|
|
235
|
+
...customActions,
|
|
220
236
|
].filter(Boolean),
|
|
221
237
|
};
|
|
222
238
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import type { ManipulateType } from 'dayjs';
|
|
4
|
-
import dayjs from 'dayjs';
|
|
5
4
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
5
|
import type { LiteralUnion } from 'type-fest';
|
|
7
6
|
|
|
8
7
|
import { fromUnitToToken } from '@ocap/util';
|
|
8
|
+
import dayjs from '../../dayjs';
|
|
9
9
|
import { getTokenSummaryByDid } from '../../../integrations/arcblock/stake';
|
|
10
10
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
11
11
|
import { translate } from '../../../locales';
|
|
@@ -22,7 +22,7 @@ import { getPaymentAmountForCycleSubscription, type PaymentDetail } from '../../
|
|
|
22
22
|
import { getMainProductName } from '../../product';
|
|
23
23
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
24
24
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
25
|
-
import { getExplorerLink } from '../../util';
|
|
25
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
26
26
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
27
27
|
|
|
28
28
|
export interface SubscriptionWillRenewEmailTemplateOptions {
|
|
@@ -49,6 +49,7 @@ interface SubscriptionWillRenewEmailTemplateContext {
|
|
|
49
49
|
viewSubscriptionLink: string;
|
|
50
50
|
addFundsLink: string;
|
|
51
51
|
paymentMethod: PaymentMethod | null;
|
|
52
|
+
customActions: any[];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export class SubscriptionWillRenewEmailTemplate
|
|
@@ -145,6 +146,11 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
145
146
|
},
|
|
146
147
|
})!;
|
|
147
148
|
|
|
149
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
150
|
+
subscription,
|
|
151
|
+
'customer.subscription.will_renew',
|
|
152
|
+
locale
|
|
153
|
+
);
|
|
148
154
|
return {
|
|
149
155
|
locale,
|
|
150
156
|
productName,
|
|
@@ -162,6 +168,7 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
162
168
|
viewSubscriptionLink,
|
|
163
169
|
addFundsLink,
|
|
164
170
|
paymentMethod,
|
|
171
|
+
customActions,
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
174
|
async getPaymentCategory({ subscriptionId }: { subscriptionId: string }): Promise<{
|
|
@@ -249,8 +256,13 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
249
256
|
viewSubscriptionLink,
|
|
250
257
|
addFundsLink,
|
|
251
258
|
paymentMethod,
|
|
259
|
+
customActions,
|
|
252
260
|
} = await this.getContext();
|
|
253
261
|
|
|
262
|
+
// 如果当前时间大于预计扣费时间,那么不发送通知
|
|
263
|
+
if (dayjs().utc().isAfter(dayjs.utc(at))) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
254
266
|
const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
|
|
255
267
|
if (canPay) {
|
|
256
268
|
// 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
|
|
@@ -260,7 +272,6 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
260
272
|
// 如果预估的价格是 0 并且货币不是 USD,那么直接不发送
|
|
261
273
|
return null;
|
|
262
274
|
}
|
|
263
|
-
|
|
264
275
|
const isStripe = paymentMethod?.type === 'stripe';
|
|
265
276
|
const template: BaseEmailTemplateType = {
|
|
266
277
|
title: `${translate('notification.subscriptionWillRenew.title', locale, {
|
|
@@ -420,6 +431,7 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
420
431
|
title: translate('notification.common.viewSubscription', locale),
|
|
421
432
|
link: viewSubscriptionLink,
|
|
422
433
|
},
|
|
434
|
+
...customActions,
|
|
423
435
|
].filter(Boolean),
|
|
424
436
|
};
|
|
425
437
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getOwnerDid } from '@api/libs/util';
|
|
2
|
+
import prettyMsI18n from 'pretty-ms-i18n';
|
|
3
|
+
import { translate } from '../../../locales';
|
|
4
|
+
import { Subscription } from '../../../store/models';
|
|
5
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
6
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
|
+
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
8
|
+
import { getMainProductName } from '../../product';
|
|
9
|
+
import { checkUsageReportEmpty, getAdminSubscriptionPageUrl } from '../../subscription';
|
|
10
|
+
|
|
11
|
+
export interface UsageReportEmptyEmailTemplateOptions {
|
|
12
|
+
subscriptionId: string;
|
|
13
|
+
usageReportStart: number;
|
|
14
|
+
usageReportEnd: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UsageReportEmptyEmailTemplateContext {
|
|
18
|
+
locale: string;
|
|
19
|
+
userDid: string;
|
|
20
|
+
productName: string;
|
|
21
|
+
subscriptionId: string;
|
|
22
|
+
currentPeriodStart: string;
|
|
23
|
+
currentPeriodEnd: string;
|
|
24
|
+
viewSubscriptionLink: string;
|
|
25
|
+
duration: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class UsageReportEmptyEmailTemplate implements BaseEmailTemplate<UsageReportEmptyEmailTemplateContext> {
|
|
29
|
+
options: UsageReportEmptyEmailTemplateOptions;
|
|
30
|
+
|
|
31
|
+
constructor(options: UsageReportEmptyEmailTemplateOptions) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getContext(): Promise<UsageReportEmptyEmailTemplateContext> {
|
|
36
|
+
const { usageReportStart, usageReportEnd, subscriptionId } = this.options;
|
|
37
|
+
const subscription: Subscription | null = await Subscription.findByPk(subscriptionId);
|
|
38
|
+
if (!subscription) {
|
|
39
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
40
|
+
}
|
|
41
|
+
const userDid = await getOwnerDid();
|
|
42
|
+
if (!userDid) {
|
|
43
|
+
throw new Error('get owner did failed');
|
|
44
|
+
}
|
|
45
|
+
const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
|
|
46
|
+
if (!usageReportEmpty) {
|
|
47
|
+
throw new Error('Usage report is not empty, no need to send email');
|
|
48
|
+
}
|
|
49
|
+
const locale = await getUserLocale(userDid);
|
|
50
|
+
const productName = await getMainProductName(subscription.id);
|
|
51
|
+
const currentPeriodStart = formatTime(subscription.current_period_start * 1000);
|
|
52
|
+
const currentPeriodEnd = formatTime(subscription.current_period_end * 1000);
|
|
53
|
+
const duration: string = prettyMsI18n(
|
|
54
|
+
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
55
|
+
{
|
|
56
|
+
locale: getPrettyMsI18nLocale(locale),
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
const viewSubscriptionLink = getAdminSubscriptionPageUrl({
|
|
60
|
+
subscriptionId: subscription.id,
|
|
61
|
+
locale,
|
|
62
|
+
userDid,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
userDid,
|
|
66
|
+
locale,
|
|
67
|
+
productName,
|
|
68
|
+
subscriptionId: subscription.id,
|
|
69
|
+
currentPeriodStart,
|
|
70
|
+
currentPeriodEnd,
|
|
71
|
+
viewSubscriptionLink,
|
|
72
|
+
duration,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
77
|
+
const {
|
|
78
|
+
locale,
|
|
79
|
+
productName,
|
|
80
|
+
subscriptionId,
|
|
81
|
+
currentPeriodStart,
|
|
82
|
+
currentPeriodEnd,
|
|
83
|
+
viewSubscriptionLink,
|
|
84
|
+
duration,
|
|
85
|
+
} = await this.getContext();
|
|
86
|
+
|
|
87
|
+
const template: BaseEmailTemplateType = {
|
|
88
|
+
title: translate('notification.usageReportEmpty.title', locale, {
|
|
89
|
+
productName,
|
|
90
|
+
}),
|
|
91
|
+
body: translate('notification.usageReportEmpty.body', locale, {
|
|
92
|
+
productName,
|
|
93
|
+
}),
|
|
94
|
+
attachments: [
|
|
95
|
+
{
|
|
96
|
+
type: 'section',
|
|
97
|
+
fields: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
data: {
|
|
101
|
+
type: 'plain',
|
|
102
|
+
color: '#9397A1',
|
|
103
|
+
text: translate('notification.common.product', locale),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
data: {
|
|
109
|
+
type: 'plain',
|
|
110
|
+
text: productName,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
data: {
|
|
116
|
+
type: 'plain',
|
|
117
|
+
color: '#9397A1',
|
|
118
|
+
text: translate('notification.common.subscriptionId', locale),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
data: {
|
|
124
|
+
type: 'plain',
|
|
125
|
+
text: subscriptionId,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
data: {
|
|
131
|
+
type: 'plain',
|
|
132
|
+
color: '#9397A1',
|
|
133
|
+
text: translate('notification.common.validityPeriod', locale),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
data: {
|
|
139
|
+
type: 'plain',
|
|
140
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
].filter(Boolean),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
// @ts-ignore
|
|
147
|
+
actions: [
|
|
148
|
+
{
|
|
149
|
+
name: translate('notification.common.viewSubscription', locale),
|
|
150
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
151
|
+
link: viewSubscriptionLink,
|
|
152
|
+
},
|
|
153
|
+
].filter(Boolean),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return template;
|
|
157
|
+
}
|
|
158
|
+
}
|