payment-kit 1.19.17 → 1.19.19

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.
Files changed (62) hide show
  1. package/api/src/index.ts +3 -1
  2. package/api/src/integrations/ethereum/tx.ts +11 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
  5. package/api/src/integrations/stripe/resource.ts +185 -1
  6. package/api/src/libs/invoice.ts +2 -1
  7. package/api/src/libs/notification/template/customer-credit-low-balance.ts +155 -0
  8. package/api/src/libs/session.ts +6 -1
  9. package/api/src/libs/ws.ts +3 -2
  10. package/api/src/locales/en.ts +6 -6
  11. package/api/src/locales/zh.ts +4 -4
  12. package/api/src/queues/auto-recharge.ts +343 -0
  13. package/api/src/queues/credit-consume.ts +51 -1
  14. package/api/src/queues/credit-grant.ts +15 -0
  15. package/api/src/queues/notification.ts +16 -13
  16. package/api/src/queues/payment.ts +14 -1
  17. package/api/src/queues/space.ts +1 -0
  18. package/api/src/routes/auto-recharge-configs.ts +454 -0
  19. package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
  20. package/api/src/routes/connect/recharge-account.ts +72 -10
  21. package/api/src/routes/connect/setup.ts +5 -3
  22. package/api/src/routes/connect/shared.ts +45 -4
  23. package/api/src/routes/customers.ts +10 -6
  24. package/api/src/routes/index.ts +2 -0
  25. package/api/src/routes/invoices.ts +10 -1
  26. package/api/src/routes/meter-events.ts +1 -1
  27. package/api/src/routes/meters.ts +1 -1
  28. package/api/src/routes/payment-currencies.ts +129 -0
  29. package/api/src/store/migrate.ts +20 -0
  30. package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
  31. package/api/src/store/models/auto-recharge-config.ts +225 -0
  32. package/api/src/store/models/credit-grant.ts +2 -11
  33. package/api/src/store/models/customer.ts +1 -0
  34. package/api/src/store/models/index.ts +3 -0
  35. package/api/src/store/models/invoice.ts +2 -1
  36. package/api/src/store/models/payment-currency.ts +10 -2
  37. package/api/src/store/models/types.ts +12 -1
  38. package/blocklet.yml +3 -3
  39. package/package.json +18 -18
  40. package/src/components/currency.tsx +3 -1
  41. package/src/components/customer/credit-overview.tsx +103 -18
  42. package/src/components/customer/overdraft-protection.tsx +5 -5
  43. package/src/components/info-metric.tsx +11 -2
  44. package/src/components/invoice/recharge.tsx +8 -2
  45. package/src/components/metadata/form.tsx +29 -27
  46. package/src/components/meter/form.tsx +1 -2
  47. package/src/components/price/form.tsx +39 -26
  48. package/src/components/product/form.tsx +1 -2
  49. package/src/components/subscription/items/index.tsx +8 -2
  50. package/src/components/subscription/metrics.tsx +5 -1
  51. package/src/locales/en.tsx +15 -0
  52. package/src/locales/zh.tsx +14 -0
  53. package/src/pages/admin/billing/meters/detail.tsx +18 -0
  54. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
  55. package/src/pages/admin/products/prices/actions.tsx +42 -2
  56. package/src/pages/admin/products/products/create.tsx +1 -2
  57. package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
  58. package/src/pages/customer/credit-grant/detail.tsx +9 -1
  59. package/src/pages/customer/recharge/account.tsx +14 -7
  60. package/src/pages/customer/recharge/subscription.tsx +4 -4
  61. package/src/pages/customer/subscription/detail.tsx +6 -1
  62. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +0 -151
@@ -0,0 +1,343 @@
1
+ import { BN } from '@ocap/util';
2
+
3
+ import { CustomError } from '@blocklet/error';
4
+ import { Op } from 'sequelize';
5
+ import createQueue from '../libs/queue';
6
+ import {
7
+ AutoRechargeConfig,
8
+ CreditGrant,
9
+ Customer,
10
+ Invoice,
11
+ PaymentCurrency,
12
+ PaymentMethod,
13
+ Price,
14
+ Product,
15
+ TPriceExpanded,
16
+ } from '../store/models';
17
+ import logger from '../libs/logger';
18
+ import { getPriceUintAmountByCurrency } from '../libs/session';
19
+ import { isDelegationSufficientForPayment } from '../libs/payment';
20
+ import { createStripeInvoiceForAutoRecharge } from '../integrations/stripe/resource';
21
+ import { ensureInvoiceAndItems } from '../libs/invoice';
22
+ import dayjs from '../libs/dayjs';
23
+ import { invoiceQueue } from './invoice';
24
+
25
+ export interface AutoRechargeJobData {
26
+ customer_id: string;
27
+ currency_id: string;
28
+ }
29
+
30
+ export interface AutoRechargeJobResult {
31
+ success: boolean;
32
+ invoice_id?: string;
33
+ error_message?: string;
34
+ recharge_amount?: string;
35
+ credit_amount?: string;
36
+ payment_method_type?: string;
37
+ }
38
+
39
+ export async function processAutoRecharge(job: AutoRechargeJobData) {
40
+ logger.info('Processing auto recharge job', { job });
41
+ const { customer_id: customerId, currency_id: currencyId } = job;
42
+
43
+ const customer = await Customer.findByPk(customerId);
44
+ if (!customer) {
45
+ logger.error('Customer not found', { customerId });
46
+ return;
47
+ }
48
+
49
+ const currency = await PaymentCurrency.findByPk(currencyId);
50
+ if (!currency) {
51
+ logger.error('Currency not found', { currencyId });
52
+ return;
53
+ }
54
+
55
+ // 1. find auto recharge config
56
+ const config = (await AutoRechargeConfig.findOne({
57
+ where: {
58
+ customer_id: customerId,
59
+ currency_id: currencyId,
60
+ },
61
+ include: [
62
+ { model: PaymentCurrency, as: 'rechargeCurrency', required: false },
63
+ { model: Price, as: 'price', include: [{ model: Product, as: 'product' }] },
64
+ { model: PaymentMethod, as: 'paymentMethod' },
65
+ ],
66
+ })) as AutoRechargeConfig & {
67
+ paymentMethod: PaymentMethod;
68
+ price: TPriceExpanded;
69
+ rechargeCurrency: PaymentCurrency;
70
+ };
71
+
72
+ if (!config || !config.enabled) {
73
+ logger.info('No auto recharge config found', { customerId, currencyId });
74
+ return;
75
+ }
76
+
77
+ if (!config.rechargeCurrency) {
78
+ logger.error('Recharge currency not found', { customerId, currencyId });
79
+ return;
80
+ }
81
+
82
+ const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId, [config.price_id]);
83
+
84
+ const totalAvailable = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0));
85
+
86
+ // 2. check if available balance is above threshold
87
+ if (new BN(totalAvailable).gt(new BN(config.threshold))) {
88
+ logger.info('Available balance is above threshold, skipping auto recharge', {
89
+ customerId,
90
+ currencyId,
91
+ totalAvailable: totalAvailable.toString(),
92
+ threshold: config.threshold,
93
+ });
94
+ return;
95
+ }
96
+
97
+ const priceAmount = await getPriceUintAmountByCurrency(config.price, config.rechargeCurrency.id);
98
+ if (!priceAmount) {
99
+ logger.error('Price amount is not valid', {
100
+ customerId,
101
+ currencyId,
102
+ });
103
+ return;
104
+ }
105
+ const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 0)).toString();
106
+
107
+ // 3. check daily limit
108
+ if (!config.canRechargeToday(totalAmount)) {
109
+ logger.info('Daily recharge limit exceeded, skipping auto recharge', {
110
+ customerId,
111
+ currencyId,
112
+ });
113
+ return;
114
+ }
115
+
116
+ // 4. execute auto recharge
117
+ await executeAutoRecharge(customer, config, currency);
118
+ }
119
+
120
+ async function createInvoiceForAutoRecharge({
121
+ customer,
122
+ config,
123
+ currency,
124
+ price,
125
+ totalAmount,
126
+ paymentMethod,
127
+ rechargeCurrency,
128
+ }: {
129
+ customer: Customer;
130
+ config: AutoRechargeConfig;
131
+ currency: PaymentCurrency;
132
+ price: TPriceExpanded;
133
+ totalAmount: BN;
134
+ paymentMethod: PaymentMethod;
135
+ rechargeCurrency: PaymentCurrency;
136
+ }) {
137
+ const now = dayjs().unix();
138
+ const expandedItems = await Price.expand([{ price_id: price.id, quantity: config.quantity }], {
139
+ product: true,
140
+ });
141
+ let status = 'open';
142
+ if (paymentMethod.type === 'stripe') {
143
+ status = 'draft'; // stripe invoice will be finalized and paid automatically
144
+ }
145
+ const existInvoice = await Invoice.findOne({
146
+ where: {
147
+ customer_id: customer.id,
148
+ currency_id: rechargeCurrency.id,
149
+ status: {
150
+ [Op.in]: ['open', 'draft'],
151
+ },
152
+ },
153
+ });
154
+ if (existInvoice) {
155
+ logger.info('Auto recharge invoice already exists, skipping', {
156
+ customerId: customer.id,
157
+ configId: config.id,
158
+ });
159
+ return existInvoice;
160
+ }
161
+ const { invoice } = await ensureInvoiceAndItems({
162
+ customer,
163
+ currency: rechargeCurrency,
164
+ trialing: false,
165
+ metered: false,
166
+ lineItems: expandedItems,
167
+ applyCredit: false,
168
+ props: {
169
+ status,
170
+ total: totalAmount.toString(),
171
+ livemode: rechargeCurrency.livemode,
172
+ description: `Auto Top-up for ${currency.name}`,
173
+ statement_descriptor: '',
174
+ period_start: now,
175
+ period_end: now,
176
+ auto_advance: true,
177
+ billing_reason: 'auto_recharge',
178
+ currency_id: rechargeCurrency.id,
179
+ default_payment_method_id: paymentMethod.id,
180
+ custom_fields: [],
181
+ footer: '',
182
+ payment_settings: config.payment_settings,
183
+ metadata: {
184
+ auto_recharge: {
185
+ config_id: config.id,
186
+ currency_id: rechargeCurrency.id,
187
+ },
188
+ },
189
+ } as unknown as Invoice,
190
+ });
191
+ logger.info('New invoice created for auto recharge', {
192
+ customerId: customer.id,
193
+ configId: config.id,
194
+ invoiceId: invoice.id,
195
+ amount: totalAmount,
196
+ });
197
+ if (status !== 'draft') {
198
+ invoiceQueue.push({
199
+ id: invoice.id,
200
+ job: { invoiceId: invoice.id, retryOnError: true },
201
+ });
202
+ logger.info('Invoice job scheduled for auto recharge', { invoice: invoice.id, customerId: customer.id });
203
+ }
204
+ return invoice;
205
+ }
206
+
207
+ async function executeAutoRecharge(
208
+ customer: Customer,
209
+ config: AutoRechargeConfig & {
210
+ paymentMethod: PaymentMethod;
211
+ price: TPriceExpanded;
212
+ rechargeCurrency: PaymentCurrency;
213
+ },
214
+ currency: PaymentCurrency
215
+ ) {
216
+ const paymentMethod = config.paymentMethod!;
217
+ const price = config.price! as TPriceExpanded;
218
+ const rechargeCurrency = config.rechargeCurrency!;
219
+
220
+ try {
221
+ const priceAmount = await getPriceUintAmountByCurrency(price, rechargeCurrency.id);
222
+ const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 0));
223
+ const paymentSettings = config.payment_settings;
224
+ if (!paymentSettings) {
225
+ throw new Error('No payment settings found for auto recharge');
226
+ }
227
+ const payer = (paymentSettings.payment_method_options as any)?.[paymentMethod.type]?.payer;
228
+ if (!payer) {
229
+ throw new Error('No payer found for auto recharge');
230
+ }
231
+ if (paymentMethod.type !== 'stripe') {
232
+ const delegationCheck = await isDelegationSufficientForPayment({
233
+ paymentMethod,
234
+ paymentCurrency: rechargeCurrency,
235
+ userDid: payer,
236
+ amount: totalAmount.toString(),
237
+ });
238
+ if (!delegationCheck.sufficient) {
239
+ throw new CustomError(delegationCheck.reason, 'insufficient delegation or balance');
240
+ }
241
+ }
242
+ const invoice = await createInvoiceForAutoRecharge({
243
+ customer,
244
+ config,
245
+ currency,
246
+ price,
247
+ totalAmount,
248
+ paymentMethod,
249
+ rechargeCurrency,
250
+ });
251
+ if (paymentMethod.type === 'stripe') {
252
+ await createStripeInvoiceForAutoRecharge({
253
+ autoRechargeConfig: config,
254
+ customer,
255
+ paymentMethod,
256
+ currency: rechargeCurrency,
257
+ invoice,
258
+ });
259
+ }
260
+ } catch (error: any) {
261
+ logger.error('Auto recharge execution failed', {
262
+ customerId: customer.id,
263
+ configId: config.id,
264
+ paymentMethodType: paymentMethod.type,
265
+ error: error.message,
266
+ });
267
+ throw error;
268
+ }
269
+ }
270
+
271
+ // 创建自动充值队列
272
+ export const autoRechargeQueue = createQueue<AutoRechargeJobData>({
273
+ name: 'auto-recharge',
274
+ onJob: processAutoRecharge,
275
+ options: {
276
+ concurrency: 5,
277
+ maxRetries: 3,
278
+ },
279
+ });
280
+
281
+ /**
282
+ * 检查并触发自动充值 (添加到队列)
283
+ */
284
+ export async function checkAndTriggerAutoRecharge(
285
+ customer: Customer,
286
+ currencyId: string,
287
+ currentBalance: string
288
+ ): Promise<void> {
289
+ try {
290
+ // 查找自动充值配置
291
+ const config = await AutoRechargeConfig.findOne({
292
+ where: {
293
+ customer_id: customer.id,
294
+ currency_id: currencyId,
295
+ enabled: true,
296
+ livemode: customer.livemode,
297
+ },
298
+ });
299
+
300
+ if (!config) {
301
+ logger.debug('No auto recharge config found', {
302
+ customerId: customer.id,
303
+ currencyId,
304
+ });
305
+ return;
306
+ }
307
+
308
+ // check if balance is above threshold
309
+ if (new BN(currentBalance).gte(new BN(config.threshold))) {
310
+ logger.debug('Balance above threshold, skipping auto recharge', {
311
+ customerId: customer.id,
312
+ currentBalance,
313
+ threshold: config.threshold,
314
+ });
315
+ return;
316
+ }
317
+
318
+ const jobData: AutoRechargeJobData = {
319
+ customer_id: customer.id,
320
+ currency_id: currencyId,
321
+ };
322
+
323
+ await autoRechargeQueue.push({
324
+ job: jobData,
325
+ id: `auto-recharge-${customer.id}-${currencyId}}`,
326
+ persist: true,
327
+ });
328
+
329
+ logger.info('Added auto recharge job to queue', {
330
+ customerId: customer.id,
331
+ currencyId,
332
+ currentBalance,
333
+ threshold: config.threshold,
334
+ });
335
+ } catch (error: any) {
336
+ logger.error('Failed to trigger auto recharge', {
337
+ customerId: customer.id,
338
+ currencyId,
339
+ currentBalance,
340
+ error: error.message,
341
+ });
342
+ }
343
+ }
@@ -19,6 +19,7 @@ import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
19
19
  import { getDaysUntilCancel, getDueUnit, getMeterPriceIdsFromSubscription } from '../libs/subscription';
20
20
  import { events } from '../libs/event';
21
21
  import { handlePastDueSubscriptionRecovery } from './payment';
22
+ import { checkAndTriggerAutoRecharge } from './auto-recharge';
22
23
 
23
24
  type CreditConsumptionJob = {
24
25
  meterEventId: string;
@@ -41,6 +42,38 @@ type CreditConsumptionResult = {
41
42
  fully_consumed: boolean;
42
43
  };
43
44
 
45
+ async function checkLowBalance(
46
+ customerId: string,
47
+ currencyId: string,
48
+ totalCreditAmount: string,
49
+ remainingBalance: string,
50
+ context: CreditConsumptionContext
51
+ ): Promise<void> {
52
+ try {
53
+ const totalCreditAmountBn = new BN(totalCreditAmount);
54
+ if (totalCreditAmountBn.lte(new BN(0))) return;
55
+ const remainingAmountBn = new BN(remainingBalance);
56
+ const threshold = totalCreditAmountBn.mul(new BN(10)).div(new BN(100));
57
+ if (remainingAmountBn.gt(new BN(0)) && remainingAmountBn.lte(threshold)) {
58
+ const percentage = remainingAmountBn.mul(new BN(100)).div(totalCreditAmountBn).toString();
59
+ await createEvent('Customer', 'customer.credit.low_balance', context.customer, {
60
+ metadata: {
61
+ currency_id: currencyId,
62
+ available_amount: remainingAmountBn.toString(),
63
+ total_amount: totalCreditAmountBn.toString(),
64
+ percentage,
65
+ subscription_id: context.subscription?.id,
66
+ },
67
+ }).catch(console.error);
68
+ }
69
+ } catch (error: any) {
70
+ logger.error('Failed to check low balance', {
71
+ customerId,
72
+ currencyId,
73
+ error: error.message,
74
+ });
75
+ }
76
+ }
44
77
  async function validateAndLoadData(meterEventId: string): Promise<CreditConsumptionContext | null> {
45
78
  const meterEvent = await MeterEvent.findByPk(meterEventId);
46
79
  if (!meterEvent) {
@@ -171,6 +204,8 @@ async function consumeAvailableCredits(
171
204
  // Get all available grants sorted by priority
172
205
  const availableGrants = await CreditGrant.getAvailableCreditsForCustomer(customerId, currencyId, context.priceIds);
173
206
 
207
+ const totalCreditAmountBN = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.amount)), new BN(0));
208
+
174
209
  // Calculate total available balance
175
210
  const totalAvailable = availableGrants.reduce((sum, grant) => sum.add(new BN(grant.remaining_amount)), new BN(0));
176
211
  logger.debug('Total available credits calculated', { totalAvailable: totalAvailable.toString() });
@@ -275,6 +310,8 @@ async function consumeAvailableCredits(
275
310
  }).catch(console.error);
276
311
  }
277
312
 
313
+ await checkLowBalance(customerId, currencyId, totalCreditAmountBN.toString(), remainingBalance, context);
314
+
278
315
  return {
279
316
  consumed: totalConsumed.toString(),
280
317
  pending: pendingAmount,
@@ -449,7 +486,20 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
449
486
 
450
487
  // Consume available credits (handles existing transactions internally)
451
488
  const consumptionResult = await consumeAvailableCredits(context, totalRequiredAmount);
452
-
489
+ // Check for auto recharge after successful consumption
490
+ try {
491
+ await checkAndTriggerAutoRecharge(
492
+ context.customer,
493
+ context.meter.currency_id!,
494
+ consumptionResult.available_balance
495
+ );
496
+ } catch (error: any) {
497
+ logger.warn('Auto recharge check failed after credit consumption', {
498
+ meterEventId,
499
+ customerId,
500
+ error: error.message,
501
+ });
502
+ }
453
503
  // Update MeterEvent with consumption details
454
504
  await context.meterEvent.update({
455
505
  credit_consumed: consumptionResult.consumed,
@@ -5,6 +5,7 @@ import { BN, fromTokenToUnit } from '@ocap/util';
5
5
  import dayjs from '../libs/dayjs';
6
6
  import createQueue from '../libs/queue';
7
7
  import {
8
+ AutoRechargeConfig,
8
9
  CreditGrant,
9
10
  Customer,
10
11
  Invoice,
@@ -285,6 +286,20 @@ async function handleInvoiceCredit(invoiceId: string) {
285
286
  return;
286
287
  }
287
288
 
289
+ // Handle auto recharge invoice - use standard processing since we now create proper InvoiceItems
290
+ if (invoice.metadata?.auto_recharge) {
291
+ logger.info('Processing auto recharge invoice with standard logic', {
292
+ invoiceId: invoice.id,
293
+ customerId: invoice.customer.id,
294
+ });
295
+ if (invoice.metadata?.auto_recharge?.config_id) {
296
+ const autoRechargeConfig = await AutoRechargeConfig.findByPk(invoice.metadata?.auto_recharge?.config_id);
297
+ if (autoRechargeConfig) {
298
+ await autoRechargeConfig.updateDailyStats(invoice.total);
299
+ }
300
+ }
301
+ }
302
+
288
303
  const lineItems = invoice.lines || [];
289
304
 
290
305
  if (!Array.isArray(lineItems) || lineItems.length === 0) {
@@ -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
 
@@ -36,7 +36,7 @@ import { SubscriptionItem } from '../store/models/subscription-item';
36
36
  import type { EVMChainType, PaymentError, PaymentSettings } from '../store/models/types';
37
37
  import { notificationQueue } from './notification';
38
38
  import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
39
- import { Lock, MeterEvent } from '../store/models';
39
+ import { AutoRechargeConfig, Lock, MeterEvent } from '../store/models';
40
40
  import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
41
41
  import createQueue from '../libs/queue';
42
42
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
@@ -663,6 +663,19 @@ export const handlePaymentFailed = async (
663
663
  attempt_count: invoice.attempt_count,
664
664
  });
665
665
 
666
+ if (invoice.billing_reason === 'auto_recharge' && invoice.metadata?.recharge_config?.recharge_id) {
667
+ const autoRechargeConfig = await AutoRechargeConfig.findByPk(invoice.metadata?.recharge_config?.recharge_id);
668
+ if (autoRechargeConfig && autoRechargeConfig.enabled) {
669
+ autoRechargeConfig.update({
670
+ enabled: false,
671
+ metadata: {
672
+ ...autoRechargeConfig.metadata,
673
+ failReason: 'Payment failed',
674
+ },
675
+ });
676
+ }
677
+ }
678
+
666
679
  return updates.terminate;
667
680
  }
668
681
 
@@ -58,6 +58,7 @@ const categoryMap = {
58
58
  stake_overdraft_protection: 'stake',
59
59
  recharge: 'recharge',
60
60
  return_stake: 'returnStake',
61
+ auto_recharge: 'recharge',
61
62
  } as const;
62
63
 
63
64
  const getBillingCategory = (billingReason: string): string => {