payment-kit 1.18.24 → 1.18.26

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 (41) hide show
  1. package/api/src/libs/event.ts +22 -2
  2. package/api/src/libs/invoice.ts +142 -0
  3. package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
  4. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
  5. package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
  8. package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
  12. package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
  13. package/api/src/libs/queue/index.ts +6 -0
  14. package/api/src/libs/queue/store.ts +13 -1
  15. package/api/src/libs/util.ts +22 -1
  16. package/api/src/locales/en.ts +5 -0
  17. package/api/src/locales/zh.ts +5 -0
  18. package/api/src/queues/invoice.ts +21 -7
  19. package/api/src/queues/notification.ts +353 -11
  20. package/api/src/queues/payment.ts +26 -10
  21. package/api/src/queues/payout.ts +21 -7
  22. package/api/src/routes/checkout-sessions.ts +26 -12
  23. package/api/src/routes/connect/recharge-account.ts +13 -1
  24. package/api/src/routes/connect/recharge.ts +13 -1
  25. package/api/src/routes/connect/shared.ts +54 -36
  26. package/api/src/routes/customers.ts +61 -0
  27. package/api/src/routes/invoices.ts +51 -1
  28. package/api/src/routes/subscriptions.ts +1 -1
  29. package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
  30. package/api/src/store/models/customer.ts +42 -1
  31. package/api/src/store/models/types.ts +17 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +24 -24
  34. package/src/components/customer/form.tsx +21 -2
  35. package/src/components/customer/notification-preference.tsx +428 -0
  36. package/src/components/layout/user.tsx +1 -1
  37. package/src/locales/en.tsx +30 -0
  38. package/src/locales/zh.tsx +30 -0
  39. package/src/pages/customer/index.tsx +27 -23
  40. package/src/pages/customer/recharge/account.tsx +19 -17
  41. package/src/pages/customer/subscription/embed.tsx +25 -9
@@ -13,8 +13,28 @@ export const events = new EventEmitter() as MyEventType;
13
13
 
14
14
  export const emitAsync = (event: string, ...args: any[]) => {
15
15
  return new Promise((resolve, reject) => {
16
+ const timeout = setTimeout(() => {
17
+ cleanup();
18
+ reject(new Error(`Event ${event} timed out after 10000ms`));
19
+ }, 10000);
20
+
21
+ const cleanup = () => {
22
+ clearTimeout(timeout);
23
+ events.removeListener(`${event}.done`, handleDone);
24
+ events.removeListener(`${event}.error`, handleError);
25
+ };
26
+
27
+ const handleDone = (...results: any[]) => {
28
+ cleanup();
29
+ resolve(results.length > 1 ? results : results[0]);
30
+ };
31
+
32
+ const handleError = (error: any) => {
33
+ cleanup();
34
+ reject(error);
35
+ };
36
+ events.once(`${event}.done`, handleDone);
37
+ events.once(`${event}.error`, handleError);
16
38
  events.emit(event, ...args);
17
- events.once(`${event}.done`, resolve);
18
- events.once(`${event}.error`, reject);
19
39
  });
20
40
  };
@@ -23,6 +23,7 @@ import {
23
23
  TInvoice,
24
24
  TLineItemExpanded,
25
25
  UsageRecord,
26
+ Lock,
26
27
  } from '../store/models';
27
28
  import { getConnectQueryParam } from './util';
28
29
  import { expandLineItems, getPriceUintAmountByCurrency } from './session';
@@ -37,6 +38,7 @@ import {
37
38
  import logger from './logger';
38
39
  import { ensureOverdraftProtectionPrice } from './overdraft-protection';
39
40
  import { CHARGE_SUPPORTED_CHAIN_TYPES } from './constants';
41
+ import { emitAsync } from './event';
40
42
 
41
43
  export function getCustomerInvoicePageUrl({
42
44
  invoiceId,
@@ -930,3 +932,143 @@ export async function handleOverdraftProtectionInvoiceAfterPayment(invoice: Invo
930
932
  });
931
933
  }
932
934
  }
935
+
936
+ /**
937
+ * retry uncollectible invoices
938
+ * @param options
939
+ */
940
+ export async function retryUncollectibleInvoices(options: {
941
+ customerId?: string;
942
+ subscriptionId?: string;
943
+ invoiceId?: string;
944
+ invoiceIds?: string[];
945
+ currencyId?: string;
946
+ }) {
947
+ const lockKey = `retry-uncollectible-${JSON.stringify(options)}`;
948
+
949
+ const isLocked = await Lock.isLocked(lockKey);
950
+ if (isLocked) {
951
+ logger.warn('Retry uncollectible invoices already in progress', {
952
+ lockKey,
953
+ options,
954
+ });
955
+ throw new Error('Retry already in progress');
956
+ }
957
+
958
+ try {
959
+ await Lock.acquire(lockKey, dayjs().add(5, 'minutes').unix());
960
+
961
+ const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = options;
962
+
963
+ const where: any = {
964
+ status: { [Op.in]: ['uncollectible'] },
965
+ payment_intent_id: { [Op.ne]: null },
966
+ };
967
+
968
+ if (customerId) {
969
+ where.customer_id = customerId;
970
+ }
971
+
972
+ if (subscriptionId) {
973
+ where.subscription_id = subscriptionId;
974
+ }
975
+
976
+ if (invoiceId) {
977
+ where.id = invoiceId;
978
+ }
979
+
980
+ if (invoiceIds && invoiceIds.length > 0) {
981
+ where.id = { [Op.in]: invoiceIds };
982
+ }
983
+
984
+ if (currencyId) {
985
+ where.currency_id = currencyId;
986
+ }
987
+
988
+ const overdueInvoices = (await Invoice.findAll({
989
+ where,
990
+ include: [{ model: PaymentIntent, as: 'paymentIntent' }],
991
+ attributes: ['id', 'payment_intent_id', 'subscription_id', 'customer_id', 'created_at', 'status', 'currency_id'],
992
+ order: [['created_at', 'ASC']],
993
+ })) as (Invoice & { paymentIntent?: PaymentIntent })[];
994
+
995
+ const startTime = Date.now();
996
+ logger.info('Found uncollectible invoices to retry', {
997
+ count: overdueInvoices.length,
998
+ criteria: options,
999
+ invoiceIds: overdueInvoices.map((inv) => inv.id),
1000
+ });
1001
+
1002
+ const results = {
1003
+ processed: overdueInvoices.length,
1004
+ successful: [] as string[],
1005
+ failed: [] as Array<{ id: string; reason: string }>,
1006
+ };
1007
+
1008
+ const settledResults = await Promise.allSettled(
1009
+ overdueInvoices.map(async (invoice) => {
1010
+ const { paymentIntent } = invoice;
1011
+ if (!paymentIntent) {
1012
+ throw new Error('No payment intent found');
1013
+ }
1014
+
1015
+ await paymentIntent.update({ status: 'requires_capture' });
1016
+ await emitAsync(
1017
+ 'payment.queued',
1018
+ paymentIntent.id,
1019
+ { paymentIntentId: paymentIntent.id, retryOnError: true, ignoreMaxRetryCheck: true },
1020
+ { sync: false }
1021
+ );
1022
+
1023
+ return invoice;
1024
+ })
1025
+ );
1026
+
1027
+ settledResults.forEach((result, index) => {
1028
+ const invoice = overdueInvoices[index];
1029
+ if (!invoice) {
1030
+ return;
1031
+ }
1032
+ if (result.status === 'fulfilled') {
1033
+ results.successful.push(invoice.id);
1034
+ logger.info('Successfully queued uncollectible invoice retry', {
1035
+ invoiceId: invoice.id,
1036
+ customerId: invoice.customer_id,
1037
+ paymentIntentId: invoice.payment_intent_id,
1038
+ });
1039
+ } else {
1040
+ const error = result.reason;
1041
+ const errorType = error.name || 'Unknown';
1042
+ const errorCode = error.code || 'UNKNOWN_ERROR';
1043
+
1044
+ results.failed.push({
1045
+ id: invoice.id,
1046
+ reason: error.message || 'Unknown error',
1047
+ });
1048
+
1049
+ logger.error('Failed to queue uncollectible invoice retry', {
1050
+ invoiceId: invoice.id,
1051
+ customerId: invoice.customer_id,
1052
+ subscriptionId: invoice.subscription_id,
1053
+ paymentIntentId: invoice.payment_intent_id,
1054
+ errorType,
1055
+ errorCode,
1056
+ error,
1057
+ });
1058
+ }
1059
+ });
1060
+
1061
+ const processingTime = Date.now() - startTime;
1062
+ logger.info('Completed retrying uncollectible invoices', {
1063
+ totalProcessed: results.processed,
1064
+ successful: results.successful.length,
1065
+ failed: results.failed.length,
1066
+ processingTimeMs: processingTime,
1067
+ });
1068
+
1069
+ return results;
1070
+ } finally {
1071
+ await Lock.release(lockKey);
1072
+ logger.info('Released retry uncollectible lock', { lockKey });
1073
+ }
1074
+ }
@@ -0,0 +1,165 @@
1
+ import { BN } from '@ocap/util';
2
+ import { translate } from '../../../locales';
3
+ import { BaseEmailTemplate, BaseEmailTemplateType } from './base';
4
+ import { Subscription, Invoice, Customer, PaymentCurrency, PaymentMethod } from '../../../store/models';
5
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
6
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
7
+ import { formatTime } from '../../time';
8
+ import { formatCurrencyInfo } from '../../util';
9
+
10
+ export interface AggregatedSubscriptionRenewedEmailTemplateOptions {
11
+ customer_id: string;
12
+ items: Array<{
13
+ event_id: string;
14
+ occurred_at: number;
15
+ data: {
16
+ subscriptionId: string;
17
+ invoiceId: string;
18
+ };
19
+ }>;
20
+ time_range: {
21
+ start: number;
22
+ end: number;
23
+ };
24
+ }
25
+
26
+ interface SubscriptionWithAmount {
27
+ subscription: Subscription;
28
+ amounts: Record<string, string>;
29
+ }
30
+
31
+ interface Context {
32
+ locale: string;
33
+ startTime: string;
34
+ endTime: string;
35
+ totalAmountStr: string;
36
+ subscriptionData: SubscriptionWithAmount[];
37
+ userDid: string;
38
+ }
39
+
40
+ export class AggregatedSubscriptionRenewedEmailTemplate implements BaseEmailTemplate {
41
+ options: AggregatedSubscriptionRenewedEmailTemplateOptions;
42
+
43
+ constructor(options: AggregatedSubscriptionRenewedEmailTemplateOptions) {
44
+ this.options = options;
45
+ }
46
+
47
+ async getContext(): Promise<Context> {
48
+ const { items, customer_id: customerId, time_range: timeRange } = this.options;
49
+
50
+ const customer = await Customer.findByPk(customerId);
51
+ if (!customer) {
52
+ throw new Error(`Customer not found: ${customerId}`);
53
+ }
54
+ const locale = await getUserLocale(customer.did);
55
+
56
+ const subscriptions = await Subscription.findAll({
57
+ where: { id: [...new Set(items.map((item) => item.data.subscriptionId))] },
58
+ });
59
+
60
+ const invoices = await Invoice.findAll({
61
+ where: { id: [...new Set(items.map((item) => item.data.invoiceId))] },
62
+ });
63
+
64
+ const currencyIds = [...new Set(invoices.map((invoice) => invoice.currency_id))];
65
+ const paymentCurrencies = (await PaymentCurrency.findAll({
66
+ where: { id: currencyIds },
67
+ include: [{ model: PaymentMethod, as: 'payment_method' }],
68
+ })) as (PaymentCurrency & { payment_method: PaymentMethod })[];
69
+ const currencyMap = paymentCurrencies.reduce(
70
+ (acc, curr) => {
71
+ acc[curr.id] = curr;
72
+ return acc;
73
+ },
74
+ {} as Record<string, PaymentCurrency & { payment_method: PaymentMethod }>
75
+ );
76
+
77
+ const { totalAmounts, subscriptionAmounts } = invoices.reduce(
78
+ (acc, invoice) => {
79
+ const currency = invoice.currency_id;
80
+ const amount = new BN(invoice.amount_paid || '0');
81
+
82
+ acc.totalAmounts[currency] = (acc.totalAmounts[currency] || new BN('0')).add(amount);
83
+
84
+ if (invoice.subscription_id) {
85
+ if (!acc.subscriptionAmounts[invoice.subscription_id]) {
86
+ acc.subscriptionAmounts[invoice.subscription_id] = {};
87
+ }
88
+ const subAmounts = acc.subscriptionAmounts[invoice.subscription_id] || {};
89
+ subAmounts[currency] = (subAmounts[currency] || new BN('0')).add(amount);
90
+ acc.subscriptionAmounts[invoice.subscription_id] = subAmounts;
91
+ }
92
+ return acc;
93
+ },
94
+ {
95
+ totalAmounts: {} as Record<string, BN>,
96
+ subscriptionAmounts: {} as Record<string, Record<string, BN>>,
97
+ }
98
+ );
99
+
100
+ const totalAmountStr = Object.entries(totalAmounts)
101
+ .map(([currencyId, amount]) => {
102
+ const currency = currencyMap[currencyId];
103
+ if (!currency) {
104
+ return undefined;
105
+ }
106
+ return formatCurrencyInfo(amount.toString(), currency, currency.payment_method);
107
+ })
108
+ .filter((amountStr) => amountStr !== undefined)
109
+ .join('、');
110
+
111
+ const subscriptionData = subscriptions.map((subscription) => ({
112
+ subscription,
113
+ amounts: Object.entries(subscriptionAmounts[subscription.id] || {}).reduce(
114
+ (acc, [currencyId, amount]) => {
115
+ const currency = currencyMap[currencyId];
116
+ if (!currency) {
117
+ return acc;
118
+ }
119
+ acc[currencyId] = formatCurrencyInfo(amount.toString(), currency, currency.payment_method);
120
+ return acc;
121
+ },
122
+ {} as Record<string, string>
123
+ ),
124
+ }));
125
+
126
+ return {
127
+ locale,
128
+ startTime: formatTime(timeRange.start * 1000),
129
+ endTime: formatTime(timeRange.end * 1000),
130
+ totalAmountStr,
131
+ subscriptionData,
132
+ userDid: customer.did,
133
+ };
134
+ }
135
+
136
+ async getTemplate(): Promise<BaseEmailTemplateType> {
137
+ const { locale, startTime, endTime, totalAmountStr, subscriptionData, userDid } = await this.getContext();
138
+
139
+ const subscriptionList = subscriptionData
140
+ .map(({ subscription, amounts }) => {
141
+ const amountStr = Object.values(amounts).join('、');
142
+ const description = subscription.description || subscription.id;
143
+ const link = getCustomerSubscriptionPageUrl({
144
+ subscriptionId: subscription.id,
145
+ locale,
146
+ userDid,
147
+ });
148
+ return `<${description} - ${amountStr}(link:${link})>`;
149
+ })
150
+ .join('\n');
151
+
152
+ return {
153
+ title: translate('notification.aggregatedSubscriptionRenewed.title', locale, {
154
+ count: subscriptionData.length,
155
+ }),
156
+ body: translate('notification.aggregatedSubscriptionRenewed.body', locale, {
157
+ startTime,
158
+ endTime,
159
+ count: subscriptionData.length,
160
+ totalAmount: totalAmountStr,
161
+ subscriptionList,
162
+ }),
163
+ };
164
+ }
165
+ }
@@ -1,6 +1,5 @@
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
 
6
5
  import { getUserLocale } from '../../../integrations/blocklet/notification';
@@ -9,7 +8,7 @@ import { CheckoutSession, Customer, NftMintItem, PaymentIntent, PaymentMethod }
9
8
  import { PaymentCurrency } from '../../../store/models/payment-currency';
10
9
  import { getMainProductNameByCheckoutSession } from '../../product';
11
10
  import { formatTime } from '../../time';
12
- import { getExplorerLink } from '../../util';
11
+ import { formatCurrencyInfo, getExplorerLink } from '../../util';
13
12
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
14
13
 
15
14
  export interface OneTimePaymentSucceededEmailTemplateOptions {
@@ -96,9 +95,7 @@ export class OneTimePaymentSucceededEmailTemplate
96
95
  const paymentIntent = await PaymentIntent.findByPk(checkoutSession!.payment_intent_id);
97
96
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentIntent!.payment_method_id);
98
97
 
99
- const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${
100
- paymentCurrency.symbol
101
- }${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
98
+ const paymentInfo = formatCurrencyInfo(checkoutSession?.amount_total, paymentCurrency, paymentMethod);
102
99
 
103
100
  // @ts-expect-error
104
101
  const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken } from '@ocap/util';
4
3
  import prettyMsI18n from 'pretty-ms-i18n';
5
4
 
6
5
  import { Op } from 'sequelize';
@@ -13,7 +12,7 @@ import { getMainProductName } from '../../product';
13
12
  import { getCustomerSubscriptionPageUrl, getSubscriptionStakeCancellation } from '../../subscription';
14
13
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
15
14
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
- import { getSubscriptionNotificationCustomActions } from '../../util';
15
+ import { formatCurrencyInfo, getSubscriptionNotificationCustomActions } from '../../util';
17
16
 
18
17
  export interface SubscriptionCanceledEmailTemplateOptions {
19
18
  subscriptionId: string;
@@ -87,7 +86,7 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
87
86
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
88
87
 
89
88
  // @ts-ignore
90
- const paymentInfo: string = `${fromUnitToToken(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
89
+ const paymentInfo: string = formatCurrencyInfo(invoice.total, invoice?.paymentCurrency, paymentMethod);
91
90
 
92
91
  const customerCancelRequest = subscription.cancelation_details?.reason === 'cancellation_requested';
93
92
  let cancellationReason = '';
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken } from '@ocap/util';
4
3
  import prettyMsI18n from 'pretty-ms-i18n';
5
4
 
6
5
  import { getUserLocale } from '../../../integrations/blocklet/notification';
@@ -11,7 +10,7 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
11
10
  import { getMainProductName } from '../../product';
12
11
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
13
12
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
14
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
13
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
15
14
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
15
 
17
16
  export interface SubscriptionRefundSucceededEmailTemplateOptions {
@@ -103,8 +102,12 @@ export class SubscriptionRefundSucceededEmailTemplate
103
102
  refund.payment_method_id || invoice?.default_payment_method_id
104
103
  );
105
104
 
106
- const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
107
- const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
105
+ const paymentInfo: string = formatCurrencyInfo(
106
+ paymentIntent?.amount_received || '0',
107
+ paymentCurrency,
108
+ paymentMethod
109
+ );
110
+ const refundInfo: string = formatCurrencyInfo(refund.amount, paymentCurrency, paymentMethod);
108
111
 
109
112
  // @ts-expect-error
110
113
  const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken, toDid } from '@ocap/util';
3
+ import { toDid } from '@ocap/util';
4
4
  import camelCase from 'lodash/camelCase';
5
5
  import prettyMsI18n from 'pretty-ms-i18n';
6
6
 
@@ -22,7 +22,7 @@ import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
22
22
  import { getMainProductName } from '../../product';
23
23
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
24
24
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
25
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
25
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
26
26
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
27
27
 
28
28
  export interface SubscriptionRenewFailedEmailTemplateOptions {
@@ -123,9 +123,7 @@ export class SubscriptionRenewFailedEmailTemplate
123
123
  );
124
124
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
125
125
 
126
- const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
127
- paymentCurrency.symbol
128
- }${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
126
+ const paymentInfo: string = formatCurrencyInfo(invoice.amount_remaining, paymentCurrency, paymentMethod);
129
127
 
130
128
  const chainHost: string | undefined =
131
129
  paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
@@ -20,7 +20,7 @@ import { getCustomerInvoicePageUrl } from '../../invoice';
20
20
  import { getMainProductName } from '../../product';
21
21
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
22
22
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
23
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
23
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
24
24
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
25
25
 
26
26
  export interface SubscriptionRenewedEmailTemplateOptions {
@@ -112,7 +112,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
112
112
  );
113
113
 
114
114
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
115
- const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
115
+ const paymentInfo: string = formatCurrencyInfo(invoice.total, paymentCurrency, paymentMethod);
116
116
 
117
117
  const chainHost: string | undefined =
118
118
  paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable prettier/prettier */
2
- import { fromUnitToToken } from '@ocap/util';
3
2
  import { getUserLocale } from '../../../integrations/blocklet/notification';
4
3
  import { translate } from '../../../locales';
5
4
  import { Customer, PaymentIntent, PaymentMethod, Subscription } from '../../../store/models';
@@ -8,7 +7,7 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
8
7
  import { getMainProductName } from '../../product';
9
8
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
10
9
  import { formatTime } from '../../time';
11
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
10
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
12
11
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
13
12
 
14
13
  export interface SubscriptionStakeSlashSucceededEmailTemplateOptions {
@@ -84,7 +83,7 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
84
83
 
85
84
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
86
85
 
87
- const slashInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
86
+ const slashInfo: string = formatCurrencyInfo(paymentIntent?.amount_received || '0', paymentCurrency, paymentMethod);
88
87
 
89
88
  // @ts-expect-error
90
89
  const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
@@ -21,7 +21,7 @@ import {
21
21
  import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
22
22
  import { getMainProductName } from '../../product';
23
23
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
24
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
24
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
25
25
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
26
26
 
27
27
  export interface SubscriptionSucceededEmailTemplateOptions {
@@ -134,7 +134,7 @@ export class SubscriptionSucceededEmailTemplate
134
134
  );
135
135
 
136
136
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
137
- const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
137
+ const paymentInfo: string = formatCurrencyInfo(paymentAmount, paymentCurrency, paymentMethod, true);
138
138
 
139
139
  // @FIXME: 获取 chainHost 困难的一批?
140
140
  const chainHost: string | undefined =
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken } from '@ocap/util';
4
3
  import prettyMsI18n from 'pretty-ms-i18n';
5
4
 
6
5
  import { getUserLocale } from '../../../integrations/blocklet/notification';
@@ -20,7 +19,7 @@ import { getCustomerInvoicePageUrl } from '../../invoice';
20
19
  import { getMainProductName } from '../../product';
21
20
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
22
21
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
23
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
22
+ import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
24
23
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
25
24
 
26
25
  export interface SubscriptionUpgradedEmailTemplateOptions {
@@ -107,10 +106,11 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
107
106
 
108
107
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
109
108
 
110
- const paymentInfo: string = `${fromUnitToToken(
109
+ const paymentInfo: string = formatCurrencyInfo(
111
110
  paymentIntent?.amount || invoice.amount_paid,
112
- paymentCurrency.decimal
113
- )} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
111
+ paymentCurrency,
112
+ paymentMethod
113
+ );
114
114
 
115
115
  // @ts-expect-error
116
116
  const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
@@ -26,7 +26,7 @@ import {
26
26
  getPaymentAmountForCycleSubscription,
27
27
  } from '../../subscription';
28
28
  import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
29
- import { getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
29
+ import { formatCurrencyInfo, getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
30
30
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
31
31
 
32
32
  export interface SubscriptionWillRenewEmailTemplateOptions {
@@ -117,7 +117,7 @@ export class SubscriptionWillRenewEmailTemplate
117
117
  const paidType: string = isPrePaid
118
118
  ? translate('notification.common.prepaid', locale)
119
119
  : translate('notification.common.postpaid', locale);
120
- const paymentInfo: string = `${paymentDetail?.price || '0'} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
120
+ const paymentInfo: string = formatCurrencyInfo(paymentDetail?.price || '0', paymentCurrency, paymentMethod, true);
121
121
  const currentPeriodStart: string = isPrePaid
122
122
  ? formatTime(invoice.period_end * 1000)
123
123
  : formatTime(invoice.period_start * 1000);
@@ -270,6 +270,11 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
270
270
  }
271
271
  };
272
272
 
273
+ const updateJob = async (id: string, updates: any) => {
274
+ const updatedJob = await store.updateJob(id, updates);
275
+ return updatedJob;
276
+ };
277
+
273
278
  // Populate the queue on startup
274
279
  process.nextTick(async () => {
275
280
  try {
@@ -335,6 +340,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
335
340
  get: getJob,
336
341
  delete: deleteJob,
337
342
  cancel,
343
+ update: updateJob,
338
344
  options: {
339
345
  concurrency,
340
346
  maxRetries,
@@ -1,4 +1,4 @@
1
- import { Op } from 'sequelize';
1
+ import { Op, type WhereOptions } from 'sequelize';
2
2
 
3
3
  import { Job, TJob } from '../../store/models/job';
4
4
  import CustomError from '../error';
@@ -26,6 +26,18 @@ export default function createQueueStore(queue: string) {
26
26
  transaction: null,
27
27
  });
28
28
  },
29
+ findJobs(predicate: WhereOptions<TJob>): Promise<TJob[]> {
30
+ return Job.findAll({
31
+ where: {
32
+ queue,
33
+ cancelled: false,
34
+ ...predicate,
35
+ },
36
+ order: [['created_at', 'ASC']],
37
+ transaction: null,
38
+ });
39
+ },
40
+
29
41
  async updateJob(id: string, updates: Partial<TJob>): Promise<TJob> {
30
42
  const job = await Job.findOne({ where: { queue, id }, transaction: null });
31
43
  if (!job) {
@@ -10,9 +10,10 @@ import { joinURL, withQuery, withTrailingSlash } from 'ufo';
10
10
 
11
11
  import axios from 'axios';
12
12
  import { ethers } from 'ethers';
13
+ import { fromUnitToToken } from '@ocap/util';
13
14
  import dayjs from './dayjs';
14
15
  import { blocklet, wallet } from './auth';
15
- import type { PaymentMethod, Subscription } from '../store/models';
16
+ import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
16
17
  import logger from './logger';
17
18
 
18
19
  export const OCAP_PAYMENT_TX_TYPE = 'fg:t:transfer_v2';
@@ -524,3 +525,23 @@ export function resolveAddressChainTypes(address: string): LiteralUnion<'ethereu
524
525
  }
525
526
  return ['arcblock'];
526
527
  }
528
+
529
+ export function formatCurrencyInfo(
530
+ amount: string | number,
531
+ paymentCurrency: PaymentCurrency,
532
+ paymentMethod?: PaymentMethod | null,
533
+ isToken?: boolean
534
+ ) {
535
+ let amountStr = '';
536
+ const defaultPaymentCurrency = {
537
+ symbol: '',
538
+ decimal: 18,
539
+ };
540
+
541
+ if (isToken) {
542
+ amountStr = `${amount || '0'} ${paymentCurrency.symbol ?? defaultPaymentCurrency.symbol}`;
543
+ } else {
544
+ amountStr = `${fromUnitToToken(amount || '0', paymentCurrency.decimal ?? defaultPaymentCurrency.decimal)} ${paymentCurrency.symbol ?? defaultPaymentCurrency.symbol}`;
545
+ }
546
+ return paymentMethod && paymentMethod.type !== 'arcblock' ? `${amountStr} (${paymentMethod.name})` : amountStr;
547
+ }