payment-kit 1.19.16 → 1.19.18
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/notification/template/customer-credit-low-balance.ts +155 -0
- package/api/src/libs/ws.ts +3 -2
- package/api/src/locales/en.ts +6 -6
- package/api/src/locales/zh.ts +4 -4
- package/api/src/queues/credit-consume.ts +36 -0
- package/api/src/queues/notification.ts +16 -13
- package/api/src/routes/meter-events.ts +3 -2
- package/api/src/store/models/credit-grant.ts +1 -10
- package/api/src/store/models/types.ts +1 -1
- package/blocklet.yml +3 -3
- package/package.json +18 -18
- package/src/components/currency.tsx +3 -1
- package/src/components/subscription/items/index.tsx +8 -2
- package/src/components/subscription/metrics.tsx +5 -1
- package/src/pages/customer/subscription/detail.tsx +6 -1
- package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +0 -151
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
4
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
5
|
+
import { translate } from '../../../locales';
|
|
6
|
+
import { Customer, PaymentCurrency } from '../../../store/models';
|
|
7
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
8
|
+
import { formatNumber, getCustomerIndexUrl } from '../../util';
|
|
9
|
+
|
|
10
|
+
export interface CustomerCreditLowBalanceEmailTemplateOptions {
|
|
11
|
+
customerId: string;
|
|
12
|
+
currencyId: string;
|
|
13
|
+
availableAmount: string; // unit amount
|
|
14
|
+
totalAmount: string; // unit amount
|
|
15
|
+
percentage: string; // 0-100 number string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CustomerCreditLowBalanceEmailTemplateContext {
|
|
19
|
+
locale: string;
|
|
20
|
+
userDid: string;
|
|
21
|
+
currencySymbol: string;
|
|
22
|
+
availableAmount: string; // formatted with symbol
|
|
23
|
+
totalAmount: string; // formatted with symbol
|
|
24
|
+
lowBalancePercentage: string; // with %
|
|
25
|
+
currencyName: string;
|
|
26
|
+
}
|
|
27
|
+
export class CustomerCreditLowBalanceEmailTemplate
|
|
28
|
+
implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext>
|
|
29
|
+
{
|
|
30
|
+
options: CustomerCreditLowBalanceEmailTemplateOptions;
|
|
31
|
+
|
|
32
|
+
constructor(options: CustomerCreditLowBalanceEmailTemplateOptions) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getContext(): Promise<CustomerCreditLowBalanceEmailTemplateContext> {
|
|
37
|
+
const { customerId, currencyId, availableAmount, totalAmount, percentage } = this.options;
|
|
38
|
+
|
|
39
|
+
const customer = await Customer.findByPk(customerId);
|
|
40
|
+
if (!customer) {
|
|
41
|
+
throw new Error(`Customer not found: ${customerId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
|
|
45
|
+
if (!paymentCurrency) {
|
|
46
|
+
throw new Error(`PaymentCurrency not found: ${currencyId}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userDid = customer.did;
|
|
50
|
+
const locale = await getUserLocale(userDid);
|
|
51
|
+
const currencySymbol = paymentCurrency.symbol;
|
|
52
|
+
|
|
53
|
+
const available = formatNumber(fromUnitToToken(availableAmount, paymentCurrency.decimal));
|
|
54
|
+
const total = formatNumber(fromUnitToToken(totalAmount, paymentCurrency.decimal));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
locale,
|
|
58
|
+
userDid,
|
|
59
|
+
currencySymbol,
|
|
60
|
+
availableAmount: `${available}`,
|
|
61
|
+
totalAmount: `${total}`,
|
|
62
|
+
lowBalancePercentage: `${percentage}%`,
|
|
63
|
+
currencyName: paymentCurrency.name,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
68
|
+
const context = await this.getContext();
|
|
69
|
+
const { locale, userDid, availableAmount, totalAmount, lowBalancePercentage, currencyName, currencySymbol } =
|
|
70
|
+
context;
|
|
71
|
+
|
|
72
|
+
const fields = [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
data: {
|
|
76
|
+
type: 'plain',
|
|
77
|
+
color: '#9397A1',
|
|
78
|
+
text: translate('notification.common.account', locale),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
data: {
|
|
84
|
+
type: 'plain',
|
|
85
|
+
text: userDid,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
data: {
|
|
91
|
+
type: 'plain',
|
|
92
|
+
color: '#9397A1',
|
|
93
|
+
text: translate('notification.creditInsufficient.availableCredit', locale),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'text',
|
|
98
|
+
data: {
|
|
99
|
+
type: 'plain',
|
|
100
|
+
color: '#FF6600',
|
|
101
|
+
text: `${availableAmount} ${currencySymbol} (${lowBalancePercentage})`,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'text',
|
|
106
|
+
data: {
|
|
107
|
+
type: 'plain',
|
|
108
|
+
color: '#9397A1',
|
|
109
|
+
text: translate('notification.creditLowBalance.totalAmount', locale),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
data: {
|
|
115
|
+
type: 'plain',
|
|
116
|
+
text: `${totalAmount} ${currencySymbol}`,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const actions = [
|
|
122
|
+
{
|
|
123
|
+
name: translate('notification.common.viewCreditGrant', locale),
|
|
124
|
+
title: translate('notification.common.viewCreditGrant', locale),
|
|
125
|
+
link: getCustomerIndexUrl({
|
|
126
|
+
locale,
|
|
127
|
+
userDid,
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const template: BaseEmailTemplateType = {
|
|
133
|
+
title: translate('notification.creditLowBalance.title', locale, {
|
|
134
|
+
lowBalancePercentage,
|
|
135
|
+
currency: currencyName,
|
|
136
|
+
}),
|
|
137
|
+
body: translate('notification.creditLowBalance.body', locale, {
|
|
138
|
+
currency: currencyName,
|
|
139
|
+
availableAmount,
|
|
140
|
+
totalAmount,
|
|
141
|
+
lowBalancePercentage,
|
|
142
|
+
}),
|
|
143
|
+
attachments: [
|
|
144
|
+
{
|
|
145
|
+
type: 'section',
|
|
146
|
+
fields,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
actions,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return template;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/api/src/libs/ws.ts
CHANGED
|
@@ -106,8 +106,9 @@ export function initEventBroadcast() {
|
|
|
106
106
|
events.on('customer.credit_grant.granted', (data: CreditGrant, extraParams?: Record<string, any>) => {
|
|
107
107
|
broadcast('customer.credit_grant.granted', data, extraParams);
|
|
108
108
|
});
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
|
|
110
|
+
events.on('customer.credit.low_balance', (data: Customer, extraParams?: Record<string, any>) => {
|
|
111
|
+
broadcast('customer.credit.low_balance', data, extraParams);
|
|
111
112
|
});
|
|
112
113
|
events.on('customer.credit_grant.depleted', (data: CreditGrant, extraParams?: Record<string, any>) => {
|
|
113
114
|
broadcast('customer.credit_grant.depleted', data, extraParams);
|
package/api/src/locales/en.ts
CHANGED
|
@@ -241,8 +241,8 @@ export default flat({
|
|
|
241
241
|
exhaustedBodyWithoutSubscription:
|
|
242
242
|
'Your credit is fully exhausted (remaining balance: 0). Please top up to ensure uninterrupted service.',
|
|
243
243
|
meterEventName: 'Service',
|
|
244
|
-
availableCredit: 'Available Credit',
|
|
245
|
-
requiredCredit: 'Required Credit',
|
|
244
|
+
availableCredit: 'Available Credit Amount',
|
|
245
|
+
requiredCredit: 'Required Credit Amount',
|
|
246
246
|
topUpNow: 'Top Up Now',
|
|
247
247
|
},
|
|
248
248
|
|
|
@@ -255,10 +255,10 @@ export default flat({
|
|
|
255
255
|
neverExpires: 'Never Expires',
|
|
256
256
|
},
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
title: '
|
|
260
|
-
body: 'Your
|
|
261
|
-
|
|
258
|
+
creditLowBalance: {
|
|
259
|
+
title: 'Your {currency} is below {lowBalancePercentage}',
|
|
260
|
+
body: 'Your {currency} available balance is below {lowBalancePercentage} of the total. Please top up to avoid service interruption.',
|
|
261
|
+
totalAmount: 'Total Credit Amount',
|
|
262
262
|
},
|
|
263
263
|
},
|
|
264
264
|
});
|
package/api/src/locales/zh.ts
CHANGED
|
@@ -247,10 +247,10 @@ export default flat({
|
|
|
247
247
|
neverExpires: '永不过期',
|
|
248
248
|
},
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
title: '
|
|
252
|
-
body: '
|
|
253
|
-
|
|
250
|
+
creditLowBalance: {
|
|
251
|
+
title: '您的{currency} 已低于 {lowBalancePercentage}',
|
|
252
|
+
body: '您的 {currency} 总可用额度已低于 {lowBalancePercentage},请及时充值以避免服务受限。',
|
|
253
|
+
totalAmount: '总额度',
|
|
254
254
|
},
|
|
255
255
|
},
|
|
256
256
|
});
|
|
@@ -41,6 +41,38 @@ type CreditConsumptionResult = {
|
|
|
41
41
|
fully_consumed: boolean;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
async function checkLowBalance(
|
|
45
|
+
customerId: string,
|
|
46
|
+
currencyId: string,
|
|
47
|
+
totalCreditAmount: string,
|
|
48
|
+
remainingBalance: string,
|
|
49
|
+
context: CreditConsumptionContext
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const totalCreditAmountBn = new BN(totalCreditAmount);
|
|
53
|
+
if (totalCreditAmountBn.lte(new BN(0))) return;
|
|
54
|
+
const remainingAmountBn = new BN(remainingBalance);
|
|
55
|
+
const threshold = totalCreditAmountBn.mul(new BN(10)).div(new BN(100));
|
|
56
|
+
if (remainingAmountBn.gt(new BN(0)) && remainingAmountBn.lte(threshold)) {
|
|
57
|
+
const percentage = remainingAmountBn.mul(new BN(100)).div(totalCreditAmountBn).toString();
|
|
58
|
+
await createEvent('Customer', 'customer.credit.low_balance', context.customer, {
|
|
59
|
+
metadata: {
|
|
60
|
+
currency_id: currencyId,
|
|
61
|
+
available_amount: remainingAmountBn.toString(),
|
|
62
|
+
total_amount: totalCreditAmountBn.toString(),
|
|
63
|
+
percentage,
|
|
64
|
+
subscription_id: context.subscription?.id,
|
|
65
|
+
},
|
|
66
|
+
}).catch(console.error);
|
|
67
|
+
}
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
logger.error('Failed to check low balance', {
|
|
70
|
+
customerId,
|
|
71
|
+
currencyId,
|
|
72
|
+
error: error.message,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
44
76
|
async function validateAndLoadData(meterEventId: string): Promise<CreditConsumptionContext | null> {
|
|
45
77
|
const meterEvent = await MeterEvent.findByPk(meterEventId);
|
|
46
78
|
if (!meterEvent) {
|
|
@@ -171,6 +203,8 @@ async function consumeAvailableCredits(
|
|
|
171
203
|
// Get all available grants sorted by priority
|
|
172
204
|
const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId, context.priceIds);
|
|
173
205
|
|
|
206
|
+
const totalCreditAmountBN = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.amount)), new BN(0));
|
|
207
|
+
|
|
174
208
|
// Calculate total available balance
|
|
175
209
|
const totalAvailable = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0));
|
|
176
210
|
logger.debug('Total available credits calculated', { totalAvailable: totalAvailable.toString() });
|
|
@@ -275,6 +309,8 @@ async function consumeAvailableCredits(
|
|
|
275
309
|
}).catch(console.error);
|
|
276
310
|
}
|
|
277
311
|
|
|
312
|
+
await checkLowBalance(customerId, currencyId, totalCreditAmountBN.toString(), remainingBalance, context);
|
|
313
|
+
|
|
278
314
|
return {
|
|
279
315
|
consumed: totalConsumed.toString(),
|
|
280
316
|
pending: pendingAmount,
|
|
@@ -93,10 +93,11 @@ import {
|
|
|
93
93
|
CustomerCreditGrantGrantedEmailTemplate,
|
|
94
94
|
CustomerCreditGrantGrantedEmailTemplateOptions,
|
|
95
95
|
} from '../libs/notification/template/customer-credit-grant-granted';
|
|
96
|
+
|
|
96
97
|
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} from '../libs/notification/template/customer-credit-
|
|
98
|
+
CustomerCreditLowBalanceEmailTemplate,
|
|
99
|
+
CustomerCreditLowBalanceEmailTemplateOptions,
|
|
100
|
+
} from '../libs/notification/template/customer-credit-low-balance';
|
|
100
101
|
import {
|
|
101
102
|
CustomerRevenueSucceededEmailTemplate,
|
|
102
103
|
CustomerRevenueSucceededEmailTemplateOptions,
|
|
@@ -128,7 +129,7 @@ export type NotificationQueueJobType =
|
|
|
128
129
|
| 'subscription.overdraftProtection.exhausted'
|
|
129
130
|
| 'customer.credit.insufficient'
|
|
130
131
|
| 'customer.credit_grant.granted'
|
|
131
|
-
| 'customer.
|
|
132
|
+
| 'customer.credit.low_balance';
|
|
132
133
|
|
|
133
134
|
export type NotificationQueueJob = {
|
|
134
135
|
type: NotificationQueueJobType;
|
|
@@ -266,10 +267,8 @@ async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseE
|
|
|
266
267
|
return new CustomerCreditGrantGrantedEmailTemplate(job.options as CustomerCreditGrantGrantedEmailTemplateOptions);
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
if (job.type === 'customer.
|
|
270
|
-
return new
|
|
271
|
-
job.options as CustomerCreditGrantLowBalanceEmailTemplateOptions
|
|
272
|
-
);
|
|
270
|
+
if (job.type === 'customer.credit.low_balance') {
|
|
271
|
+
return new CustomerCreditLowBalanceEmailTemplate(job.options as CustomerCreditLowBalanceEmailTemplateOptions);
|
|
273
272
|
}
|
|
274
273
|
|
|
275
274
|
throw new Error(`Unknown job type: ${job.type}`);
|
|
@@ -600,15 +599,19 @@ export async function startNotificationQueue() {
|
|
|
600
599
|
);
|
|
601
600
|
});
|
|
602
601
|
|
|
603
|
-
events.on('customer.
|
|
602
|
+
events.on('customer.credit.low_balance', (customer: Customer, { metadata }: { metadata: any }) => {
|
|
604
603
|
addNotificationJob(
|
|
605
|
-
'customer.
|
|
604
|
+
'customer.credit.low_balance',
|
|
606
605
|
{
|
|
607
|
-
|
|
606
|
+
customerId: customer.id,
|
|
607
|
+
currencyId: metadata.currency_id,
|
|
608
|
+
availableAmount: metadata.available_amount,
|
|
609
|
+
totalAmount: metadata.total_amount,
|
|
610
|
+
percentage: metadata.percentage,
|
|
608
611
|
},
|
|
609
|
-
[
|
|
612
|
+
[customer.id, metadata.currency_id],
|
|
610
613
|
true,
|
|
611
|
-
24 * 3600
|
|
614
|
+
24 * 3600
|
|
612
615
|
);
|
|
613
616
|
});
|
|
614
617
|
|
|
@@ -238,6 +238,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
238
238
|
|
|
239
239
|
const timestamp = req.body.timestamp || req.body.payload.timestamp || Math.floor(Date.now() / 1000);
|
|
240
240
|
|
|
241
|
+
const value = parseFloat(req.body.payload.value).toFixed(paymentCurrency.decimal);
|
|
241
242
|
const eventData = {
|
|
242
243
|
event_name: req.body.event_name,
|
|
243
244
|
payload: {
|
|
@@ -246,7 +247,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
246
247
|
decimal: paymentCurrency.decimal,
|
|
247
248
|
unit: paymentCurrency.name,
|
|
248
249
|
subscription_id: req.body.payload.subscription_id,
|
|
249
|
-
value: fromTokenToUnit(
|
|
250
|
+
value: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
|
|
250
251
|
},
|
|
251
252
|
identifier: req.body.identifier,
|
|
252
253
|
livemode: !!req.livemode,
|
|
@@ -254,7 +255,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
254
255
|
status: 'pending' as MeterEventStatus,
|
|
255
256
|
attempt_count: 0,
|
|
256
257
|
credit_consumed: '0',
|
|
257
|
-
credit_pending: fromTokenToUnit(
|
|
258
|
+
credit_pending: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
|
|
258
259
|
created_via: req.user?.via || 'api',
|
|
259
260
|
metadata: formatMetadata(req.body.metadata),
|
|
260
261
|
timestamp,
|
|
@@ -198,7 +198,7 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
198
198
|
// consume credit
|
|
199
199
|
public async consumeCredit(
|
|
200
200
|
amount: string,
|
|
201
|
-
|
|
201
|
+
_context: {
|
|
202
202
|
subscription_id?: string;
|
|
203
203
|
meter_event_id?: string;
|
|
204
204
|
},
|
|
@@ -242,15 +242,6 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
242
242
|
await this.save();
|
|
243
243
|
|
|
244
244
|
await createEvent('CreditGrant', 'customer.credit_grant.consumed', this).catch(console.error);
|
|
245
|
-
|
|
246
|
-
// check low balance warning
|
|
247
|
-
const originalAmount = new BN(this.amount);
|
|
248
|
-
const threshold = originalAmount.mul(new BN(10)).div(new BN(100)); // 10%
|
|
249
|
-
if (newRemainingAmount.gt(new BN(0)) && newRemainingAmount.lte(threshold)) {
|
|
250
|
-
await createEvent('CreditGrant', 'customer.credit_grant.low_balance', this, {
|
|
251
|
-
metadata: context,
|
|
252
|
-
}).catch(console.error);
|
|
253
|
-
}
|
|
254
245
|
}
|
|
255
246
|
|
|
256
247
|
return {
|
|
@@ -725,8 +725,8 @@ export type EventType = LiteralUnion<
|
|
|
725
725
|
| 'billing.discrepancy'
|
|
726
726
|
| 'usage.report.empty'
|
|
727
727
|
| 'customer.credit.insufficient'
|
|
728
|
+
| 'customer.credit.low_balance'
|
|
728
729
|
| 'customer.credit_grant.granted'
|
|
729
|
-
| 'customer.credit_grant.low_balance'
|
|
730
730
|
| 'customer.credit_grant.depleted',
|
|
731
731
|
string
|
|
732
732
|
>;
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.19.
|
|
17
|
+
version: 1.19.18
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -171,9 +171,9 @@ events:
|
|
|
171
171
|
description: Application will send notification to user manually
|
|
172
172
|
- type: customer.credit_grant.granted
|
|
173
173
|
description: Credit grant has been successfully granted
|
|
174
|
-
- type: customer.credit_grant.low_balance
|
|
175
|
-
description: Credit grant has low balance
|
|
176
174
|
- type: customer.credit_grant.depleted
|
|
177
175
|
description: Credit grant has been depleted
|
|
178
176
|
- type: customer.credit.insufficient
|
|
179
177
|
description: Customer has insufficient credit
|
|
178
|
+
- type: customer.credit.low_balance
|
|
179
|
+
description: Customer has low credit balance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.18",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -44,31 +44,31 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@abtnode/cron": "^1.16.48",
|
|
47
|
-
"@arcblock/did": "^1.21.
|
|
48
|
-
"@arcblock/did-connect-react": "^3.1.
|
|
47
|
+
"@arcblock/did": "^1.21.3",
|
|
48
|
+
"@arcblock/did-connect-react": "^3.1.18",
|
|
49
49
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
50
|
-
"@arcblock/did-util": "^1.21.
|
|
51
|
-
"@arcblock/jwt": "^1.21.
|
|
52
|
-
"@arcblock/ux": "^3.1.
|
|
53
|
-
"@arcblock/validator": "^1.21.
|
|
54
|
-
"@blocklet/did-space-js": "^1.1.
|
|
50
|
+
"@arcblock/did-util": "^1.21.3",
|
|
51
|
+
"@arcblock/jwt": "^1.21.3",
|
|
52
|
+
"@arcblock/ux": "^3.1.18",
|
|
53
|
+
"@arcblock/validator": "^1.21.3",
|
|
54
|
+
"@blocklet/did-space-js": "^1.1.16",
|
|
55
55
|
"@blocklet/error": "^0.2.5",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.48",
|
|
57
57
|
"@blocklet/logger": "^1.16.48",
|
|
58
|
-
"@blocklet/payment-react": "1.19.
|
|
58
|
+
"@blocklet/payment-react": "1.19.18",
|
|
59
59
|
"@blocklet/sdk": "^1.16.48",
|
|
60
|
-
"@blocklet/ui-react": "^3.1.
|
|
60
|
+
"@blocklet/ui-react": "^3.1.18",
|
|
61
61
|
"@blocklet/uploader": "^0.2.7",
|
|
62
|
-
"@blocklet/xss": "^0.2.
|
|
62
|
+
"@blocklet/xss": "^0.2.5",
|
|
63
63
|
"@mui/icons-material": "^7.1.2",
|
|
64
64
|
"@mui/lab": "7.0.0-beta.14",
|
|
65
65
|
"@mui/material": "^7.1.2",
|
|
66
66
|
"@mui/system": "^7.1.1",
|
|
67
|
-
"@ocap/asset": "^1.21.
|
|
68
|
-
"@ocap/client": "^1.21.
|
|
69
|
-
"@ocap/mcrypto": "^1.21.
|
|
70
|
-
"@ocap/util": "^1.21.
|
|
71
|
-
"@ocap/wallet": "^1.21.
|
|
67
|
+
"@ocap/asset": "^1.21.3",
|
|
68
|
+
"@ocap/client": "^1.21.3",
|
|
69
|
+
"@ocap/mcrypto": "^1.21.3",
|
|
70
|
+
"@ocap/util": "^1.21.3",
|
|
71
|
+
"@ocap/wallet": "^1.21.3",
|
|
72
72
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
73
73
|
"@stripe/stripe-js": "^2.4.0",
|
|
74
74
|
"ahooks": "^3.8.5",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@abtnode/types": "^1.16.48",
|
|
126
126
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
127
|
-
"@blocklet/payment-types": "1.19.
|
|
127
|
+
"@blocklet/payment-types": "1.19.18",
|
|
128
128
|
"@types/cookie-parser": "^1.4.9",
|
|
129
129
|
"@types/cors": "^2.8.19",
|
|
130
130
|
"@types/debug": "^4.1.12",
|
|
@@ -170,5 +170,5 @@
|
|
|
170
170
|
"parser": "typescript"
|
|
171
171
|
}
|
|
172
172
|
},
|
|
173
|
-
"gitHead": "
|
|
173
|
+
"gitHead": "b57baf21f22ae453247bc31444673aa01e35e6dc"
|
|
174
174
|
}
|
|
@@ -42,7 +42,10 @@ export default function SubscriptionItemList({ data, currency, mode = 'customer'
|
|
|
42
42
|
sm: 2,
|
|
43
43
|
},
|
|
44
44
|
}}>
|
|
45
|
-
<Box
|
|
45
|
+
<Box
|
|
46
|
+
sx={{
|
|
47
|
+
order: isMobile ? 2 : 1,
|
|
48
|
+
}}>
|
|
46
49
|
{item.price.product.images.length > 0 ? (
|
|
47
50
|
// @ts-ignore
|
|
48
51
|
<Avatar
|
|
@@ -58,7 +61,10 @@ export default function SubscriptionItemList({ data, currency, mode = 'customer'
|
|
|
58
61
|
</Avatar>
|
|
59
62
|
)}
|
|
60
63
|
</Box>
|
|
61
|
-
<Box
|
|
64
|
+
<Box
|
|
65
|
+
sx={{
|
|
66
|
+
order: isMobile ? 1 : 2,
|
|
67
|
+
}}>
|
|
62
68
|
{isAdmin ? (
|
|
63
69
|
<>
|
|
64
70
|
<Typography
|
|
@@ -169,7 +169,11 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
|
|
|
169
169
|
alignItems: 'center',
|
|
170
170
|
fontWeight: 500,
|
|
171
171
|
}}>
|
|
172
|
-
<Typography
|
|
172
|
+
<Typography
|
|
173
|
+
sx={{
|
|
174
|
+
fontWeight: 500,
|
|
175
|
+
fontSize: 14,
|
|
176
|
+
}}>
|
|
173
177
|
{t('admin.subscription.currentBalance')}
|
|
174
178
|
</Typography>
|
|
175
179
|
<Tooltip
|
|
@@ -455,7 +455,12 @@ export default function CustomerSubscriptionDetail() {
|
|
|
455
455
|
sx={{
|
|
456
456
|
alignItems: 'center',
|
|
457
457
|
}}>
|
|
458
|
-
<Typography
|
|
458
|
+
<Typography
|
|
459
|
+
component="span"
|
|
460
|
+
sx={{
|
|
461
|
+
fontSize: 14,
|
|
462
|
+
fontWeight: 500,
|
|
463
|
+
}}>
|
|
459
464
|
{t('customer.overdraftProtection.title')}
|
|
460
465
|
</Typography>
|
|
461
466
|
<MuiLink
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
|
-
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { BN, fromUnitToToken } from '@ocap/util';
|
|
4
|
-
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
5
|
-
import { translate } from '../../../locales';
|
|
6
|
-
import { CreditGrant, Customer, PaymentCurrency } from '../../../store/models';
|
|
7
|
-
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
8
|
-
import { formatNumber, getCustomerIndexUrl } from '../../util';
|
|
9
|
-
|
|
10
|
-
export interface CustomerCreditGrantLowBalanceEmailTemplateOptions {
|
|
11
|
-
creditGrantId: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface CustomerCreditGrantLowBalanceEmailTemplateContext {
|
|
15
|
-
locale: string;
|
|
16
|
-
userDid: string;
|
|
17
|
-
currencySymbol: string;
|
|
18
|
-
availableAmount: string;
|
|
19
|
-
totalGrantedAmount: string;
|
|
20
|
-
lowBalancePercentage: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class CustomerCreditGrantLowBalanceEmailTemplate
|
|
24
|
-
implements BaseEmailTemplate<CustomerCreditGrantLowBalanceEmailTemplateContext>
|
|
25
|
-
{
|
|
26
|
-
options: CustomerCreditGrantLowBalanceEmailTemplateOptions;
|
|
27
|
-
|
|
28
|
-
constructor(options: CustomerCreditGrantLowBalanceEmailTemplateOptions) {
|
|
29
|
-
this.options = options;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async getContext(): Promise<CustomerCreditGrantLowBalanceEmailTemplateContext> {
|
|
33
|
-
const creditGrant = await CreditGrant.findByPk(this.options.creditGrantId);
|
|
34
|
-
if (!creditGrant) {
|
|
35
|
-
throw new Error(`CreditGrant not found: ${this.options.creditGrantId}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const customer = await Customer.findByPk(creditGrant.customer_id);
|
|
39
|
-
if (!customer) {
|
|
40
|
-
throw new Error(`Customer not found: ${creditGrant.customer_id}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const paymentCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);
|
|
44
|
-
if (!paymentCurrency) {
|
|
45
|
-
throw new Error(`PaymentCurrency not found: ${creditGrant.currency_id}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const userDid = customer.did;
|
|
49
|
-
const locale = await getUserLocale(userDid);
|
|
50
|
-
const currencySymbol = paymentCurrency.symbol;
|
|
51
|
-
|
|
52
|
-
// 计算百分比
|
|
53
|
-
const available = new BN(creditGrant.remaining_amount);
|
|
54
|
-
const total = new BN(creditGrant.amount);
|
|
55
|
-
const percentage = total.gt(new BN(0)) ? available.mul(new BN(100)).div(total).toString() : '0';
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
locale,
|
|
59
|
-
userDid,
|
|
60
|
-
currencySymbol,
|
|
61
|
-
availableAmount: `${formatNumber(fromUnitToToken(available.toString(), paymentCurrency.decimal))} ${currencySymbol}`,
|
|
62
|
-
totalGrantedAmount: `${formatNumber(fromUnitToToken(total.toString(), paymentCurrency.decimal))} ${currencySymbol}`,
|
|
63
|
-
lowBalancePercentage: `${percentage}%`,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
68
|
-
const context = await this.getContext();
|
|
69
|
-
const { locale, userDid, availableAmount, totalGrantedAmount, lowBalancePercentage } = context;
|
|
70
|
-
|
|
71
|
-
// 构建字段
|
|
72
|
-
const fields = [
|
|
73
|
-
{
|
|
74
|
-
type: 'text',
|
|
75
|
-
data: {
|
|
76
|
-
type: 'plain',
|
|
77
|
-
color: '#9397A1',
|
|
78
|
-
text: translate('notification.common.account', locale),
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
type: 'text',
|
|
83
|
-
data: {
|
|
84
|
-
type: 'plain',
|
|
85
|
-
text: userDid,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
type: 'text',
|
|
90
|
-
data: {
|
|
91
|
-
type: 'plain',
|
|
92
|
-
color: '#9397A1',
|
|
93
|
-
text: translate('notification.creditInsufficient.availableCredit', locale),
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'text',
|
|
98
|
-
data: {
|
|
99
|
-
type: 'plain',
|
|
100
|
-
color: '#FF6600', // 橙色警告
|
|
101
|
-
text: `${availableAmount} (${lowBalancePercentage})`,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
type: 'text',
|
|
106
|
-
data: {
|
|
107
|
-
type: 'plain',
|
|
108
|
-
color: '#9397A1',
|
|
109
|
-
text: translate('notification.creditGrantLowBalance.totalGrantedCredit', locale),
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
type: 'text',
|
|
114
|
-
data: {
|
|
115
|
-
type: 'plain',
|
|
116
|
-
text: `${totalGrantedAmount}`,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
// 构建操作按钮
|
|
122
|
-
const actions = [
|
|
123
|
-
{
|
|
124
|
-
name: translate('notification.common.viewCreditGrant', locale),
|
|
125
|
-
title: translate('notification.common.viewCreditGrant', locale),
|
|
126
|
-
link: getCustomerIndexUrl({
|
|
127
|
-
locale,
|
|
128
|
-
userDid,
|
|
129
|
-
}),
|
|
130
|
-
},
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
const template: BaseEmailTemplateType = {
|
|
134
|
-
title: translate('notification.creditGrantLowBalance.title', locale),
|
|
135
|
-
body: translate('notification.creditGrantLowBalance.body', locale, {
|
|
136
|
-
availableAmount,
|
|
137
|
-
totalGrantedAmount,
|
|
138
|
-
}),
|
|
139
|
-
attachments: [
|
|
140
|
-
{
|
|
141
|
-
type: 'section',
|
|
142
|
-
fields,
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
// @ts-ignore
|
|
146
|
-
actions,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return template;
|
|
150
|
-
}
|
|
151
|
-
}
|