payment-kit 1.13.64 → 1.13.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/integrations/blocklet/notification.ts +5 -3
  3. package/api/src/jobs/notification.ts +142 -0
  4. package/api/src/jobs/payment.ts +14 -0
  5. package/api/src/jobs/subscription.ts +2 -2
  6. package/api/src/libs/audit.ts +3 -1
  7. package/api/src/libs/env.ts +3 -0
  8. package/api/src/libs/event.ts +10 -1
  9. package/api/src/libs/invoice.ts +5 -0
  10. package/api/src/libs/notification/index.ts +23 -0
  11. package/api/src/libs/notification/template/base.ts +12 -0
  12. package/api/src/libs/notification/template/subscription-renew-failed.ts +286 -0
  13. package/api/src/libs/notification/template/subscription-renewed.ts +259 -0
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +279 -0
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +267 -0
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +250 -0
  17. package/api/src/libs/notification/template/subscription-will-renew.ts +232 -0
  18. package/api/src/libs/payment.ts +100 -3
  19. package/api/src/libs/product.ts +19 -0
  20. package/api/src/libs/queue/index.ts +13 -0
  21. package/api/src/libs/subscription.ts +5 -0
  22. package/api/src/libs/time.ts +17 -0
  23. package/api/src/libs/util.ts +39 -0
  24. package/api/src/locales/en.ts +67 -0
  25. package/api/src/locales/zh.ts +64 -0
  26. package/api/src/routes/connect/collect.ts +6 -0
  27. package/api/src/schedule/index.ts +28 -0
  28. package/api/src/schedule/interface/diff.ts +9 -0
  29. package/api/src/schedule/subscription-trail-will-end.ts +197 -0
  30. package/api/src/schedule/subscription-will-renew.ts +195 -0
  31. package/api/src/store/models/subscription.ts +30 -12
  32. package/api/src/store/models/types.ts +13 -12
  33. package/api/third.d.ts +2 -0
  34. package/blocklet.yml +1 -1
  35. package/package.json +23 -21
  36. package/src/app.tsx +2 -0
  37. package/src/components/invoice/action.tsx +25 -7
  38. package/src/components/invoice/list.tsx +19 -4
  39. package/src/components/portal/invoice/list.tsx +1 -1
  40. package/src/components/portal/subscription/list.tsx +6 -5
  41. package/src/components/subscription/items/index.tsx +8 -4
  42. package/src/libs/util.ts +2 -2
  43. package/src/locales/en.tsx +5 -1
  44. package/src/locales/zh.tsx +5 -1
  45. package/src/pages/checkout/pricing-table.tsx +1 -1
  46. package/src/pages/customer/index.tsx +13 -2
  47. package/src/pages/customer/invoice.tsx +5 -4
  48. package/src/pages/customer/subscription/index.tsx +163 -0
  49. package/tsconfig.api.json +6 -1
@@ -0,0 +1,267 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { toDid } from '@ocap/util';
4
+ import pWaitFor from 'p-wait-for';
5
+ import prettyMsI18n from 'pretty-ms-i18n';
6
+
7
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
8
+ import { translate } from '../../../locales';
9
+ import { CheckoutSession, Customer, Invoice, NftMintItem, PaymentMethod, Subscription } from '../../../store/models';
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 type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
+
17
+ export interface SubscriptionTrailStartEmailTemplateOptions {
18
+ subscriptionId: string;
19
+ }
20
+
21
+ interface SubscriptionTrailStartEmailTemplateContext {
22
+ locale: string;
23
+ productName: string;
24
+ subscriptionTrialEnd: string;
25
+
26
+ nftMintItem: NftMintItem | undefined;
27
+ chainHost: string | undefined;
28
+ userDid: string;
29
+ paymentInfo: string;
30
+ currentPeriodStart: string;
31
+ currentPeriodEnd: string;
32
+ duration: string;
33
+
34
+ viewSubscriptionLink: string;
35
+ viewInvoiceLink: string;
36
+ }
37
+
38
+ export class SubscriptionTrailStartEmailTemplate
39
+ implements BaseEmailTemplate<SubscriptionTrailStartEmailTemplateContext>
40
+ {
41
+ options: SubscriptionTrailStartEmailTemplateOptions;
42
+
43
+ constructor(options: SubscriptionTrailStartEmailTemplateOptions) {
44
+ this.options = options;
45
+ }
46
+
47
+ async getContext(): Promise<SubscriptionTrailStartEmailTemplateContext> {
48
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
49
+ if (!subscription) {
50
+ throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
51
+ }
52
+
53
+ const customer = await Customer.findByPk(subscription.customer_id);
54
+ if (!customer) {
55
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
56
+ }
57
+
58
+ await pWaitFor(
59
+ async () => {
60
+ const checkoutSession = await CheckoutSession.findOne({
61
+ where: {
62
+ subscription_id: subscription.id,
63
+ },
64
+ });
65
+
66
+ return ['minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string);
67
+ },
68
+ { timeout: 1000 * 10, interval: 1000 }
69
+ );
70
+
71
+ const invoice: Invoice = (await Invoice.findOne({
72
+ where: {
73
+ subscription_id: subscription.id,
74
+ total: 0,
75
+ },
76
+ })) as Invoice;
77
+
78
+ const paymentCurrency = (await PaymentCurrency.findOne({
79
+ where: {
80
+ id: subscription.currency_id,
81
+ },
82
+ })) as PaymentCurrency;
83
+
84
+ const checkoutSession = await CheckoutSession.findOne({
85
+ where: {
86
+ subscription_id: subscription.id,
87
+ },
88
+ });
89
+
90
+ const locale = await getUserLocale(customer.did);
91
+ const productName = await getMainProductName(subscription.id);
92
+ const subscriptionTrialEnd: string = formatTime((subscription.trail_end as number) * 1000);
93
+
94
+ const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
95
+ const nftMintItem: NftMintItem | undefined = hasNft
96
+ ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
97
+ : undefined;
98
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
99
+ const chainHost: string | undefined =
100
+ paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
101
+ const paymentInfo: string = `0 ${paymentCurrency.symbol}`;
102
+ const currentPeriodStart: string = formatTime((subscription.trail_start as number) * 1000);
103
+ const currentPeriodEnd: string = formatTime((subscription.trail_end as number) * 1000);
104
+ const duration: string = prettyMsI18n(
105
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
106
+ {
107
+ locale: getPrettyMsI18nLocale(locale),
108
+ }
109
+ );
110
+
111
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
112
+ const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
113
+
114
+ return {
115
+ locale,
116
+ productName,
117
+ subscriptionTrialEnd,
118
+
119
+ nftMintItem,
120
+ chainHost,
121
+ userDid: customer.did,
122
+ paymentInfo,
123
+ currentPeriodStart,
124
+ currentPeriodEnd,
125
+ duration,
126
+
127
+ viewSubscriptionLink,
128
+ viewInvoiceLink,
129
+ };
130
+ }
131
+
132
+ async getTemplate(): Promise<BaseEmailTemplateType> {
133
+ const {
134
+ locale,
135
+ productName,
136
+ subscriptionTrialEnd,
137
+
138
+ nftMintItem,
139
+ chainHost,
140
+ userDid,
141
+ paymentInfo,
142
+ currentPeriodStart,
143
+ currentPeriodEnd,
144
+ duration,
145
+
146
+ viewSubscriptionLink,
147
+ viewInvoiceLink,
148
+ } = await this.getContext();
149
+
150
+ const template: BaseEmailTemplateType = {
151
+ title: `${translate('notification.subscriptionTrialStart.title', locale, {
152
+ productName: `(${productName})`,
153
+ })}`,
154
+ body: `${translate('notification.subscriptionTrialStart.body', locale, {
155
+ subscriptionTrialEnd,
156
+ productName: `(${productName})`,
157
+ trialDuration: duration,
158
+ })}`,
159
+ // @ts-expect-error
160
+ attachments: [
161
+ nftMintItem &&
162
+ chainHost && {
163
+ type: 'asset',
164
+ data: {
165
+ chainHost,
166
+ did: nftMintItem.address,
167
+ },
168
+ },
169
+ {
170
+ type: 'section',
171
+ fields: [
172
+ {
173
+ type: 'text',
174
+ data: {
175
+ type: 'plain',
176
+ color: '#9397A1',
177
+ text: translate('notification.common.account', locale),
178
+ },
179
+ },
180
+ {
181
+ type: 'text',
182
+ data: {
183
+ type: 'plain',
184
+ text: userDid,
185
+ },
186
+ },
187
+ {
188
+ type: 'text',
189
+ data: {
190
+ type: 'plain',
191
+ color: '#9397A1',
192
+ text: translate('notification.common.product', locale),
193
+ },
194
+ },
195
+ {
196
+ type: 'text',
197
+ data: {
198
+ type: 'plain',
199
+ text: productName,
200
+ },
201
+ },
202
+ {
203
+ type: 'text',
204
+ data: {
205
+ type: 'plain',
206
+ color: '#9397A1',
207
+ text: translate('notification.common.paymentInfo', locale),
208
+ },
209
+ },
210
+ {
211
+ type: 'text',
212
+ data: {
213
+ type: 'plain',
214
+ text: paymentInfo,
215
+ },
216
+ },
217
+ {
218
+ type: 'text',
219
+ data: {
220
+ type: 'plain',
221
+ color: '#9397A1',
222
+ text: translate('notification.common.trialPeriod', locale),
223
+ },
224
+ },
225
+ {
226
+ type: 'text',
227
+ data: {
228
+ type: 'plain',
229
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
230
+ },
231
+ },
232
+ nftMintItem && {
233
+ type: 'text',
234
+ data: {
235
+ type: 'plain',
236
+ color: '#9397A1',
237
+ text: translate('notification.common.nftAddress', locale),
238
+ },
239
+ },
240
+ nftMintItem && {
241
+ type: 'text',
242
+ data: {
243
+ type: 'plain',
244
+ text: toDid(nftMintItem.address),
245
+ },
246
+ },
247
+ ].filter(Boolean),
248
+ },
249
+ ].filter(Boolean),
250
+ // @ts-ignore
251
+ actions: [
252
+ {
253
+ name: 'viewSubscription',
254
+ title: translate('notification.common.viewSubscription', locale),
255
+ link: viewSubscriptionLink,
256
+ },
257
+ {
258
+ name: 'viewSubscription',
259
+ title: translate('notification.common.viewInvoice', locale),
260
+ link: viewInvoiceLink,
261
+ },
262
+ ].filter(Boolean),
263
+ };
264
+
265
+ return template;
266
+ }
267
+ }
@@ -0,0 +1,250 @@
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
+ import prettyMsI18n from 'pretty-ms-i18n';
6
+
7
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
8
+ import { translate } from '../../../locales';
9
+ import { Customer, Invoice, Subscription } from '../../../store/models';
10
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
11
+ import { PaymentDetail, getPaymentDetail } from '../../payment';
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 SubscriptionTrialWillEndEmailTemplateOptions {
18
+ subscriptionId: string;
19
+ willRenewValue: number;
20
+ willRenewUnit: ManipulateType;
21
+ required?: false | true;
22
+ }
23
+
24
+ interface SubscriptionTrailWilEndEmailTemplateContext {
25
+ locale: string;
26
+ productName: string;
27
+ at: string;
28
+ willRenewDuration: string;
29
+ paymentDetail: PaymentDetail;
30
+
31
+ userDid: string;
32
+ paymentInfo: string;
33
+ currentPeriodStart: string;
34
+ currentPeriodEnd: string;
35
+ duration: string;
36
+
37
+ viewSubscriptionLink: string;
38
+ }
39
+
40
+ export class SubscriptionTrailWilEndEmailTemplate
41
+ implements BaseEmailTemplate<SubscriptionTrailWilEndEmailTemplateContext>
42
+ {
43
+ options: SubscriptionTrialWillEndEmailTemplateOptions;
44
+
45
+ constructor(options: SubscriptionTrialWillEndEmailTemplateOptions) {
46
+ this.options = options;
47
+ }
48
+
49
+ async getContext(): Promise<SubscriptionTrailWilEndEmailTemplateContext> {
50
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
51
+ if (!subscription) {
52
+ throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
53
+ }
54
+
55
+ const customer = await Customer.findByPk(subscription.customer_id);
56
+ if (!customer) {
57
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
58
+ }
59
+
60
+ const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
61
+ const paymentCurrency = (await PaymentCurrency.findOne({
62
+ where: {
63
+ id: subscription.currency_id,
64
+ },
65
+ })) as PaymentCurrency;
66
+
67
+ const locale = await getUserLocale(customer.did);
68
+ const productName = await getMainProductName(subscription.id);
69
+ const at: string = formatTime((subscription.trail_end as number) * 1000);
70
+ const willRenewDuration: string =
71
+ locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).replaceAll(' ', '');
72
+
73
+ const userDid = customer.did;
74
+ const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
75
+ const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
76
+ const currentPeriodStart: string = formatTime((subscription.trail_start as number) * 1000);
77
+ const currentPeriodEnd: string = formatTime((subscription.trail_end as number) * 1000);
78
+ const duration: string = prettyMsI18n(
79
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
80
+ {
81
+ locale: getPrettyMsI18nLocale(locale),
82
+ }
83
+ );
84
+
85
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
86
+
87
+ return {
88
+ locale,
89
+ productName,
90
+ at,
91
+ willRenewDuration,
92
+ paymentDetail,
93
+
94
+ userDid,
95
+ paymentInfo,
96
+ currentPeriodStart,
97
+ currentPeriodEnd,
98
+ duration,
99
+
100
+ viewSubscriptionLink,
101
+ };
102
+ }
103
+
104
+ getWillRenewDuration(locale: string): string {
105
+ if (this.options.willRenewUnit === 'M') {
106
+ if (this.options.willRenewValue > 1) {
107
+ return `${this.options.willRenewValue} ${translate('notification.common.months', locale)}`;
108
+ }
109
+
110
+ return `${this.options.willRenewValue} ${translate('notification.common.month', locale)}`;
111
+ }
112
+ if (this.options.willRenewUnit === 'd') {
113
+ if (this.options.willRenewValue > 1) {
114
+ return `${this.options.willRenewValue} ${translate('notification.common.days', locale)}`;
115
+ }
116
+ return `${this.options.willRenewValue} ${translate('notification.common.day', locale)}`;
117
+ }
118
+
119
+ if (this.options.willRenewUnit === 'm') {
120
+ if (this.options.willRenewValue > 1) {
121
+ return `${this.options.willRenewValue} ${translate('notification.common.minutes', locale)}`;
122
+ }
123
+ return `${this.options.willRenewValue} ${translate('notification.common.minute', locale)}`;
124
+ }
125
+
126
+ return `${this.options.willRenewValue} ${this.options.willRenewUnit}`;
127
+ }
128
+
129
+ async getTemplate(): Promise<BaseEmailTemplateType | null> {
130
+ const {
131
+ locale,
132
+ productName,
133
+ at,
134
+ willRenewDuration,
135
+ paymentDetail,
136
+
137
+ userDid,
138
+ paymentInfo,
139
+ currentPeriodStart,
140
+ currentPeriodEnd,
141
+ duration,
142
+
143
+ viewSubscriptionLink,
144
+ } = await this.getContext();
145
+
146
+ const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
147
+ if (canPay && !this.options.required) {
148
+ // 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
149
+ return null;
150
+ }
151
+
152
+ const template: BaseEmailTemplateType = {
153
+ title: `${translate('notification.subscriptionTrialWillEnd.title', locale, {
154
+ productName: `(${productName})`,
155
+ willRenewDuration,
156
+ })}`,
157
+ body: canPay
158
+ ? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
159
+ at,
160
+ productName: `(${productName})`,
161
+ willRenewDuration,
162
+ })}`
163
+ : `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
164
+ at,
165
+ productName: `(${productName})`,
166
+ willRenewDuration,
167
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
168
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
169
+ })}`,
170
+ // @ts-expect-error
171
+ attachments: [
172
+ {
173
+ type: 'section',
174
+ fields: [
175
+ {
176
+ type: 'text',
177
+ data: {
178
+ type: 'plain',
179
+ color: '#9397A1',
180
+ text: translate('notification.common.account', locale),
181
+ },
182
+ },
183
+ {
184
+ type: 'text',
185
+ data: {
186
+ type: 'plain',
187
+ text: userDid,
188
+ },
189
+ },
190
+ {
191
+ type: 'text',
192
+ data: {
193
+ type: 'plain',
194
+ color: '#9397A1',
195
+ text: translate('notification.common.product', locale),
196
+ },
197
+ },
198
+ {
199
+ type: 'text',
200
+ data: {
201
+ type: 'plain',
202
+ text: productName,
203
+ },
204
+ },
205
+ {
206
+ type: 'text',
207
+ data: {
208
+ type: 'plain',
209
+ color: '#9397A1',
210
+ text: translate('notification.common.paymentInfo', locale),
211
+ },
212
+ },
213
+ {
214
+ type: 'text',
215
+ data: {
216
+ type: 'plain',
217
+ text: paymentInfo,
218
+ },
219
+ },
220
+ {
221
+ type: 'text',
222
+ data: {
223
+ type: 'plain',
224
+ color: '#9397A1',
225
+ text: translate('notification.common.trialPeriod', locale),
226
+ },
227
+ },
228
+ {
229
+ type: 'text',
230
+ data: {
231
+ type: 'plain',
232
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
233
+ },
234
+ },
235
+ ].filter(Boolean),
236
+ },
237
+ ].filter(Boolean),
238
+ // @ts-ignore
239
+ actions: [
240
+ {
241
+ name: 'viewSubscription',
242
+ title: translate('notification.common.viewSubscription', locale),
243
+ link: viewSubscriptionLink,
244
+ },
245
+ ].filter(Boolean),
246
+ };
247
+
248
+ return template;
249
+ }
250
+ }