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.
@@ -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
+ }
@@ -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
- events.on('customer.credit_grant.low_balance', (data: CreditGrant, extraParams?: Record<string, any>) => {
110
- broadcast('customer.credit_grant.low_balance', data, extraParams);
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);
@@ -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
- creditGrantLowBalance: {
259
- title: 'Low Granted Credit Balance Warning',
260
- body: 'Your granted credit balance is below 10%. Current available credit is {availableAmount}. Please top up or contact support to avoid service interruption.',
261
- totalGrantedCredit: 'Total Granted Credit',
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
  });
@@ -247,10 +247,10 @@ export default flat({
247
247
  neverExpires: '永不过期',
248
248
  },
249
249
 
250
- creditGrantLowBalance: {
251
- title: '额度余额不足提醒',
252
- body: '您的额度已低于 10%,当前剩余额度为 {availableAmount}。请及时充值或联系管理员以避免服务受限。',
253
- totalGrantedCredit: '总授予额度',
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
- CustomerCreditGrantLowBalanceEmailTemplate,
98
- CustomerCreditGrantLowBalanceEmailTemplateOptions,
99
- } from '../libs/notification/template/customer-credit-grant-low-balance';
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.credit_grant.low_balance';
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.credit_grant.low_balance') {
270
- return new CustomerCreditGrantLowBalanceEmailTemplate(
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.credit_grant.low_balance', (creditGrant: CreditGrant) => {
602
+ events.on('customer.credit.low_balance', (customer: Customer, { metadata }: { metadata: any }) => {
604
603
  addNotificationJob(
605
- 'customer.credit_grant.low_balance',
604
+ 'customer.credit.low_balance',
606
605
  {
607
- creditGrantId: creditGrant.id,
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
- [creditGrant.id],
612
+ [customer.id, metadata.currency_id],
610
613
  true,
611
- 24 * 3600 // 1 天
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(req.body.payload.value, paymentCurrency.decimal).toString(),
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(req.body.payload.value, paymentCurrency.decimal).toString(),
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
- context: {
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.16
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.16",
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.2",
48
- "@arcblock/did-connect-react": "^3.1.5",
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.2",
51
- "@arcblock/jwt": "^1.21.2",
52
- "@arcblock/ux": "^3.1.5",
53
- "@arcblock/validator": "^1.21.2",
54
- "@blocklet/did-space-js": "^1.1.14",
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.16",
58
+ "@blocklet/payment-react": "1.19.18",
59
59
  "@blocklet/sdk": "^1.16.48",
60
- "@blocklet/ui-react": "^3.1.5",
60
+ "@blocklet/ui-react": "^3.1.18",
61
61
  "@blocklet/uploader": "^0.2.7",
62
- "@blocklet/xss": "^0.2.4",
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.2",
68
- "@ocap/client": "^1.21.2",
69
- "@ocap/mcrypto": "^1.21.2",
70
- "@ocap/util": "^1.21.2",
71
- "@ocap/wallet": "^1.21.2",
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.16",
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": "88c70aaf84cba8bcf0471dc7d6ee4c35ed2838a2"
173
+ "gitHead": "b57baf21f22ae453247bc31444673aa01e35e6dc"
174
174
  }
@@ -11,9 +11,11 @@ export default function Currency({ logo, name, sx = {}, size = 20 }: Props) {
11
11
  return (
12
12
  <Stack
13
13
  direction="row"
14
- alignItems="center"
15
14
  spacing={0.5}
16
15
  sx={[
16
+ {
17
+ alignItems: 'center',
18
+ },
17
19
  {
18
20
  alignItems: 'center',
19
21
  },
@@ -42,7 +42,10 @@ export default function SubscriptionItemList({ data, currency, mode = 'customer'
42
42
  sm: 2,
43
43
  },
44
44
  }}>
45
- <Box order={isMobile ? 2 : 1}>
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 order={isMobile ? 1 : 2}>
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 fontWeight={500} sx={{ fontSize: 14 }}>
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 component="span" fontSize={14} fontWeight={500}>
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
- }