payment-kit 1.24.2 → 1.24.4

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.
@@ -1,4 +1,5 @@
1
1
  import { Op } from 'sequelize';
2
+ import debounce from 'lodash/debounce';
2
3
  /* eslint-disable @typescript-eslint/indent */
3
4
  import { events } from '../libs/event';
4
5
  import logger from '../libs/logger';
@@ -102,6 +103,10 @@ import {
102
103
  CustomerAutoRechargeFailedEmailTemplate,
103
104
  CustomerAutoRechargeFailedEmailTemplateOptions,
104
105
  } from '../libs/notification/template/customer-auto-recharge-failed';
106
+ import {
107
+ CustomerAutoRechargeDailyLimitExceededEmailTemplate,
108
+ CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions,
109
+ } from '../libs/notification/template/customer-auto-recharge-daily-limit-exceeded';
105
110
  import {
106
111
  CustomerRevenueSucceededEmailTemplate,
107
112
  CustomerRevenueSucceededEmailTemplateOptions,
@@ -139,6 +144,7 @@ export type NotificationQueueJobType =
139
144
  | 'customer.credit_grant.granted'
140
145
  | 'customer.credit.low_balance'
141
146
  | 'customer.auto_recharge.failed'
147
+ | 'customer.auto_recharge.daily_limit_exceeded'
142
148
  | 'webhook.endpoint.failed';
143
149
 
144
150
  export type NotificationQueueJob = {
@@ -285,6 +291,12 @@ async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseE
285
291
  return new CustomerAutoRechargeFailedEmailTemplate(job.options as CustomerAutoRechargeFailedEmailTemplateOptions);
286
292
  }
287
293
 
294
+ if (job.type === 'customer.auto_recharge.daily_limit_exceeded') {
295
+ return new CustomerAutoRechargeDailyLimitExceededEmailTemplate(
296
+ job.options as CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions
297
+ );
298
+ }
299
+
288
300
  if (job.type === 'webhook.endpoint.failed') {
289
301
  return new WebhookEndpointFailedEmailTemplate(job.options as WebhookEndpointFailedEmailTemplateOptions);
290
302
  }
@@ -296,6 +308,18 @@ async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
296
308
  try {
297
309
  const template = await getNotificationTemplate(job);
298
310
 
311
+ // Check if template class has a preflightCheck
312
+ const TemplateClass = template.constructor as any;
313
+ if (TemplateClass.preflightCheck) {
314
+ const shouldSend = await TemplateClass.preflightCheck(job.options);
315
+ if (!shouldSend) {
316
+ logger.info('handleNotificationJob.skipped: preflight check returned false', {
317
+ type: job.type,
318
+ });
319
+ return;
320
+ }
321
+ }
322
+
299
323
  await new Notification(template, job.type).send();
300
324
  logger.info('handleImmediateNotificationJob.success', { job });
301
325
  } catch (error) {
@@ -314,6 +338,8 @@ interface NotificationItem<T = Record<string, any>> {
314
338
 
315
339
  // 内存缓存,记录最近发送的通知
316
340
  const notificationCache = new Map<string, number>();
341
+ // 去抖函数(按 key 去重)
342
+ const debounceHandlers = new Map<string, ReturnType<typeof debounce>>();
317
343
 
318
344
  // 清理过期缓存的定时器
319
345
  setInterval(
@@ -329,6 +355,19 @@ setInterval(
329
355
  60 * 60 * 1000
330
356
  ); // 每小时清理一次
331
357
 
358
+ /**
359
+ * Clear notification cache entries that match given prefix
360
+ * Used to reset rate limiting when conditions change (e.g., credit recharged)
361
+ */
362
+ function clearNotificationCache(prefix: string) {
363
+ for (const [key] of notificationCache.entries()) {
364
+ if (key.startsWith(prefix)) {
365
+ notificationCache.delete(key);
366
+ logger.info('Notification cache cleared', { key });
367
+ }
368
+ }
369
+ }
370
+
332
371
  /**
333
372
  * Handles immediate notifications by pushing them directly to the notification queue
334
373
  */
@@ -624,7 +663,7 @@ export async function startNotificationQueue() {
624
663
  });
625
664
 
626
665
  events.on('customer.credit.low_balance', (customer: Customer, { metadata }: { metadata: any }) => {
627
- addNotificationJob(
666
+ addNotificationJobWithDelay(
628
667
  'customer.credit.low_balance',
629
668
  {
630
669
  customerId: customer.id,
@@ -678,6 +717,54 @@ export async function startNotificationQueue() {
678
717
  }
679
718
  }
680
719
  );
720
+
721
+ events.on(
722
+ 'customer.auto_recharge.daily_limit_exceeded',
723
+ (customer: Customer, { autoRechargeConfigId, currencyId, currentBalance }) => {
724
+ logger.info('addNotificationJob:customer.auto_recharge.daily_limit_exceeded', {
725
+ customerId: customer.id,
726
+ autoRechargeConfigId,
727
+ currencyId,
728
+ });
729
+ addNotificationJob(
730
+ 'customer.auto_recharge.daily_limit_exceeded',
731
+ {
732
+ customerId: customer.id,
733
+ autoRechargeConfigId,
734
+ currencyId,
735
+ currentBalance,
736
+ },
737
+ [customer.id, autoRechargeConfigId, currencyId],
738
+ true, // prevent duplicate
739
+ 24 * 3600 // 24 hours
740
+ );
741
+ }
742
+ );
743
+
744
+ // Clear credit notification cache when customer recharges
745
+ // This allows customer to receive insufficient/low_balance notifications again
746
+ events.on('customer.credit_grant.granted', (creditGrant: CreditGrant) => {
747
+ const cacheKeyBase = `${creditGrant.customer_id}.${creditGrant.currency_id}`;
748
+ const debounceKey = `credit_grant_clear.${cacheKeyBase}`;
749
+ const delayMs = 5 * 60 * 1000;
750
+
751
+ let handler = debounceHandlers.get(debounceKey);
752
+ if (!handler) {
753
+ handler = debounce(() => {
754
+ clearNotificationCache(`customer.credit.insufficient.${cacheKeyBase}`);
755
+ clearNotificationCache(`customer.credit.low_balance.${cacheKeyBase}`);
756
+ debounceHandlers.delete(debounceKey);
757
+ logger.info('Notification cache cleared after delay', {
758
+ customerId: creditGrant.customer_id,
759
+ currencyId: creditGrant.currency_id,
760
+ delayMs,
761
+ });
762
+ }, delayMs);
763
+ debounceHandlers.set(debounceKey, handler);
764
+ }
765
+
766
+ handler();
767
+ });
681
768
  }
682
769
 
683
770
  export async function handleNotificationPreferenceChange(
@@ -812,3 +899,55 @@ export function addNotificationJob(
812
899
  },
813
900
  });
814
901
  }
902
+
903
+ /**
904
+ * Add a notification job with delay support
905
+ * Use this when the template class has a static `delay` property
906
+ */
907
+ export async function addNotificationJobWithDelay(
908
+ type: NotificationQueueJobType,
909
+ options: NotificationQueueJobOptions,
910
+ extraIds: string[] = [],
911
+ preventDuplicate: boolean = false,
912
+ duplicateWindow: number = 600
913
+ ) {
914
+ const idParts = [type];
915
+
916
+ if (extraIds.length) {
917
+ idParts.push(...extraIds);
918
+ } else {
919
+ idParts.push(Date.now().toString());
920
+ }
921
+
922
+ if (preventDuplicate) {
923
+ const cacheKey = `${type}.${extraIds.join('.')}`;
924
+ const lastSent = notificationCache.get(cacheKey);
925
+ const now = Date.now();
926
+
927
+ if (lastSent && now - lastSent < duplicateWindow * 1000) {
928
+ logger.info('Notification skipped due to duplicate prevention', {
929
+ type,
930
+ cacheKey,
931
+ lastSent: new Date(lastSent),
932
+ duplicateWindow,
933
+ });
934
+ return Promise.resolve();
935
+ }
936
+
937
+ notificationCache.set(cacheKey, now);
938
+ }
939
+
940
+ // Get delay from template class
941
+ const template = await getNotificationTemplate({ type, options });
942
+ const TemplateClass = template.constructor as any;
943
+ const { delay } = TemplateClass;
944
+
945
+ return notificationQueue.push({
946
+ id: idParts.join('.'),
947
+ job: {
948
+ type,
949
+ options,
950
+ },
951
+ ...(delay && delay > 0 ? { delay } : {}),
952
+ });
953
+ }
@@ -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,178 @@ 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
+ AND m.status = 'active'
376
+ GROUP BY m.currency_id
377
+ HAVING total_pending > 0
378
+ ORDER BY total_pending DESC`,
379
+ {
380
+ replacements: { livemode: req.livemode ? 1 : 0 },
381
+ type: QueryTypes.SELECT,
382
+ }
383
+ );
384
+
385
+ // Fetch currency details
386
+ const currencyIds = results.map((r) => r.currency_id);
387
+ const currencies =
388
+ currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
389
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
390
+
391
+ const list = results.map((r) => ({
392
+ currency: currencyMap.get(r.currency_id),
393
+ total_pending: r.total_pending,
394
+ customer_count: r.customer_count,
395
+ event_count: r.event_count,
396
+ }));
397
+
398
+ return res.json({ list });
399
+ } catch (err) {
400
+ logger.error('Error getting overdue summary', err);
401
+ return res.status(400).json({ error: err?.message });
402
+ }
403
+ });
404
+
405
+ // Get all customers with credit overdue (action-required amount > 0), grouped by customer + currency
406
+ router.get('/overdue-customers', auth, async (req, res) => {
407
+ try {
408
+ const page = Math.max(1, parseInt(String(req.query.page || '1'), 10));
409
+ const pageSize = Math.min(100, Math.max(1, parseInt(String(req.query.pageSize || '20'), 10)));
410
+ const offset = (page - 1) * pageSize;
411
+ const currencyId = req.query.currency_id as string | undefined;
412
+ const searchQuery = req.query.q as string | undefined;
413
+
414
+ // If search query provided, first find matching customer IDs
415
+ let customerFilter = '';
416
+ const replacements: any = { livemode: req.livemode ? 1 : 0, limit: pageSize, offset };
417
+ const countReplacements: any = { livemode: req.livemode ? 1 : 0 };
418
+
419
+ // Build currency filter
420
+ let currencyFilter = '';
421
+ if (currencyId) {
422
+ currencyFilter = 'AND m.currency_id = :currencyId';
423
+ replacements.currencyId = currencyId;
424
+ countReplacements.currencyId = currencyId;
425
+ }
426
+
427
+ if (searchQuery) {
428
+ // Escape LIKE wildcards to prevent unintended matches
429
+ const escapedQuery = searchQuery.replace(/[%_]/g, '\\$&');
430
+ const searchCustomers = await Customer.findAll({
431
+ where: {
432
+ [Op.or]: [
433
+ { name: { [Op.like]: `%${escapedQuery}%` } },
434
+ { email: { [Op.like]: `%${escapedQuery}%` } },
435
+ { did: { [Op.like]: `%${escapedQuery}%` } },
436
+ ],
437
+ },
438
+ attributes: ['id'],
439
+ });
440
+ const searchCustomerIds = searchCustomers.map((c) => c.id);
441
+ if (searchCustomerIds.length === 0) {
442
+ return res.json({ list: [], count: 0, paging: { page, pageSize } });
443
+ }
444
+ // Generate parameterized placeholders for customer IDs
445
+ const placeholders = searchCustomerIds.map((_, i) => `:searchId${i}`);
446
+ customerFilter = `AND json_extract(me.payload, '$.customer_id') IN (${placeholders.join(',')})`;
447
+ searchCustomerIds.forEach((id, i) => {
448
+ replacements[`searchId${i}`] = id;
449
+ countReplacements[`searchId${i}`] = id;
450
+ });
451
+ }
452
+
453
+ // Query to aggregate credit_pending by customer + currency, order by event_count DESC
454
+ const results = await MeterEvent.sequelize!.query<{
455
+ customer_id: string;
456
+ currency_id: string;
457
+ total_pending: string;
458
+ event_count: number;
459
+ }>(
460
+ `SELECT
461
+ json_extract(me.payload, '$.customer_id') as customer_id,
462
+ m.currency_id as currency_id,
463
+ SUM(CAST(me.credit_pending AS DECIMAL(40,0))) as total_pending,
464
+ COUNT(*) as event_count
465
+ FROM meter_events me
466
+ JOIN meters m ON me.event_name = m.event_name
467
+ WHERE me.livemode = :livemode
468
+ AND me.status IN ('requires_capture', 'requires_action')
469
+ AND m.status = 'active'
470
+ ${currencyFilter}
471
+ ${customerFilter}
472
+ GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
473
+ HAVING total_pending > 0
474
+ ORDER BY event_count DESC
475
+ LIMIT :limit OFFSET :offset`,
476
+ {
477
+ replacements,
478
+ type: QueryTypes.SELECT,
479
+ }
480
+ );
481
+
482
+ // Get total count
483
+ const countResult = await MeterEvent.sequelize!.query<{ count: number }>(
484
+ `SELECT COUNT(*) as count FROM (
485
+ SELECT json_extract(me.payload, '$.customer_id') as customer_id, m.currency_id
486
+ FROM meter_events me
487
+ JOIN meters m ON me.event_name = m.event_name
488
+ WHERE me.livemode = :livemode
489
+ AND me.status IN ('requires_capture', 'requires_action')
490
+ AND m.status = 'active'
491
+ ${currencyFilter}
492
+ ${customerFilter}
493
+ GROUP BY json_extract(me.payload, '$.customer_id'), m.currency_id
494
+ HAVING SUM(CAST(me.credit_pending AS DECIMAL(40,0))) > 0
495
+ )`,
496
+ {
497
+ replacements: countReplacements,
498
+ type: QueryTypes.SELECT,
499
+ }
500
+ );
501
+
502
+ const count = countResult[0]?.count || 0;
503
+ const customerIds = [...new Set(results.map((r) => r.customer_id))];
504
+ const currencyIds = [...new Set(results.map((r) => r.currency_id))];
505
+
506
+ // Fetch customer and currency details
507
+ const customers = customerIds.length > 0 ? await Customer.findAll({ where: { id: { [Op.in]: customerIds } } }) : [];
508
+ const customerMap = new Map(customers.map((c) => [c.id, c]));
509
+
510
+ const currencies =
511
+ currencyIds.length > 0 ? await PaymentCurrency.findAll({ where: { id: { [Op.in]: currencyIds } } }) : [];
512
+ const currencyMap = new Map(currencies.map((c) => [c.id, c]));
513
+
514
+ const list = results.map((r) => ({
515
+ customer: customerMap.get(r.customer_id),
516
+ currency: currencyMap.get(r.currency_id),
517
+ total_pending: r.total_pending,
518
+ event_count: r.event_count,
519
+ }));
520
+
521
+ return res.json({ list, count, paging: { page, pageSize } });
522
+ } catch (err) {
523
+ logger.error('Error getting overdue customers', err);
524
+ return res.status(400).json({ error: err?.message });
525
+ }
526
+ });
355
527
  router.get('/:id', authMine, async (req, res) => {
356
528
  try {
357
529
  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.4
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.4",
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.4",
63
+ "@blocklet/payment-react": "1.24.4",
64
+ "@blocklet/payment-vendor": "1.24.4",
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.4",
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": "d4a5f67e657cafa8862912bb8de38a3d56a7919d"
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',