payment-kit 1.13.65 → 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 +9 -7
  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,259 @@
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 { 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 SubscriptionRenewedEmailTemplateOptions {
26
+ subscriptionId: string;
27
+ invoiceId?: string;
28
+ }
29
+
30
+ interface SubscriptionRenewedEmailTemplateContext {
31
+ locale: string;
32
+ productName: string;
33
+ at: string;
34
+
35
+ nftMintItem: NftMintItem | undefined;
36
+ userDid: string;
37
+ paymentInfo: string;
38
+ currentPeriodStart: string;
39
+ currentPeriodEnd: string;
40
+ duration: string;
41
+
42
+ viewSubscriptionLink: string;
43
+ viewInvoiceLink: string;
44
+ viewTxHashLink: string | undefined;
45
+ }
46
+
47
+ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewedEmailTemplateContext> {
48
+ options: SubscriptionRenewedEmailTemplateOptions;
49
+
50
+ constructor(options: SubscriptionRenewedEmailTemplateOptions) {
51
+ this.options = options;
52
+ }
53
+
54
+ async getContext(): Promise<SubscriptionRenewedEmailTemplateContext> {
55
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
56
+ if (!subscription) {
57
+ throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
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 = (
66
+ this.options.invoiceId
67
+ ? await Invoice.findByPk(this.options.invoiceId)
68
+ : await Invoice.findByPk(subscription.latest_invoice_id)
69
+ ) as Invoice;
70
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
71
+ const paymentCurrency = (await PaymentCurrency.findOne({
72
+ where: {
73
+ id: subscription.currency_id,
74
+ },
75
+ })) as PaymentCurrency;
76
+
77
+ const checkoutSession = await CheckoutSession.findOne({
78
+ where: {
79
+ subscription_id: subscription.id,
80
+ },
81
+ });
82
+
83
+ const locale = await getUserLocale(customer.did);
84
+ const productName = await getMainProductName(subscription.id);
85
+ const at: string = formatTime(Date.now());
86
+
87
+ const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
88
+ const nftMintItem: NftMintItem | undefined = hasNft
89
+ ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
90
+ : undefined;
91
+ const paymentInfo: string = `${fromUnitToToken(invoice.amount_paid, paymentCurrency.decimal)} ${
92
+ paymentCurrency.symbol
93
+ }`;
94
+ const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
95
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
96
+ const duration: string = prettyMsI18n(
97
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
98
+ {
99
+ locale: getPrettyMsI18nLocale(locale),
100
+ }
101
+ );
102
+
103
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
104
+ const chainHost: string | undefined =
105
+ paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
106
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
107
+ const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
108
+ const txHash: string | undefined =
109
+ paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
110
+ const viewTxHashLink: string | undefined = hasNft && txHash ? getExplorerLink(chainHost, txHash, 'tx') : undefined;
111
+
112
+ return {
113
+ locale,
114
+ productName,
115
+ at,
116
+
117
+ nftMintItem,
118
+ userDid: customer.did,
119
+ paymentInfo,
120
+ currentPeriodStart,
121
+ currentPeriodEnd,
122
+ duration,
123
+
124
+ viewSubscriptionLink,
125
+ viewInvoiceLink,
126
+ viewTxHashLink,
127
+ };
128
+ }
129
+
130
+ async getTemplate(): Promise<BaseEmailTemplateType> {
131
+ const {
132
+ locale,
133
+ productName,
134
+ at,
135
+ nftMintItem,
136
+ userDid,
137
+ paymentInfo,
138
+ currentPeriodStart,
139
+ currentPeriodEnd,
140
+ duration,
141
+ viewSubscriptionLink,
142
+ viewInvoiceLink,
143
+ viewTxHashLink,
144
+ } = await this.getContext();
145
+
146
+ const template: BaseEmailTemplateType = {
147
+ title: `${translate('notification.subscriptionRenewed.title', locale, {
148
+ productName: `(${productName})`,
149
+ })}`,
150
+ body: `${translate('notification.subscriptionRenewed.body', locale, {
151
+ at,
152
+ productName: `(${productName})`,
153
+ })}`,
154
+ // @ts-expect-error
155
+ attachments: [
156
+ {
157
+ type: 'section',
158
+ fields: [
159
+ {
160
+ type: 'text',
161
+ data: {
162
+ type: 'plain',
163
+ color: '#9397A1',
164
+ text: translate('notification.common.account', locale),
165
+ },
166
+ },
167
+ {
168
+ type: 'text',
169
+ data: {
170
+ type: 'plain',
171
+ text: userDid,
172
+ },
173
+ },
174
+ {
175
+ type: 'text',
176
+ data: {
177
+ type: 'plain',
178
+ color: '#9397A1',
179
+ text: translate('notification.common.product', locale),
180
+ },
181
+ },
182
+ {
183
+ type: 'text',
184
+ data: {
185
+ type: 'plain',
186
+ text: productName,
187
+ },
188
+ },
189
+ {
190
+ type: 'text',
191
+ data: {
192
+ type: 'plain',
193
+ color: '#9397A1',
194
+ text: translate('notification.common.paymentInfo', locale),
195
+ },
196
+ },
197
+ {
198
+ type: 'text',
199
+ data: {
200
+ type: 'plain',
201
+ text: paymentInfo,
202
+ },
203
+ },
204
+ {
205
+ type: 'text',
206
+ data: {
207
+ type: 'plain',
208
+ color: '#9397A1',
209
+ text: translate('notification.common.validityPeriod', locale),
210
+ },
211
+ },
212
+ {
213
+ type: 'text',
214
+ data: {
215
+ type: 'plain',
216
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
217
+ },
218
+ },
219
+ nftMintItem && {
220
+ type: 'text',
221
+ data: {
222
+ type: 'plain',
223
+ color: '#9397A1',
224
+ text: translate('notification.common.nftAddress', locale),
225
+ },
226
+ },
227
+ nftMintItem && {
228
+ type: 'text',
229
+ data: {
230
+ type: 'plain',
231
+ text: toDid(nftMintItem.address),
232
+ },
233
+ },
234
+ ].filter(Boolean),
235
+ },
236
+ ].filter(Boolean),
237
+ // @ts-ignore
238
+ actions: [
239
+ {
240
+ name: 'viewSubscription',
241
+ title: translate('notification.common.viewSubscription', locale),
242
+ link: viewSubscriptionLink,
243
+ },
244
+ {
245
+ name: 'viewSubscription',
246
+ title: translate('notification.common.viewInvoice', locale),
247
+ link: viewInvoiceLink,
248
+ },
249
+ viewTxHashLink && {
250
+ name: 'viewTxHash',
251
+ title: translate('notification.common.viewTxHash', locale),
252
+ link: viewTxHashLink as string,
253
+ },
254
+ ].filter(Boolean),
255
+ };
256
+
257
+ return template;
258
+ }
259
+ }
@@ -0,0 +1,279 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken } 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 {
10
+ CheckoutSession,
11
+ Customer,
12
+ NftMintItem,
13
+ PaymentIntent,
14
+ PaymentMethod,
15
+ Subscription,
16
+ } from '../../../store/models';
17
+ import { Invoice } from '../../../store/models/invoice';
18
+ import { PaymentCurrency } from '../../../store/models/payment-currency';
19
+ import { getCustomerInvoicePageUrl } from '../../invoice';
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 SubscriptionSucceededEmailTemplateOptions {
27
+ subscriptionId: string;
28
+ }
29
+
30
+ interface SubscriptionSucceededEmailTemplateContext {
31
+ locale: string;
32
+ productName: string;
33
+ at: string;
34
+ nftMintItem: NftMintItem | undefined;
35
+ chainHost: string | undefined;
36
+ userDid: string;
37
+ paymentInfo: string;
38
+ currentPeriodStart: string;
39
+ currentPeriodEnd: string;
40
+ duration: string;
41
+ viewSubscriptionLink: string;
42
+ viewInvoiceLink: string;
43
+ viewTxHashLink: string | undefined;
44
+ }
45
+
46
+ export class SubscriptionSucceededEmailTemplate
47
+ implements BaseEmailTemplate<SubscriptionSucceededEmailTemplateContext>
48
+ {
49
+ options: SubscriptionSucceededEmailTemplateOptions;
50
+
51
+ constructor(options: SubscriptionSucceededEmailTemplateOptions) {
52
+ this.options = options;
53
+ }
54
+
55
+ async getContext(): Promise<SubscriptionSucceededEmailTemplateContext> {
56
+ const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
57
+ if (!subscription) {
58
+ throw new Error(`Subscription not found: ${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
+ await pWaitFor(
67
+ async () => {
68
+ const [checkoutSession, invoice] = await Promise.all([
69
+ CheckoutSession.findOne({
70
+ where: {
71
+ subscription_id: subscription.id,
72
+ },
73
+ }),
74
+ Invoice.findOne({
75
+ where: {
76
+ subscription_id: subscription.id,
77
+ },
78
+ order: [['created_at', 'ASC']],
79
+ }),
80
+ ]);
81
+
82
+ return Boolean(
83
+ ['minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) && invoice?.payment_intent_id
84
+ );
85
+ },
86
+ { timeout: 1000 * 10, interval: 1000 }
87
+ );
88
+
89
+ const invoice = (await Invoice.findOne({
90
+ where: {
91
+ subscription_id: subscription.id,
92
+ },
93
+ order: [['created_at', 'ASC']],
94
+ })) as Invoice;
95
+ const paymentCurrency = (await PaymentCurrency.findOne({
96
+ where: {
97
+ id: subscription.currency_id,
98
+ },
99
+ })) as PaymentCurrency;
100
+
101
+ const checkoutSession = await CheckoutSession.findOne({
102
+ where: {
103
+ subscription_id: subscription.id,
104
+ },
105
+ });
106
+
107
+ const locale = await getUserLocale(customer.did);
108
+ const productName = await getMainProductName(subscription.id);
109
+ const at: string = formatTime(subscription.created_at);
110
+
111
+ const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
112
+ const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
113
+ const nftMintItem: NftMintItem | undefined = hasNft
114
+ ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
115
+ : undefined;
116
+ const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
117
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
118
+ const duration: string = prettyMsI18n(
119
+ new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
120
+ {
121
+ locale: getPrettyMsI18nLocale(locale),
122
+ }
123
+ );
124
+
125
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
126
+ // @FIXME: 获取 chainHost 困难的一批?
127
+ const chainHost: string | undefined =
128
+ paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.api_host;
129
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl(subscription.id, locale);
130
+ const viewInvoiceLink = getCustomerInvoicePageUrl(invoice.id, locale);
131
+
132
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
133
+ const txHash: string | undefined =
134
+ paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']?.tx_hash;
135
+ const viewTxHashLink: string | undefined = txHash && getExplorerLink(chainHost, txHash as string, 'tx');
136
+
137
+ return {
138
+ locale,
139
+ productName,
140
+ at,
141
+
142
+ userDid: customer.did,
143
+ nftMintItem,
144
+ chainHost,
145
+ paymentInfo,
146
+ currentPeriodStart,
147
+ currentPeriodEnd,
148
+ duration,
149
+
150
+ viewSubscriptionLink,
151
+ viewInvoiceLink,
152
+ viewTxHashLink,
153
+ };
154
+ }
155
+
156
+ async getTemplate(): Promise<BaseEmailTemplateType> {
157
+ const {
158
+ locale,
159
+ productName,
160
+ at,
161
+ nftMintItem,
162
+ chainHost,
163
+ userDid,
164
+ paymentInfo,
165
+ currentPeriodStart,
166
+ currentPeriodEnd,
167
+ duration,
168
+ viewSubscriptionLink,
169
+ viewInvoiceLink,
170
+ viewTxHashLink,
171
+ } = await this.getContext();
172
+
173
+ const template: BaseEmailTemplateType = {
174
+ title: `${translate('notification.subscriptionSucceed.title', locale, {
175
+ productName: `(${productName})`,
176
+ })}`,
177
+ body: `${translate('notification.subscriptionSucceed.body', locale, {
178
+ at,
179
+ productName: `(${productName})`,
180
+ })}`,
181
+ // @ts-expect-error
182
+ attachments: [
183
+ nftMintItem &&
184
+ chainHost && {
185
+ type: 'asset',
186
+ data: {
187
+ chainHost,
188
+ did: nftMintItem.address,
189
+ },
190
+ },
191
+ {
192
+ type: 'section',
193
+ fields: [
194
+ {
195
+ type: 'text',
196
+ data: {
197
+ type: 'plain',
198
+ color: '#9397A1',
199
+ text: translate('notification.common.account', locale),
200
+ },
201
+ },
202
+ {
203
+ type: 'text',
204
+ data: {
205
+ type: 'plain',
206
+ text: userDid,
207
+ },
208
+ },
209
+ {
210
+ type: 'text',
211
+ data: {
212
+ type: 'plain',
213
+ color: '#9397A1',
214
+ text: translate('notification.common.product', locale),
215
+ },
216
+ },
217
+ {
218
+ type: 'text',
219
+ data: {
220
+ type: 'plain',
221
+ text: productName,
222
+ },
223
+ },
224
+ {
225
+ type: 'text',
226
+ data: {
227
+ type: 'plain',
228
+ color: '#9397A1',
229
+ text: translate('notification.common.paymentInfo', locale),
230
+ },
231
+ },
232
+ {
233
+ type: 'text',
234
+ data: {
235
+ type: 'plain',
236
+ text: paymentInfo,
237
+ },
238
+ },
239
+ {
240
+ type: 'text',
241
+ data: {
242
+ type: 'plain',
243
+ color: '#9397A1',
244
+ text: translate('notification.common.validityPeriod', locale),
245
+ },
246
+ },
247
+ {
248
+ type: 'text',
249
+ data: {
250
+ type: 'plain',
251
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
252
+ },
253
+ },
254
+ ].filter(Boolean),
255
+ },
256
+ ].filter(Boolean),
257
+ // @ts-ignore
258
+ actions: [
259
+ {
260
+ name: 'viewSubscription',
261
+ title: translate('notification.common.viewSubscription', locale),
262
+ link: viewSubscriptionLink,
263
+ },
264
+ {
265
+ name: 'viewSubscription',
266
+ title: translate('notification.common.viewInvoice', locale),
267
+ link: viewInvoiceLink,
268
+ },
269
+ viewTxHashLink && {
270
+ name: 'viewTxHash',
271
+ title: translate('notification.common.viewTxHash', locale),
272
+ link: viewTxHashLink as string,
273
+ },
274
+ ].filter(Boolean),
275
+ };
276
+
277
+ return template;
278
+ }
279
+ }