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.
- package/api/src/libs/event.ts +22 -2
- package/api/src/libs/invoice.ts +142 -0
- package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
- package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
- package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
- package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
- package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
- package/api/src/libs/queue/index.ts +6 -0
- package/api/src/libs/queue/store.ts +13 -1
- package/api/src/libs/util.ts +22 -1
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/invoice.ts +21 -7
- package/api/src/queues/notification.ts +353 -11
- package/api/src/queues/payment.ts +26 -10
- package/api/src/queues/payout.ts +21 -7
- package/api/src/routes/checkout-sessions.ts +26 -12
- package/api/src/routes/connect/recharge-account.ts +13 -1
- package/api/src/routes/connect/recharge.ts +13 -1
- package/api/src/routes/connect/shared.ts +54 -36
- package/api/src/routes/customers.ts +61 -0
- package/api/src/routes/invoices.ts +51 -1
- package/api/src/routes/subscriptions.ts +1 -1
- package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
- package/api/src/store/models/customer.ts +42 -1
- package/api/src/store/models/types.ts +17 -1
- package/blocklet.yml +1 -1
- package/package.json +24 -24
- package/src/components/customer/form.tsx +21 -2
- package/src/components/customer/notification-preference.tsx +428 -0
- package/src/components/layout/user.tsx +1 -1
- package/src/locales/en.tsx +30 -0
- package/src/locales/zh.tsx +30 -0
- package/src/pages/customer/index.tsx +27 -23
- package/src/pages/customer/recharge/account.tsx +19 -17
- package/src/pages/customer/subscription/embed.tsx +25 -9
package/api/src/libs/event.ts
CHANGED
|
@@ -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
|
};
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
107
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
109
|
+
const paymentInfo: string = formatCurrencyInfo(
|
|
111
110
|
paymentIntent?.amount || invoice.amount_paid,
|
|
112
|
-
paymentCurrency
|
|
113
|
-
|
|
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 =
|
|
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) {
|
package/api/src/libs/util.ts
CHANGED
|
@@ -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
|
+
}
|