payment-kit 1.23.10 → 1.23.11

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.
@@ -1121,20 +1121,27 @@ export async function retryUncollectibleInvoices(options: {
1121
1121
  failed: [] as Array<{ id: string; reason: string }>,
1122
1122
  };
1123
1123
 
1124
+ const now = dayjs().unix();
1125
+ const BATCH_SIZE = 10;
1126
+ const BATCH_DELAY_SECONDS = 0.1;
1127
+
1124
1128
  const settledResults = await Promise.allSettled(
1125
1129
  overdueInvoices.map(async (invoice, index) => {
1126
1130
  const { paymentIntent } = invoice;
1127
- const delay = index * 2;
1128
1131
  if (!paymentIntent) {
1129
1132
  throw new Error('No payment intent found');
1130
1133
  }
1131
1134
 
1132
1135
  await paymentIntent.update({ status: 'requires_capture' });
1136
+
1137
+ const batchIndex = Math.floor(index / BATCH_SIZE);
1138
+ const runAt = now + batchIndex * BATCH_DELAY_SECONDS;
1139
+
1133
1140
  await emitAsync(
1134
1141
  'payment.queued',
1135
1142
  paymentIntent.id,
1136
1143
  { paymentIntentId: paymentIntent.id, retryOnError: true, ignoreMaxRetryCheck: true },
1137
- { sync: false, delay }
1144
+ { sync: false, runAt }
1138
1145
  );
1139
1146
 
1140
1147
  return invoice;
@@ -2,6 +2,7 @@ import { BN, fromUnitToToken } from '@ocap/util';
2
2
 
3
3
  import { getLock } from '../libs/lock';
4
4
  import logger from '../libs/logger';
5
+ import dayjs from '../libs/dayjs';
5
6
  import createQueue from '../libs/queue';
6
7
  import { createEvent } from '../libs/audit';
7
8
  import {
@@ -551,7 +552,7 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
551
552
  });
552
553
  await context.meterEvent.markAsCompleted();
553
554
  if (context.subscription && context.subscription.status === 'past_due') {
554
- handlePastDueSubscriptionRecovery(context.subscription, null);
555
+ await handlePastDueSubscriptionRecovery(context.subscription, null);
555
556
  }
556
557
  } else {
557
558
  logger.warn('Credit consumption partially completed - insufficient balance', {
@@ -622,6 +623,7 @@ export const creditQueue = createQueue<CreditConsumptionJob>({
622
623
  options: {
623
624
  concurrency: 1,
624
625
  maxRetries: 0,
626
+ enableScheduledJob: true,
625
627
  },
626
628
  });
627
629
 
@@ -755,58 +757,128 @@ events.on('customer.credit_grant.granted', async (creditGrant: CreditGrant) => {
755
757
  });
756
758
 
757
759
  async function retryFailedEventsForCustomer(creditGrant: CreditGrant): Promise<void> {
760
+ const grant = await CreditGrant.findByPk(creditGrant.id);
761
+ if (!grant) {
762
+ logger.error('Credit grant not found', {
763
+ creditGrantId: creditGrant.id,
764
+ });
765
+ return;
766
+ }
767
+
768
+ const customerId = grant.customer_id;
769
+ const currencyId = grant.currency_id;
770
+ const lock = getLock(`retry-failed-events-${customerId}-${currencyId}`);
771
+
758
772
  try {
773
+ await lock.acquire();
774
+
759
775
  logger.info('Retrying failed events for customer', {
760
- customerId: creditGrant.customer_id,
761
- currencyId: creditGrant.currency_id,
762
- livemode: creditGrant.livemode,
776
+ customerId,
777
+ currencyId,
778
+ livemode: grant.livemode,
779
+ grantAmount: grant.amount,
780
+ grantRemaining: grant.remaining_amount,
763
781
  });
782
+
783
+ const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId);
784
+ const totalAvailableCredit = availableGrants.reduce((sum, g) => sum.add(new BN(g.remaining_amount)), new BN(0));
785
+
764
786
  const [, , failedEvents] = await MeterEvent.getPendingAmounts({
765
- customerId: creditGrant.customer_id,
766
- currencyId: creditGrant.currency_id,
787
+ customerId,
788
+ currencyId,
767
789
  status: ['requires_action', 'requires_capture'],
768
- livemode: creditGrant.livemode,
790
+ livemode: grant.livemode,
769
791
  });
770
792
 
771
793
  if (failedEvents.length === 0) {
772
794
  logger.debug('No failed events with pending credit found', {
773
- customerId: creditGrant.customer_id,
774
- currencyId: creditGrant.currency_id,
795
+ customerId,
796
+ currencyId,
775
797
  });
776
798
  return;
777
799
  }
778
800
 
779
- logger.info('Updating failed events status after credit grant', {
780
- customerId: creditGrant.customer_id,
781
- currencyId: creditGrant.currency_id,
782
- eventCount: failedEvents.length,
801
+ if (totalAvailableCredit.lte(new BN(0))) {
802
+ logger.debug('No available credit to retry failed events', {
803
+ customerId,
804
+ currencyId,
805
+ totalAvailableCredit: totalAvailableCredit.toString(),
806
+ });
807
+ return;
808
+ }
809
+
810
+ const sortedEvents = failedEvents.sort((a, b) => {
811
+ const aPending = new BN(a.credit_pending);
812
+ const bPending = new BN(b.credit_pending);
813
+ return aPending.cmp(bPending);
783
814
  });
784
815
 
785
- await Promise.all(
786
- failedEvents.map((event) =>
787
- event.update({
788
- status: 'pending',
789
- attempt_count: 0,
790
- next_attempt: undefined,
791
- })
792
- )
793
- );
816
+ let cumulativePending = new BN(0);
817
+ const affordableEvents: MeterEvent[] = [];
818
+ const now = dayjs().unix();
819
+ const BATCH_SIZE = 10;
820
+ const BATCH_DELAY_SECONDS = 0.1;
794
821
 
795
- failedEvents.forEach((event, index) => {
796
- const delay = index * 1000;
797
- addCreditConsumptionJob(event.id, true, { delay });
798
- });
822
+ for (let i = 0; i < sortedEvents.length; i++) {
823
+ const event = sortedEvents[i];
824
+ if (!event) {
825
+ break;
826
+ }
799
827
 
800
- logger.info('Successfully updated failed events status', {
801
- customerId: creditGrant.customer_id,
802
- currencyId: creditGrant.currency_id,
803
- eventCount: failedEvents.length,
828
+ const eventPending = new BN(event.credit_pending);
829
+ const newCumulative = cumulativePending.add(eventPending);
830
+
831
+ if (newCumulative.gt(totalAvailableCredit)) {
832
+ if (eventPending.gt(totalAvailableCredit)) {
833
+ break;
834
+ }
835
+ }
836
+
837
+ // eslint-disable-next-line no-await-in-loop
838
+ await event.update({
839
+ status: 'pending',
840
+ attempt_count: 0,
841
+ next_attempt: undefined,
842
+ });
843
+
844
+ const batchIndex = Math.floor(i / BATCH_SIZE);
845
+ const runAt = now + batchIndex * BATCH_DELAY_SECONDS;
846
+
847
+ try {
848
+ // eslint-disable-next-line no-await-in-loop
849
+ await addCreditConsumptionJob(event.id, true, { runAt });
850
+ } catch (jobError: any) {
851
+ logger.error('Failed to add credit consumption job after status update', {
852
+ eventId: event.id,
853
+ customerId,
854
+ currencyId,
855
+ error: jobError.message,
856
+ });
857
+ }
858
+
859
+ affordableEvents.push(event);
860
+ cumulativePending = newCumulative;
861
+ }
862
+
863
+ const skippedEvents = failedEvents.length - affordableEvents.length;
864
+
865
+ logger.info('Completed retrying failed events', {
866
+ customerId,
867
+ currencyId,
868
+ totalEvents: failedEvents.length,
869
+ affordableEvents: affordableEvents.length,
870
+ skippedEvents,
871
+ totalAvailableCredit: totalAvailableCredit.toString(),
872
+ totalAffordablePending: cumulativePending.toString(),
873
+ grantCount: availableGrants.length,
804
874
  });
805
875
  } catch (error: any) {
806
- logger.error('Failed to update failed events for customer', {
807
- customerId: creditGrant.customer_id,
808
- currencyId: creditGrant.currency_id,
876
+ logger.error('Failed to retry failed events for customer', {
877
+ customerId: grant.customer_id,
878
+ currencyId: grant.currency_id,
809
879
  error: error.message,
810
880
  });
881
+ } finally {
882
+ lock.release();
811
883
  }
812
884
  }
@@ -327,7 +327,6 @@ router.post('/', auth, async (req, res) => {
327
327
  router.get('/pending-amount', authMine, async (req, res) => {
328
328
  try {
329
329
  const params: any = {
330
- status: ['requires_action', 'requires_capture'],
331
330
  livemode: !!req.livemode,
332
331
  };
333
332
  if (req.query.subscription_id) {
@@ -427,7 +427,7 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
427
427
  subscriptionId,
428
428
  livemode,
429
429
  currencyId,
430
- status = ['requires_action', 'requires_capture'],
430
+ status,
431
431
  customerId,
432
432
  }: {
433
433
  subscriptionId?: string;
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.23.10
17
+ version: 1.23.11
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.23.10",
3
+ "version": "1.23.11",
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.23.10",
63
- "@blocklet/payment-react": "1.23.10",
64
- "@blocklet/payment-vendor": "1.23.10",
62
+ "@blocklet/payment-broker-client": "1.23.11",
63
+ "@blocklet/payment-react": "1.23.11",
64
+ "@blocklet/payment-vendor": "1.23.11",
65
65
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
66
  "@blocklet/ui-react": "^3.3.10",
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.23.10",
134
+ "@blocklet/payment-types": "1.23.11",
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": "9d058650e81ae43ffafb3e1253b50b6245d510ac"
181
+ "gitHead": "72af80fc6e91e88058665212985002966f55787c"
182
182
  }