payment-kit 1.13.136 → 1.13.138

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 (39) hide show
  1. package/api/src/crons/base.ts +7 -33
  2. package/api/src/crons/index.ts +7 -0
  3. package/api/src/crons/interface/base.ts +17 -0
  4. package/api/src/crons/subscription-trail-will-end.ts +29 -1
  5. package/api/src/crons/subscription-will-canceled.ts +48 -0
  6. package/api/src/crons/subscription-will-renew.ts +29 -2
  7. package/api/src/libs/invoice.ts +20 -6
  8. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +245 -0
  9. package/api/src/libs/notification/template/subscription-cacceled.ts +241 -0
  10. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +286 -0
  11. package/api/src/libs/notification/template/subscription-renew-failed.ts +17 -6
  12. package/api/src/libs/notification/template/subscription-renewed.ts +27 -6
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +14 -5
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +13 -4
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +7 -3
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +261 -0
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +225 -0
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +30 -3
  19. package/api/src/libs/product.ts +24 -0
  20. package/api/src/libs/queue/index.ts +2 -0
  21. package/api/src/libs/queue/store.ts +1 -1
  22. package/api/src/libs/security.ts +1 -1
  23. package/api/src/libs/subscription.ts +19 -3
  24. package/api/src/libs/util.ts +33 -0
  25. package/api/src/locales/en.ts +38 -4
  26. package/api/src/locales/zh.ts +36 -2
  27. package/api/src/queues/notification.ts +91 -2
  28. package/api/src/routes/connect/setup.ts +2 -2
  29. package/api/src/routes/connect/shared.ts +2 -2
  30. package/api/src/store/models/subscription.ts +5 -0
  31. package/api/src/store/models/types.ts +3 -0
  32. package/blocklet.yml +1 -1
  33. package/package.json +44 -44
  34. package/src/contexts/session.ts +2 -2
  35. package/src/pages/admin/payments/links/create.tsx +1 -1
  36. package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
  37. package/src/pages/customer/invoice.tsx +12 -3
  38. package/src/pages/customer/subscription/update.tsx +1 -1
  39. package/api/src/crons/interface/diff.ts +0 -9
@@ -0,0 +1,241 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken } from '@ocap/util';
4
+ import prettyMsI18n from 'pretty-ms-i18n';
5
+
6
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
7
+ import { translate } from '../../../locales';
8
+ import { Customer, PaymentMethod, Refund, Subscription } from '../../../store/models';
9
+ import { Invoice } from '../../../store/models/invoice';
10
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
11
+ import logger from '../../logger';
12
+ import { getMainProductName } from '../../product';
13
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
14
+ import { formatTime, getPrettyMsI18nLocale } from '../../time';
15
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
+
17
+ export interface SubscriptionCanceledEmailTemplateOptions {
18
+ subscriptionId: string;
19
+ }
20
+
21
+ interface SubscriptionCanceledEmailTemplateContext {
22
+ locale: string;
23
+ productName: string;
24
+ at: string;
25
+
26
+ chainHost: string | undefined;
27
+ userDid: string;
28
+ paymentInfo: string;
29
+ currentPeriodStart: string;
30
+ currentPeriodEnd: string;
31
+ duration: string;
32
+ cancellationReason: string;
33
+
34
+ viewSubscriptionLink: string;
35
+ }
36
+
37
+ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<SubscriptionCanceledEmailTemplateContext> {
38
+ options: SubscriptionCanceledEmailTemplateOptions;
39
+
40
+ constructor(options: SubscriptionCanceledEmailTemplateOptions) {
41
+ this.options = options;
42
+ }
43
+
44
+ async getContext(): Promise<SubscriptionCanceledEmailTemplateContext> {
45
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
46
+ if (!subscription) {
47
+ throw new Error(`Subscription(${this.options.subscriptionId}) not found`);
48
+ }
49
+ if (subscription.status !== 'canceled') {
50
+ throw new Error(`Subscription(${this.options.subscriptionId}) status(${subscription.status}) must be canceled`);
51
+ }
52
+ if (subscription.cancelation_details?.reason !== 'payment_disputed') {
53
+ // 非管理员取消的订阅不需要发送邮件
54
+ logger.error(
55
+ `Subscription(${this.options.subscriptionId}) cancelation reason must be payment_disputed`,
56
+ subscription.cancelation_details
57
+ );
58
+ throw new Error(`Subscription(${this.options.subscriptionId}) cancelation reason must be payment_disputed`);
59
+ }
60
+
61
+ const customer = await Customer.findByPk(subscription.customer_id);
62
+ if (!customer) {
63
+ throw new Error(`Customer(${subscription.customer_id}) not found`);
64
+ }
65
+
66
+ const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
67
+ const paymentCurrency = (await PaymentCurrency.findOne({
68
+ where: {
69
+ id: subscription.currency_id,
70
+ },
71
+ })) as PaymentCurrency;
72
+
73
+ const userDid: string = customer.did;
74
+ const locale = await getUserLocale(userDid);
75
+ const productName = await getMainProductName(subscription.id);
76
+ const at: string = formatTime(subscription.canceled_at * 1000);
77
+
78
+ const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
79
+ const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
80
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
81
+ const duration: string = prettyMsI18n(
82
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
83
+ {
84
+ locale: getPrettyMsI18nLocale(locale),
85
+ }
86
+ );
87
+ const refund = await Refund.findOne({
88
+ where: {
89
+ subscription_id: subscription.id,
90
+ },
91
+ });
92
+ const cancellationReason = refund
93
+ ? translate('notification.subscriptionCanceled.adminCanceledAndRefunded', locale)
94
+ : translate('notification.subscriptionCanceled.adminCanceled', locale);
95
+
96
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
97
+ // @ts-expect-error
98
+ const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
99
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
100
+ subscriptionId: subscription.id,
101
+ locale,
102
+ userDid,
103
+ });
104
+
105
+ return {
106
+ locale,
107
+ productName,
108
+ at,
109
+
110
+ userDid,
111
+ chainHost,
112
+ paymentInfo,
113
+ currentPeriodStart,
114
+ currentPeriodEnd,
115
+ duration,
116
+ cancellationReason,
117
+
118
+ viewSubscriptionLink,
119
+ };
120
+ }
121
+
122
+ async getTemplate(): Promise<BaseEmailTemplateType> {
123
+ const {
124
+ locale,
125
+ productName,
126
+ at,
127
+
128
+ userDid,
129
+ paymentInfo,
130
+ currentPeriodStart,
131
+ currentPeriodEnd,
132
+ duration,
133
+ cancellationReason,
134
+
135
+ viewSubscriptionLink,
136
+ } = await this.getContext();
137
+
138
+ const template: BaseEmailTemplateType = {
139
+ title: `${translate('notification.subscriptionCanceled.title', locale, {
140
+ productName: `(${productName})`,
141
+ })}`,
142
+ body: `${translate('notification.subscriptionCanceled.body', locale, {
143
+ at,
144
+ productName: `(${productName})`,
145
+ })}`,
146
+ // @ts-expect-error
147
+ attachments: [
148
+ {
149
+ type: 'section',
150
+ fields: [
151
+ {
152
+ type: 'text',
153
+ data: {
154
+ type: 'plain',
155
+ color: '#9397A1',
156
+ text: translate('notification.common.account', locale),
157
+ },
158
+ },
159
+ {
160
+ type: 'text',
161
+ data: {
162
+ type: 'plain',
163
+ text: userDid,
164
+ },
165
+ },
166
+ {
167
+ type: 'text',
168
+ data: {
169
+ type: 'plain',
170
+ color: '#9397A1',
171
+ text: translate('notification.common.product', locale),
172
+ },
173
+ },
174
+ {
175
+ type: 'text',
176
+ data: {
177
+ type: 'plain',
178
+ text: productName,
179
+ },
180
+ },
181
+ {
182
+ type: 'text',
183
+ data: {
184
+ type: 'plain',
185
+ color: '#9397A1',
186
+ text: translate('notification.common.paymentInfo', locale),
187
+ },
188
+ },
189
+ {
190
+ type: 'text',
191
+ data: {
192
+ type: 'plain',
193
+ text: paymentInfo,
194
+ },
195
+ },
196
+ {
197
+ type: 'text',
198
+ data: {
199
+ type: 'plain',
200
+ color: '#9397A1',
201
+ text: translate('notification.common.validityPeriod', locale),
202
+ },
203
+ },
204
+ {
205
+ type: 'text',
206
+ data: {
207
+ type: 'plain',
208
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
209
+ },
210
+ },
211
+ {
212
+ type: 'text',
213
+ data: {
214
+ type: 'plain',
215
+ color: '#9397A1',
216
+ text: translate('notification.common.cancellationReason', locale),
217
+ },
218
+ },
219
+ {
220
+ type: 'text',
221
+ data: {
222
+ type: 'plain',
223
+ text: cancellationReason,
224
+ },
225
+ },
226
+ ].filter(Boolean),
227
+ },
228
+ ].filter(Boolean),
229
+ // @ts-ignore
230
+ actions: [
231
+ viewSubscriptionLink && {
232
+ name: 'viewSubscription',
233
+ title: translate('notification.common.viewSubscription', locale),
234
+ link: viewSubscriptionLink,
235
+ },
236
+ ].filter(Boolean),
237
+ };
238
+
239
+ return template;
240
+ }
241
+ }
@@ -0,0 +1,286 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken } from '@ocap/util';
4
+ import prettyMsI18n from 'pretty-ms-i18n';
5
+
6
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
7
+ import { translate } from '../../../locales';
8
+ import { Customer, PaymentIntent, PaymentMethod, Refund } from '../../../store/models';
9
+ import { Invoice } from '../../../store/models/invoice';
10
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
11
+ import { getCustomerInvoicePageUrl } from '../../invoice';
12
+ import { getMainProductName } from '../../product';
13
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
14
+ import { formatTime, getPrettyMsI18nLocale } from '../../time';
15
+ import { getExplorerLink } from '../../util';
16
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
17
+
18
+ export interface SubscriptionRefundSucceededEmailTemplateOptions {
19
+ refundId: string;
20
+ }
21
+
22
+ interface SubscriptionRefundSucceededEmailTemplateContext {
23
+ locale: string;
24
+ productName: string;
25
+ at: string;
26
+
27
+ chainHost: string | undefined;
28
+ userDid: string;
29
+ paymentInfo: string;
30
+ refundInfo: string;
31
+ currentPeriodStart: string;
32
+ currentPeriodEnd: string;
33
+ duration: string;
34
+ unusedPeriodStart: string;
35
+ unusedPeriodEnd: string;
36
+ unusedDuration: string;
37
+
38
+ viewSubscriptionLink: string;
39
+ viewInvoiceLink: string;
40
+ viewTxHashLink: string | undefined;
41
+ }
42
+
43
+ export class SubscriptionRefundSucceededEmailTemplate
44
+ implements BaseEmailTemplate<SubscriptionRefundSucceededEmailTemplateContext>
45
+ {
46
+ options: SubscriptionRefundSucceededEmailTemplateOptions;
47
+
48
+ constructor(options: SubscriptionRefundSucceededEmailTemplateOptions) {
49
+ this.options = options;
50
+ }
51
+
52
+ async getContext(): Promise<SubscriptionRefundSucceededEmailTemplateContext> {
53
+ const refund: Refund | null = await Refund.findByPk(this.options.refundId);
54
+ if (!refund) {
55
+ throw new Error(`Refund not found: ${this.options.refundId}`);
56
+ }
57
+ if (refund.status !== 'succeeded') {
58
+ throw new Error(`Refund not succeeded: ${this.options.refundId}`);
59
+ }
60
+
61
+ const customer = await Customer.findByPk(refund.customer_id);
62
+ if (!customer) {
63
+ throw new Error(`Customer not found: ${refund.customer_id}`);
64
+ }
65
+
66
+ const invoice = (await Invoice.findByPk(refund.invoice_id)) as Invoice;
67
+ const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
68
+ const paymentCurrency = (await PaymentCurrency.findOne({
69
+ where: {
70
+ id: refund.currency_id,
71
+ },
72
+ })) as PaymentCurrency;
73
+
74
+ const userDid: string = customer.did;
75
+ const locale = await getUserLocale(userDid);
76
+ const productName = await getMainProductName(refund.subscription_id!);
77
+ const at: string = formatTime(refund.created_at);
78
+
79
+ const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount, paymentCurrency.decimal)} ${
80
+ paymentCurrency.symbol
81
+ }`;
82
+ const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
83
+
84
+ const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
85
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
86
+ const duration: string = prettyMsI18n(
87
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
88
+ {
89
+ locale: getPrettyMsI18nLocale(locale),
90
+ }
91
+ );
92
+
93
+ const unusedPeriodStart: string = formatTime(refund!.metadata!.unused_period_start! * 1000);
94
+ const unusedPeriodEnd: string = formatTime(refund!.metadata!.unused_period_end! * 1000);
95
+ const unusedDuration: string = prettyMsI18n(
96
+ new Date(unusedPeriodEnd).getTime() - new Date(unusedPeriodStart).getTime(),
97
+ {
98
+ locale: getPrettyMsI18nLocale(locale),
99
+ }
100
+ );
101
+
102
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
103
+ // @ts-expect-error
104
+ const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
105
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
106
+ subscriptionId: refund.id,
107
+ locale,
108
+ userDid,
109
+ });
110
+ const viewInvoiceLink = getCustomerInvoicePageUrl({
111
+ invoiceId: invoice.id,
112
+ userDid,
113
+ locale,
114
+ });
115
+
116
+ // @ts-expect-error
117
+ const txHash: string | undefined = refund?.payment_details?.[paymentMethod.type]?.tx_hash;
118
+ const viewTxHashLink: string | undefined = txHash && getExplorerLink(chainHost, txHash as string, 'tx');
119
+
120
+ return {
121
+ locale,
122
+ productName,
123
+ at,
124
+
125
+ userDid,
126
+ chainHost,
127
+ paymentInfo,
128
+ refundInfo,
129
+ currentPeriodStart,
130
+ currentPeriodEnd,
131
+ duration,
132
+ unusedPeriodStart,
133
+ unusedPeriodEnd,
134
+ unusedDuration,
135
+
136
+ viewSubscriptionLink,
137
+ viewInvoiceLink,
138
+ viewTxHashLink,
139
+ };
140
+ }
141
+
142
+ async getTemplate(): Promise<BaseEmailTemplateType> {
143
+ const {
144
+ locale,
145
+ productName,
146
+ at,
147
+
148
+ userDid,
149
+ paymentInfo,
150
+ refundInfo,
151
+ currentPeriodStart,
152
+ currentPeriodEnd,
153
+ duration,
154
+ unusedPeriodStart,
155
+ unusedPeriodEnd,
156
+ unusedDuration,
157
+
158
+ viewSubscriptionLink,
159
+ viewTxHashLink,
160
+ } = await this.getContext();
161
+
162
+ const template: BaseEmailTemplateType = {
163
+ title: `${translate('notification.subscriptionRefundSucceeded.title', locale, {
164
+ productName: `(${productName})`,
165
+ })}`,
166
+ body: `${translate('notification.subscriptionRefundSucceeded.body', locale, {
167
+ at,
168
+ productName: `(${productName})`,
169
+ refundInfo,
170
+ })}`,
171
+ // @ts-expect-error
172
+ attachments: [
173
+ {
174
+ type: 'section',
175
+ fields: [
176
+ {
177
+ type: 'text',
178
+ data: {
179
+ type: 'plain',
180
+ color: '#9397A1',
181
+ text: translate('notification.common.account', locale),
182
+ },
183
+ },
184
+ {
185
+ type: 'text',
186
+ data: {
187
+ type: 'plain',
188
+ text: userDid,
189
+ },
190
+ },
191
+ {
192
+ type: 'text',
193
+ data: {
194
+ type: 'plain',
195
+ color: '#9397A1',
196
+ text: translate('notification.common.product', locale),
197
+ },
198
+ },
199
+ {
200
+ type: 'text',
201
+ data: {
202
+ type: 'plain',
203
+ text: productName,
204
+ },
205
+ },
206
+ {
207
+ type: 'text',
208
+ data: {
209
+ type: 'plain',
210
+ color: '#9397A1',
211
+ text: translate('notification.common.paymentInfo', locale),
212
+ },
213
+ },
214
+ {
215
+ type: 'text',
216
+ data: {
217
+ type: 'plain',
218
+ text: paymentInfo,
219
+ },
220
+ },
221
+ {
222
+ type: 'text',
223
+ data: {
224
+ type: 'plain',
225
+ color: '#9397A1',
226
+ text: translate('notification.common.refundAmount', locale),
227
+ },
228
+ },
229
+ {
230
+ type: 'text',
231
+ data: {
232
+ type: 'plain',
233
+ text: refundInfo,
234
+ },
235
+ },
236
+ {
237
+ type: 'text',
238
+ data: {
239
+ type: 'plain',
240
+ color: '#9397A1',
241
+ text: translate('notification.common.validityPeriod', locale),
242
+ },
243
+ },
244
+ {
245
+ type: 'text',
246
+ data: {
247
+ type: 'plain',
248
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
249
+ },
250
+ },
251
+ {
252
+ type: 'text',
253
+ data: {
254
+ type: 'plain',
255
+ color: '#9397A1',
256
+ text: translate('notification.common.refundPeriod', locale),
257
+ },
258
+ },
259
+ {
260
+ type: 'text',
261
+ data: {
262
+ type: 'plain',
263
+ text: `${unusedPeriodStart} ~ ${unusedPeriodEnd}(${unusedDuration})`,
264
+ },
265
+ },
266
+ ].filter(Boolean),
267
+ },
268
+ ].filter(Boolean),
269
+ // @ts-ignore
270
+ actions: [
271
+ {
272
+ name: 'viewSubscription',
273
+ title: translate('notification.common.viewSubscription', locale),
274
+ link: viewSubscriptionLink,
275
+ },
276
+ viewTxHashLink && {
277
+ name: 'viewTxHash',
278
+ title: translate('notification.common.viewTxHash', locale),
279
+ link: viewTxHashLink as string,
280
+ },
281
+ ].filter(Boolean),
282
+ };
283
+
284
+ return template;
285
+ }
286
+ }
@@ -97,10 +97,11 @@ export class SubscriptionRenewFailedEmailTemplate
97
97
  },
98
98
  });
99
99
 
100
- const locale = await getUserLocale(customer.did);
100
+ const userDid: string = customer.did;
101
+ const locale = await getUserLocale(userDid);
101
102
  const productName = await getMainProductName(subscription.id);
102
103
  const at: string = formatTime(Date.now());
103
- const reason: string = await this.getReason(customer.did, invoice, locale);
104
+ const reason: string = await this.getReason(userDid, invoice, locale);
104
105
 
105
106
  const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
106
107
  const nftMintItem: NftMintItem | undefined = hasNft
@@ -109,6 +110,7 @@ export class SubscriptionRenewFailedEmailTemplate
109
110
  const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
110
111
  paymentCurrency.symbol
111
112
  }`;
113
+
112
114
  const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
113
115
  const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
114
116
  const duration: string = prettyMsI18n(
@@ -121,8 +123,17 @@ export class SubscriptionRenewFailedEmailTemplate
121
123
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
122
124
  const chainHost: string | undefined =
123
125
  paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
124
- const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
125
- const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale, 'pay');
126
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
127
+ subscriptionId: subscription.id,
128
+ locale,
129
+ userDid,
130
+ });
131
+ const viewInvoiceLink = getCustomerInvoicePageUrl({
132
+ invoiceId: invoice.id,
133
+ userDid,
134
+ locale,
135
+ action: 'pay',
136
+ });
126
137
  const txHash: string | undefined =
127
138
  paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
128
139
  const viewTxHashLink: string | undefined = hasNft && txHash ? getExplorerLink(chainHost, txHash, 'tx') : undefined;
@@ -134,7 +145,7 @@ export class SubscriptionRenewFailedEmailTemplate
134
145
  reason,
135
146
 
136
147
  nftMintItem,
137
- userDid: customer.did,
148
+ userDid,
138
149
  paymentInfo,
139
150
  currentPeriodStart,
140
151
  currentPeriodEnd,
@@ -263,7 +274,7 @@ export class SubscriptionRenewFailedEmailTemplate
263
274
  link: viewSubscriptionLink,
264
275
  },
265
276
  {
266
- name: 'viewSubscription',
277
+ name: 'viewInvoice',
267
278
  title: translate('notification.subscriptionRenewFailed.renewNow', locale),
268
279
  link: viewInvoiceLink,
269
280
  },
@@ -35,6 +35,12 @@ interface SubscriptionRenewedEmailTemplateContext {
35
35
  nftMintItem: NftMintItem | undefined;
36
36
  userDid: string;
37
37
  paymentInfo: string;
38
+ /**
39
+ * @description 支付金额
40
+ * @type {number}
41
+ * @memberof SubscriptionRenewedEmailTemplateContext
42
+ */
43
+ amountPaid: number;
38
44
  currentPeriodStart: string;
39
45
  currentPeriodEnd: string;
40
46
  duration: string;
@@ -82,8 +88,8 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
82
88
  subscription_id: subscription.id,
83
89
  },
84
90
  });
85
-
86
- const locale = await getUserLocale(customer.did);
91
+ const userDid: string = customer.did;
92
+ const locale = await getUserLocale(userDid);
87
93
  const productName = await getMainProductName(subscription.id);
88
94
  const at: string = formatTime(Date.now());
89
95
 
@@ -91,6 +97,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
91
97
  const nftMintItem: NftMintItem | undefined = hasNft
92
98
  ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
93
99
  : undefined;
100
+ const amountPaid = +fromUnitToToken(invoice.amount_paid, paymentCurrency.decimal);
94
101
  const paymentInfo: string = `${fromUnitToToken(invoice.amount_paid, paymentCurrency.decimal)} ${
95
102
  paymentCurrency.symbol
96
103
  }`;
@@ -106,8 +113,16 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
106
113
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
107
114
  const chainHost: string | undefined =
108
115
  paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
109
- const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
110
- const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
116
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
117
+ subscriptionId: subscription.id,
118
+ locale,
119
+ userDid,
120
+ });
121
+ const viewInvoiceLink = getCustomerInvoicePageUrl({
122
+ invoiceId: invoice.id,
123
+ userDid,
124
+ locale,
125
+ });
111
126
  const txHash: string | undefined =
112
127
  paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
113
128
  const viewTxHashLink: string | undefined = hasNft && txHash ? getExplorerLink(chainHost, txHash, 'tx') : undefined;
@@ -118,8 +133,9 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
118
133
  at,
119
134
 
120
135
  nftMintItem,
121
- userDid: customer.did,
136
+ userDid,
122
137
  paymentInfo,
138
+ amountPaid,
123
139
  currentPeriodStart,
124
140
  currentPeriodEnd,
125
141
  duration,
@@ -138,6 +154,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
138
154
  nftMintItem,
139
155
  userDid,
140
156
  paymentInfo,
157
+ amountPaid,
141
158
  currentPeriodStart,
142
159
  currentPeriodEnd,
143
160
  duration,
@@ -201,7 +218,11 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
201
218
  type: 'text',
202
219
  data: {
203
220
  type: 'plain',
204
- text: paymentInfo,
221
+ text: `${paymentInfo}${
222
+ amountPaid === 0
223
+ ? ` (${translate('notification.subscriptionRenewed.noExpenseIncurred', locale)})`
224
+ : ''
225
+ }`,
205
226
  },
206
227
  },
207
228
  {