payment-kit 1.16.17 → 1.16.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 (70) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/hooks/pre-start.ts +2 -0
  3. package/api/src/index.ts +2 -0
  4. package/api/src/integrations/arcblock/stake.ts +7 -1
  5. package/api/src/integrations/stripe/resource.ts +1 -1
  6. package/api/src/libs/env.ts +12 -0
  7. package/api/src/libs/event.ts +8 -0
  8. package/api/src/libs/invoice.ts +585 -3
  9. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
  10. package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
  12. package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
  13. package/api/src/libs/overdraft-protection.ts +86 -0
  14. package/api/src/libs/payment.ts +1 -65
  15. package/api/src/libs/queue/index.ts +0 -1
  16. package/api/src/libs/subscription.ts +532 -2
  17. package/api/src/libs/util.ts +4 -0
  18. package/api/src/locales/en.ts +5 -0
  19. package/api/src/locales/zh.ts +5 -0
  20. package/api/src/queues/event.ts +3 -2
  21. package/api/src/queues/invoice.ts +28 -3
  22. package/api/src/queues/notification.ts +25 -3
  23. package/api/src/queues/payment.ts +154 -3
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +215 -4
  26. package/api/src/queues/webhook.ts +1 -0
  27. package/api/src/routes/connect/change-payment.ts +1 -1
  28. package/api/src/routes/connect/change-plan.ts +1 -1
  29. package/api/src/routes/connect/overdraft-protection.ts +120 -0
  30. package/api/src/routes/connect/recharge.ts +2 -1
  31. package/api/src/routes/connect/setup.ts +1 -1
  32. package/api/src/routes/connect/shared.ts +117 -350
  33. package/api/src/routes/connect/subscribe.ts +1 -1
  34. package/api/src/routes/customers.ts +2 -2
  35. package/api/src/routes/invoices.ts +9 -4
  36. package/api/src/routes/subscriptions.ts +172 -2
  37. package/api/src/store/migrate.ts +9 -10
  38. package/api/src/store/migrations/20240905-index.ts +95 -60
  39. package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
  40. package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
  41. package/api/src/store/models/customer.ts +2 -2
  42. package/api/src/store/models/invoice.ts +7 -0
  43. package/api/src/store/models/lock.ts +7 -0
  44. package/api/src/store/models/subscription.ts +15 -0
  45. package/api/src/store/sequelize.ts +6 -1
  46. package/blocklet.yml +1 -1
  47. package/package.json +23 -23
  48. package/src/components/customer/overdraft-protection.tsx +367 -0
  49. package/src/components/event/list.tsx +3 -4
  50. package/src/components/product/edit-price.tsx +2 -2
  51. package/src/components/subscription/actions/cancel.tsx +3 -0
  52. package/src/components/subscription/portal/actions.tsx +324 -77
  53. package/src/components/uploader.tsx +31 -26
  54. package/src/env.d.ts +1 -0
  55. package/src/hooks/subscription.ts +30 -0
  56. package/src/libs/env.ts +4 -0
  57. package/src/locales/en.tsx +41 -0
  58. package/src/locales/zh.tsx +37 -0
  59. package/src/pages/admin/billing/invoices/detail.tsx +16 -15
  60. package/src/pages/admin/index.tsx +3 -1
  61. package/src/pages/admin/products/prices/detail.tsx +1 -1
  62. package/src/pages/admin/products/products/detail.tsx +6 -2
  63. package/src/pages/customer/index.tsx +7 -2
  64. package/src/pages/customer/invoice/detail.tsx +29 -5
  65. package/src/pages/customer/invoice/past-due.tsx +18 -4
  66. package/src/pages/customer/recharge.tsx +2 -4
  67. package/src/pages/customer/subscription/change-payment.tsx +7 -1
  68. package/src/pages/customer/subscription/detail.tsx +69 -51
  69. package/tsconfig.json +0 -5
  70. package/api/tests/libs/payment.spec.ts +0 -168
@@ -2,7 +2,7 @@ import { component } from '@blocklet/sdk';
2
2
  import type { LiteralUnion } from 'type-fest';
3
3
  import { withQuery } from 'ufo';
4
4
 
5
- import { fromUnitToToken } from '@ocap/util';
5
+ import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { Op } from 'sequelize';
7
7
  import { cloneDeep, pick } from 'lodash';
8
8
  import {
@@ -10,18 +10,32 @@ import {
10
10
  Invoice,
11
11
  InvoiceItem,
12
12
  PaymentCurrency,
13
+ PaymentIntent,
13
14
  PaymentMethod,
15
+ PaymentSettings,
14
16
  Price,
15
17
  Product,
16
18
  Refund,
17
19
  SetupIntent,
20
+ SimpleCustomField,
18
21
  Subscription,
19
22
  SubscriptionItem,
23
+ TInvoice,
24
+ TLineItemExpanded,
20
25
  UsageRecord,
21
26
  } from '../store/models';
22
27
  import { getConnectQueryParam } from './util';
23
- import { expandLineItems } from './session';
24
- import { getSubscriptionCycleAmount, getSubscriptionCycleSetup, getSubscriptionStakeAmountSetup } from './subscription';
28
+ import { expandLineItems, getPriceUintAmountByCurrency } from './session';
29
+ import dayjs from './dayjs';
30
+ import {
31
+ getSubscriptionCycleAmount,
32
+ getSubscriptionCycleSetup,
33
+ getSubscriptionItemPrice,
34
+ getSubscriptionStakeAmountSetup,
35
+ isSubscriptionOverdraftProtectionEnabled,
36
+ } from './subscription';
37
+ import logger from './logger';
38
+ import { ensureOverdraftProtectionPrice } from './overdraft-protection';
25
39
 
26
40
  export function getCustomerInvoicePageUrl({
27
41
  invoiceId,
@@ -345,3 +359,571 @@ export async function getStakingInvoices(subscription: Subscription): Promise<In
345
359
  );
346
360
  return invoices;
347
361
  }
362
+
363
+ type BaseInvoiceProps = {
364
+ customer: Customer;
365
+ subscription?: Subscription;
366
+ currency_id: string;
367
+ livemode: boolean;
368
+ period_start: number;
369
+ period_end: number;
370
+ status?: string;
371
+ billing_reason: string;
372
+ description: string;
373
+ statement_descriptor?: string;
374
+ total: string;
375
+ amount_due?: string;
376
+ amount_paid?: string;
377
+ amount_remaining?: string;
378
+ default_payment_method_id: string;
379
+ payment_intent_id?: string;
380
+ checkout_session_id?: string;
381
+ metadata?: Record<string, any>;
382
+ items?: Array<{
383
+ price_id: string;
384
+ amount: string;
385
+ quantity: number;
386
+ description: string;
387
+ period?: {
388
+ start: number;
389
+ end: number;
390
+ };
391
+ metadata?: Record<string, any>;
392
+ subscription_item_id?: string;
393
+ }>;
394
+ auto_advance?: boolean;
395
+ paid?: boolean;
396
+ paid_out_of_band?: boolean;
397
+ payment_settings?: PaymentSettings;
398
+ footer?: string;
399
+ custom_fields?: SimpleCustomField[];
400
+ starting_token_balance?: Record<string, string>;
401
+ ending_token_balance?: Record<string, string>;
402
+ subtotal_excluding_tax?: string;
403
+ collection_method?: 'charge_automatically' | 'send_invoice';
404
+ };
405
+
406
+ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
407
+ invoice: Invoice;
408
+ items: InvoiceItem[];
409
+ }> {
410
+ const {
411
+ customer,
412
+ subscription,
413
+ currency_id: currencyId,
414
+ livemode,
415
+ period_start: periodStart,
416
+ period_end: periodEnd,
417
+ status = 'open',
418
+ billing_reason: billingReason,
419
+ description,
420
+ statement_descriptor: statementDescriptor = '',
421
+ total,
422
+ amount_due: amountDue = total,
423
+ amount_paid: amountPaid = '0',
424
+ amount_remaining: amountRemaining = total,
425
+ default_payment_method_id: paymentMethodId,
426
+ payment_intent_id: paymentIntentId = '',
427
+ checkout_session_id: checkoutSessionId = '',
428
+ items: itemsData,
429
+ metadata = {},
430
+ paid = false,
431
+ auto_advance: autoAdvance = true,
432
+ paid_out_of_band: paidOutOfBand = false,
433
+ payment_settings: paymentSettings,
434
+ ...extraProps
435
+ } = props;
436
+
437
+ // create invoice
438
+ const invoice = await Invoice.create({
439
+ amount_shipping: '0',
440
+ starting_balance: '0',
441
+ ending_balance: '0',
442
+ subtotal_excluding_tax: '0',
443
+ starting_token_balance: {},
444
+ ending_token_balance: {},
445
+ attempt_count: 0,
446
+ attempted: false,
447
+ tax: '0',
448
+ discounts: [],
449
+ total_discount_amounts: [],
450
+ collection_method: 'charge_automatically',
451
+ ...extraProps,
452
+ livemode,
453
+ number: await customer.getInvoiceNumber(),
454
+ description,
455
+ statement_descriptor: statementDescriptor,
456
+ period_start: periodStart,
457
+ period_end: periodEnd,
458
+ auto_advance: autoAdvance,
459
+ paid,
460
+ paid_out_of_band: paidOutOfBand,
461
+ status,
462
+ billing_reason: billingReason,
463
+ currency_id: currencyId,
464
+ customer_id: customer.id,
465
+ default_payment_method_id: paymentMethodId,
466
+ payment_intent_id: paymentIntentId,
467
+ subscription_id: subscription?.id,
468
+ checkout_session_id: checkoutSessionId,
469
+ total,
470
+ subtotal: total,
471
+ amount_due: amountDue,
472
+ amount_paid: amountPaid,
473
+ amount_remaining: amountRemaining,
474
+
475
+ customer_address: customer.address,
476
+ customer_email: customer.email,
477
+ customer_name: customer.name,
478
+ customer_phone: customer.phone,
479
+
480
+ effective_at: dayjs().unix(),
481
+ status_transitions: {
482
+ finalized_at: dayjs().unix(),
483
+ },
484
+ payment_settings: paymentSettings || subscription?.payment_settings,
485
+ metadata,
486
+ });
487
+
488
+ if (!itemsData) {
489
+ return { invoice, items: [] };
490
+ }
491
+ // create invoice items
492
+ const items = await Promise.all(
493
+ itemsData.map((item) =>
494
+ InvoiceItem.create({
495
+ livemode,
496
+ amount: item.amount,
497
+ quantity: item.quantity,
498
+ description: item.description,
499
+ period: item.period,
500
+ currency_id: currencyId,
501
+ customer_id: customer.id,
502
+ price_id: item.price_id,
503
+ invoice_id: invoice.id,
504
+ subscription_id: subscription?.id,
505
+ subscription_item_id: item.subscription_item_id,
506
+ discountable: false,
507
+ discounts: [],
508
+ discount_amounts: [],
509
+ proration: false,
510
+ proration_details: {},
511
+ metadata: item.metadata || {},
512
+ })
513
+ )
514
+ );
515
+
516
+ return { invoice, items };
517
+ }
518
+
519
+ export async function ensureInvoiceAndItems({
520
+ customer,
521
+ currency,
522
+ subscription,
523
+ props,
524
+ lineItems,
525
+ trialing,
526
+ metered,
527
+ applyCredit = true,
528
+ }: {
529
+ customer: Customer;
530
+ currency: PaymentCurrency;
531
+ subscription?: Subscription;
532
+ props: TInvoice;
533
+ lineItems: TLineItemExpanded[];
534
+ trialing: boolean; // do we have trialing
535
+ metered: boolean; // is the quantity metered
536
+ applyCredit?: boolean; // should we apply customer credit?
537
+ }): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
538
+ // apply possible balance to invoice
539
+ let remaining = props.total;
540
+ let result = { starting: {}, ending: {} };
541
+ if (applyCredit && props.total > '0') {
542
+ const balance = customer.getBalanceToApply(currency.id, props.total);
543
+ result = await customer.decreaseTokenBalance(currency.id, balance);
544
+ remaining = new BN(props.total).sub(new BN(balance)).toString();
545
+ logger.info('Invoice will use customer credit', { result, remaining, total: props.total });
546
+ }
547
+
548
+ // get subscription items
549
+ const subscriptionItems = subscription
550
+ ? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
551
+ : [];
552
+
553
+ function getLineSetup(x: TLineItemExpanded) {
554
+ const price = getSubscriptionItemPrice(x);
555
+ if (price.type === 'recurring' && trialing) {
556
+ return {
557
+ price,
558
+ amount: '0',
559
+ // @ts-ignore
560
+ description: trialing ? `${price.product.name} (trialing)` : price.product.name,
561
+ period: {
562
+ start: props.period_start,
563
+ end: props.period_end,
564
+ },
565
+ };
566
+ }
567
+
568
+ return {
569
+ price,
570
+ amount:
571
+ x.custom_amount ||
572
+ new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
573
+ description: price.product.name,
574
+ period: undefined,
575
+ };
576
+ }
577
+ // process line items
578
+ const processedItems = lineItems.map((x: TLineItemExpanded) => {
579
+ const setup = getLineSetup(x);
580
+ const { price } = setup;
581
+ let { quantity } = x;
582
+ if (price.type === 'recurring') {
583
+ if (price.recurring?.usage_type === 'metered' && !metered) {
584
+ quantity = 0;
585
+ }
586
+ if (trialing) {
587
+ quantity = 0;
588
+ }
589
+ }
590
+
591
+ return {
592
+ price_id: price.id,
593
+ amount: quantity > 0 ? setup.amount : '0',
594
+ quantity,
595
+ description: setup.description,
596
+ period: setup.period,
597
+ metadata: x.metadata || {},
598
+ subscription_item_id: subscriptionItems.find((si) => si.price_id === price.id)?.id,
599
+ };
600
+ });
601
+
602
+ return createInvoiceWithItems({
603
+ customer,
604
+ subscription,
605
+ items: processedItems,
606
+ livemode: props.livemode,
607
+ status: props.status || 'open',
608
+ billing_reason: props.billing_reason,
609
+ description: props.description || '',
610
+ statement_descriptor: props.statement_descriptor,
611
+
612
+ currency_id: props.currency_id,
613
+ payment_intent_id: props.payment_intent_id,
614
+ checkout_session_id: props.checkout_session_id,
615
+ default_payment_method_id: props.default_payment_method_id,
616
+
617
+ period_start: props.period_start,
618
+ period_end: props.period_end,
619
+ auto_advance: props.auto_advance,
620
+
621
+ total: props.total,
622
+ amount_remaining: props.amount_remaining || remaining,
623
+ amount_paid: props.amount_paid,
624
+ amount_due: props.amount_due || remaining,
625
+ subtotal_excluding_tax: props.total || '0',
626
+ starting_token_balance: result.starting,
627
+ ending_token_balance: result.ending,
628
+
629
+ footer: props.footer,
630
+ custom_fields: props.custom_fields,
631
+ payment_settings: props.payment_settings || subscription?.payment_settings,
632
+ metadata: {
633
+ ...props.metadata,
634
+ starting_token_balance: result.starting,
635
+ ending_token_balance: result.ending,
636
+ },
637
+ });
638
+ }
639
+
640
+ export async function cleanupInvoiceAndItems(invoiceId: string) {
641
+ const invoice = await Invoice.findByPk(invoiceId);
642
+ if (!invoice) {
643
+ return;
644
+ }
645
+ if (invoice.isImmutable()) {
646
+ return;
647
+ }
648
+
649
+ const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
650
+ const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
651
+ logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
652
+ }
653
+
654
+ export async function ensureRechargeInvoice(
655
+ invoiceProps: {
656
+ total: string;
657
+ description?: string;
658
+ checkout_session_id?: string;
659
+ currency_id: string;
660
+ metadata?: any;
661
+ payment_settings?: any;
662
+ },
663
+ subscription: Subscription,
664
+ paymentMethod: PaymentMethod,
665
+ customer: Customer
666
+ ) {
667
+ try {
668
+ const { invoice } = await createInvoiceWithItems({
669
+ customer,
670
+ subscription,
671
+ currency_id: invoiceProps.currency_id,
672
+ livemode: subscription.livemode,
673
+ period_start: dayjs().unix(),
674
+ period_end: dayjs().unix(),
675
+ status: 'paid',
676
+ billing_reason: 'recharge',
677
+ description: invoiceProps?.description || 'Add funds for subscription',
678
+ total: invoiceProps.total || '0',
679
+ amount_due: '0',
680
+ amount_paid: invoiceProps.total || '0',
681
+ amount_remaining: '0',
682
+ default_payment_method_id: paymentMethod.id,
683
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
684
+ metadata: invoiceProps.metadata || {},
685
+ auto_advance: false,
686
+ paid: true,
687
+ paid_out_of_band: false,
688
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
689
+ });
690
+
691
+ logger.info('create recharge invoice success', {
692
+ invoice,
693
+ subscriptionId: subscription?.id,
694
+ paymentMethod: paymentMethod.id,
695
+ });
696
+ } catch (error) {
697
+ logger.error('ensureRechargeInvoice: create invoice failed', {
698
+ error,
699
+ subscriptionId: subscription?.id,
700
+ paymentMethod: paymentMethod.id,
701
+ });
702
+ throw error;
703
+ }
704
+ }
705
+
706
+ export async function ensureOverdraftProtectionInvoiceAndItems({
707
+ customer,
708
+ subscription,
709
+ paymentIntent,
710
+ props,
711
+ }: {
712
+ customer: Customer;
713
+ subscription: Subscription;
714
+ paymentIntent: PaymentIntent;
715
+ props: Partial<TInvoice> & { period_start: number; period_end: number };
716
+ }): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
717
+ const { price, product } = await ensureOverdraftProtectionPrice(subscription.livemode);
718
+ const invoicePrice = price.currency_options.find((x: any) => x.currency_id === paymentIntent?.currency_id);
719
+
720
+ if (!subscription.overdraft_protection?.enabled) {
721
+ throw new Error('create overdraft protection invoice skipped due to overdraft protection not enabled');
722
+ }
723
+
724
+ if (!invoicePrice) {
725
+ throw new Error('overdraft protection invoice price not found');
726
+ }
727
+ const currency = await PaymentCurrency.findByPk(invoicePrice.currency_id);
728
+ if (!currency) {
729
+ throw new Error('overdraft protection invoice currency not found');
730
+ }
731
+ const paymentMethod = await PaymentMethod.findByPk(currency?.payment_method_id);
732
+ if (!paymentMethod) {
733
+ throw new Error('overdraft protection invoice payment method not found');
734
+ }
735
+
736
+ if (paymentMethod.type !== 'arcblock') {
737
+ throw new Error(`Payment method ${paymentMethod.type} not supported for overdraft protection`);
738
+ }
739
+
740
+ const { unused } = await isSubscriptionOverdraftProtectionEnabled(subscription, paymentIntent?.currency_id);
741
+ if (new BN(unused).lt(new BN(invoicePrice.unit_amount))) {
742
+ throw new Error('create overdraft protection invoice skipped due to insufficient overdraft protection');
743
+ }
744
+
745
+ const result = await createInvoiceWithItems({
746
+ customer,
747
+ subscription,
748
+ currency_id: invoicePrice.currency_id,
749
+ livemode: subscription.livemode,
750
+ period_start: props.period_start || 0,
751
+ period_end: props.period_end || 0,
752
+ status: props.status,
753
+ billing_reason: 'overdraft_protection',
754
+ description: 'Overdraft protection',
755
+ collection_method: 'send_invoice',
756
+ default_payment_method_id: paymentMethod.id,
757
+ total: invoicePrice.unit_amount || '0',
758
+ items: [
759
+ {
760
+ price_id: price.id,
761
+ amount: invoicePrice.unit_amount || '0',
762
+ quantity: 1,
763
+ description: product?.name || '',
764
+ period: {
765
+ start: props.period_start || 0,
766
+ end: props.period_end || 0,
767
+ },
768
+ },
769
+ ],
770
+ metadata: {
771
+ ...(props.metadata || {}),
772
+ payment_intent_id: paymentIntent.id,
773
+ },
774
+ });
775
+
776
+ await Price.increment({ quantity_sold: 1 }, { where: { id: price.id } });
777
+ return result;
778
+ }
779
+
780
+ export async function ensureStakeInvoice(
781
+ invoiceProps: {
782
+ total: string;
783
+ description?: string;
784
+ checkout_session_id?: string;
785
+ currency_id: string;
786
+ billing_reason?: string;
787
+ metadata?: any;
788
+ payment_settings?: any;
789
+ },
790
+ subscription: Subscription,
791
+ paymentMethod: PaymentMethod,
792
+ customer: Customer
793
+ ) {
794
+ if (paymentMethod.type !== 'arcblock') {
795
+ return;
796
+ }
797
+
798
+ try {
799
+ const { invoice } = await createInvoiceWithItems({
800
+ customer,
801
+ subscription,
802
+ currency_id: invoiceProps.currency_id,
803
+ livemode: subscription.livemode,
804
+ period_start: dayjs().unix(),
805
+ period_end: subscription.current_period_end,
806
+ status: 'paid',
807
+ billing_reason: invoiceProps?.billing_reason || 'stake',
808
+ description: invoiceProps?.description || 'Stake for subscription',
809
+ total: invoiceProps.total || '0',
810
+ amount_due: '0',
811
+ amount_paid: invoiceProps.total || '0',
812
+ amount_remaining: '0',
813
+ default_payment_method_id: paymentMethod.id,
814
+ checkout_session_id: invoiceProps?.checkout_session_id || '',
815
+ metadata: invoiceProps.metadata || {},
816
+ auto_advance: false,
817
+ paid: true,
818
+ paid_out_of_band: false,
819
+ payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
820
+ });
821
+
822
+ logger.info('create staking invoice success', {
823
+ invoice,
824
+ subscriptionId: subscription?.id,
825
+ paymentMethod: paymentMethod.id,
826
+ });
827
+ } catch (error) {
828
+ logger.error('ensureStake: create invoice failed', {
829
+ error,
830
+ subscriptionId: subscription?.id,
831
+ paymentMethod: paymentMethod.id,
832
+ });
833
+ throw error;
834
+ }
835
+ }
836
+
837
+ // mark overdraft protection invoice as void after payment
838
+ export async function handleOverdraftProtectionInvoiceAfterPayment(invoice: Invoice) {
839
+ try {
840
+ if (['stake', 'overdraft_protection', 'stake_overdraft_protection'].includes(invoice.billing_reason)) {
841
+ return;
842
+ }
843
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
844
+ if (!subscription) {
845
+ logger.info('handle overdraft protection invoice skipped due to subscription not found', {
846
+ invoiceId: invoice.id,
847
+ });
848
+ return;
849
+ }
850
+ if (!subscription.overdraft_protection?.enabled) {
851
+ logger.info('handle overdraft protection invoice skipped due to overdraft protection not enabled', {
852
+ subscriptionId: subscription.id,
853
+ });
854
+ return;
855
+ }
856
+ const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
857
+ if (!paymentMethod || paymentMethod?.type !== 'arcblock') {
858
+ logger.info('handle overdraft protection invoice skipped due to payment method not supported', {
859
+ invoiceId: invoice.id,
860
+ paymentMethodId: paymentMethod?.id,
861
+ });
862
+ return;
863
+ }
864
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
865
+ if (!paymentIntent) {
866
+ logger.info('handle overdraft protection invoice skipped due to payment intent not found', {
867
+ invoiceId: invoice.id,
868
+ });
869
+ return;
870
+ }
871
+ if (paymentIntent.status !== 'succeeded') {
872
+ logger.info('handle overdraft protection invoice skipped due to payment intent not succeeded', {
873
+ invoiceId: invoice.id,
874
+ });
875
+ return;
876
+ }
877
+ if (paymentIntent.payment_details?.arcblock?.type !== 'transfer') {
878
+ logger.info('handle overdraft protection invoice skipped due to payment type not transfer', {
879
+ invoiceId: invoice.id,
880
+ paymentIntentId: paymentIntent.id,
881
+ });
882
+ return;
883
+ }
884
+ const overdraftProtectionInvoice = await Invoice.findOne({
885
+ where: {
886
+ subscription_id: subscription.id,
887
+ billing_reason: 'overdraft_protection',
888
+ 'metadata.invoice_id': invoice.id,
889
+ status: {
890
+ [Op.notIn]: ['paid', 'void'],
891
+ },
892
+ },
893
+ });
894
+ if (!overdraftProtectionInvoice) {
895
+ logger.info('handle overdraft protection invoice skipped due to overdraft protection invoice not found', {
896
+ invoiceId: invoice.id,
897
+ });
898
+ return;
899
+ }
900
+
901
+ if (overdraftProtectionInvoice.payment_intent_id) {
902
+ const overdraftProtectionPaymentIntent = await PaymentIntent.findOne({
903
+ where: {
904
+ id: overdraftProtectionInvoice.payment_intent_id,
905
+ },
906
+ });
907
+ if (overdraftProtectionPaymentIntent) {
908
+ await overdraftProtectionPaymentIntent.update({
909
+ status: 'canceled',
910
+ canceled_at: dayjs().unix(),
911
+ cancellation_reason: 'void_invoice',
912
+ });
913
+ }
914
+ }
915
+
916
+ await overdraftProtectionInvoice.update({
917
+ status: 'void',
918
+ status_transitions: {
919
+ ...(overdraftProtectionInvoice.status_transitions || {}),
920
+ voided_at: dayjs().unix(),
921
+ },
922
+ });
923
+ } catch (error) {
924
+ logger.error('handle overdraft protection invoice failed', {
925
+ error,
926
+ invoiceId: invoice.id,
927
+ });
928
+ }
929
+ }
@@ -4,7 +4,7 @@ import pWaitFor from 'p-wait-for';
4
4
  import prettyMsI18n from 'pretty-ms-i18n';
5
5
  import isEmpty from 'lodash/isEmpty';
6
6
 
7
- import { getPaymentAmountForCycleSubscription } from '../../payment';
7
+ import { getPaymentAmountForCycleSubscription, getCustomerSubscriptionPageUrl } from '../../subscription';
8
8
  import { getUserLocale } from '../../../integrations/blocklet/notification';
9
9
  import { translate } from '../../../locales';
10
10
  import {
@@ -19,7 +19,6 @@ import {
19
19
  } from '../../../store/models';
20
20
  import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
21
21
  import { getMainProductName } from '../../product';
22
- import { getCustomerSubscriptionPageUrl } from '../../subscription';
23
22
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
24
23
  import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
25
24
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
@@ -8,9 +8,9 @@ import { getTokenByAddress } from '../../../integrations/arcblock/stake';
8
8
  import { getUserLocale } from '../../../integrations/blocklet/notification';
9
9
  import { translate } from '../../../locales';
10
10
  import { Customer, PaymentMethod, Subscription, PaymentCurrency } from '../../../store/models';
11
- import { PaymentDetail, getPaymentAmountForCycleSubscription } from '../../payment';
11
+ import { PaymentDetail } from '../../payment';
12
12
  import { getMainProductName } from '../../product';
13
- import { getCustomerSubscriptionPageUrl } from '../../subscription';
13
+ import { getCustomerSubscriptionPageUrl, getPaymentAmountForCycleSubscription } from '../../subscription';
14
14
  import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
15
15
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
16
  import dayjs from '../../dayjs';
@@ -18,9 +18,13 @@ import {
18
18
  SubscriptionItem,
19
19
  PaymentCurrency,
20
20
  } from '../../../store/models';
21
- import { getPaymentAmountForCycleSubscription, type PaymentDetail } from '../../payment';
21
+ import type { PaymentDetail } from '../../payment';
22
22
  import { getMainProductName } from '../../product';
23
- import { getCustomerSubscriptionPageUrl, getSubscriptionPaymentAddress } from '../../subscription';
23
+ import {
24
+ getCustomerSubscriptionPageUrl,
25
+ getSubscriptionPaymentAddress,
26
+ getPaymentAmountForCycleSubscription,
27
+ } from '../../subscription';
24
28
  import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
25
29
  import { getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
26
30
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';