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,232 @@
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 SubscriptionWillRenewEmailTemplateOptions {
18
+ subscriptionId: string;
19
+ willRenewValue: number;
20
+ willRenewUnit: ManipulateType;
21
+ required?: false | true;
22
+ }
23
+
24
+ interface SubscriptionWillRenewEmailTemplateContext {
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 SubscriptionWillRenewEmailTemplate
41
+ implements BaseEmailTemplate<SubscriptionWillRenewEmailTemplateContext>
42
+ {
43
+ options: SubscriptionWillRenewEmailTemplateOptions;
44
+
45
+ constructor(options: SubscriptionWillRenewEmailTemplateOptions) {
46
+ this.options = options;
47
+ }
48
+
49
+ async getContext(): Promise<SubscriptionWillRenewEmailTemplateContext> {
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(invoice.period_end * 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(invoice.period_start * 1000);
77
+ const currentPeriodEnd: string = formatTime(invoice.period_end * 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
+
140
+ viewSubscriptionLink,
141
+ } = await this.getContext();
142
+
143
+ const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
144
+ if (canPay && !this.options.required) {
145
+ // 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
146
+ return null;
147
+ }
148
+
149
+ const template: BaseEmailTemplateType = {
150
+ title: `${translate('notification.subscriptionWillRenew.title', locale, {
151
+ productName: `(${productName})`,
152
+ willRenewDuration,
153
+ })}`,
154
+ body: canPay
155
+ ? `${translate('notification.subscriptionWillRenew.body', locale, {
156
+ at,
157
+ productName: `(${productName})`,
158
+ willRenewDuration,
159
+ })}`
160
+ : `${translate('notification.subscriptionWillRenew.unableToPayBody', locale, {
161
+ at,
162
+ productName: `(${productName})`,
163
+ willRenewDuration,
164
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
165
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
166
+ })}`,
167
+ // @ts-expect-error
168
+ attachments: [
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.subscriptionWillRenew.renewAmount', locale),
208
+ },
209
+ },
210
+ {
211
+ type: 'text',
212
+ data: {
213
+ type: 'plain',
214
+ text: paymentInfo,
215
+ },
216
+ },
217
+ ].filter(Boolean),
218
+ },
219
+ ].filter(Boolean),
220
+ // @ts-ignore
221
+ actions: [
222
+ {
223
+ name: 'viewSubscription',
224
+ title: translate('notification.common.viewSubscription', locale),
225
+ link: viewSubscriptionLink,
226
+ },
227
+ ].filter(Boolean),
228
+ };
229
+
230
+ return template;
231
+ }
232
+ }
@@ -1,20 +1,35 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  import { toDelegateAddress } from '@arcblock/did-util';
2
3
  import { sign } from '@arcblock/jwt';
3
4
  import { getWalletDid } from '@blocklet/sdk/lib/did';
4
5
  import type { DelegateState } from '@ocap/client';
5
6
  import { toTxHash } from '@ocap/mcrypto';
6
- import { BN } from '@ocap/util';
7
+ import { BN, fromUnitToToken } from '@ocap/util';
7
8
 
9
+ import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod } from '../store/models';
8
10
  import type { TPaymentCurrency } from '../store/models/payment-currency';
9
- import type { PaymentMethod } from '../store/models/payment-method';
10
11
  import { blocklet, wallet } from './auth';
12
+ import logger from './logger';
13
+
14
+ export interface SufficientForPaymentResult {
15
+ sufficient: boolean;
16
+ reason?:
17
+ | 'NO_DID_WALLET'
18
+ | 'NO_DELEGATION'
19
+ | 'NO_TRANSFER_PERMISSION'
20
+ | 'NO_TOKEN'
21
+ | 'NO_ENOUGH_TOKEN'
22
+ | 'NOT_SUPPORTED';
23
+ delegator?: string;
24
+ state?: DelegateState;
25
+ }
11
26
 
12
27
  export async function isDelegationSufficientForPayment(args: {
13
28
  paymentMethod: PaymentMethod;
14
29
  paymentCurrency: TPaymentCurrency;
15
30
  userDid: string;
16
31
  amount: string;
17
- }): Promise<{ sufficient: boolean; reason?: string; delegator?: string; state?: DelegateState }> {
32
+ }): Promise<SufficientForPaymentResult> {
18
33
  const { paymentCurrency, paymentMethod, userDid, amount } = args;
19
34
  if (paymentMethod.type === 'arcblock') {
20
35
  // user have bond wallet did?
@@ -67,3 +82,85 @@ export function getGasPayerExtra(txBuffer: Buffer) {
67
82
  },
68
83
  };
69
84
  }
85
+
86
+ export interface PaymentDetail {
87
+ balance: number;
88
+ price: number;
89
+ symbol: string;
90
+ }
91
+ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promise<PaymentDetail> {
92
+ const defaultResult = {
93
+ balance: 0,
94
+ price: 0,
95
+ symbol: '',
96
+ };
97
+
98
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
99
+ if (!paymentIntent) {
100
+ logger.error('getPayment.error', { reason: 'NO_PAYMENT_INTENT' });
101
+ return defaultResult;
102
+ }
103
+
104
+ const paymentMethod = await PaymentMethod.findByPk(paymentIntent?.payment_method_id);
105
+ if (!paymentMethod) {
106
+ logger.error('getPayment.error', { reason: 'NO_PAYMENT_METHOD' });
107
+ return defaultResult;
108
+ }
109
+
110
+ const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
111
+ if (!paymentCurrency) {
112
+ logger.error('getPayment.error', { reason: 'NO_PAYMENT_CURRENCY' });
113
+ return defaultResult;
114
+ }
115
+
116
+ if (paymentMethod.type === 'arcblock') {
117
+ // user have bond wallet did?
118
+ const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
119
+ const delegator = getWalletDid(user);
120
+ if (!delegator) {
121
+ logger.error('getPayment.error', { reason: 'NO_DID_WALLET' });
122
+ return defaultResult;
123
+ }
124
+
125
+ const client = paymentMethod.getOcapClient();
126
+
127
+ // have delegated before?
128
+ const address = toDelegateAddress(delegator, wallet.address);
129
+ const { state } = await client.getDelegateState({ address });
130
+ if (!state) {
131
+ logger.error('getPayment.error', { reason: 'NO_DELEGATION' });
132
+ return defaultResult;
133
+ }
134
+
135
+ // have enough permissions
136
+ if (state.ops.some((x: any) => x.key === 'fg:t:transfer_v2') === false) {
137
+ logger.error('getPayment.error', { reason: 'NO_TRANSFER_PERMISSION' });
138
+ return defaultResult;
139
+ }
140
+
141
+ // balance enough token for payment?
142
+ const amount: number = +fromUnitToToken(paymentIntent.amount, paymentCurrency.decimal);
143
+ const { symbol } = paymentCurrency;
144
+
145
+ const { tokens } = await client.getAccountTokens({
146
+ address: delegator,
147
+ token: paymentCurrency.contract as string,
148
+ });
149
+ const [token] = tokens;
150
+ if (!token) {
151
+ return {
152
+ balance: 0,
153
+ price: amount,
154
+ symbol,
155
+ };
156
+ }
157
+
158
+ return {
159
+ balance: +fromUnitToToken(token.balance, paymentCurrency.decimal),
160
+ price: amount,
161
+ symbol,
162
+ };
163
+ }
164
+
165
+ return defaultResult;
166
+ }
@@ -0,0 +1,19 @@
1
+ import { CheckoutSession, Price, Product } from '../store/models';
2
+
3
+ export async function getMainProductName(subscriptionId: string): Promise<string> {
4
+ const checkoutSession = await CheckoutSession.findOne({
5
+ where: {
6
+ subscription_id: subscriptionId,
7
+ },
8
+ attributes: ['line_items'],
9
+ });
10
+ const priceId: string = checkoutSession?.line_items.find((x) => !x.cross_sell)?.price_id as string;
11
+
12
+ const price = await Price.findByPk(priceId, {
13
+ attributes: ['product_id'],
14
+ });
15
+
16
+ const product = (await Product.findByPk(price?.product_id)) as Product;
17
+
18
+ return product.name;
19
+ }
@@ -198,6 +198,18 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
198
198
  return doc ? doc.job : null;
199
199
  };
200
200
 
201
+ const deleteJob = async (id: string): Promise<boolean> => {
202
+ const exists = await getJob(id);
203
+
204
+ if (exists) {
205
+ await cancel(id);
206
+ await store.deleteJob(id);
207
+ return true;
208
+ }
209
+
210
+ return false;
211
+ };
212
+
201
213
  // Populate the queue on startup
202
214
  process.nextTick(async () => {
203
215
  try {
@@ -251,6 +263,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
251
263
  saturated: (cb: any) => (queue.saturated = cb),
252
264
  error: (cb: any) => (queue.error = cb),
253
265
  get: getJob,
266
+ delete: deleteJob,
254
267
  cancel,
255
268
  options: {
256
269
  concurrency,
@@ -0,0 +1,5 @@
1
+ import { component } from '@blocklet/sdk';
2
+
3
+ export function getCustomerSubscriptionPageUrl(subscriptionId: string, locale: string = 'en') {
4
+ return component.getUrl(`customer/subscription/${subscriptionId}?locale=${locale}`);
5
+ }
@@ -0,0 +1,17 @@
1
+ import type { LiteralUnion } from 'type-fest';
2
+
3
+ import dayjs from './dayjs';
4
+
5
+ export function formatTime(time: dayjs.ConfigType): string {
6
+ return dayjs(time).utc().format('YYYY-MM-DD HH:mm:ss [UTC]');
7
+ }
8
+
9
+ export function getPrettyMsI18nLocale(
10
+ locale: LiteralUnion<'en' | 'zh', string> = 'en'
11
+ ): LiteralUnion<'zh_CN' | 'en', string> {
12
+ if (locale === 'zh' || locale === 'zh_CN') {
13
+ return 'zh_CN';
14
+ }
15
+
16
+ return 'en';
17
+ }
@@ -3,6 +3,7 @@ import crypto from 'crypto';
3
3
  import { getUrl } from '@blocklet/sdk/lib/component';
4
4
  import env from '@blocklet/sdk/lib/env';
5
5
  import { customAlphabet } from 'nanoid';
6
+ import type { LiteralUnion } from 'type-fest';
6
7
 
7
8
  import dayjs from './dayjs';
8
9
 
@@ -154,3 +155,41 @@ export function getMetadataFromQuery(query: Record<string, any> = {}): Record<st
154
155
 
155
156
  return metadata;
156
157
  }
158
+
159
+ // @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
160
+ export function getExplorerLink(
161
+ chainHost: string | undefined,
162
+ did: string | undefined,
163
+ type: LiteralUnion<'asset' | 'account' | 'tx' | 'token' | 'factory' | ' bridge', string>
164
+ ) {
165
+ if (!chainHost) return undefined;
166
+ try {
167
+ const chainUrl = new URL(chainHost);
168
+ switch (type) {
169
+ case 'asset':
170
+ chainUrl.pathname = `/explorer/assets/${did}`;
171
+ break;
172
+ case 'account':
173
+ chainUrl.pathname = `/explorer/accounts/${did}`;
174
+ break;
175
+ case 'tx':
176
+ chainUrl.pathname = `/explorer/txs/${did}`;
177
+ break;
178
+ case 'token':
179
+ chainUrl.pathname = `/explorer/tokens/${did}`;
180
+ break;
181
+ case 'factory':
182
+ chainUrl.pathname = `/explorer/factories/${did}`;
183
+ break;
184
+ case 'bridge':
185
+ chainUrl.pathname = `/explorer/bridges/${did}`;
186
+ break;
187
+ default:
188
+ chainUrl.pathname = '/';
189
+ }
190
+
191
+ return chainUrl.href;
192
+ } catch {
193
+ return undefined;
194
+ }
195
+ }
@@ -2,10 +2,77 @@ import flat from 'flat';
2
2
 
3
3
  export default flat({
4
4
  notification: {
5
+ common: {
6
+ account: 'Account',
7
+ product: 'Product',
8
+ paymentInfo: 'Payment Info',
9
+ validityPeriod: 'Validity period',
10
+ trialPeriod: 'Trail period',
11
+ viewSubscription: 'View subscription',
12
+ viewInvoice: 'View invoice',
13
+ viewTxHash: 'View transaction',
14
+ trialDuration: 'Trial duration',
15
+ duration: 'Duration',
16
+ nftAddress: 'NFT Address',
17
+ month: 'month',
18
+ months: 'months',
19
+ day: 'day',
20
+ days: 'days',
21
+ minute: 'minute',
22
+ minutes: 'minutes',
23
+ },
24
+
5
25
  sendTo: 'Sent to',
6
26
  mintNFT: {
7
27
  title: '{collection} NFT minted',
8
28
  message: 'A new {collection} NFT is minted and sent to your wallet, please check it out.',
9
29
  },
30
+
31
+ subscriptionTrialStart: {
32
+ title: 'Welcome to the start of your {productName} trial',
33
+ body: 'Congratulations on your {productName} trial! The length of the trial is {trialDuration} and will end at {subscriptionTrialEnd}. Have fun with {productName}!',
34
+ },
35
+
36
+ subscriptionTrialWillEnd: {
37
+ title: 'The {productName} trial will end soon',
38
+ body: 'Your {productName} trial subscription will end after {willRenewDuration}. Please make sure your account balance is sufficient to automatically renew your subscription at the end of the trial period. Thank you for your support and trust!',
39
+ unableToPayBody:
40
+ 'Your {productName} trial subscription will end after {willRenewDuration}.<span style="color: red;">Your current balance is {balance}, which is less than {price}</span>,please make sure your balance is sufficient for automatic renewal. Thank you for your support and trust!',
41
+ },
42
+
43
+ subscriptionSucceed: {
44
+ title: "Congratulations! You've successfully subscribed to {productName}",
45
+ body: 'Thank you for successfully subscribing to {productName} on {at}. We will be happy to provide you with excellent service, and we wish you a pleasant experience.',
46
+ },
47
+
48
+ subscriptionWillRenew: {
49
+ title: '{productName} will be auto-renewed soon',
50
+ body: 'Your subscription to {productName} and {willRenewDuration} is expiring. Please make sure you have enough balance in your account to auto-renew your subscription when it expires ({at}). We wish you all the best!',
51
+ unableToPayBody:
52
+ 'Your subscription to {productName} and {willRenewDuration} expires. <span style="color: red;">Your current balance is {balance}, which is less than {price}</span>, so please make sure you have enough balance in your account to automatically renew your subscription when it expires ({at}). We wish you all the best!',
53
+ renewAmount: 'Amount',
54
+ },
55
+
56
+ subscriptionRenewed: {
57
+ title: '{productName} Renewal Successful',
58
+ body: 'Your {productName} was successfully renewed on {at}. Thank you for your continued support and trust, have a great day!',
59
+ },
60
+
61
+ subscriptionRenewFailed: {
62
+ title: 'Renewal of {productName} failed',
63
+ body: "We're sorry to inform you that your {productName} renewal failed on {at}. The reason for the failure is <span style='color: red;'>{reason}</span>. If you have any questions, please do not hesitate to contact us. Thank you.",
64
+ renewNow: 'Renew now',
65
+ reason: {
66
+ noDidWallet:
67
+ 'You have not yet bound the DID Wallet, please bind the DID Wallet, make sure the balance is sufficient and then renew it',
68
+ noDelegation: 'Your DID Wallet is not yet authorized, please renew it after completing the authorization',
69
+ noTransferPermission:
70
+ 'Your DID Wallet transfer privileges are insufficient, please complete the authorization and renew it again',
71
+ noToken: "You don't have any tokens in your account, please replenish your tokens and renew your account",
72
+ noEnoughToken:
73
+ 'Your account token balance is {balance}, not enough for {price}, please replenish your tokens and renew your account',
74
+ noSupported: 'Token renewal is not supported, please check your subscription',
75
+ },
76
+ },
10
77
  },
11
78
  });
@@ -2,10 +2,74 @@ import flat from 'flat';
2
2
 
3
3
  export default flat({
4
4
  notification: {
5
+ common: {
6
+ account: '账号',
7
+ product: '商品',
8
+ paymentInfo: '支付信息',
9
+ validityPeriod: '有效期',
10
+ trialPeriod: '试用期',
11
+ viewSubscription: '查看订阅',
12
+ viewInvoice: '查看发票',
13
+ viewTxHash: '查看交易',
14
+ trialDuration: '试用期时长',
15
+ duration: '时长',
16
+ nftAddress: 'NFT 地址',
17
+ month: '个月',
18
+ months: '个月',
19
+ day: '天',
20
+ days: '天',
21
+ minute: '分钟',
22
+ minutes: '分钟',
23
+ },
24
+
5
25
  sendTo: '发送给',
6
26
  mintNFT: {
7
27
  title: '{collection} NFT 铸造成功',
8
28
  message: '{collection} NFT 已经铸造完成并发送到你的钱包,请查收',
9
29
  },
30
+
31
+ subscriptionTrialStart: {
32
+ title: '欢迎开始您的 {productName} 试用之旅',
33
+ body: '恭喜您获得了 {productName} 的试用资格!试用期时长为 {trialDuration},将于 {subscriptionTrialEnd} 结束。祝您使用愉快!',
34
+ },
35
+
36
+ subscriptionTrialWillEnd: {
37
+ title: '{productName} 试用期即将结束',
38
+ body: '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。请确保您的账户余额充足,以便在试用期结束后自动续费。感谢您的支持与信任!',
39
+ unableToPayBody:
40
+ '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。<span style="color: red;">您的当前余额为 {balance},不足 {price}</span>,请确保余额充足以便自动续费。感谢您的支持与信任!',
41
+ },
42
+
43
+ subscriptionSucceed: {
44
+ title: '恭喜!您已成功订阅 {productName}',
45
+ body: '感谢您于 {at} 成功订阅了 {productName}。我们将竭诚为您提供优质的服务,祝您使用愉快!',
46
+ },
47
+
48
+ subscriptionWillRenew: {
49
+ title: '{productName} 即将自动续费',
50
+ body: '您订阅的 {productName} 还有 {willRenewDuration} 到期。请确保您的账户余额充足,以便在到期后({at})自动续费。祝您一切顺利!',
51
+ unableToPayBody:
52
+ '您订阅的 {productName} 还有 {willRenewDuration} 到期。<span style="color: red;">您的当前余额为 {balance},不足 {price}</span>,请确保您的账户余额充足,以便在到期后({at})自动续费。祝您一切顺利!',
53
+ renewAmount: '扣款金额',
54
+ },
55
+
56
+ subscriptionRenewed: {
57
+ title: '{productName} 续费成功',
58
+ body: '您的 {productName} 已于 {at} 成功续费。感谢您的持续支持与信任,祝您使用愉快!',
59
+ },
60
+
61
+ subscriptionRenewFailed: {
62
+ title: '{productName} 续费失败',
63
+ body: '很抱歉地通知您,您的 {productName} 续费于 {at} 失败。失败原因为 <span style="color: red;">{reason}</span>。如有任何疑问,请及时联系我们。谢谢!',
64
+ renewNow: '立即续费',
65
+ reason: {
66
+ noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足后重新续费',
67
+ noDelegation: '您的 DID Wallet 尚未授权,请完成授权后重新续费',
68
+ noTransferPermission: '您的 DID Wallet 转账权限不足,请完成授权后重新续费',
69
+ noToken: '您的账户没有任何代币,请充值代币后重新续费',
70
+ noEnoughToken: '您的账户代币余额为 {balance},不足 {price},请充值代币后重新续费',
71
+ noSupported: '不支持使用代币续费,请检查您的套餐',
72
+ },
73
+ },
10
74
  },
11
75
  });
@@ -5,8 +5,10 @@ import { invoiceQueue } from '../../jobs/invoice';
5
5
  import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { wallet } from '../../libs/auth';
8
+ import { events } from '../../libs/event';
8
9
  import { getGasPayerExtra } from '../../libs/payment';
9
10
  import { getTxMetadata } from '../../libs/util';
11
+ import { Subscription } from '../../store/models';
10
12
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
11
13
 
12
14
  // Used to collect an open invoice failed to collect automatically
@@ -98,6 +100,10 @@ export default {
98
100
  await invoiceQueue.cancel(invoice.id);
99
101
  }
100
102
 
103
+ if (invoice.subscription_id) {
104
+ events.emit('customer.subscription.renewed', await Subscription.findByPk(invoice.subscription_id), invoice);
105
+ }
106
+
101
107
  return { hash: txHash };
102
108
  }
103
109
 
@@ -0,0 +1,28 @@
1
+ // @ts-nocheck
2
+ import Cron from '@abtnode/cron';
3
+
4
+ import { notificationCronTime } from '../libs/env';
5
+ import logger from '../libs/logger';
6
+ import { SubscriptionTrailWillEndSchedule } from './subscription-trail-will-end';
7
+ import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
8
+
9
+ Cron.init({
10
+ context: {},
11
+ jobs: [
12
+ {
13
+ name: 'subscription.will.renew',
14
+ time: notificationCronTime,
15
+ fn: () => new SubscriptionWillRenewSchedule().run(),
16
+ options: { runOnInit: true },
17
+ },
18
+ {
19
+ name: 'subscription.trial.will.end',
20
+ time: notificationCronTime,
21
+ fn: () => new SubscriptionTrailWillEndSchedule().run(),
22
+ options: { runOnInit: true },
23
+ },
24
+ ],
25
+ onError: (error: Error, name: string) => {
26
+ logger.error('run job failed', { name, error: error.message, stack: error.stack });
27
+ },
28
+ });