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
|
@@ -84,7 +84,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
84
84
|
|
|
85
85
|
return Boolean(
|
|
86
86
|
['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
|
|
87
|
-
invoice?.payment_intent_id
|
|
87
|
+
(invoice?.payment_intent_id || (invoice && +invoice.total === 0))
|
|
88
88
|
);
|
|
89
89
|
},
|
|
90
90
|
{ timeout: 1000 * 10, interval: 1000 }
|
|
@@ -108,7 +108,8 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
108
108
|
},
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const userDid: string = customer.did;
|
|
112
|
+
const locale = await getUserLocale(userDid);
|
|
112
113
|
const productName = await getMainProductName(subscription.id);
|
|
113
114
|
const at: string = formatTime(subscription.created_at);
|
|
114
115
|
|
|
@@ -130,8 +131,16 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
130
131
|
// @FIXME: 获取 chainHost 困难的一批?
|
|
131
132
|
const chainHost: string | undefined =
|
|
132
133
|
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
|
|
133
|
-
const viewSubscriptionLink = getCustomerSubscriptionPageUrl(
|
|
134
|
-
|
|
134
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
135
|
+
subscriptionId: subscription.id,
|
|
136
|
+
locale,
|
|
137
|
+
userDid,
|
|
138
|
+
});
|
|
139
|
+
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
140
|
+
invoiceId: invoice.id,
|
|
141
|
+
userDid,
|
|
142
|
+
locale,
|
|
143
|
+
});
|
|
135
144
|
|
|
136
145
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
137
146
|
const txHash: string | undefined =
|
|
@@ -143,7 +152,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
143
152
|
productName,
|
|
144
153
|
at,
|
|
145
154
|
|
|
146
|
-
userDid
|
|
155
|
+
userDid,
|
|
147
156
|
nftMintItem,
|
|
148
157
|
chainHost,
|
|
149
158
|
paymentInfo,
|
|
@@ -90,7 +90,8 @@ export class SubscriptionTrailStartEmailTemplate
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
const userDid: string = customer.did;
|
|
94
|
+
const locale = await getUserLocale(userDid);
|
|
94
95
|
const productName = await getMainProductName(subscription.id);
|
|
95
96
|
const subscriptionTrialEnd: string = formatTime((subscription.trail_end as number) * 1000);
|
|
96
97
|
|
|
@@ -111,8 +112,16 @@ export class SubscriptionTrailStartEmailTemplate
|
|
|
111
112
|
}
|
|
112
113
|
);
|
|
113
114
|
|
|
114
|
-
const viewSubscriptionLink = getCustomerSubscriptionPageUrl(
|
|
115
|
-
|
|
115
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
116
|
+
subscriptionId: subscription.id,
|
|
117
|
+
locale,
|
|
118
|
+
userDid,
|
|
119
|
+
});
|
|
120
|
+
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
121
|
+
invoiceId: invoice.id,
|
|
122
|
+
userDid,
|
|
123
|
+
locale,
|
|
124
|
+
});
|
|
116
125
|
|
|
117
126
|
return {
|
|
118
127
|
locale,
|
|
@@ -121,7 +130,7 @@ export class SubscriptionTrailStartEmailTemplate
|
|
|
121
130
|
|
|
122
131
|
nftMintItem,
|
|
123
132
|
chainHost,
|
|
124
|
-
userDid
|
|
133
|
+
userDid,
|
|
125
134
|
paymentInfo,
|
|
126
135
|
currentPeriodStart,
|
|
127
136
|
currentPeriodEnd,
|
|
@@ -70,13 +70,13 @@ export class SubscriptionTrailWilEndEmailTemplate
|
|
|
70
70
|
},
|
|
71
71
|
})) as PaymentCurrency;
|
|
72
72
|
|
|
73
|
-
const
|
|
73
|
+
const userDid = customer.did;
|
|
74
|
+
const locale = await getUserLocale(userDid);
|
|
74
75
|
const productName = await getMainProductName(subscription.id);
|
|
75
76
|
const at: string = formatTime((subscription.trail_end as number) * 1000);
|
|
76
77
|
const willRenewDuration: string =
|
|
77
78
|
locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).split(' ').join('');
|
|
78
79
|
|
|
79
|
-
const userDid = customer.did;
|
|
80
80
|
const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
|
|
81
81
|
const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
82
82
|
const currentPeriodStart: string = formatTime((subscription.trail_start as number) * 1000);
|
|
@@ -88,7 +88,11 @@ export class SubscriptionTrailWilEndEmailTemplate
|
|
|
88
88
|
}
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
-
const viewSubscriptionLink = getCustomerSubscriptionPageUrl(
|
|
91
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
92
|
+
subscriptionId: subscription.id,
|
|
93
|
+
locale,
|
|
94
|
+
userDid,
|
|
95
|
+
});
|
|
92
96
|
|
|
93
97
|
return {
|
|
94
98
|
locale,
|
|
@@ -0,0 +1,261 @@
|
|
|
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 {
|
|
9
|
+
CheckoutSession,
|
|
10
|
+
Customer,
|
|
11
|
+
NftMintItem,
|
|
12
|
+
PaymentIntent,
|
|
13
|
+
PaymentMethod,
|
|
14
|
+
Subscription,
|
|
15
|
+
} from '../../../store/models';
|
|
16
|
+
import { Invoice } from '../../../store/models/invoice';
|
|
17
|
+
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
18
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
19
|
+
import { getMainProductName } from '../../product';
|
|
20
|
+
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
21
|
+
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
22
|
+
import { getExplorerLink } from '../../util';
|
|
23
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
24
|
+
|
|
25
|
+
export interface SubscriptionUpgradedEmailTemplateOptions {
|
|
26
|
+
subscriptionId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SubscriptionUpgradedEmailTemplateContext {
|
|
30
|
+
locale: string;
|
|
31
|
+
productName: string;
|
|
32
|
+
at: string;
|
|
33
|
+
nftMintItem: NftMintItem | undefined;
|
|
34
|
+
chainHost: string | undefined;
|
|
35
|
+
userDid: string;
|
|
36
|
+
paymentInfo: string;
|
|
37
|
+
currentPeriodStart: string;
|
|
38
|
+
currentPeriodEnd: string;
|
|
39
|
+
duration: string;
|
|
40
|
+
viewSubscriptionLink: string;
|
|
41
|
+
viewInvoiceLink: string;
|
|
42
|
+
viewTxHashLink: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<SubscriptionUpgradedEmailTemplateContext> {
|
|
46
|
+
options: SubscriptionUpgradedEmailTemplateOptions;
|
|
47
|
+
|
|
48
|
+
constructor(options: SubscriptionUpgradedEmailTemplateOptions) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getContext(): Promise<SubscriptionUpgradedEmailTemplateContext> {
|
|
53
|
+
const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
|
|
54
|
+
if (!subscription) {
|
|
55
|
+
throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
|
|
56
|
+
}
|
|
57
|
+
if (subscription.status !== 'active') {
|
|
58
|
+
throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
62
|
+
if (!customer) {
|
|
63
|
+
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
|
|
67
|
+
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
68
|
+
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
69
|
+
where: {
|
|
70
|
+
id: subscription.currency_id,
|
|
71
|
+
},
|
|
72
|
+
})) as PaymentCurrency;
|
|
73
|
+
|
|
74
|
+
const checkoutSession = await CheckoutSession.findOne({
|
|
75
|
+
where: {
|
|
76
|
+
subscription_id: subscription.id,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const userDid: string = customer.did;
|
|
81
|
+
const locale = await getUserLocale(userDid);
|
|
82
|
+
const productName = await getMainProductName(subscription.id);
|
|
83
|
+
const at: string = formatTime(subscription.created_at);
|
|
84
|
+
|
|
85
|
+
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount, paymentCurrency.decimal)} ${
|
|
86
|
+
paymentCurrency.symbol
|
|
87
|
+
}`;
|
|
88
|
+
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
89
|
+
const nftMintItem: NftMintItem | undefined = hasNft
|
|
90
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
|
|
91
|
+
: undefined;
|
|
92
|
+
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
93
|
+
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
94
|
+
const duration: string = prettyMsI18n(
|
|
95
|
+
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
96
|
+
{
|
|
97
|
+
locale: getPrettyMsI18nLocale(locale),
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
102
|
+
// @ts-expect-error
|
|
103
|
+
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
104
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
105
|
+
subscriptionId: subscription.id,
|
|
106
|
+
locale,
|
|
107
|
+
userDid,
|
|
108
|
+
});
|
|
109
|
+
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
110
|
+
invoiceId: invoice.id,
|
|
111
|
+
userDid,
|
|
112
|
+
locale,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// @ts-expect-error
|
|
116
|
+
const txHash: string | undefined = paymentIntent?.payment_details?.[paymentMethod.type]?.tx_hash;
|
|
117
|
+
const viewTxHashLink: string | undefined = txHash && getExplorerLink(chainHost, txHash, 'tx');
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
locale,
|
|
121
|
+
productName,
|
|
122
|
+
at,
|
|
123
|
+
|
|
124
|
+
userDid,
|
|
125
|
+
nftMintItem,
|
|
126
|
+
chainHost,
|
|
127
|
+
paymentInfo,
|
|
128
|
+
currentPeriodStart,
|
|
129
|
+
currentPeriodEnd,
|
|
130
|
+
duration,
|
|
131
|
+
|
|
132
|
+
viewSubscriptionLink,
|
|
133
|
+
viewInvoiceLink,
|
|
134
|
+
viewTxHashLink,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
139
|
+
const {
|
|
140
|
+
locale,
|
|
141
|
+
productName,
|
|
142
|
+
at,
|
|
143
|
+
nftMintItem,
|
|
144
|
+
chainHost,
|
|
145
|
+
userDid,
|
|
146
|
+
paymentInfo,
|
|
147
|
+
currentPeriodStart,
|
|
148
|
+
currentPeriodEnd,
|
|
149
|
+
duration,
|
|
150
|
+
viewSubscriptionLink,
|
|
151
|
+
viewInvoiceLink,
|
|
152
|
+
viewTxHashLink,
|
|
153
|
+
} = await this.getContext();
|
|
154
|
+
|
|
155
|
+
const template: BaseEmailTemplateType = {
|
|
156
|
+
title: `${translate('notification.subscriptionUpgraded.title', locale, {
|
|
157
|
+
productName: `(${productName})`,
|
|
158
|
+
})}`,
|
|
159
|
+
body: `${translate('notification.subscriptionUpgraded.body', locale, {
|
|
160
|
+
at,
|
|
161
|
+
productName: `(${productName})`,
|
|
162
|
+
})}`,
|
|
163
|
+
// @ts-expect-error
|
|
164
|
+
attachments: [
|
|
165
|
+
nftMintItem &&
|
|
166
|
+
chainHost && {
|
|
167
|
+
type: 'asset',
|
|
168
|
+
data: {
|
|
169
|
+
chainHost,
|
|
170
|
+
did: nftMintItem.address,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
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.validityPeriod', locale),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: 'text',
|
|
231
|
+
data: {
|
|
232
|
+
type: 'plain',
|
|
233
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
].filter(Boolean),
|
|
237
|
+
},
|
|
238
|
+
].filter(Boolean),
|
|
239
|
+
// @ts-ignore
|
|
240
|
+
actions: [
|
|
241
|
+
{
|
|
242
|
+
name: 'viewSubscription',
|
|
243
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
244
|
+
link: viewSubscriptionLink,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'viewSubscription',
|
|
248
|
+
title: translate('notification.common.viewInvoice', locale),
|
|
249
|
+
link: viewInvoiceLink,
|
|
250
|
+
},
|
|
251
|
+
viewTxHashLink && {
|
|
252
|
+
name: 'viewTxHash',
|
|
253
|
+
title: translate('notification.common.viewTxHash', locale),
|
|
254
|
+
link: viewTxHashLink,
|
|
255
|
+
},
|
|
256
|
+
].filter(Boolean),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return template;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
4
|
+
import type { ManipulateType } from 'dayjs';
|
|
5
|
+
|
|
6
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
|
+
import { translate } from '../../../locales';
|
|
8
|
+
import { Customer, Invoice, Subscription } from '../../../store/models';
|
|
9
|
+
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
10
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
11
|
+
import logger from '../../logger';
|
|
12
|
+
import { getMainProductName } from '../../product';
|
|
13
|
+
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
14
|
+
import { formatTime } from '../../time';
|
|
15
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
16
|
+
|
|
17
|
+
export interface SubscriptionWillCanceledEmailTemplateOptions {
|
|
18
|
+
subscriptionId: string;
|
|
19
|
+
willCancelValue: number;
|
|
20
|
+
willCancelUnit: ManipulateType;
|
|
21
|
+
required?: false | true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SubscriptionWillCanceledEmailTemplateContext {
|
|
25
|
+
locale: string;
|
|
26
|
+
productName: string;
|
|
27
|
+
at: string;
|
|
28
|
+
willCancelDuration: string;
|
|
29
|
+
|
|
30
|
+
userDid: string;
|
|
31
|
+
paymentInfo: string;
|
|
32
|
+
|
|
33
|
+
viewSubscriptionLink: string;
|
|
34
|
+
viewInvoiceLink: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SubscriptionWillCanceledEmailTemplate
|
|
38
|
+
implements BaseEmailTemplate<SubscriptionWillCanceledEmailTemplateContext>
|
|
39
|
+
{
|
|
40
|
+
options: SubscriptionWillCanceledEmailTemplateOptions;
|
|
41
|
+
|
|
42
|
+
constructor(options: SubscriptionWillCanceledEmailTemplateOptions) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getContext(): Promise<SubscriptionWillCanceledEmailTemplateContext> {
|
|
47
|
+
if (!this.options.required) {
|
|
48
|
+
logger.error('SubscriptionWillCanceledEmailTemplate: This execution will be skipped', this.options);
|
|
49
|
+
throw new Error('SubscriptionWillCanceledEmailTemplate: This execution will be skipped');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
|
|
53
|
+
if (!subscription) {
|
|
54
|
+
throw new Error(`Subscription(${this.options.subscriptionId}) not found`);
|
|
55
|
+
}
|
|
56
|
+
if (subscription.status !== 'past_due') {
|
|
57
|
+
throw new Error(`Subscription(${this.options.subscriptionId}) status(${subscription.status}) must be past_due`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
61
|
+
if (!customer) {
|
|
62
|
+
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
|
|
66
|
+
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
67
|
+
where: {
|
|
68
|
+
id: subscription.currency_id,
|
|
69
|
+
},
|
|
70
|
+
})) as PaymentCurrency;
|
|
71
|
+
|
|
72
|
+
const userDid = customer.did;
|
|
73
|
+
const locale = await getUserLocale(userDid);
|
|
74
|
+
const productName = await getMainProductName(subscription.id);
|
|
75
|
+
const at: string = formatTime(invoice.period_end * 1000);
|
|
76
|
+
const willCancelDuration: string =
|
|
77
|
+
locale === 'en' ? this.getWillCancelDuration(locale) : this.getWillCancelDuration(locale).split(' ').join('');
|
|
78
|
+
|
|
79
|
+
const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
80
|
+
|
|
81
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
82
|
+
subscriptionId: subscription.id,
|
|
83
|
+
locale,
|
|
84
|
+
userDid,
|
|
85
|
+
});
|
|
86
|
+
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
87
|
+
invoiceId: invoice.id,
|
|
88
|
+
userDid,
|
|
89
|
+
locale,
|
|
90
|
+
action: 'pay',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
locale,
|
|
95
|
+
productName,
|
|
96
|
+
at,
|
|
97
|
+
willCancelDuration,
|
|
98
|
+
|
|
99
|
+
userDid,
|
|
100
|
+
paymentInfo,
|
|
101
|
+
|
|
102
|
+
viewSubscriptionLink,
|
|
103
|
+
viewInvoiceLink,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getWillCancelDuration(locale: string): string {
|
|
108
|
+
if (this.options.willCancelUnit === 'M') {
|
|
109
|
+
if (this.options.willCancelValue > 1) {
|
|
110
|
+
return `${this.options.willCancelValue} ${translate('notification.common.months', locale)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `${this.options.willCancelValue} ${translate('notification.common.month', locale)}`;
|
|
114
|
+
}
|
|
115
|
+
if (this.options.willCancelUnit === 'd') {
|
|
116
|
+
if (this.options.willCancelValue > 1) {
|
|
117
|
+
return `${this.options.willCancelValue} ${translate('notification.common.days', locale)}`;
|
|
118
|
+
}
|
|
119
|
+
return `${this.options.willCancelValue} ${translate('notification.common.day', locale)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.options.willCancelUnit === 'm') {
|
|
123
|
+
if (this.options.willCancelValue > 1) {
|
|
124
|
+
return `${this.options.willCancelValue} ${translate('notification.common.minutes', locale)}`;
|
|
125
|
+
}
|
|
126
|
+
return `${this.options.willCancelValue} ${translate('notification.common.minute', locale)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `${this.options.willCancelValue} ${this.options.willCancelUnit}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getTemplate(): Promise<BaseEmailTemplateType | null> {
|
|
133
|
+
const {
|
|
134
|
+
locale,
|
|
135
|
+
productName,
|
|
136
|
+
at,
|
|
137
|
+
willCancelDuration,
|
|
138
|
+
|
|
139
|
+
userDid,
|
|
140
|
+
paymentInfo,
|
|
141
|
+
|
|
142
|
+
viewSubscriptionLink,
|
|
143
|
+
viewInvoiceLink,
|
|
144
|
+
} = await this.getContext();
|
|
145
|
+
|
|
146
|
+
const template: BaseEmailTemplateType = {
|
|
147
|
+
title: `${translate('notification.subscriptWillCanceled.title', locale, {
|
|
148
|
+
productName: `(${productName})`,
|
|
149
|
+
})}`,
|
|
150
|
+
body: translate('notification.subscriptWillCanceled.body', locale, {
|
|
151
|
+
productName,
|
|
152
|
+
willCancelDuration,
|
|
153
|
+
at,
|
|
154
|
+
}),
|
|
155
|
+
// @ts-expect-error
|
|
156
|
+
attachments: [
|
|
157
|
+
{
|
|
158
|
+
type: 'section',
|
|
159
|
+
fields: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
data: {
|
|
163
|
+
type: 'plain',
|
|
164
|
+
color: '#9397A1',
|
|
165
|
+
text: translate('notification.common.account', locale),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
data: {
|
|
171
|
+
type: 'plain',
|
|
172
|
+
text: userDid,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'text',
|
|
177
|
+
data: {
|
|
178
|
+
type: 'plain',
|
|
179
|
+
color: '#9397A1',
|
|
180
|
+
text: translate('notification.common.product', locale),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'text',
|
|
185
|
+
data: {
|
|
186
|
+
type: 'plain',
|
|
187
|
+
text: productName,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'text',
|
|
192
|
+
data: {
|
|
193
|
+
type: 'plain',
|
|
194
|
+
color: '#9397A1',
|
|
195
|
+
text: translate('notification.subscriptWillCanceled.renewAmount', locale),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
data: {
|
|
201
|
+
type: 'plain',
|
|
202
|
+
text: paymentInfo,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
].filter(Boolean),
|
|
206
|
+
},
|
|
207
|
+
].filter(Boolean),
|
|
208
|
+
// @ts-ignore
|
|
209
|
+
actions: [
|
|
210
|
+
viewSubscriptionLink && {
|
|
211
|
+
name: 'viewSubscription',
|
|
212
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
213
|
+
link: viewSubscriptionLink,
|
|
214
|
+
},
|
|
215
|
+
viewInvoiceLink && {
|
|
216
|
+
name: 'viewInvoice',
|
|
217
|
+
title: translate('notification.common.renewNow', locale),
|
|
218
|
+
link: viewInvoiceLink,
|
|
219
|
+
},
|
|
220
|
+
].filter(Boolean),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return template;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import { fromUnitToToken } from '@ocap/util';
|
|
4
4
|
import type { ManipulateType } from 'dayjs';
|
|
5
|
+
import dayjs from 'dayjs';
|
|
5
6
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
7
|
|
|
7
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
@@ -57,6 +58,9 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
57
58
|
if (subscription.isScheduledToCancel()) {
|
|
58
59
|
throw new Error(`Subscription is scheduled to cancel: ${this.options.subscriptionId}`);
|
|
59
60
|
}
|
|
61
|
+
if (await this.subscriptionIsRenewed(subscription)) {
|
|
62
|
+
throw new Error(`The subscription(${this.options.subscriptionId}) has been renewed`);
|
|
63
|
+
}
|
|
60
64
|
|
|
61
65
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
62
66
|
if (!customer) {
|
|
@@ -70,13 +74,13 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
70
74
|
},
|
|
71
75
|
})) as PaymentCurrency;
|
|
72
76
|
|
|
73
|
-
const
|
|
77
|
+
const userDid = customer.did;
|
|
78
|
+
const locale = await getUserLocale(userDid);
|
|
74
79
|
const productName = await getMainProductName(subscription.id);
|
|
75
80
|
const at: string = formatTime(invoice.period_end * 1000);
|
|
76
81
|
const willRenewDuration: string =
|
|
77
82
|
locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).split(' ').join('');
|
|
78
83
|
|
|
79
|
-
const userDid = customer.did;
|
|
80
84
|
const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
|
|
81
85
|
const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
82
86
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
@@ -88,7 +92,11 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
88
92
|
}
|
|
89
93
|
);
|
|
90
94
|
|
|
91
|
-
const viewSubscriptionLink = getCustomerSubscriptionPageUrl(
|
|
95
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
96
|
+
subscriptionId: subscription.id,
|
|
97
|
+
locale,
|
|
98
|
+
userDid,
|
|
99
|
+
});
|
|
92
100
|
|
|
93
101
|
return {
|
|
94
102
|
locale,
|
|
@@ -107,6 +115,25 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
107
115
|
};
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
/**
|
|
119
|
+
* @see https://github.com/blocklet/payment-kit/issues/307
|
|
120
|
+
* @description 该订阅已经在指定期限内续费过了吗?
|
|
121
|
+
* @param {Subscription} subscription
|
|
122
|
+
* @return {*} {Promise<void>}
|
|
123
|
+
* @memberof SubscriptionWillRenewEmailTemplate
|
|
124
|
+
*/
|
|
125
|
+
// eslint-disable-next-line require-await
|
|
126
|
+
async subscriptionIsRenewed(subscription: Subscription): Promise<boolean> {
|
|
127
|
+
// @note: 为了最精确的验证,这里的期限 this.options.willRenewValue 还要 + 1,如果没有订阅没有续费,那么 expectedCurrentPeriodEnd 一定是大于 currentPeriodEnd
|
|
128
|
+
const expectedCurrentPeriodEnd: number = dayjs()
|
|
129
|
+
.add(this.options.willRenewValue + 1, this.options.willRenewUnit)
|
|
130
|
+
.toDate()
|
|
131
|
+
.getTime();
|
|
132
|
+
const currentPeriodEnd: number = subscription.current_period_end * 1000;
|
|
133
|
+
|
|
134
|
+
return currentPeriodEnd > expectedCurrentPeriodEnd;
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
getWillRenewDuration(locale: string): string {
|
|
111
138
|
if (this.options.willRenewUnit === 'M') {
|
|
112
139
|
if (this.options.willRenewValue > 1) {
|
package/api/src/libs/product.ts
CHANGED
|
@@ -38,3 +38,27 @@ export async function getMainProductName(subscriptionId: string): Promise<string
|
|
|
38
38
|
|
|
39
39
|
return product.name;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
export async function getMainProductNameByCheckoutSession(checkoutSession: CheckoutSession): Promise<string> {
|
|
43
|
+
const priceId: string = checkoutSession?.line_items.find((x) => !x.cross_sell)?.price_id as string;
|
|
44
|
+
|
|
45
|
+
if (!priceId) {
|
|
46
|
+
throw new Error(`PriceId can't be found by checkoutSession(${checkoutSession.id})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const price = await Price.findByPk(priceId, {
|
|
50
|
+
attributes: ['product_id'],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!price) {
|
|
54
|
+
throw new Error(`Price can't be found by priceId(${priceId})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const product = await Product.findByPk(price?.product_id);
|
|
58
|
+
|
|
59
|
+
if (!product) {
|
|
60
|
+
throw new Error(`Product can't be found by productId(${price?.product_id})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return product.name;
|
|
64
|
+
}
|