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
package/api/src/index.ts CHANGED
@@ -43,6 +43,7 @@ import delegationHandlers from './routes/connect/delegation';
43
43
  import overdraftProtectionHandlers from './routes/connect/overdraft-protection';
44
44
  import rechargeAccountHandlers from './routes/connect/recharge-account';
45
45
  import reStakeHandlers from './routes/connect/re-stake';
46
+ import autoRechargeAuthorizationHandlers from './routes/connect/auto-recharge-auth';
46
47
  import { initialize } from './store/models';
47
48
  import { sequelize } from './store/sequelize';
48
49
  import { initUserHandler } from './integrations/blocklet/user';
@@ -84,6 +85,7 @@ handlers.attach(Object.assign({ app: router }, rechargeAccountHandlers));
84
85
  handlers.attach(Object.assign({ app: router }, delegationHandlers));
85
86
  handlers.attach(Object.assign({ app: router }, overdraftProtectionHandlers));
86
87
  handlers.attach(Object.assign({ app: router }, reStakeHandlers));
88
+ handlers.attach(Object.assign({ app: router }, autoRechargeAuthorizationHandlers));
87
89
  router.use('/api', routes);
88
90
 
89
91
  const isProduction = process.env.BLOCKLET_MODE === 'production';
@@ -99,7 +101,7 @@ if (isProduction) {
99
101
 
100
102
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
101
103
  app.use(<ErrorRequestHandler>((err, req, res, _next) => {
102
- logger.error(err);
104
+ logger.error('handle router error', err);
103
105
  if (err instanceof CustomError) {
104
106
  res.status(getStatusFromError(err)).json({ error: formatError(err) });
105
107
  return;
@@ -78,3 +78,14 @@ export function broadcastEvmTransaction(checkoutSessionId: string, status: strin
78
78
  });
79
79
  }
80
80
  }
81
+
82
+ export function broadcastAutoRechargeEvmTransaction(autoRechargeConfigId: string, status: string, claims: any[]) {
83
+ const claim = claims.find((x) => x.type === 'signature');
84
+ if (claim?.hash) {
85
+ broadcast('auto_recharge.evm_transaction', {
86
+ id: autoRechargeConfigId,
87
+ txHash: claim.hash,
88
+ status,
89
+ });
90
+ }
91
+ }
@@ -9,6 +9,7 @@ import { createEvent } from '../../../libs/audit';
9
9
  import { getLock } from '../../../libs/lock';
10
10
  import logger from '../../../libs/logger';
11
11
  import {
12
+ AutoRechargeConfig,
12
13
  CheckoutSession,
13
14
  Customer,
14
15
  Invoice,
@@ -110,6 +111,9 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
110
111
  await lock.acquire();
111
112
 
112
113
  const customer = await Customer.findByPk(subscription.customer_id);
114
+ if (!customer) {
115
+ throw new Error('Customer not found');
116
+ }
113
117
  const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
114
118
 
115
119
  let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
@@ -118,10 +122,10 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
118
122
  return invoice;
119
123
  }
120
124
 
125
+ const invoiceNumber = await customer.getInvoiceNumber();
121
126
  // @ts-ignore
122
127
  invoice = await Invoice.create({
123
- // @ts-ignore
124
- number: await customer.getInvoiceNumber(),
128
+ number: invoiceNumber,
125
129
  ...pick(stripeInvoice, [
126
130
  'amount_due',
127
131
  'amount_paid',
@@ -313,9 +317,12 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
313
317
 
314
318
  // in case we missed some of the events
315
319
  const subscriptionId = event.data.object.subscription_details?.metadata?.id;
316
- const appPid = event.data.object.subscription_details?.metadata?.appPid;
320
+ const subscriptionAppPid = event.data.object.subscription_details?.metadata?.appPid;
321
+ const autoRechargeConfigId = event.data.object.metadata?.recharge_id;
322
+
317
323
  if (!localInvoiceId) {
318
- if (subscriptionId && appPid && appPid === env.appPid) {
324
+ // Handle subscription invoices
325
+ if (subscriptionId && subscriptionAppPid && subscriptionAppPid === env.appPid) {
319
326
  logger.warn('try mirror invoice from stripe', { invoiceId: event.data.object.id });
320
327
  const subscription = await Subscription.findByPk(subscriptionId);
321
328
  if (subscription) {
@@ -332,7 +339,7 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
332
339
  logger.error('wait for stripe invoice mirror error', { id: event.id, type: event.type, error: err });
333
340
  }
334
341
 
335
- logger.warn('local invoice id not found in strip event', { id: event.id, type: event.type });
342
+ logger.warn('local invoice id not found in stripe event', { id: event.id, type: event.type });
336
343
  return;
337
344
  }
338
345
  }
@@ -390,12 +397,25 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
390
397
 
391
398
  const failedEvents = ['invoice.marked_uncollectible', 'invoice.finalization_failed', 'invoice.payment_failed'];
392
399
  if (failedEvents.includes(event.type)) {
393
- logger.info('Invoice failed', { invoiceId: invoice.id });
400
+ logger.info('invoice finalized and failed', { invoiceId: invoice.id });
401
+ // Handle subscription payment failures
394
402
  if (invoice.subscription_id) {
395
403
  const subscription = await Subscription.findByPk(invoice.subscription_id);
396
404
  if (subscription) {
397
405
  handleSubscriptionOnPaymentFailure(subscription, event.type, client);
398
406
  }
399
407
  }
408
+ if (autoRechargeConfigId && invoice) {
409
+ const autoRechargeConfig = await AutoRechargeConfig.findByPk(autoRechargeConfigId);
410
+ if (autoRechargeConfig) {
411
+ autoRechargeConfig.update({
412
+ enabled: false,
413
+ metadata: {
414
+ ...autoRechargeConfig.metadata,
415
+ failReason: `Payment failed: ${event.type}`,
416
+ },
417
+ });
418
+ }
419
+ }
400
420
  }
401
421
  }
@@ -1,8 +1,15 @@
1
1
  import type Stripe from 'stripe';
2
2
 
3
3
  import logger from '../../../libs/logger';
4
- import { CheckoutSession, Lock, SetupIntent, Subscription, TEventExpanded } from '../../../store/models';
5
- import { updateGroupSubscriptionsPaymentMethod } from '../resource';
4
+ import {
5
+ AutoRechargeConfig,
6
+ CheckoutSession,
7
+ Lock,
8
+ SetupIntent,
9
+ Subscription,
10
+ TEventExpanded,
11
+ } from '../../../store/models';
12
+ import { updateGroupSubscriptionsPaymentMethod, updateAutoRechargeConfigPaymentMethod } from '../resource';
6
13
  import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
7
14
 
8
15
  async function handleSubscriptionOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
@@ -119,6 +126,30 @@ async function handleCheckoutSessionOnSetupSucceeded(event: TEventExpanded, stri
119
126
  }
120
127
  }
121
128
 
129
+ async function handleAutoRechargeOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
130
+ const autoRechargeConfig = await AutoRechargeConfig.findOne({
131
+ where: { 'payment_details.stripe.setup_intent_id': stripeIntentId },
132
+ });
133
+ if (!autoRechargeConfig) {
134
+ logger.warn('local auto recharge config not found for setup intent', {
135
+ id: event.id,
136
+ type: event.type,
137
+ stripeIntentId,
138
+ });
139
+ return;
140
+ }
141
+
142
+ if (event.type === 'setup_intent.succeeded') {
143
+ const stripePaymentMethod = event.data.object.payment_method;
144
+ if (stripePaymentMethod) {
145
+ await updateAutoRechargeConfigPaymentMethod({
146
+ stripePaymentMethodId: stripePaymentMethod,
147
+ autoRechargeConfig,
148
+ });
149
+ }
150
+ }
151
+ }
152
+
122
153
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
123
154
  export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
124
155
  const stripeIntentId = event.data.object.id;
@@ -127,4 +158,5 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
127
158
  await handleSubscriptionOnSetupSucceeded(event, stripeIntentId);
128
159
  await handleSetupIntentOnSetupSucceeded(event, stripeIntentId);
129
160
  await handleCheckoutSessionOnSetupSucceeded(event, stripeIntentId);
161
+ await handleAutoRechargeOnSetupSucceeded(event, stripeIntentId);
130
162
  }
@@ -11,9 +11,11 @@ import { getPriceUintAmountByCurrency } from '../../libs/session';
11
11
  import { getSubscriptionItemPrice } from '../../libs/subscription';
12
12
  import { sleep } from '../../libs/util';
13
13
  import {
14
+ AutoRechargeConfig,
14
15
  CheckoutSession,
15
16
  Customer,
16
17
  Invoice,
18
+ InvoiceItem,
17
19
  PaymentCurrency,
18
20
  PaymentIntent,
19
21
  PaymentMethod,
@@ -208,7 +210,6 @@ export async function ensureStripePaymentIntent(
208
210
  let stripeIntent = null;
209
211
  if (internal.payment_details?.stripe?.payment_intent_id) {
210
212
  stripeIntent = await client.paymentIntents.retrieve(internal.payment_details.stripe.payment_intent_id);
211
- // FIXME: update?
212
213
  } else {
213
214
  const customer = await ensureStripePaymentCustomer(internal, method);
214
215
  stripeIntent = await client.paymentIntents.create({
@@ -570,6 +571,94 @@ export async function batchHandleStripePayments() {
570
571
  }
571
572
  }
572
573
 
574
+ export async function ensureStripeSetupIntentForAutoRecharge(
575
+ customer: Customer,
576
+ method: PaymentMethod,
577
+ autoRechargeConfig: AutoRechargeConfig,
578
+ forceReauthorize: boolean = false
579
+ ) {
580
+ const client = method.getStripeClient();
581
+ if (autoRechargeConfig.payment_details?.stripe?.setup_intent_id && !forceReauthorize) {
582
+ const setupIntent = await client.setupIntents.retrieve(autoRechargeConfig.payment_details.stripe.setup_intent_id);
583
+ return setupIntent;
584
+ }
585
+ const stripeCustomer = await ensureStripeCustomer(customer, method);
586
+
587
+ const setupIntent = await client.setupIntents.create({
588
+ customer: stripeCustomer.id,
589
+ payment_method_types: ['card'],
590
+ usage: 'off_session',
591
+ metadata: {
592
+ appPid: env.appPid,
593
+ auto_recharge_config_id: autoRechargeConfig.id,
594
+ customer_id: customer.id,
595
+ },
596
+ });
597
+
598
+ logger.info('stripe setup intent created for auto recharge', {
599
+ customerId: customer.id,
600
+ autoRechargeConfigId: autoRechargeConfig.id,
601
+ setupIntentId: setupIntent.id,
602
+ });
603
+
604
+ return setupIntent;
605
+ }
606
+
607
+ export async function updateAutoRechargeConfigPaymentMethod(params: {
608
+ stripePaymentMethodId: string;
609
+ autoRechargeConfig: AutoRechargeConfig;
610
+ }) {
611
+ const { stripePaymentMethodId, autoRechargeConfig } = params;
612
+
613
+ try {
614
+ const paymentMethod = await PaymentMethod.findByPk(autoRechargeConfig.payment_method_id);
615
+ if (!paymentMethod) {
616
+ throw new Error(`Payment method not found: ${autoRechargeConfig.payment_method_id}`);
617
+ }
618
+
619
+ const customer = await Customer.findByPk(autoRechargeConfig.customer_id);
620
+ if (!customer) {
621
+ throw new Error(`Customer not found: ${autoRechargeConfig.customer_id}`);
622
+ }
623
+
624
+ const stripeClient = paymentMethod.getStripeClient();
625
+
626
+ const stripePaymentMethod = await stripeClient.paymentMethods.retrieve(stripePaymentMethodId);
627
+
628
+ const paymentSettings = {
629
+ payment_method_types: ['stripe'],
630
+ payment_method_options: {
631
+ ...(autoRechargeConfig.payment_settings?.payment_method_options || {}),
632
+ stripe: {
633
+ payer: stripePaymentMethodId,
634
+ card_last4: stripePaymentMethod.card?.last4,
635
+ card_brand: stripePaymentMethod.card?.brand,
636
+ exp_time: `${stripePaymentMethod.card?.exp_month}/${stripePaymentMethod.card?.exp_year}`,
637
+ },
638
+ },
639
+ };
640
+
641
+ await autoRechargeConfig.update({
642
+ payment_settings: paymentSettings,
643
+ payment_details: {
644
+ ...autoRechargeConfig.payment_details,
645
+ stripe: {
646
+ ...(autoRechargeConfig.payment_details?.stripe || {}),
647
+ payment_method_id: stripePaymentMethodId,
648
+ },
649
+ },
650
+ });
651
+ return autoRechargeConfig;
652
+ } catch (error: any) {
653
+ logger.error('Failed to update auto recharge config payment method', {
654
+ error: error.message,
655
+ configId: autoRechargeConfig.id,
656
+ paymentMethod: stripePaymentMethodId,
657
+ });
658
+ throw error;
659
+ }
660
+ }
661
+
573
662
  export async function updateGroupSubscriptionsPaymentMethod(params: {
574
663
  stripePaymentMethodId: string;
575
664
  subscriptionIds: string[];
@@ -623,3 +712,98 @@ export async function updateGroupSubscriptionsPaymentMethod(params: {
623
712
  throw error;
624
713
  }
625
714
  }
715
+
716
+ export async function createStripeInvoiceForAutoRecharge(params: {
717
+ autoRechargeConfig: AutoRechargeConfig;
718
+ customer: Customer;
719
+ paymentMethod: PaymentMethod;
720
+ currency: PaymentCurrency;
721
+ invoice: Invoice;
722
+ }) {
723
+ const { autoRechargeConfig, customer, paymentMethod, currency, invoice } = params;
724
+ const client = paymentMethod.getStripeClient();
725
+
726
+ const stripePaymentMethodId = autoRechargeConfig.payment_settings?.payment_method_options?.stripe?.payer;
727
+ if (!stripePaymentMethodId) {
728
+ throw new Error('Stripe payment method not found');
729
+ }
730
+
731
+ try {
732
+ // Ensure stripe customer exists
733
+ const stripeCustomer = await ensureStripeCustomer(customer, paymentMethod);
734
+
735
+ const stripeInvoice = await client.invoices.create({
736
+ customer: stripeCustomer.id,
737
+ currency: currency.symbol.toLowerCase(),
738
+ description: invoice.description || 'Auto recharge',
739
+ collection_method: 'charge_automatically',
740
+ auto_advance: true,
741
+ default_payment_method: stripePaymentMethodId,
742
+ metadata: {
743
+ appPid: env.appPid,
744
+ recharge_id: autoRechargeConfig.id,
745
+ id: invoice.id,
746
+ },
747
+ });
748
+
749
+ logger.info('Stripe invoice created for auto recharge', {
750
+ stripeInvoiceId: stripeInvoice.id,
751
+ customerId: customer.id,
752
+ });
753
+
754
+ // Create invoice items from local invoice items
755
+ const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: invoice.id } });
756
+ for (const item of invoiceItems) {
757
+ const price = await Price.findByPk(item.price_id);
758
+ if (!price) {
759
+ continue;
760
+ }
761
+ const stripePrice = await ensureStripePrice(price as Price, paymentMethod, currency);
762
+ await client.invoiceItems.create({
763
+ customer: stripeCustomer.id,
764
+ invoice: stripeInvoice.id,
765
+ price: stripePrice.id,
766
+ quantity: item.quantity,
767
+ description: item.description || price.nickname || '',
768
+ metadata: {
769
+ appPid: env.appPid,
770
+ invoice_item_id: item.id,
771
+ },
772
+ });
773
+
774
+ logger.info('Stripe invoice item created', {
775
+ localItemId: item.id,
776
+ stripeInvoiceId: stripeInvoice.id,
777
+ priceId: price.id,
778
+ quantity: item.quantity,
779
+ });
780
+ }
781
+
782
+ // Finalize and pay the invoice automatically
783
+ const finalizedInvoice = await client.invoices.finalizeInvoice(stripeInvoice.id);
784
+ logger.info('Stripe invoice finalized', {
785
+ localInvoiceId: invoice.id,
786
+ stripeInvoiceId: stripeInvoice.id,
787
+ });
788
+
789
+ // Attempt automatic payment
790
+ if (finalizedInvoice.status === 'open') {
791
+ const paidInvoice = await client.invoices.pay(stripeInvoice.id);
792
+ logger.info('Stripe invoice payment attempted', {
793
+ localInvoiceId: invoice.id,
794
+ stripeInvoiceId: stripeInvoice.id,
795
+ status: paidInvoice.status,
796
+ });
797
+ return paidInvoice;
798
+ }
799
+
800
+ return finalizedInvoice;
801
+ } catch (error: any) {
802
+ logger.error('Failed to create Stripe invoice for auto recharge', {
803
+ localInvoiceId: invoice.id,
804
+ customerId: customer.id,
805
+ error,
806
+ });
807
+ throw error;
808
+ }
809
+ }
@@ -437,6 +437,7 @@ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
437
437
  ...extraProps
438
438
  } = props;
439
439
 
440
+ const invoiceNumber = await customer.getInvoiceNumber();
440
441
  // create invoice
441
442
  const invoice = await Invoice.create({
442
443
  amount_shipping: '0',
@@ -453,7 +454,7 @@ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
453
454
  collection_method: 'charge_automatically',
454
455
  ...extraProps,
455
456
  livemode,
456
- number: await customer.getInvoiceNumber(),
457
+ number: invoiceNumber,
457
458
  description,
458
459
  statement_descriptor: statementDescriptor,
459
460
  period_start: periodStart,
@@ -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
+ }
@@ -296,7 +296,7 @@ export function getFastCheckoutAmount(
296
296
 
297
297
  const { total, renew } = getCheckoutAmount(items, currencyId, trialing);
298
298
 
299
- if (mode === 'payment') {
299
+ if (mode === 'payment' || mode === 'auto-recharge-auth') {
300
300
  return total;
301
301
  }
302
302
 
@@ -984,6 +984,11 @@ export function isCreditMeteredLineItems(lineItems: TLineItemExpanded[]) {
984
984
  return lineItems.every((item) => item.price && isCreditMetered(item.price));
985
985
  }
986
986
 
987
+ export function validateStripePaymentAmounts(amount: string, currency: PaymentCurrency) {
988
+ const minAmountInUnits = fromTokenToUnit(0.5, currency.decimal);
989
+ return new BN(amount).gte(new BN(minAmountInUnits)); // 0.5 USD
990
+ }
991
+
987
992
  /**
988
993
  * Validates payment amounts meet minimum requirements
989
994
  * @param lineItems Line items to validate
@@ -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
  });