payment-kit 1.13.65 → 1.13.67
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 +4 -0
- package/api/src/integrations/blocklet/notification.ts +5 -3
- package/api/src/jobs/notification.ts +142 -0
- package/api/src/jobs/payment.ts +14 -0
- package/api/src/jobs/subscription.ts +2 -2
- package/api/src/libs/audit.ts +3 -1
- package/api/src/libs/env.ts +3 -0
- package/api/src/libs/event.ts +10 -1
- package/api/src/libs/invoice.ts +5 -0
- package/api/src/libs/notification/index.ts +23 -0
- package/api/src/libs/notification/template/base.ts +12 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +286 -0
- package/api/src/libs/notification/template/subscription-renewed.ts +259 -0
- package/api/src/libs/notification/template/subscription-succeeded.ts +279 -0
- package/api/src/libs/notification/template/subscription-trial-start.ts +267 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +250 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +232 -0
- package/api/src/libs/payment.ts +100 -3
- package/api/src/libs/product.ts +19 -0
- package/api/src/libs/queue/index.ts +13 -0
- package/api/src/libs/subscription.ts +5 -0
- package/api/src/libs/time.ts +17 -0
- package/api/src/libs/util.ts +39 -0
- package/api/src/locales/en.ts +67 -0
- package/api/src/locales/zh.ts +64 -0
- package/api/src/routes/connect/collect.ts +6 -0
- package/api/src/schedule/index.ts +28 -0
- package/api/src/schedule/interface/diff.ts +9 -0
- package/api/src/schedule/subscription-trail-will-end.ts +197 -0
- package/api/src/schedule/subscription-will-renew.ts +195 -0
- package/api/src/store/models/subscription.ts +30 -12
- package/api/src/store/models/types.ts +13 -12
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -7
- package/src/app.tsx +2 -0
- package/src/components/invoice/action.tsx +25 -7
- package/src/components/invoice/list.tsx +19 -4
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/list.tsx +6 -5
- package/src/components/subscription/items/index.tsx +8 -4
- package/src/libs/util.ts +2 -2
- package/src/locales/en.tsx +5 -1
- package/src/locales/zh.tsx +5 -1
- package/src/pages/checkout/pricing-table.tsx +1 -1
- package/src/pages/customer/index.tsx +13 -2
- package/src/pages/customer/invoice.tsx +5 -4
- package/src/pages/customer/subscription/index.tsx +163 -0
- package/tsconfig.api.json +6 -1
package/api/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import 'express-async-errors';
|
|
2
2
|
|
|
3
|
+
import './schedule';
|
|
4
|
+
|
|
3
5
|
import path from 'path';
|
|
4
6
|
|
|
5
7
|
import fallback from '@blocklet/sdk/lib/middlewares/fallback';
|
|
@@ -14,6 +16,7 @@ import { ensureWebhookRegistered } from './integrations/stripe/setup';
|
|
|
14
16
|
import { startCheckoutSessionQueue } from './jobs/checkout-session';
|
|
15
17
|
import { startEventQueue } from './jobs/event';
|
|
16
18
|
import { startInvoiceQueue } from './jobs/invoice';
|
|
19
|
+
import { startNotificationQueue } from './jobs/notification';
|
|
17
20
|
import { startPaymentQueue } from './jobs/payment';
|
|
18
21
|
import { startSubscriptionQueue } from './jobs/subscription';
|
|
19
22
|
import { handlers } from './libs/auth';
|
|
@@ -101,6 +104,7 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
101
104
|
startSubscriptionQueue().then(() => logger.info('subscription queue started'));
|
|
102
105
|
startEventQueue().then(() => logger.info('event queue started'));
|
|
103
106
|
startCheckoutSessionQueue().then(() => logger.info('checkoutSession queue started'));
|
|
107
|
+
startNotificationQueue().then(() => logger.info('notification queue started'));
|
|
104
108
|
|
|
105
109
|
if (process.env.BLOCKLET_MODE === 'production') {
|
|
106
110
|
ensureWebhookRegistered().catch(console.error);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import Notification from '@blocklet/sdk/service/notification';
|
|
2
2
|
import { toDid } from '@ocap/util';
|
|
3
|
+
import { get } from 'lodash';
|
|
4
|
+
import type { LiteralUnion } from 'type-fest';
|
|
3
5
|
|
|
4
6
|
import { blocklet } from '../../libs/auth';
|
|
5
7
|
import logger from '../../libs/logger';
|
|
6
8
|
import { translate } from '../../locales';
|
|
7
9
|
|
|
8
|
-
export async
|
|
10
|
+
export const getUserLocale = async (userDid: string): Promise<LiteralUnion<'zh' | 'en', string>> => {
|
|
9
11
|
const { user } = await blocklet.getUser(userDid);
|
|
10
|
-
return user
|
|
11
|
-
}
|
|
12
|
+
return get(user, 'locale', 'en');
|
|
13
|
+
};
|
|
12
14
|
|
|
13
15
|
export function createInfoRows(info: any = {}) {
|
|
14
16
|
const fields = Object.keys(info).reduce((list, cur) => {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { events } from '../libs/event';
|
|
2
|
+
import logger from '../libs/logger';
|
|
3
|
+
import { Notification } from '../libs/notification';
|
|
4
|
+
import type { BaseEmailTemplate } from '../libs/notification/template/base';
|
|
5
|
+
import {
|
|
6
|
+
SubscriptionRenewFailedEmailTemplate,
|
|
7
|
+
SubscriptionRenewFailedEmailTemplateOptions,
|
|
8
|
+
} from '../libs/notification/template/subscription-renew-failed';
|
|
9
|
+
import {
|
|
10
|
+
SubscriptionRenewedEmailTemplate,
|
|
11
|
+
SubscriptionRenewedEmailTemplateOptions,
|
|
12
|
+
} from '../libs/notification/template/subscription-renewed';
|
|
13
|
+
import {
|
|
14
|
+
SubscriptionSucceededEmailTemplate,
|
|
15
|
+
SubscriptionSucceededEmailTemplateOptions,
|
|
16
|
+
} from '../libs/notification/template/subscription-succeeded';
|
|
17
|
+
import {
|
|
18
|
+
SubscriptionTrailStartEmailTemplate,
|
|
19
|
+
SubscriptionTrailStartEmailTemplateOptions,
|
|
20
|
+
} from '../libs/notification/template/subscription-trial-start';
|
|
21
|
+
import {
|
|
22
|
+
SubscriptionTrailWilEndEmailTemplate,
|
|
23
|
+
SubscriptionTrialWillEndEmailTemplateOptions,
|
|
24
|
+
} from '../libs/notification/template/subscription-trial-will-end';
|
|
25
|
+
import {
|
|
26
|
+
SubscriptionWillRenewEmailTemplate,
|
|
27
|
+
SubscriptionWillRenewEmailTemplateOptions,
|
|
28
|
+
} from '../libs/notification/template/subscription-will-renew';
|
|
29
|
+
import type { SufficientForPaymentResult } from '../libs/payment';
|
|
30
|
+
import createQueue from '../libs/queue';
|
|
31
|
+
import type { EventType, Invoice, Subscription } from '../store/models';
|
|
32
|
+
|
|
33
|
+
export type NotificationQueueJobOptions = any;
|
|
34
|
+
export type NotificationQueueJob = {
|
|
35
|
+
type: EventType | 'customer.subscription.will_renew' | 'customer.subscription.trial_will_end';
|
|
36
|
+
options: NotificationQueueJobOptions;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
40
|
+
if (job.type === 'customer.subscription.started') {
|
|
41
|
+
return new SubscriptionSucceededEmailTemplate(job.options as SubscriptionSucceededEmailTemplateOptions);
|
|
42
|
+
}
|
|
43
|
+
if (job.type === 'customer.subscription.renewed') {
|
|
44
|
+
return new SubscriptionRenewedEmailTemplate(job.options as SubscriptionRenewedEmailTemplateOptions);
|
|
45
|
+
}
|
|
46
|
+
if (job.type === 'customer.subscription.renew_failed') {
|
|
47
|
+
return new SubscriptionRenewFailedEmailTemplate(job.options as SubscriptionRenewFailedEmailTemplateOptions);
|
|
48
|
+
}
|
|
49
|
+
if (job.type === 'customer.subscription.trial_start') {
|
|
50
|
+
return new SubscriptionTrailStartEmailTemplate(job.options as SubscriptionTrailStartEmailTemplateOptions);
|
|
51
|
+
}
|
|
52
|
+
if (job.type === 'customer.subscription.will_renew') {
|
|
53
|
+
return new SubscriptionWillRenewEmailTemplate(job.options as SubscriptionWillRenewEmailTemplateOptions);
|
|
54
|
+
}
|
|
55
|
+
if (job.type === 'customer.subscription.trial_will_end') {
|
|
56
|
+
return new SubscriptionTrailWilEndEmailTemplate(job.options as SubscriptionTrialWillEndEmailTemplateOptions);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`Unknown job type: ${job.type}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const template = getNotificationTemplate(job);
|
|
65
|
+
await new Notification(template).send();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error('handleNotificationJob.error.$job', job);
|
|
68
|
+
logger.error('handleNotificationJob.error', error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const notificationQueue = createQueue<NotificationQueueJob>({
|
|
74
|
+
name: 'notification',
|
|
75
|
+
onJob: handleNotificationJob,
|
|
76
|
+
options: {
|
|
77
|
+
concurrency: 10,
|
|
78
|
+
maxRetries: 3,
|
|
79
|
+
enableScheduledJob: true,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
*
|
|
85
|
+
* @see https://team.arcblock.io/comment/discussions/0fba06ff-75b4-47d2-8c5c-f0379540ce03
|
|
86
|
+
* @description
|
|
87
|
+
* @export
|
|
88
|
+
*/
|
|
89
|
+
// eslint-disable-next-line require-await
|
|
90
|
+
export async function startNotificationQueue() {
|
|
91
|
+
events.on('customer.subscription.trial_start', (subscription: Subscription) => {
|
|
92
|
+
notificationQueue.push({
|
|
93
|
+
job: {
|
|
94
|
+
type: 'customer.subscription.trial_start',
|
|
95
|
+
options: {
|
|
96
|
+
subscriptionId: subscription.id,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
events.on('customer.subscription.started', (subscription: Subscription) => {
|
|
103
|
+
if (!subscription.trail_start) {
|
|
104
|
+
// 没有试用期的 subscription 通知
|
|
105
|
+
notificationQueue.push({
|
|
106
|
+
job: {
|
|
107
|
+
type: 'customer.subscription.started',
|
|
108
|
+
options: {
|
|
109
|
+
subscriptionId: subscription.id,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
events.on('customer.subscription.renewed', (subscription: Subscription, invoice: Invoice) => {
|
|
117
|
+
notificationQueue.push({
|
|
118
|
+
job: {
|
|
119
|
+
type: 'customer.subscription.renewed',
|
|
120
|
+
options: {
|
|
121
|
+
subscriptionId: subscription.id,
|
|
122
|
+
invoiceId: invoice?.id,
|
|
123
|
+
} as SubscriptionRenewedEmailTemplateOptions,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
events.on(
|
|
129
|
+
'customer.subscription.renew_failed',
|
|
130
|
+
({ invoice, result }: { invoice: Invoice; result: SufficientForPaymentResult }) => {
|
|
131
|
+
notificationQueue.push({
|
|
132
|
+
job: {
|
|
133
|
+
type: 'customer.subscription.renew_failed',
|
|
134
|
+
options: {
|
|
135
|
+
invoice,
|
|
136
|
+
result,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
package/api/src/jobs/payment.ts
CHANGED
|
@@ -65,6 +65,16 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
|
65
65
|
logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
const count: number = await Invoice.count({
|
|
70
|
+
where: {
|
|
71
|
+
subscription_id: invoice.subscription_id,
|
|
72
|
+
status: 'paid',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (count >= 2) {
|
|
76
|
+
events.emit('customer.subscription.renewed', subscription, invoice);
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
if (invoice.checkout_session_id) {
|
|
@@ -136,6 +146,10 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
136
146
|
});
|
|
137
147
|
if (result.sufficient === false) {
|
|
138
148
|
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
149
|
+
events.emit('customer.subscription.renew_failed', {
|
|
150
|
+
invoice,
|
|
151
|
+
result,
|
|
152
|
+
});
|
|
139
153
|
// FIXME: send email to customer, pause subscription
|
|
140
154
|
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
141
155
|
}
|
|
@@ -174,8 +174,8 @@ export const handleSubscription = async (job: SubscriptionJob) => {
|
|
|
174
174
|
number: await customer.getInvoiceNumber(),
|
|
175
175
|
description: 'Subscription cycle',
|
|
176
176
|
statement_descriptor: getStatementDescriptor(expandedItems),
|
|
177
|
-
period_start:
|
|
178
|
-
period_end:
|
|
177
|
+
period_start: setup.period.start,
|
|
178
|
+
period_end: setup.period.end,
|
|
179
179
|
|
|
180
180
|
auto_advance: true,
|
|
181
181
|
paid: false,
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import pick from 'lodash/pick';
|
|
2
|
+
import type { LiteralUnion } from 'type-fest';
|
|
2
3
|
|
|
4
|
+
import type { EventType } from '../store/models';
|
|
3
5
|
import { Event } from '../store/models/event';
|
|
4
6
|
import { events } from './event';
|
|
5
7
|
|
|
6
8
|
const API_VERSION = '2023-09-05';
|
|
7
9
|
|
|
8
|
-
export async function createEvent(scope: string, type:
|
|
10
|
+
export async function createEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any) {
|
|
9
11
|
// console.log('createEvent', scope, type, model, options);
|
|
10
12
|
const data: any = {
|
|
11
13
|
object: model.dataValues,
|
package/api/src/libs/env.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import env from '@blocklet/sdk/lib/env';
|
|
2
2
|
|
|
3
|
+
export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
|
|
4
|
+
export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
|
|
5
|
+
|
|
3
6
|
export default {
|
|
4
7
|
...env,
|
|
5
8
|
};
|
package/api/src/libs/event.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { LiteralUnion } from 'type-fest';
|
|
4
|
+
|
|
5
|
+
import type { EventType } from '../store/models';
|
|
6
|
+
|
|
7
|
+
interface MyEventType extends EventEmitter {
|
|
8
|
+
on(eventName: LiteralUnion<EventType, string | symbol>, listener: (...args: any[]) => void): this;
|
|
9
|
+
emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const events = new EventEmitter() as MyEventType;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Notification as BlockletNotification } from '@blocklet/sdk';
|
|
2
|
+
|
|
3
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
|
|
4
|
+
|
|
5
|
+
export class Notification {
|
|
6
|
+
template: BaseEmailTemplate;
|
|
7
|
+
|
|
8
|
+
constructor(template: BaseEmailTemplate) {
|
|
9
|
+
this.template = template;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async send() {
|
|
13
|
+
const template: BaseEmailTemplateType | null = await this.template.getTemplate();
|
|
14
|
+
|
|
15
|
+
if (!template) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { userDid } = await this.template.getContext();
|
|
20
|
+
|
|
21
|
+
await BlockletNotification.sendToUser(userDid, template as any);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TNotification, TNotificationInput } from '@blocklet/sdk/lib/types/notification';
|
|
2
|
+
|
|
3
|
+
export type BaseEmailTemplateType = TNotificationInput | TNotification;
|
|
4
|
+
export type BaseEmailTemplateContext = {
|
|
5
|
+
userDid: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface BaseEmailTemplate<C = BaseEmailTemplateContext> {
|
|
9
|
+
getTemplate(): Promise<BaseEmailTemplateType | null>;
|
|
10
|
+
|
|
11
|
+
getContext(): Promise<C>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
+
import { fromUnitToToken, toDid } 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
|
+
Invoice,
|
|
12
|
+
NftMintItem,
|
|
13
|
+
PaymentIntent,
|
|
14
|
+
PaymentMethod,
|
|
15
|
+
Subscription,
|
|
16
|
+
} from '../../../store/models';
|
|
17
|
+
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
18
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
19
|
+
import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
|
|
20
|
+
import { getMainProductName } from '../../product';
|
|
21
|
+
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
22
|
+
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
23
|
+
import { getExplorerLink } from '../../util';
|
|
24
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
25
|
+
|
|
26
|
+
export interface SubscriptionRenewFailedEmailTemplateOptions {
|
|
27
|
+
invoice: Invoice;
|
|
28
|
+
result: SufficientForPaymentResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SubscriptionRenewFailedEmailTemplateContext {
|
|
32
|
+
locale: string;
|
|
33
|
+
productName: string;
|
|
34
|
+
at: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
|
|
37
|
+
nftMintItem: NftMintItem | undefined;
|
|
38
|
+
userDid: string;
|
|
39
|
+
paymentInfo: string;
|
|
40
|
+
currentPeriodStart: string;
|
|
41
|
+
currentPeriodEnd: string;
|
|
42
|
+
duration: string;
|
|
43
|
+
|
|
44
|
+
viewSubscriptionLink: string;
|
|
45
|
+
viewInvoiceLink: string;
|
|
46
|
+
viewTxHashLink: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class SubscriptionRenewFailedEmailTemplate
|
|
50
|
+
implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext>
|
|
51
|
+
{
|
|
52
|
+
options: SubscriptionRenewFailedEmailTemplateOptions;
|
|
53
|
+
|
|
54
|
+
constructor(options: SubscriptionRenewFailedEmailTemplateOptions) {
|
|
55
|
+
this.options = options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async getReason(userDid: string, invoice: Invoice, locale: string): Promise<string> {
|
|
59
|
+
if (this.options.result.sufficient) {
|
|
60
|
+
throw new Error(`SufficientForPaymentResult.sufficient should be false: ${JSON.stringify(this.options.result)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 类似 NO_DID_WALLET 字符串将转成 noDidWallet
|
|
64
|
+
const toCamelCase = (input: string): string => {
|
|
65
|
+
return input.toLowerCase().replace(/_([a-z])/g, (_match, group1) => {
|
|
66
|
+
return group1.toUpperCase();
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const i18nText = `notification.subscriptionRenewFailed.reason.${toCamelCase(this.options.result.reason as string)}`;
|
|
71
|
+
|
|
72
|
+
const paymentDetail = await getPaymentDetail(userDid, invoice);
|
|
73
|
+
const reason = translate(i18nText, locale, {
|
|
74
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
75
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return reason;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getContext(): Promise<SubscriptionRenewFailedEmailTemplateContext> {
|
|
82
|
+
const subscription: Subscription | null = await Subscription.findByPk(this.options.invoice.subscription_id);
|
|
83
|
+
if (!subscription) {
|
|
84
|
+
throw new Error(`Subscription not found: ${this.options.invoice.subscription_id}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
88
|
+
if (!customer) {
|
|
89
|
+
throw new Error(`Customer not found: ${subscription.customer_id}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { invoice } = this.options;
|
|
93
|
+
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
94
|
+
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
95
|
+
where: {
|
|
96
|
+
id: subscription.currency_id,
|
|
97
|
+
},
|
|
98
|
+
})) as PaymentCurrency;
|
|
99
|
+
|
|
100
|
+
const checkoutSession = await CheckoutSession.findOne({
|
|
101
|
+
where: {
|
|
102
|
+
subscription_id: subscription.id,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const locale = await getUserLocale(customer.did);
|
|
107
|
+
const productName = await getMainProductName(subscription.id);
|
|
108
|
+
const at: string = formatTime(Date.now());
|
|
109
|
+
const reason: string = await this.getReason(customer.did, invoice, locale);
|
|
110
|
+
|
|
111
|
+
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
112
|
+
const nftMintItem: NftMintItem | undefined = hasNft
|
|
113
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
|
|
114
|
+
: undefined;
|
|
115
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
|
|
116
|
+
paymentCurrency.symbol
|
|
117
|
+
}`;
|
|
118
|
+
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
119
|
+
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
120
|
+
const duration: string = prettyMsI18n(
|
|
121
|
+
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
122
|
+
{
|
|
123
|
+
locale: getPrettyMsI18nLocale(locale),
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
128
|
+
const chainHost: string | undefined =
|
|
129
|
+
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
|
|
130
|
+
const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
|
|
131
|
+
const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
|
|
132
|
+
const txHash: string | undefined =
|
|
133
|
+
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
|
|
134
|
+
const viewTxHashLink: string | undefined = hasNft && txHash ? getExplorerLink(chainHost, txHash, 'tx') : undefined;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
locale,
|
|
138
|
+
productName,
|
|
139
|
+
at,
|
|
140
|
+
reason,
|
|
141
|
+
|
|
142
|
+
nftMintItem,
|
|
143
|
+
userDid: customer.did,
|
|
144
|
+
paymentInfo,
|
|
145
|
+
currentPeriodStart,
|
|
146
|
+
currentPeriodEnd,
|
|
147
|
+
duration,
|
|
148
|
+
|
|
149
|
+
viewSubscriptionLink,
|
|
150
|
+
viewInvoiceLink,
|
|
151
|
+
viewTxHashLink,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
156
|
+
const {
|
|
157
|
+
locale,
|
|
158
|
+
productName,
|
|
159
|
+
at,
|
|
160
|
+
nftMintItem,
|
|
161
|
+
userDid,
|
|
162
|
+
paymentInfo,
|
|
163
|
+
currentPeriodStart,
|
|
164
|
+
currentPeriodEnd,
|
|
165
|
+
duration,
|
|
166
|
+
reason,
|
|
167
|
+
viewSubscriptionLink,
|
|
168
|
+
viewInvoiceLink,
|
|
169
|
+
viewTxHashLink,
|
|
170
|
+
} = await this.getContext();
|
|
171
|
+
|
|
172
|
+
const template: BaseEmailTemplateType = {
|
|
173
|
+
title: `${translate('notification.subscriptionRenewFailed.title', locale, {
|
|
174
|
+
productName: `(${productName})`,
|
|
175
|
+
})}`,
|
|
176
|
+
body: `${translate('notification.subscriptionRenewFailed.body', locale, {
|
|
177
|
+
at,
|
|
178
|
+
productName: `(${productName})`,
|
|
179
|
+
reason: `${reason}`,
|
|
180
|
+
})}`,
|
|
181
|
+
// @ts-expect-error
|
|
182
|
+
attachments: [
|
|
183
|
+
{
|
|
184
|
+
type: 'section',
|
|
185
|
+
fields: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
data: {
|
|
189
|
+
type: 'plain',
|
|
190
|
+
color: '#9397A1',
|
|
191
|
+
text: translate('notification.common.account', locale),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
data: {
|
|
197
|
+
type: 'plain',
|
|
198
|
+
text: userDid,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: 'text',
|
|
203
|
+
data: {
|
|
204
|
+
type: 'plain',
|
|
205
|
+
color: '#9397A1',
|
|
206
|
+
text: translate('notification.common.product', locale),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
data: {
|
|
212
|
+
type: 'plain',
|
|
213
|
+
text: productName,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'text',
|
|
218
|
+
data: {
|
|
219
|
+
type: 'plain',
|
|
220
|
+
color: '#9397A1',
|
|
221
|
+
text: translate('notification.common.paymentInfo', locale),
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
data: {
|
|
227
|
+
type: 'plain',
|
|
228
|
+
text: paymentInfo,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
type: 'text',
|
|
233
|
+
data: {
|
|
234
|
+
type: 'plain',
|
|
235
|
+
color: '#9397A1',
|
|
236
|
+
text: translate('notification.common.validityPeriod', locale),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
type: 'text',
|
|
241
|
+
data: {
|
|
242
|
+
type: 'plain',
|
|
243
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
nftMintItem && {
|
|
247
|
+
type: 'text',
|
|
248
|
+
data: {
|
|
249
|
+
type: 'plain',
|
|
250
|
+
color: '#9397A1',
|
|
251
|
+
text: translate('notification.common.nftAddress', locale),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
nftMintItem && {
|
|
255
|
+
type: 'text',
|
|
256
|
+
data: {
|
|
257
|
+
type: 'plain',
|
|
258
|
+
text: toDid(nftMintItem.address),
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
].filter(Boolean),
|
|
262
|
+
},
|
|
263
|
+
].filter(Boolean),
|
|
264
|
+
// @ts-ignore
|
|
265
|
+
actions: [
|
|
266
|
+
{
|
|
267
|
+
name: 'viewSubscription',
|
|
268
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
269
|
+
link: viewSubscriptionLink,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'viewSubscription',
|
|
273
|
+
title: translate('notification.subscriptionRenewFailed.renewNow', locale),
|
|
274
|
+
link: viewInvoiceLink,
|
|
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
|
+
}
|