payment-kit 1.15.3 → 1.15.5
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/index.ts +3 -0
- package/api/src/integrations/blocklet/user.ts +30 -0
- package/api/src/libs/invoice.ts +41 -0
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +41 -20
- package/api/src/libs/notification/template/subscription-renew-failed.ts +19 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +94 -9
- package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +45 -15
- package/api/src/libs/notification/template/subscription-will-renew.ts +28 -11
- package/api/src/libs/util.ts +18 -1
- package/api/src/locales/en.ts +12 -3
- package/api/src/locales/zh.ts +12 -3
- package/api/src/queues/payment.ts +3 -1
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/subscriptions.ts +30 -5
- package/api/src/routes/usage-records.ts +13 -4
- package/api/src/store/migrations/20240910-customer-sync.ts +21 -0
- package/api/src/store/models/customer.ts +5 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -9
- package/scripts/sdk.js +25 -2
- package/src/components/payment-link/before-pay.tsx +41 -29
- package/src/components/pricing-table/product-settings.tsx +37 -25
- package/src/pages/admin/index.tsx +0 -1
- package/src/pages/admin/payments/intents/detail.tsx +14 -1
- package/src/pages/admin/payments/payouts/detail.tsx +6 -1
- package/src/pages/admin/products/pricing-tables/create.tsx +3 -0
- package/src/pages/checkout/pricing-table.tsx +26 -7
- package/src/pages/customer/index.tsx +3 -3
- package/src/pages/customer/invoice/past-due.tsx +14 -2
package/api/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import setupHandlers from './routes/connect/setup';
|
|
|
37
37
|
import subscribeHandlers from './routes/connect/subscribe';
|
|
38
38
|
import { initialize } from './store/models';
|
|
39
39
|
import { sequelize } from './store/sequelize';
|
|
40
|
+
import { initUserHandler } from './integrations/blocklet/user';
|
|
40
41
|
|
|
41
42
|
dotenv.config();
|
|
42
43
|
|
|
@@ -131,4 +132,6 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
131
132
|
initEventBroadcast();
|
|
132
133
|
|
|
133
134
|
initResourceHandler();
|
|
135
|
+
|
|
136
|
+
initUserHandler();
|
|
134
137
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Customer } from '@api/store/models';
|
|
2
|
+
import notification from '@blocklet/sdk/service/notification';
|
|
3
|
+
|
|
4
|
+
import logger from '../../libs/logger';
|
|
5
|
+
|
|
6
|
+
const handleUserUpdate = async ({ user }: { user: any }) => {
|
|
7
|
+
logger.info('user Updated', {
|
|
8
|
+
did: user.did,
|
|
9
|
+
updated_at: user.updatedAt,
|
|
10
|
+
});
|
|
11
|
+
const customer = await Customer.findOne({
|
|
12
|
+
where: {
|
|
13
|
+
did: user.did,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (customer) {
|
|
17
|
+
const now = Math.floor(Date.now() / 1000);
|
|
18
|
+
await customer.update({
|
|
19
|
+
name: user.fullName,
|
|
20
|
+
email: user.email,
|
|
21
|
+
phone: user.phone,
|
|
22
|
+
last_sync_at: now,
|
|
23
|
+
});
|
|
24
|
+
logger.info(`customer info updated: ${customer.did}`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function initUserHandler() {
|
|
29
|
+
notification.on('user.updated', handleUserUpdate);
|
|
30
|
+
}
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { component } from '@blocklet/sdk';
|
|
|
2
2
|
import type { LiteralUnion } from 'type-fest';
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
4
|
|
|
5
|
+
import { Invoice, InvoiceItem, PaymentCurrency, Price, Product } from '@api/store/models';
|
|
6
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
5
7
|
import { getConnectQueryParam } from './util';
|
|
8
|
+
import { expandLineItems } from './session';
|
|
6
9
|
|
|
7
10
|
export function getCustomerInvoicePageUrl({
|
|
8
11
|
invoiceId,
|
|
@@ -23,3 +26,41 @@ export function getCustomerInvoicePageUrl({
|
|
|
23
26
|
})
|
|
24
27
|
);
|
|
25
28
|
}
|
|
29
|
+
|
|
30
|
+
export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency: PaymentCurrency) {
|
|
31
|
+
try {
|
|
32
|
+
const doc = await Invoice.findOne({
|
|
33
|
+
where: { id: invoiceId },
|
|
34
|
+
include: [{ model: InvoiceItem, as: 'lines' }],
|
|
35
|
+
});
|
|
36
|
+
if (!doc) {
|
|
37
|
+
throw new Error(`Invoice not found in ${invoiceId}`);
|
|
38
|
+
}
|
|
39
|
+
const json = doc.toJSON();
|
|
40
|
+
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
41
|
+
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
expandLineItems(json.lines, products, prices);
|
|
44
|
+
const oneTimePaymentInfo: Array<{
|
|
45
|
+
productName: string;
|
|
46
|
+
paymentInfo: string;
|
|
47
|
+
quantity: number;
|
|
48
|
+
}> = [];
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
(json.lines || []).forEach((x) => {
|
|
51
|
+
const { recurring, product } = x.price;
|
|
52
|
+
if (!recurring) {
|
|
53
|
+
// it's one-time product
|
|
54
|
+
oneTimePaymentInfo.push({
|
|
55
|
+
productName: product?.name,
|
|
56
|
+
paymentInfo: `${fromUnitToToken(x.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
|
|
57
|
+
quantity: x.quantity,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return oneTimePaymentInfo;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(err);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
5
5
|
import pWaitFor from 'p-wait-for';
|
|
6
6
|
import type { LiteralUnion } from 'type-fest';
|
|
7
7
|
|
|
8
|
+
import { joinURL } from 'ufo';
|
|
8
9
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
10
|
import { translate } from '../../../locales';
|
|
10
11
|
import {
|
|
@@ -21,7 +22,7 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
|
21
22
|
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
22
23
|
import logger from '../../logger';
|
|
23
24
|
import { formatTime } from '../../time';
|
|
24
|
-
import { getExplorerLink } from '../../util';
|
|
25
|
+
import { getCustomerProfileUrl, getExplorerLink } from '../../util';
|
|
25
26
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
26
27
|
import { blocklet } from '../../auth';
|
|
27
28
|
|
|
@@ -37,7 +38,14 @@ interface CustomerRewardSucceededEmailTemplateContext {
|
|
|
37
38
|
chainHost: string | undefined;
|
|
38
39
|
userDid: string;
|
|
39
40
|
paymentInfo: string;
|
|
40
|
-
rewardDetail:
|
|
41
|
+
rewardDetail:
|
|
42
|
+
| {
|
|
43
|
+
url: string;
|
|
44
|
+
title: string;
|
|
45
|
+
appDID: string;
|
|
46
|
+
logo?: string;
|
|
47
|
+
}[]
|
|
48
|
+
| null;
|
|
41
49
|
donationSettings: DonationSettings;
|
|
42
50
|
|
|
43
51
|
viewInvoiceLink: string;
|
|
@@ -117,7 +125,7 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
117
125
|
`Payment intent cannot be found for checkoutSession.payment_intent_id${checkoutSession!.payment_intent_id}`
|
|
118
126
|
);
|
|
119
127
|
}
|
|
120
|
-
const rewardDetail
|
|
128
|
+
const rewardDetail = await this.getRewardDetail({
|
|
121
129
|
paymentIntent,
|
|
122
130
|
paymentCurrency,
|
|
123
131
|
locale,
|
|
@@ -173,10 +181,18 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
173
181
|
paymentIntent: PaymentIntent;
|
|
174
182
|
paymentCurrency: PaymentCurrency;
|
|
175
183
|
locale: LiteralUnion<'zh' | 'en', string>;
|
|
176
|
-
}): Promise<
|
|
184
|
+
}): Promise<
|
|
185
|
+
| {
|
|
186
|
+
url: string;
|
|
187
|
+
title: string;
|
|
188
|
+
appDID: string;
|
|
189
|
+
logo?: string;
|
|
190
|
+
}[]
|
|
191
|
+
| null
|
|
192
|
+
> {
|
|
177
193
|
if (isEmpty(paymentIntent.beneficiaries)) {
|
|
178
194
|
logger.warn('Payment intent not available', { paymentIntentId: paymentIntent.id });
|
|
179
|
-
return
|
|
195
|
+
return null;
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
const promises = paymentIntent.beneficiaries!.map((x: PaymentBeneficiary) => {
|
|
@@ -185,16 +201,22 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
185
201
|
const users = await Promise.all(promises);
|
|
186
202
|
if (!users.length) {
|
|
187
203
|
logger.warn('No users found for payment intent', { paymentIntentId: paymentIntent.id });
|
|
188
|
-
return
|
|
204
|
+
return null;
|
|
189
205
|
}
|
|
190
|
-
const rewardDetail:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
206
|
+
const rewardDetail = paymentIntent.beneficiaries!.map((x: PaymentBeneficiary, index: number) => {
|
|
207
|
+
return {
|
|
208
|
+
url: getCustomerProfileUrl({ userDid: x.address, locale }),
|
|
209
|
+
title: translate('notification.customerRewardSucceeded.received', locale, {
|
|
210
|
+
address: users[index]?.user?.fullName || x.address,
|
|
194
211
|
amount: `${fromUnitToToken(x.share, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
}),
|
|
213
|
+
logo:
|
|
214
|
+
process.env.BLOCKLET_APP_URL && users[index]?.user?.avatar
|
|
215
|
+
? joinURL(process.env.BLOCKLET_APP_URL, users[index]?.user?.avatar as string)
|
|
216
|
+
: '',
|
|
217
|
+
appDID: x.address,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
198
220
|
|
|
199
221
|
return rewardDetail;
|
|
200
222
|
}
|
|
@@ -274,15 +296,14 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
274
296
|
text: translate('notification.common.rewardDetail', locale),
|
|
275
297
|
},
|
|
276
298
|
},
|
|
277
|
-
{
|
|
278
|
-
type: 'text',
|
|
279
|
-
data: {
|
|
280
|
-
type: 'plain',
|
|
281
|
-
text: `${rewardDetail}`,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
299
|
].filter(Boolean),
|
|
285
300
|
},
|
|
301
|
+
...(rewardDetail
|
|
302
|
+
? rewardDetail.map((x) => ({
|
|
303
|
+
type: 'dapp',
|
|
304
|
+
data: x,
|
|
305
|
+
}))
|
|
306
|
+
: []),
|
|
286
307
|
].filter(Boolean),
|
|
287
308
|
// @ts-ignore
|
|
288
309
|
actions: [
|
|
@@ -78,6 +78,9 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
78
78
|
throw new Error(`Subscription not found: ${this.options.invoice.subscription_id}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (subscription.isImmutable()) {
|
|
82
|
+
throw new Error(`Cannot renew an immutable subscription: ${subscription.id}`);
|
|
83
|
+
}
|
|
81
84
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
82
85
|
if (!customer) {
|
|
83
86
|
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
@@ -188,7 +191,6 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
188
191
|
body: `${translate('notification.subscriptionRenewFailed.body', locale, {
|
|
189
192
|
at,
|
|
190
193
|
productName,
|
|
191
|
-
reason: `<span style="color: red;">${reason}</span>`,
|
|
192
194
|
})}`,
|
|
193
195
|
// @ts-expect-error
|
|
194
196
|
attachments: [
|
|
@@ -240,6 +242,22 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
240
242
|
text: paymentInfo,
|
|
241
243
|
},
|
|
242
244
|
},
|
|
245
|
+
{
|
|
246
|
+
type: 'text',
|
|
247
|
+
data: {
|
|
248
|
+
type: 'plain',
|
|
249
|
+
color: '#9397A1',
|
|
250
|
+
text: translate('notification.common.failReason', locale),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: 'text',
|
|
255
|
+
data: {
|
|
256
|
+
type: 'plain',
|
|
257
|
+
color: '#FF0000',
|
|
258
|
+
text: reason,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
243
261
|
{
|
|
244
262
|
type: 'text',
|
|
245
263
|
data: {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
4
3
|
import pWaitFor from 'p-wait-for';
|
|
5
4
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
5
|
+
import isEmpty from 'lodash/isEmpty';
|
|
6
6
|
|
|
7
|
+
import { getPaymentAmountForCycleSubscription } from '@api/libs/payment';
|
|
7
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
8
9
|
import { translate } from '../../../locales';
|
|
9
10
|
import {
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
} from '../../../store/models';
|
|
17
18
|
import { Invoice } from '../../../store/models/invoice';
|
|
18
19
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
19
|
-
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
20
|
+
import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
|
|
20
21
|
import { getMainProductName } from '../../product';
|
|
21
22
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
22
23
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
@@ -27,6 +28,12 @@ export interface SubscriptionSucceededEmailTemplateOptions {
|
|
|
27
28
|
subscriptionId: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
interface OneTimeProductInfo {
|
|
32
|
+
paymentInfo: string;
|
|
33
|
+
productName: string;
|
|
34
|
+
quantity: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
interface SubscriptionSucceededEmailTemplateContext {
|
|
31
38
|
locale: string;
|
|
32
39
|
productName: string;
|
|
@@ -41,6 +48,7 @@ interface SubscriptionSucceededEmailTemplateContext {
|
|
|
41
48
|
viewSubscriptionLink: string;
|
|
42
49
|
viewInvoiceLink: string;
|
|
43
50
|
viewTxHashLink: string | undefined;
|
|
51
|
+
oneTimeProductInfo?: Array<OneTimeProductInfo>;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export class SubscriptionSucceededEmailTemplate
|
|
@@ -90,12 +98,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
90
98
|
{ timeout: 1000 * 10, interval: 1000 }
|
|
91
99
|
);
|
|
92
100
|
|
|
93
|
-
const invoice = (await Invoice.
|
|
94
|
-
where: {
|
|
95
|
-
subscription_id: subscription.id,
|
|
96
|
-
},
|
|
97
|
-
order: [['created_at', 'ASC']],
|
|
98
|
-
})) as Invoice;
|
|
101
|
+
const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
|
|
99
102
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
100
103
|
where: {
|
|
101
104
|
id: subscription.currency_id,
|
|
@@ -113,7 +116,10 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
113
116
|
const productName = await getMainProductName(subscription.id);
|
|
114
117
|
const at: string = formatTime(subscription.created_at);
|
|
115
118
|
|
|
116
|
-
const
|
|
119
|
+
const oneTimeProductInfo = await getOneTimeProductInfo(subscription.latest_invoice_id as string, paymentCurrency);
|
|
120
|
+
const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
121
|
+
|
|
122
|
+
const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}`;
|
|
117
123
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
118
124
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
119
125
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
|
|
@@ -169,9 +175,76 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
169
175
|
viewSubscriptionLink,
|
|
170
176
|
viewInvoiceLink,
|
|
171
177
|
viewTxHashLink,
|
|
178
|
+
oneTimeProductInfo,
|
|
172
179
|
};
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
getOneTimeProductTemplate(oneTimeProductInfo: Array<OneTimeProductInfo>, locale: string = 'en') {
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
type: 'divider',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
data: {
|
|
190
|
+
type: 'plain',
|
|
191
|
+
text: translate('notification.common.expandPayment', locale),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: 'section',
|
|
196
|
+
fields: (oneTimeProductInfo || []).flatMap((x: any) => [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
data: {
|
|
200
|
+
type: 'plain',
|
|
201
|
+
color: '#9397A1',
|
|
202
|
+
text: translate('notification.common.product', locale),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
data: {
|
|
208
|
+
type: 'plain',
|
|
209
|
+
text: x?.productName,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: 'text',
|
|
214
|
+
data: {
|
|
215
|
+
type: 'plain',
|
|
216
|
+
color: '#9397A1',
|
|
217
|
+
text: translate('notification.common.paymentAmount', locale),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
data: {
|
|
223
|
+
type: 'plain',
|
|
224
|
+
text: x?.paymentInfo,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'text',
|
|
229
|
+
data: {
|
|
230
|
+
type: 'plain',
|
|
231
|
+
color: '#9397A1',
|
|
232
|
+
text: translate('notification.common.paymentQuantity', locale),
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
data: {
|
|
238
|
+
type: 'plain',
|
|
239
|
+
text: translate('notification.common.qty', locale, {
|
|
240
|
+
count: x.quantity,
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
]),
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
}
|
|
175
248
|
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
176
249
|
const {
|
|
177
250
|
locale,
|
|
@@ -187,7 +260,9 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
187
260
|
viewSubscriptionLink,
|
|
188
261
|
viewInvoiceLink,
|
|
189
262
|
viewTxHashLink,
|
|
263
|
+
oneTimeProductInfo,
|
|
190
264
|
} = await this.getContext();
|
|
265
|
+
const hasOneTimeProduct = !isEmpty(oneTimeProductInfo);
|
|
191
266
|
|
|
192
267
|
const template: BaseEmailTemplateType = {
|
|
193
268
|
title: `${translate('notification.subscriptionSucceed.title', locale, {
|
|
@@ -207,6 +282,13 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
207
282
|
did: nftMintItem.address,
|
|
208
283
|
},
|
|
209
284
|
},
|
|
285
|
+
hasOneTimeProduct && {
|
|
286
|
+
type: 'text',
|
|
287
|
+
data: {
|
|
288
|
+
type: 'plain',
|
|
289
|
+
text: translate('notification.common.subscribeProduct', locale),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
210
292
|
{
|
|
211
293
|
type: 'section',
|
|
212
294
|
fields: [
|
|
@@ -272,6 +354,9 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
272
354
|
},
|
|
273
355
|
].filter(Boolean),
|
|
274
356
|
},
|
|
357
|
+
...(hasOneTimeProduct
|
|
358
|
+
? this.getOneTimeProductTemplate(oneTimeProductInfo as Array<OneTimeProductInfo>, locale)
|
|
359
|
+
: []),
|
|
275
360
|
].filter(Boolean),
|
|
276
361
|
// @ts-ignore
|
|
277
362
|
actions: [
|
|
@@ -4,11 +4,12 @@ import { fromUnitToToken, toDid } from '@ocap/util';
|
|
|
4
4
|
import pWaitFor from 'p-wait-for';
|
|
5
5
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
6
|
|
|
7
|
+
import isEmpty from 'lodash/isEmpty';
|
|
7
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
8
9
|
import { translate } from '../../../locales';
|
|
9
10
|
import { CheckoutSession, Customer, Invoice, NftMintItem, PaymentMethod, Subscription } from '../../../store/models';
|
|
10
11
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
11
|
-
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
12
|
+
import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
|
|
12
13
|
import { getMainProductName } from '../../product';
|
|
13
14
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
14
15
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
@@ -18,6 +19,12 @@ export interface SubscriptionTrialStartEmailTemplateOptions {
|
|
|
18
19
|
subscriptionId: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
interface OneTimeProductInfo {
|
|
23
|
+
paymentInfo: string;
|
|
24
|
+
productName: string;
|
|
25
|
+
quantity: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
interface SubscriptionTrialStartEmailTemplateContext {
|
|
22
29
|
locale: string;
|
|
23
30
|
productName: string;
|
|
@@ -33,6 +40,7 @@ interface SubscriptionTrialStartEmailTemplateContext {
|
|
|
33
40
|
|
|
34
41
|
viewSubscriptionLink: string;
|
|
35
42
|
viewInvoiceLink: string;
|
|
43
|
+
oneTimeProductInfo?: Array<OneTimeProductInfo>;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export class SubscriptionTrialStartEmailTemplate
|
|
@@ -81,6 +89,8 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
81
89
|
},
|
|
82
90
|
})) as PaymentCurrency;
|
|
83
91
|
|
|
92
|
+
const oneTimeProductInfo = await getOneTimeProductInfo(subscription.latest_invoice_id as string, paymentCurrency);
|
|
93
|
+
|
|
84
94
|
const checkoutSession = await CheckoutSession.findOne({
|
|
85
95
|
where: {
|
|
86
96
|
subscription_id: subscription.id,
|
|
@@ -99,7 +109,7 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
99
109
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
100
110
|
const chainHost: string | undefined =
|
|
101
111
|
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
|
|
102
|
-
const paymentInfo: string = `${fromUnitToToken(
|
|
112
|
+
const paymentInfo: string = `${fromUnitToToken('0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
103
113
|
const currentPeriodStart: string = formatTime((subscription.trial_start as number) * 1000);
|
|
104
114
|
const currentPeriodEnd: string = formatTime((subscription.trial_end as number) * 1000);
|
|
105
115
|
const duration: string = prettyMsI18n(
|
|
@@ -135,9 +145,77 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
135
145
|
|
|
136
146
|
viewSubscriptionLink,
|
|
137
147
|
viewInvoiceLink,
|
|
148
|
+
oneTimeProductInfo,
|
|
138
149
|
};
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
getOneTimeProductTemplate(oneTimeProductInfo: Array<OneTimeProductInfo>, locale: string = 'en') {
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
type: 'divider',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'text',
|
|
159
|
+
data: {
|
|
160
|
+
type: 'plain',
|
|
161
|
+
text: translate('notification.common.expandPayment', locale),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'section',
|
|
166
|
+
fields: (oneTimeProductInfo || []).flatMap((x: any) => [
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
data: {
|
|
170
|
+
type: 'plain',
|
|
171
|
+
color: '#9397A1',
|
|
172
|
+
text: translate('notification.common.product', locale),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'text',
|
|
177
|
+
data: {
|
|
178
|
+
type: 'plain',
|
|
179
|
+
text: x?.productName,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
data: {
|
|
185
|
+
type: 'plain',
|
|
186
|
+
color: '#9397A1',
|
|
187
|
+
text: translate('notification.common.paymentAmount', locale),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'text',
|
|
192
|
+
data: {
|
|
193
|
+
type: 'plain',
|
|
194
|
+
text: x?.paymentInfo,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
data: {
|
|
200
|
+
type: 'plain',
|
|
201
|
+
color: '#9397A1',
|
|
202
|
+
text: translate('notification.common.paymentQuantity', locale),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
data: {
|
|
208
|
+
type: 'plain',
|
|
209
|
+
text: translate('notification.common.qty', locale, {
|
|
210
|
+
count: x.quantity,
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
]),
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
142
220
|
const {
|
|
143
221
|
locale,
|
|
@@ -147,15 +225,16 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
147
225
|
nftMintItem,
|
|
148
226
|
chainHost,
|
|
149
227
|
userDid,
|
|
150
|
-
paymentInfo,
|
|
151
228
|
currentPeriodStart,
|
|
152
229
|
currentPeriodEnd,
|
|
153
230
|
duration,
|
|
154
231
|
|
|
155
232
|
viewSubscriptionLink,
|
|
156
233
|
viewInvoiceLink,
|
|
234
|
+
oneTimeProductInfo,
|
|
157
235
|
} = await this.getContext();
|
|
158
236
|
|
|
237
|
+
const hasOneTimeProduct = !isEmpty(oneTimeProductInfo);
|
|
159
238
|
const template: BaseEmailTemplateType = {
|
|
160
239
|
title: `${translate('notification.subscriptionTrialStart.title', locale, {
|
|
161
240
|
productName,
|
|
@@ -175,6 +254,13 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
175
254
|
did: nftMintItem.address,
|
|
176
255
|
},
|
|
177
256
|
},
|
|
257
|
+
hasOneTimeProduct && {
|
|
258
|
+
type: 'text',
|
|
259
|
+
data: {
|
|
260
|
+
type: 'plain',
|
|
261
|
+
text: translate('notification.common.trialProduct', locale),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
178
264
|
{
|
|
179
265
|
type: 'section',
|
|
180
266
|
fields: [
|
|
@@ -208,21 +294,6 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
208
294
|
text: productName,
|
|
209
295
|
},
|
|
210
296
|
},
|
|
211
|
-
{
|
|
212
|
-
type: 'text',
|
|
213
|
-
data: {
|
|
214
|
-
type: 'plain',
|
|
215
|
-
color: '#9397A1',
|
|
216
|
-
text: translate('notification.common.paymentAmount', locale),
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
type: 'text',
|
|
221
|
-
data: {
|
|
222
|
-
type: 'plain',
|
|
223
|
-
text: paymentInfo,
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
297
|
{
|
|
227
298
|
type: 'text',
|
|
228
299
|
data: {
|
|
@@ -255,6 +326,9 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
255
326
|
},
|
|
256
327
|
].filter(Boolean),
|
|
257
328
|
},
|
|
329
|
+
...(hasOneTimeProduct
|
|
330
|
+
? this.getOneTimeProductTemplate(oneTimeProductInfo as Array<OneTimeProductInfo>, locale)
|
|
331
|
+
: []),
|
|
258
332
|
].filter(Boolean),
|
|
259
333
|
// @ts-ignore
|
|
260
334
|
actions: [
|