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.
Files changed (51) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +20 -0
  2. package/api/src/integrations/stripe/resource.ts +2 -2
  3. package/api/src/libs/audit.ts +1 -1
  4. package/api/src/libs/invoice.ts +81 -1
  5. package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
  6. package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
  7. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
  8. package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
  9. package/api/src/libs/notification/template/subscription-renewed.ts +11 -3
  10. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
  11. package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
  12. package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
  13. package/api/src/libs/notification/template/subscription-trial-will-end.ts +17 -0
  14. package/api/src/libs/notification/template/subscription-upgraded.ts +51 -26
  15. package/api/src/libs/notification/template/subscription-will-canceled.ts +16 -0
  16. package/api/src/libs/notification/template/subscription-will-renew.ts +15 -3
  17. package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
  18. package/api/src/libs/queue/index.ts +69 -19
  19. package/api/src/libs/queue/store.ts +28 -5
  20. package/api/src/libs/subscription.ts +129 -19
  21. package/api/src/libs/util.ts +30 -0
  22. package/api/src/locales/en.ts +13 -0
  23. package/api/src/locales/zh.ts +13 -0
  24. package/api/src/queues/invoice.ts +58 -20
  25. package/api/src/queues/notification.ts +43 -1
  26. package/api/src/queues/payment.ts +5 -1
  27. package/api/src/queues/subscription.ts +64 -15
  28. package/api/src/routes/checkout-sessions.ts +26 -0
  29. package/api/src/routes/invoices.ts +11 -31
  30. package/api/src/routes/subscriptions.ts +43 -7
  31. package/api/src/store/models/checkout-session.ts +2 -0
  32. package/api/src/store/models/job.ts +4 -0
  33. package/api/src/store/models/types.ts +22 -4
  34. package/api/src/store/models/usage-record.ts +5 -1
  35. package/api/tests/libs/subscription.spec.ts +154 -0
  36. package/api/tests/libs/util.spec.ts +135 -0
  37. package/blocklet.yml +1 -1
  38. package/package.json +10 -10
  39. package/scripts/sdk.js +37 -3
  40. package/src/components/invoice/list.tsx +0 -1
  41. package/src/components/invoice/table.tsx +7 -2
  42. package/src/components/subscription/items/index.tsx +26 -7
  43. package/src/components/subscription/items/usage-records.tsx +21 -10
  44. package/src/components/subscription/portal/actions.tsx +16 -14
  45. package/src/libs/util.ts +51 -0
  46. package/src/locales/en.tsx +2 -0
  47. package/src/locales/zh.tsx +2 -0
  48. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
  49. package/src/pages/customer/subscription/change-plan.tsx +1 -1
  50. package/src/pages/customer/subscription/embed.tsx +16 -14
  51. 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.status !== 'active') {
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
- const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
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(invoice.period_start * 1000);
94
- const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
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
- type: 'text',
215
- data: {
216
- type: 'plain',
217
- color: '#9397A1',
218
- text: translate('notification.common.paymentAmount', locale),
219
- },
220
- },
221
- {
222
- type: 'text',
223
- data: {
224
- type: 'plain',
225
- text: paymentInfo,
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
- viewTxHashLink && {
259
- name: translate('notification.common.viewTxHash', locale),
260
- title: translate('notification.common.viewTxHash', locale),
261
- link: viewTxHashLink,
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
+ }