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.
- package/api/src/crons/base.ts +7 -33
- package/api/src/crons/index.ts +7 -0
- package/api/src/crons/interface/base.ts +17 -0
- package/api/src/crons/subscription-trail-will-end.ts +29 -1
- package/api/src/crons/subscription-will-canceled.ts +48 -0
- package/api/src/crons/subscription-will-renew.ts +29 -2
- package/api/src/libs/invoice.ts +20 -6
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +245 -0
- package/api/src/libs/notification/template/subscription-cacceled.ts +241 -0
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +286 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +17 -6
- package/api/src/libs/notification/template/subscription-renewed.ts +27 -6
- package/api/src/libs/notification/template/subscription-succeeded.ts +14 -5
- package/api/src/libs/notification/template/subscription-trial-start.ts +13 -4
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +7 -3
- package/api/src/libs/notification/template/subscription-upgraded.ts +261 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +225 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +30 -3
- package/api/src/libs/product.ts +24 -0
- package/api/src/libs/queue/index.ts +2 -0
- package/api/src/libs/queue/store.ts +1 -1
- package/api/src/libs/security.ts +1 -1
- package/api/src/libs/subscription.ts +19 -3
- package/api/src/libs/util.ts +33 -0
- package/api/src/locales/en.ts +38 -4
- package/api/src/locales/zh.ts +36 -2
- package/api/src/queues/notification.ts +91 -2
- package/api/src/routes/connect/setup.ts +2 -2
- package/api/src/routes/connect/shared.ts +2 -2
- package/api/src/store/models/subscription.ts +5 -0
- package/api/src/store/models/types.ts +3 -0
- package/blocklet.yml +1 -1
- package/package.json +44 -44
- package/src/contexts/session.ts +2 -2
- package/src/pages/admin/payments/links/create.tsx +1 -1
- package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
- package/src/pages/customer/invoice.tsx +12 -3
- package/src/pages/customer/subscription/update.tsx +1 -1
- 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
|
|
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(
|
|
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(
|
|
125
|
-
|
|
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
|
|
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: '
|
|
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(
|
|
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(
|
|
110
|
-
|
|
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
|
|
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
|
{
|