payment-kit 1.24.2 → 1.24.3

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.
@@ -102,6 +102,10 @@ import {
102
102
  CustomerAutoRechargeFailedEmailTemplate,
103
103
  CustomerAutoRechargeFailedEmailTemplateOptions,
104
104
  } from '../libs/notification/template/customer-auto-recharge-failed';
105
+ import {
106
+ CustomerAutoRechargeDailyLimitExceededEmailTemplate,
107
+ CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions,
108
+ } from '../libs/notification/template/customer-auto-recharge-daily-limit-exceeded';
105
109
  import {
106
110
  CustomerRevenueSucceededEmailTemplate,
107
111
  CustomerRevenueSucceededEmailTemplateOptions,
@@ -139,6 +143,7 @@ export type NotificationQueueJobType =
139
143
  | 'customer.credit_grant.granted'
140
144
  | 'customer.credit.low_balance'
141
145
  | 'customer.auto_recharge.failed'
146
+ | 'customer.auto_recharge.daily_limit_exceeded'
142
147
  | 'webhook.endpoint.failed';
143
148
 
144
149
  export type NotificationQueueJob = {
@@ -285,6 +290,12 @@ async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseE
285
290
  return new CustomerAutoRechargeFailedEmailTemplate(job.options as CustomerAutoRechargeFailedEmailTemplateOptions);
286
291
  }
287
292
 
293
+ if (job.type === 'customer.auto_recharge.daily_limit_exceeded') {
294
+ return new CustomerAutoRechargeDailyLimitExceededEmailTemplate(
295
+ job.options as CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions
296
+ );
297
+ }
298
+
288
299
  if (job.type === 'webhook.endpoint.failed') {
289
300
  return new WebhookEndpointFailedEmailTemplate(job.options as WebhookEndpointFailedEmailTemplateOptions);
290
301
  }
@@ -296,6 +307,18 @@ async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
296
307
  try {
297
308
  const template = await getNotificationTemplate(job);
298
309
 
310
+ // Check if template class has a preflightCheck
311
+ const TemplateClass = template.constructor as any;
312
+ if (TemplateClass.preflightCheck) {
313
+ const shouldSend = await TemplateClass.preflightCheck(job.options);
314
+ if (!shouldSend) {
315
+ logger.info('handleNotificationJob.skipped: preflight check returned false', {
316
+ type: job.type,
317
+ });
318
+ return;
319
+ }
320
+ }
321
+
299
322
  await new Notification(template, job.type).send();
300
323
  logger.info('handleImmediateNotificationJob.success', { job });
301
324
  } catch (error) {
@@ -329,6 +352,19 @@ setInterval(
329
352
  60 * 60 * 1000
330
353
  ); // 每小时清理一次
331
354
 
355
+ /**
356
+ * Clear notification cache entries that match given prefix
357
+ * Used to reset rate limiting when conditions change (e.g., credit recharged)
358
+ */
359
+ function clearNotificationCache(prefix: string) {
360
+ for (const [key] of notificationCache.entries()) {
361
+ if (key.startsWith(prefix)) {
362
+ notificationCache.delete(key);
363
+ logger.info('Notification cache cleared', { key });
364
+ }
365
+ }
366
+ }
367
+
332
368
  /**
333
369
  * Handles immediate notifications by pushing them directly to the notification queue
334
370
  */
@@ -624,7 +660,7 @@ export async function startNotificationQueue() {
624
660
  });
625
661
 
626
662
  events.on('customer.credit.low_balance', (customer: Customer, { metadata }: { metadata: any }) => {
627
- addNotificationJob(
663
+ addNotificationJobWithDelay(
628
664
  'customer.credit.low_balance',
629
665
  {
630
666
  customerId: customer.id,
@@ -678,6 +714,36 @@ export async function startNotificationQueue() {
678
714
  }
679
715
  }
680
716
  );
717
+
718
+ events.on(
719
+ 'customer.auto_recharge.daily_limit_exceeded',
720
+ (customer: Customer, { autoRechargeConfigId, currencyId, currentBalance }) => {
721
+ logger.info('addNotificationJob:customer.auto_recharge.daily_limit_exceeded', {
722
+ customerId: customer.id,
723
+ autoRechargeConfigId,
724
+ currencyId,
725
+ });
726
+ addNotificationJob(
727
+ 'customer.auto_recharge.daily_limit_exceeded',
728
+ {
729
+ customerId: customer.id,
730
+ autoRechargeConfigId,
731
+ currencyId,
732
+ currentBalance,
733
+ },
734
+ [customer.id, autoRechargeConfigId, currencyId],
735
+ true, // prevent duplicate
736
+ 24 * 3600 // 24 hours
737
+ );
738
+ }
739
+ );
740
+
741
+ // Clear credit notification cache when customer recharges
742
+ // This allows customer to receive insufficient/low_balance notifications again
743
+ events.on('customer.credit_grant.granted', (creditGrant: CreditGrant) => {
744
+ clearNotificationCache(`customer.credit.insufficient.${creditGrant.customer_id}.${creditGrant.currency_id}`);
745
+ clearNotificationCache(`customer.credit.low_balance.${creditGrant.customer_id}.${creditGrant.currency_id}`);
746
+ });
681
747
  }
682
748
 
683
749
  export async function handleNotificationPreferenceChange(
@@ -812,3 +878,55 @@ export function addNotificationJob(
812
878
  },
813
879
  });
814
880
  }
881
+
882
+ /**
883
+ * Add a notification job with delay support
884
+ * Use this when the template class has a static `delay` property
885
+ */
886
+ export async function addNotificationJobWithDelay(
887
+ type: NotificationQueueJobType,
888
+ options: NotificationQueueJobOptions,
889
+ extraIds: string[] = [],
890
+ preventDuplicate: boolean = false,
891
+ duplicateWindow: number = 600
892
+ ) {
893
+ const idParts = [type];
894
+
895
+ if (extraIds.length) {
896
+ idParts.push(...extraIds);
897
+ } else {
898
+ idParts.push(Date.now().toString());
899
+ }
900
+
901
+ if (preventDuplicate) {
902
+ const cacheKey = `${type}.${extraIds.join('.')}`;
903
+ const lastSent = notificationCache.get(cacheKey);
904
+ const now = Date.now();
905
+
906
+ if (lastSent && now - lastSent < duplicateWindow * 1000) {
907
+ logger.info('Notification skipped due to duplicate prevention', {
908
+ type,
909
+ cacheKey,
910
+ lastSent: new Date(lastSent),
911
+ duplicateWindow,
912
+ });
913
+ return Promise.resolve();
914
+ }
915
+
916
+ notificationCache.set(cacheKey, now);
917
+ }
918
+
919
+ // Get delay from template class
920
+ const template = await getNotificationTemplate({ type, options });
921
+ const TemplateClass = template.constructor as any;
922
+ const { delay } = TemplateClass;
923
+
924
+ return notificationQueue.push({
925
+ id: idParts.join('.'),
926
+ job: {
927
+ type,
928
+ options,
929
+ },
930
+ ...(delay && delay > 0 ? { delay } : {}),
931
+ });
932
+ }
@@ -15,6 +15,7 @@ import {
15
15
  Subscription,
16
16
  PaymentCurrency,
17
17
  PaymentMethod,
18
+ Invoice,
18
19
  TCreditTransactionExpanded,
19
20
  } from '../store/models';
20
21
 
@@ -118,7 +119,12 @@ router.get('/', authMine, async (req, res) => {
118
119
  { model: Meter, as: 'meter' },
119
120
  { model: Subscription, as: 'subscription', attributes: ['id', 'description', 'status'], required: false },
120
121
  { model: CreditGrant, as: 'creditGrant', attributes: ['id', 'name', 'currency_id'] },
121
- { model: MeterEvent, as: 'meterEvent', attributes: ['id', 'source_data'], required: false },
122
+ {
123
+ model: MeterEvent,
124
+ as: 'meterEvent',
125
+ attributes: ['id', 'source_data', 'timestamp', 'processed_at', 'created_at'],
126
+ required: false,
127
+ },
122
128
  ],
123
129
  });
124
130
  // Transform transactions
@@ -135,6 +141,9 @@ router.get('/', authMine, async (req, res) => {
135
141
  customer_id: query.customer_id,
136
142
  status: ['granted', 'depleted', 'expired'],
137
143
  };
144
+ if (query.subscription_id) {
145
+ grantWhere['metadata.subscription_id'] = query.subscription_id;
146
+ }
138
147
  if (query.start) {
139
148
  grantWhere.created_at = {
140
149
  [Op.gte]: new Date(query.start * 1000),
@@ -162,11 +171,80 @@ router.get('/', authMine, async (req, res) => {
162
171
  },
163
172
  ],
164
173
  });
165
- // Transform grants
166
- return rows.map((item: any) => ({
167
- ...item.toJSON(),
168
- activity_type: 'grant',
169
- }));
174
+
175
+ // Fetch invoice info for paid grants
176
+ const invoiceIds = rows
177
+ .map((item: any) => item.metadata?.invoice_id)
178
+ .filter((id: string | undefined): id is string => !!id);
179
+
180
+ // For first scheduled grants (schedule_seq === 1) without invoice_id, find invoice by subscription_id
181
+ const subscriptionIdsForFirstGrant = rows
182
+ .filter(
183
+ (item: any) =>
184
+ item.metadata?.schedule_seq === 1 && !item.metadata?.invoice_id && item.metadata?.subscription_id
185
+ )
186
+ .map((item: any) => item.metadata.subscription_id);
187
+
188
+ const [invoicesByIds, invoicesBySubscription] = await Promise.all([
189
+ invoiceIds.length > 0
190
+ ? Invoice.findAll({
191
+ where: { id: { [Op.in]: invoiceIds } },
192
+ attributes: ['id', 'total', 'amount_paid', 'currency_id', 'billing_reason', 'subscription_id'],
193
+ include: [
194
+ {
195
+ model: PaymentCurrency,
196
+ as: 'paymentCurrency',
197
+ attributes: ['id', 'symbol', 'decimal', 'type'],
198
+ },
199
+ ],
200
+ })
201
+ : [],
202
+ subscriptionIdsForFirstGrant.length > 0
203
+ ? Invoice.findAll({
204
+ where: {
205
+ subscription_id: { [Op.in]: subscriptionIdsForFirstGrant },
206
+ status: 'paid',
207
+ },
208
+ attributes: ['id', 'total', 'amount_paid', 'currency_id', 'billing_reason', 'subscription_id'],
209
+ include: [
210
+ {
211
+ model: PaymentCurrency,
212
+ as: 'paymentCurrency',
213
+ attributes: ['id', 'symbol', 'decimal', 'type'],
214
+ },
215
+ ],
216
+ order: [['created_at', 'ASC']],
217
+ })
218
+ : [],
219
+ ]);
220
+
221
+ const invoiceMap = new Map(invoicesByIds.map((inv: any) => [inv.id, inv]));
222
+ for (const inv of invoicesBySubscription) {
223
+ const subId = inv.subscription_id as string;
224
+ if (subId && !invoiceMap.has(subId)) {
225
+ invoiceMap.set(subId, inv);
226
+ }
227
+ }
228
+
229
+ // Transform grants with invoice info
230
+ return rows.map((item: any) => {
231
+ const grantData = item.toJSON();
232
+ const invoice = invoiceMap.get(grantData.metadata.invoice_id);
233
+ return {
234
+ ...grantData,
235
+ activity_type: 'grant',
236
+ invoice: invoice
237
+ ? {
238
+ total: invoice.total,
239
+ amount_paid: invoice.amount_paid,
240
+ currency_id: invoice.currency_id,
241
+ billing_reason: invoice.billing_reason,
242
+ subscription_id: invoice.subscription_id,
243
+ paymentCurrency: invoice.paymentCurrency,
244
+ }
245
+ : null,
246
+ };
247
+ });
170
248
  },
171
249
  meta: { type: 'database' },
172
250
  };
@@ -344,7 +422,7 @@ router.get('/:id', authPortal, async (req, res) => {
344
422
  {
345
423
  model: MeterEvent,
346
424
  as: 'meterEvent',
347
- attributes: ['id', 'source_data'],
425
+ attributes: ['id', 'source_data', 'timestamp', 'processed_at', 'created_at'],
348
426
  required: false,
349
427
  },
350
428
  ],
@@ -311,6 +311,18 @@ router.get('/', authMine, async (req, res) => {
311
311
  { model: PaymentCurrency, as: 'paymentCurrency' },
312
312
  { model: PaymentMethod, as: 'paymentMethod' },
313
313
  { model: Subscription, as: 'subscription', attributes: ['id', 'description'] },
314
+ {
315
+ model: InvoiceItem,
316
+ as: 'lines',
317
+ include: [
318
+ {
319
+ model: Price,
320
+ as: 'price',
321
+ attributes: ['id', 'metadata'],
322
+ include: [{ model: Product, as: 'product', attributes: ['id', 'type', 'name'] }],
323
+ },
324
+ ],
325
+ },
314
326
  { model: Customer, as: 'customer' },
315
327
  ],
316
328
  });
@@ -352,6 +352,175 @@ router.get('/pending-amount', authMine, async (req, res) => {
352
352
  return res.status(400).json({ error: err?.message });
353
353
  }
354
354
  });
355
+
356
+ // Get overdue summary by credit currency (action-required amounts grouped by currency)
357
+ router.get('/overdue-summary', auth, async (req, res) => {
358
+ try {
359
+ // Query to aggregate credit_pending by currency, showing overdue amounts per credit currency
360
+ const results = await MeterEvent.sequelize!.query<{
361
+ currency_id: string;
362
+ total_pending: string;
363
+ customer_count: number;
364
+ event_count: number;
365
+ }>(
366
+ `SELECT
367
+ m.currency_id,
368
+ SUM(CAST(me.credit_pending AS DECIMAL(40,0))) as total_pending,
369
+ COUNT(DISTINCT json_extract(me.payload, '$.customer_id')) as customer_count,
370
+ COUNT(*) as event_count
371
+ FROM meter_events me
372
+ JOIN meters m ON me.event_name = m.event_name
373
+ WHERE me.livemode = :livemode
374
+ AND me.status IN ('requires_capture', 'requires_action')
375
+ GROUP BY m.currency_id
376
+ HAVING total_pending > 0
377
+ ORDER BY total_pending DESC`,
378
+ {
379
+ replacements: { livemode: req.livemode ? 1 : 0 },
380
+ type: QueryTypes.SELECT,
381
+ }
382
+ );
383
+
384
+ // Fetch currency details
385
+ const currencyIds = results.map((r) => r.currency_id);
386
+ const currencies =
387
+ currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
388
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
389
+
390
+ const list = results.map((r) => ({
391
+ currency: currencyMap.get(r.currency_id),
392
+ total_pending: r.total_pending,
393
+ customer_count: r.customer_count,
394
+ event_count: r.event_count,
395
+ }));
396
+
397
+ return res.json({ list });
398
+ } catch (err) {
399
+ logger.error('Error getting overdue summary', err);
400
+ return res.status(400).json({ error: err?.message });
401
+ }
402
+ });
403
+
404
+ // Get all customers with credit overdue (action-required amount > 0), grouped by customer + currency
405
+ router.get('/overdue-customers', auth, async (req, res) => {
406
+ try {
407
+ const page = Math.max(1, parseInt(String(req.query.page || '1'), 10));
408
+ const pageSize = Math.min(100, Math.max(1, parseInt(String(req.query.pageSize || '20'), 10)));
409
+ const offset = (page - 1) * pageSize;
410
+ const currencyId = req.query.currency_id as string | undefined;
411
+ const searchQuery = req.query.q as string | undefined;
412
+
413
+ // If search query provided, first find matching customer IDs
414
+ let customerFilter = '';
415
+ const replacements: any = { livemode: req.livemode ? 1 : 0, limit: pageSize, offset };
416
+ const countReplacements: any = { livemode: req.livemode ? 1 : 0 };
417
+
418
+ // Build currency filter
419
+ let currencyFilter = '';
420
+ if (currencyId) {
421
+ currencyFilter = 'AND m.currency_id = :currencyId';
422
+ replacements.currencyId = currencyId;
423
+ countReplacements.currencyId = currencyId;
424
+ }
425
+
426
+ if (searchQuery) {
427
+ // Escape LIKE wildcards to prevent unintended matches
428
+ const escapedQuery = searchQuery.replace(/[%_]/g, '\\$&');
429
+ const searchCustomers = await Customer.findAll({
430
+ where: {
431
+ [Op.or]: [
432
+ { name: { [Op.like]: `%${escapedQuery}%` } },
433
+ { email: { [Op.like]: `%${escapedQuery}%` } },
434
+ { did: { [Op.like]: `%${escapedQuery}%` } },
435
+ ],
436
+ },
437
+ attributes: ['id'],
438
+ });
439
+ const searchCustomerIds = searchCustomers.map((c) => c.id);
440
+ if (searchCustomerIds.length === 0) {
441
+ return res.json({ list: [], count: 0, paging: { page, pageSize } });
442
+ }
443
+ // Generate parameterized placeholders for customer IDs
444
+ const placeholders = searchCustomerIds.map((_, i) => `:searchId${i}`);
445
+ customerFilter = `AND json_extract(me.payload, '$.customer_id') IN (${placeholders.join(',')})`;
446
+ searchCustomerIds.forEach((id, i) => {
447
+ replacements[`searchId${i}`] = id;
448
+ countReplacements[`searchId${i}`] = id;
449
+ });
450
+ }
451
+
452
+ // Query to aggregate credit_pending by customer + currency, order by event_count DESC
453
+ const results = await MeterEvent.sequelize!.query<{
454
+ customer_id: string;
455
+ currency_id: string;
456
+ total_pending: string;
457
+ event_count: number;
458
+ }>(
459
+ `SELECT
460
+ json_extract(me.payload, '$.customer_id') as customer_id,
461
+ m.currency_id as currency_id,
462
+ SUM(CAST(me.credit_pending AS DECIMAL(40,0))) as total_pending,
463
+ COUNT(*) as event_count
464
+ FROM meter_events me
465
+ JOIN meters m ON me.event_name = m.event_name
466
+ WHERE me.livemode = :livemode
467
+ AND me.status IN ('requires_capture', 'requires_action')
468
+ ${currencyFilter}
469
+ ${customerFilter}
470
+ GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
471
+ HAVING total_pending > 0
472
+ ORDER BY event_count DESC
473
+ LIMIT :limit OFFSET :offset`,
474
+ {
475
+ replacements,
476
+ type: QueryTypes.SELECT,
477
+ }
478
+ );
479
+
480
+ // Get total count
481
+ const countResult = await MeterEvent.sequelize!.query<{ count: number }>(
482
+ `SELECT COUNT(*) as count FROM (
483
+ SELECT json_extract(me.payload, '$.customer_id') as customer_id, m.currency_id
484
+ FROM meter_events me
485
+ JOIN meters m ON me.event_name = m.event_name
486
+ WHERE me.livemode = :livemode
487
+ AND me.status IN ('requires_capture', 'requires_action')
488
+ ${currencyFilter}
489
+ ${customerFilter}
490
+ GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
491
+ HAVING SUM(CAST(me.credit_pending AS DECIMAL(40,0))) > 0
492
+ )`,
493
+ {
494
+ replacements: countReplacements,
495
+ type: QueryTypes.SELECT,
496
+ }
497
+ );
498
+
499
+ const count = countResult[0]?.count || 0;
500
+ const customerIds = [...new Set(results.map((r) => r.customer_id))];
501
+ const currencyIds = [...new Set(results.map((r) => r.currency_id))];
502
+
503
+ // Fetch customer and currency details
504
+ const customers = customerIds.length > 0 ? await Customer.findAll({ where: { id: { [Op.in]: customerIds } } }) : [];
505
+ const customerMap = new Map(customers.map((c) => [c.id, c]));
506
+
507
+ const currencies =
508
+ currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
509
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
510
+
511
+ const list = results.map((r) => ({
512
+ customer: customerMap.get(r.customer_id),
513
+ currency: currencyMap.get(r.currency_id),
514
+ total_pending: r.total_pending,
515
+ event_count: r.event_count,
516
+ }));
517
+
518
+ return res.json({ list, count, paging: { page, pageSize } });
519
+ } catch (err) {
520
+ logger.error('Error getting overdue customers', err);
521
+ return res.status(400).json({ error: err?.message });
522
+ }
523
+ });
355
524
  router.get('/:id', authMine, async (req, res) => {
356
525
  try {
357
526
  logger.info('get meter event', { id: req.params.id });
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.24.2
17
+ version: 1.24.3
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.24.2",
3
+ "version": "1.24.3",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -59,9 +59,9 @@
59
59
  "@blocklet/error": "^0.3.5",
60
60
  "@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
61
61
  "@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
62
- "@blocklet/payment-broker-client": "1.24.2",
63
- "@blocklet/payment-react": "1.24.2",
64
- "@blocklet/payment-vendor": "1.24.2",
62
+ "@blocklet/payment-broker-client": "1.24.3",
63
+ "@blocklet/payment-react": "1.24.3",
64
+ "@blocklet/payment-vendor": "1.24.3",
65
65
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
66
  "@blocklet/ui-react": "^3.4.7",
67
67
  "@blocklet/uploader": "^0.3.19",
@@ -131,7 +131,7 @@
131
131
  "devDependencies": {
132
132
  "@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
133
133
  "@arcblock/eslint-config-ts": "^0.3.3",
134
- "@blocklet/payment-types": "1.24.2",
134
+ "@blocklet/payment-types": "1.24.3",
135
135
  "@types/cookie-parser": "^1.4.9",
136
136
  "@types/cors": "^2.8.19",
137
137
  "@types/debug": "^4.1.12",
@@ -178,5 +178,5 @@
178
178
  "parser": "typescript"
179
179
  }
180
180
  },
181
- "gitHead": "8cefbfb9066513c7fb33beb2185ea4e58968aad7"
181
+ "gitHead": "6f2a963875ed4aced1db11f1c3a044c7f5deedd6"
182
182
  }
@@ -259,7 +259,13 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
259
259
 
260
260
  const filteredCreditCurrencies = useMemo(() => {
261
261
  return creditCurrencies.filter((currency: TPaymentCurrency) => {
262
- return creditSummary?.grants?.[currency.id];
262
+ const grantData = creditSummary?.grants?.[currency.id];
263
+ if (!grantData) return false;
264
+ // Filter out credits with zero balance
265
+ if (grantData.remainingAmount === '0') {
266
+ return false;
267
+ }
268
+ return true;
263
269
  });
264
270
  }, [creditCurrencies, creditSummary?.grants]);
265
271
 
@@ -98,6 +98,7 @@ export default flat({
98
98
  creditGrant: 'Grant',
99
99
  date: 'Date',
100
100
  subscription: 'Subscription',
101
+ meter: 'Meter',
101
102
  meterEvent: 'Meter Event',
102
103
  creditAmount: 'Credit',
103
104
  createdAt: 'Created At',
@@ -181,6 +182,14 @@ export default flat({
181
182
  transaction: 'Transaction Metrics',
182
183
  essential: 'Essential Metrics',
183
184
  },
185
+ overdue: {
186
+ title: 'Pending Consumption',
187
+ subtitle: 'Unpaid credit consumption',
188
+ customers: '{count} customers',
189
+ events: '{count} pending records',
190
+ viewAll: 'View All',
191
+ noOverdue: 'No pending consumption',
192
+ },
184
193
  },
185
194
  payments: 'Payments',
186
195
  connections: 'Connections',
@@ -270,6 +279,13 @@ export default flat({
270
279
  meterEvents: {
271
280
  title: 'Meter Events',
272
281
  },
282
+ overdue: {
283
+ title: 'Pending Consumption',
284
+ pendingAmount: 'Pending Amount',
285
+ eventCount: 'Pending Records',
286
+ noOverdue: 'No pending consumption',
287
+ selectCurrency: 'Currency',
288
+ },
273
289
  meter: {
274
290
  add: 'Add meter',
275
291
  edit: 'Edit meter',
@@ -482,6 +498,7 @@ export default flat({
482
498
  },
483
499
  meterEvent: {
484
500
  title: 'Meter Event Details',
501
+ id: 'Event ID',
485
502
  totalEvents: 'Total events: {count}',
486
503
  noEvents: 'No events found',
487
504
  noEventsHint: 'You can manually add test events in test mode.',
@@ -492,6 +509,9 @@ export default flat({
492
509
  subscription: 'Subscription',
493
510
  creditConsumed: 'Credit Consumed',
494
511
  usageValue: 'Usage Value',
512
+ settlementAmount: 'Settlement Amount',
513
+ reportedAmount: 'Reported Amount',
514
+ overdueAmount: 'Overdue Amount',
495
515
  reportedAt: 'Reported At',
496
516
  processedAt: 'Processed At',
497
517
  eventIdentifier: 'Event Identifier',
@@ -97,6 +97,7 @@ export default flat({
97
97
  creditGrant: '信用额度',
98
98
  date: '日期',
99
99
  subscription: '订阅',
100
+ meter: '计量器',
100
101
  meterEvent: '计量事件',
101
102
  creditAmount: '额度',
102
103
  createdAt: '创建时间',
@@ -180,6 +181,14 @@ export default flat({
180
181
  transaction: '交易指标',
181
182
  essential: '关键指标',
182
183
  },
184
+ overdue: {
185
+ title: '欠费额度',
186
+ subtitle: '待支付的额度',
187
+ customers: '{count} 位用户',
188
+ events: '{count} 笔欠费记录',
189
+ viewAll: '查看全部',
190
+ noOverdue: '暂无欠费',
191
+ },
183
192
  },
184
193
  payments: '支付管理',
185
194
  connections: '连接',
@@ -267,6 +276,13 @@ export default flat({
267
276
  meterEvents: {
268
277
  title: '计量事件',
269
278
  },
279
+ overdue: {
280
+ title: '欠费额度',
281
+ pendingAmount: '欠费额度',
282
+ eventCount: '欠费笔数',
283
+ noOverdue: '暂无欠费',
284
+ selectCurrency: '币种',
285
+ },
270
286
  meter: {
271
287
  add: '添加计量器',
272
288
  edit: '编辑计量器',
@@ -479,6 +495,7 @@ export default flat({
479
495
  },
480
496
  meterEvent: {
481
497
  title: '计量事件详情',
498
+ id: '事件ID',
482
499
  totalEvents: '总事件数:{count}',
483
500
  noEvents: '暂无事件',
484
501
  noEventsHint: '您可以在测试模式下手动添加测试事件。',
@@ -489,6 +506,9 @@ export default flat({
489
506
  subscription: '订阅',
490
507
  creditConsumed: '消耗额度',
491
508
  usageValue: '使用量',
509
+ settlementAmount: '结算额度',
510
+ reportedAmount: '上报额度',
511
+ overdueAmount: '欠费额度',
492
512
  reportedAt: '上报时间',
493
513
  processedAt: '处理时间',
494
514
  eventIdentifier: '事件标识符',
@@ -16,6 +16,8 @@ const pages = {
16
16
  invoices: React.lazy(() => import('./invoices')),
17
17
  subscriptions: React.lazy(() => import('./subscriptions')),
18
18
  meters: React.lazy(() => import('./meters')),
19
+ 'meter-events': React.lazy(() => import('./meter-events')),
20
+ overdue: React.lazy(() => import('./overdue')),
19
21
  };
20
22
 
21
23
  export default function BillingIndex() {
@@ -52,6 +54,8 @@ export default function BillingIndex() {
52
54
  { label: t('admin.invoices'), value: 'invoices' },
53
55
  { label: t('admin.subscriptions'), value: 'subscriptions' },
54
56
  { label: t('admin.meters'), value: 'meters' },
57
+ { label: t('admin.meterEvents.title'), value: 'meter-events' },
58
+ { label: t('admin.overdue.title'), value: 'overdue' },
55
59
  ];
56
60
 
57
61
  let extra = null;