payment-kit 1.13.266 → 1.13.267

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.
@@ -17,6 +17,8 @@ export const handlers = new WalletHandler({
17
17
  authenticator,
18
18
  tokenStorage: new AuthStorage({
19
19
  dbPath: path.join(env.dataDir, 'auth.db'),
20
+ // @ts-ignore
21
+ onload: console.warn,
20
22
  }),
21
23
  });
22
24
 
@@ -82,9 +82,10 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
82
82
  const productName = await getMainProductName(subscription.id);
83
83
  const at: string = formatTime(subscription.created_at);
84
84
 
85
- const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount, paymentCurrency.decimal)} ${
86
- paymentCurrency.symbol
87
- }`;
85
+ const paymentInfo: string = `${fromUnitToToken(
86
+ paymentIntent?.amount || invoice.amount_paid,
87
+ paymentCurrency.decimal
88
+ )} ${paymentCurrency.symbol}`;
88
89
  const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
89
90
  const nftMintItem: NftMintItem | undefined = hasNft
90
91
  ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable no-await-in-loop */
2
2
  import component from '@blocklet/sdk/lib/component';
3
3
  import { BN } from '@ocap/util';
4
+ import isEmpty from 'lodash/isEmpty';
5
+ import pick from 'lodash/pick';
4
6
  import type { LiteralUnion } from 'type-fest';
5
7
  import { withQuery } from 'ufo';
6
8
 
@@ -8,11 +10,13 @@ import {
8
10
  Customer,
9
11
  Invoice,
10
12
  InvoiceItem,
13
+ Lock,
11
14
  PaymentCurrency,
12
15
  Price,
13
16
  PriceRecurring,
14
17
  Subscription,
15
18
  SubscriptionItem,
19
+ SubscriptionUpdateItem,
16
20
  TLineItemExpanded,
17
21
  UsageRecord,
18
22
  } from '../store/models';
@@ -427,3 +431,135 @@ export async function getUpcomingInvoiceAmount(subscriptionId: string) {
427
431
  currency,
428
432
  };
429
433
  }
434
+
435
+ export async function finalizeSubscriptionUpdate({
436
+ subscription,
437
+ customer,
438
+ invoice,
439
+ paymentCurrency,
440
+ appliedCredit,
441
+ newCredit,
442
+ addedItems,
443
+ updatedItems,
444
+ deletedItems,
445
+ updates,
446
+ }: {
447
+ subscription: Subscription;
448
+ customer: Customer;
449
+ invoice: Invoice;
450
+ paymentCurrency: PaymentCurrency;
451
+ appliedCredit: string;
452
+ newCredit: string;
453
+ addedItems: SubscriptionUpdateItem[];
454
+ updatedItems: SubscriptionUpdateItem[];
455
+ deletedItems: SubscriptionUpdateItem[];
456
+ updates: any;
457
+ }) {
458
+ if (isEmpty(updates)) {
459
+ logger.info('subscription update aborted', { subscription: subscription.id, updates });
460
+ return;
461
+ }
462
+
463
+ logger.info('subscription update finalized', { subscription: subscription.id, updates });
464
+ await subscription.update({ ...updates, pending_update: null });
465
+
466
+ // update subscription items
467
+ for (const item of addedItems) {
468
+ await SubscriptionItem.create({
469
+ price_id: item.price_id as string,
470
+ quantity: item.quantity as number,
471
+ livemode: subscription.livemode,
472
+ subscription_id: subscription.id,
473
+ metadata: {},
474
+ });
475
+ logger.info('subscription item added on update finalize', { subscription: subscription.id, item: item.id });
476
+ }
477
+ for (const item of updatedItems) {
478
+ await SubscriptionItem.update(pick(item, ['quantity', 'metadata', 'billing_thresholds']), {
479
+ where: { id: item.id },
480
+ });
481
+ logger.info('subscription item updated on update finalize', { subscription: subscription.id, item: item.id });
482
+ }
483
+ for (const item of deletedItems) {
484
+ if (item.clear_usage) {
485
+ await UsageRecord.destroy({ where: { subscription_item_id: item.id } });
486
+ logger.info('subscription item usage cleared on update finalize', {
487
+ subscription: subscription.id,
488
+ item: item.id,
489
+ });
490
+ }
491
+ await SubscriptionItem.destroy({ where: { id: item.id } });
492
+ logger.info('subscription item deleted on update finalize', { subscription: subscription.id, item: item.id });
493
+ }
494
+
495
+ // update customer credits
496
+ if (appliedCredit !== '0') {
497
+ const creditResult = await customer.decreaseTokenBalance(paymentCurrency.id, appliedCredit);
498
+ await invoice.update({
499
+ starting_token_balance: creditResult.starting,
500
+ ending_token_balance: creditResult.ending,
501
+ });
502
+ logger.info('customer credit applied to invoice after proration', {
503
+ subscription: subscription.id,
504
+ appliedCredit,
505
+ creditResult,
506
+ });
507
+ }
508
+ if (newCredit !== '0') {
509
+ const creditResult = await customer.increaseTokenBalance(paymentCurrency.id, newCredit);
510
+ await invoice.update({
511
+ starting_token_balance: creditResult.starting,
512
+ ending_token_balance: creditResult.ending,
513
+ });
514
+ logger.info('subscription proration credit applied to customer', {
515
+ subscription: subscription.id,
516
+ newCredit,
517
+ creditResult,
518
+ });
519
+ }
520
+
521
+ // lock for next update
522
+ const releaseAt = subscription.current_period_end;
523
+ await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
524
+ logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
525
+ }
526
+
527
+ export async function onSubscriptionUpdateConnected(subscriptionId: string) {
528
+ const subscription = await Subscription.findByPk(subscriptionId);
529
+ if (!subscription) {
530
+ return;
531
+ }
532
+
533
+ const now = dayjs().unix();
534
+ const pending = subscription.pending_update;
535
+ logger.info('subscription update connected', { subscription: subscription.id, pending });
536
+ if (pending?.updates && pending?.expires_at && pending.expires_at >= now && pending.updates?.latest_invoice_id) {
537
+ const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
538
+ if (!paymentCurrency) {
539
+ return;
540
+ }
541
+
542
+ const customer = await Customer.findByPk(subscription.customer_id);
543
+ if (!customer) {
544
+ return;
545
+ }
546
+
547
+ const invoice = await Invoice.findByPk(pending.updates.latest_invoice_id);
548
+ if (!invoice) {
549
+ return;
550
+ }
551
+
552
+ await finalizeSubscriptionUpdate({
553
+ subscription,
554
+ customer,
555
+ invoice,
556
+ paymentCurrency,
557
+ appliedCredit: pending.appliedCredit || '0',
558
+ newCredit: pending.newCredit || '0',
559
+ addedItems: pending.addedItems || [],
560
+ deletedItems: pending.deletedItems || [],
561
+ updatedItems: pending.updatedItems || [],
562
+ updates: pending.updates,
563
+ });
564
+ }
565
+ }
@@ -14,6 +14,7 @@ import { paymentQueue } from './payment';
14
14
 
15
15
  type InvoiceJob = {
16
16
  invoiceId: string;
17
+ justCreate?: boolean;
17
18
  retryOnError?: boolean;
18
19
  waitForPayment?: boolean;
19
20
  };
@@ -159,6 +160,10 @@ export const handleInvoice = async (job: InvoiceJob) => {
159
160
  }
160
161
  }
161
162
  if (paymentIntent) {
163
+ if (job.justCreate) {
164
+ return;
165
+ }
166
+
162
167
  logger.info('Payment job scheduled', { invoice: invoice.id, paymentIntent: paymentIntent.id });
163
168
  if (job.waitForPayment) {
164
169
  await paymentQueue.pushAndWait({
@@ -2,6 +2,7 @@ import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/e
2
2
  import type { CallbackArgs } from '../../libs/auth';
3
3
  import { isDelegationSufficientForPayment } from '../../libs/payment';
4
4
  import { getFastCheckoutAmount } from '../../libs/session';
5
+ import { onSubscriptionUpdateConnected } from '../../libs/subscription';
5
6
  import { getTxMetadata } from '../../libs/util';
6
7
  import { invoiceQueue } from '../../queues/invoice';
7
8
  import { addSubscriptionJob } from '../../queues/subscription';
@@ -132,6 +133,7 @@ export default {
132
133
  });
133
134
  }
134
135
  if (subscription) {
136
+ await onSubscriptionUpdateConnected(subscriptionId);
135
137
  await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
136
138
  }
137
139
  };
@@ -8,6 +8,7 @@ import type { CallbackArgs } from '../../libs/auth';
8
8
  import { ethWallet, wallet } from '../../libs/auth';
9
9
  import logger from '../../libs/logger';
10
10
  import { getGasPayerExtra } from '../../libs/payment';
11
+ import { onSubscriptionUpdateConnected } from '../../libs/subscription';
11
12
  import { getTxMetadata } from '../../libs/util';
12
13
  import { invoiceQueue } from '../../queues/invoice';
13
14
  import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
@@ -122,6 +123,17 @@ export default {
122
123
  if (exist) {
123
124
  await invoiceQueue.delete(invoice.id);
124
125
  }
126
+
127
+ if (invoice.subscription_id && invoice.billing_reason === 'subscription_update') {
128
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
129
+ if (subscription?.pending_update?.updates?.latest_invoice_id === invoice.id) {
130
+ logger.info('Try to finalize subscription update on invoice paid', {
131
+ invoice: invoice.id,
132
+ subscription: subscription.id,
133
+ });
134
+ await onSubscriptionUpdateConnected(invoice.subscription_id);
135
+ }
136
+ }
125
137
  };
126
138
 
127
139
  if (paymentMethod.type === 'arcblock') {
@@ -5,8 +5,8 @@ import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
5
5
  import type { Transaction } from '@ocap/client';
6
6
  import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
7
7
  import { fromPublicKey } from '@ocap/wallet';
8
- import isEmpty from 'lodash/isEmpty';
9
8
  import type { Request } from 'express';
9
+ import isEmpty from 'lodash/isEmpty';
10
10
 
11
11
  import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
12
12
  import { encodeApproveItx } from '../../integrations/ethereum/token';
@@ -22,6 +22,7 @@ import {
22
22
  getSubscriptionStakeSetup,
23
23
  } from '../../libs/subscription';
24
24
  import { OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
25
+ import { invoiceQueue } from '../../queues/invoice';
25
26
  import type { TLineItemExpanded } from '../../store/models';
26
27
  import { CheckoutSession } from '../../store/models/checkout-session';
27
28
  import { Customer } from '../../store/models/customer';
@@ -504,6 +505,20 @@ export async function ensureInvoiceAndItems({
504
505
  return { invoice, items };
505
506
  }
506
507
 
508
+ export async function cleanupInvoiceAndItems(invoiceId: string) {
509
+ const invoice = await Invoice.findByPk(invoiceId);
510
+ if (!invoice) {
511
+ return;
512
+ }
513
+ if (invoice.isImmutable()) {
514
+ return;
515
+ }
516
+
517
+ const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
518
+ const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
519
+ logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
520
+ }
521
+
507
522
  export async function ensureInvoiceForCollect(invoiceId: string) {
508
523
  const invoice = await Invoice.findByPk(invoiceId);
509
524
  if (!invoice) {
@@ -519,6 +534,14 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
519
534
  throw new Error(`Invoice ${invoiceId} is draft`);
520
535
  }
521
536
 
537
+ if (!invoice.payment_intent_id) {
538
+ await invoiceQueue.pushAndWait({
539
+ id: invoice.id,
540
+ job: { invoiceId: invoice.id, retryOnError: false, justCreate: true },
541
+ });
542
+ await invoice.reload();
543
+ }
544
+
522
545
  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
523
546
  if (!paymentIntent) {
524
547
  throw new Error(`Payment intent not found for invoice ${invoiceId}`);
@@ -922,7 +945,7 @@ export async function executeOcapTransactions(
922
945
  userPk: string,
923
946
  claims: any[],
924
947
  paymentMethod: PaymentMethod,
925
- request: Request,
948
+ request: Request
926
949
  ) {
927
950
  const client = paymentMethod.getOcapClient();
928
951
  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
@@ -14,6 +14,7 @@ import { authenticate } from '../libs/security';
14
14
  import { expandLineItems, getFastCheckoutAmount, isLineItemAligned } from '../libs/session';
15
15
  import {
16
16
  createProration,
17
+ finalizeSubscriptionUpdate,
17
18
  getSubscriptionCreateSetup,
18
19
  getSubscriptionRefundSetup,
19
20
  getUpcomingInvoiceAmount,
@@ -38,7 +39,7 @@ import { Subscription, TSubscription } from '../store/models/subscription';
38
39
  import { SubscriptionItem } from '../store/models/subscription-item';
39
40
  import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/models/types';
40
41
  import { UsageRecord } from '../store/models/usage-record';
41
- import { ensureInvoiceAndItems } from './connect/shared';
42
+ import { cleanupInvoiceAndItems, ensureInvoiceAndItems } from './connect/shared';
42
43
  import { createUsageRecordQueryFn } from './usage-records';
43
44
 
44
45
  const router = Router();
@@ -482,10 +483,10 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
482
483
  }
483
484
 
484
485
  // split items into added, deleted
486
+ const existingItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
485
487
  const addedItems = items.filter((x: any) => x.price_id && !x.id);
486
488
  const deletedItems = items.filter((x: any) => x.deleted && x.id);
487
- const updatedItems = items.filter((x: any) => !x.deleted && x.id);
488
- const existingItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
489
+ const updatedItems = items.filter((x: any) => !x.deleted && x.id && existingItems.some((i) => i.id === x.id));
489
490
 
490
491
  // try handle cross-sell with different interval, just replace with new price that have same interval
491
492
  let addedExpanded = await Price.expand(addedItems as LineItem[]);
@@ -569,7 +570,6 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
569
570
  }
570
571
 
571
572
  return {
572
- existingItems,
573
573
  addedItems,
574
574
  updatedItems,
575
575
  deletedItems,
@@ -692,35 +692,10 @@ router.put('/:id', authPortal, async (req, res) => {
692
692
  }
693
693
 
694
694
  // validate the request
695
- const { existingItems, addedItems, updatedItems, deletedItems, newItems } =
696
- await validateSubscriptionUpdateRequest(subscription, value.items);
697
-
698
- // update subscription items
699
- for (const item of addedItems) {
700
- await SubscriptionItem.create({
701
- price_id: item.price_id as string,
702
- quantity: item.quantity as number,
703
- livemode: subscription.livemode,
704
- subscription_id: subscription.id,
705
- metadata: {},
706
- });
707
- logger.info('subscription item added', { subscription: req.params.id, item: item.id });
708
- }
709
- for (const item of updatedItems) {
710
- const exist = existingItems.find((x) => x.id === item.id);
711
- if (exist) {
712
- await exist.update(pick(item, ['quantity', 'metadata', 'billing_thresholds']));
713
- logger.info('subscription item updated', { subscription: req.params.id, item: item.id });
714
- }
715
- }
716
- for (const item of deletedItems) {
717
- if (item.clear_usage) {
718
- await UsageRecord.destroy({ where: { subscription_item_id: item.id } });
719
- logger.info('subscription item usage cleared', { subscription: req.params.id, item: item.id });
720
- }
721
- await SubscriptionItem.destroy({ where: { id: item.id } });
722
- logger.info('subscription item deleted', { subscription: req.params.id, item: item.id });
723
- }
695
+ const { addedItems, updatedItems, deletedItems, newItems } = await validateSubscriptionUpdateRequest(
696
+ subscription,
697
+ value.items
698
+ );
724
699
 
725
700
  // update subscription period settings
726
701
  // HINT: if we are adding new items, we need to reset the anchor to now
@@ -736,6 +711,13 @@ router.put('/:id', authPortal, async (req, res) => {
736
711
  // handle proration
737
712
  const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
738
713
  if (prorationBehavior === 'create_prorations') {
714
+ // 0. cleanup open invoices
715
+ if (subscription.pending_update?.updates?.latest_invoice_id) {
716
+ await cleanupInvoiceAndItems(subscription.pending_update?.updates?.latest_invoice_id);
717
+ // @ts-ignore
718
+ await subscription.update({ pending_update: null });
719
+ }
720
+
739
721
  // 1. create proration
740
722
  const { lastInvoice, due, newCredit, appliedCredit, prorations } = await createProration(
741
723
  subscription,
@@ -797,86 +779,84 @@ router.put('/:id', authPortal, async (req, res) => {
797
779
  items: prorationInvoiceItems.map((x) => x.id),
798
780
  });
799
781
 
800
- // 5. adjust invoice total or update customer credit balance
801
- const invoiceUpdates: Partial<Invoice> = {
802
- status: 'open',
803
- amount_due: due,
804
- amount_remaining: due,
805
- };
806
- if (appliedCredit !== '0') {
807
- const creditResult = await customer.decreaseTokenBalance(paymentCurrency.id, appliedCredit);
808
- invoiceUpdates.starting_token_balance = creditResult.starting;
809
- invoiceUpdates.ending_token_balance = creditResult.ending;
810
- logger.info('customer credit applied to invoice after proration', {
811
- subscription: req.params.id,
812
- appliedCredit,
813
- creditResult,
814
- });
815
- }
816
- if (newCredit !== '0') {
817
- const creditResult = await customer.increaseTokenBalance(paymentCurrency.id, newCredit);
818
- invoiceUpdates.starting_token_balance = creditResult.starting;
819
- invoiceUpdates.ending_token_balance = creditResult.ending;
820
- logger.info('subscription proration credit applied to customer', {
821
- subscription: req.params.id,
822
- newCredit,
823
- creditResult,
782
+ // 5. check do we need to connect
783
+ let hasNext = true;
784
+ if (due === '0') {
785
+ hasNext = false;
786
+ } else {
787
+ const delegation = await isDelegationSufficientForPayment({
788
+ paymentMethod,
789
+ paymentCurrency,
790
+ userDid: customer.did,
791
+ amount: setup.amount.setup,
824
792
  });
793
+ if (delegation.sufficient) {
794
+ hasNext = false;
795
+ } else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
796
+ throw new Error('Subscription update can only be done when you do have connected DID Wallet');
797
+ } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
798
+ // FIXME: this is not supported at frontend
799
+ connectAction = 'collect';
800
+ } else {
801
+ connectAction = 'change-plan';
802
+ }
825
803
  }
826
804
 
827
- await invoice.update(invoiceUpdates);
828
- await subscription.update(updates);
829
-
830
- // 6. process the invoice as usual: push into queue
831
- await invoiceQueue.pushAndWait({
832
- id: invoice.id,
833
- job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
805
+ // 6. adjust invoice total
806
+ await invoice.update({
807
+ status: 'open',
808
+ amount_due: due,
809
+ amount_remaining: due,
834
810
  });
835
- logger.info('subscription update invoice processed', { subscription: subscription.id, invoice: invoice.id });
836
-
837
- // check if we have succeeded
838
- await Promise.all([invoice.reload(), subscription.reload()]);
839
811
 
840
- if (invoice.status === 'paid') {
841
- await subscriptionQueue.delete(subscription.id);
842
- await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
843
- } else {
812
+ // 7. wait for succeed
813
+ if (hasNext) {
844
814
  await subscription.update({
845
- status: 'past_due',
846
- cancel_at_period_end: true,
847
- cancelation_details: {
848
- comment: 'subscription_update',
849
- feedback: 'other',
850
- reason: 'payment_failed',
815
+ pending_update: {
816
+ expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
817
+ updates,
818
+ appliedCredit,
819
+ newCredit,
820
+ addedItems,
821
+ deletedItems,
822
+ updatedItems,
851
823
  },
852
824
  });
853
- logger.info('subscription past_due on invoice auto advance failed', {
825
+ logger.info('subscription update invoice wait for connect', {
854
826
  subscription: subscription.id,
855
827
  invoice: invoice.id,
856
828
  });
829
+ } else {
830
+ await invoiceQueue.pushAndWait({
831
+ id: invoice.id,
832
+ job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
833
+ });
834
+ logger.info('subscription update invoice processed', { subscription: subscription.id, invoice: invoice.id });
857
835
 
858
- const delegation = await isDelegationSufficientForPayment({
859
- paymentMethod,
836
+ // check if we have succeeded
837
+ await Promise.all([invoice.reload(), subscription.reload()]);
838
+
839
+ if (invoice.status === 'paid') {
840
+ await subscriptionQueue.delete(subscription.id);
841
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
842
+ } else {
843
+ throw new Error('Subscription update invoice failed to advance');
844
+ }
845
+
846
+ await finalizeSubscriptionUpdate({
847
+ subscription,
848
+ customer,
849
+ invoice,
860
850
  paymentCurrency,
861
- userDid: customer.did,
862
- amount: setup.amount.setup,
851
+ appliedCredit,
852
+ newCredit,
853
+ addedItems,
854
+ deletedItems,
855
+ updatedItems,
856
+ updates,
863
857
  });
864
- if (delegation.sufficient === false) {
865
- if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
866
- connectAction = 'bind';
867
- } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
868
- connectAction = 'collect';
869
- } else {
870
- connectAction = 'change-plan';
871
- }
872
- }
873
858
  }
874
859
  }
875
-
876
- // rate limit for plan change
877
- const releaseAt = updates.current_period_end || subscription.current_period_end;
878
- await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
879
- logger.info('subscription plan change lock acquired', { subscription: req.params.id, releaseAt });
880
860
  } else if (req.body.billing_cycle_anchor === 'now') {
881
861
  if (subscription.isActive() === false) {
882
862
  throw new Error('Updating billing_cycle_anchor not allowed for inactive subscriptions');
@@ -36,6 +36,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
36
36
  expires_at?: number;
37
37
  subscription_items?: any[];
38
38
  trial_end?: number;
39
+ [key: string]: any;
39
40
  };
40
41
 
41
42
  declare status: LiteralUnion<
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.13.266
17
+ version: 1.13.267
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.13.266",
3
+ "version": "1.13.267",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -51,7 +51,7 @@
51
51
  "@arcblock/ux": "^2.9.80",
52
52
  "@arcblock/validator": "^1.18.120",
53
53
  "@blocklet/logger": "1.16.26",
54
- "@blocklet/payment-react": "1.13.266",
54
+ "@blocklet/payment-react": "1.13.267",
55
55
  "@blocklet/sdk": "1.16.26",
56
56
  "@blocklet/ui-react": "^2.9.80",
57
57
  "@blocklet/uploader": "^0.1.6",
@@ -116,7 +116,7 @@
116
116
  "devDependencies": {
117
117
  "@abtnode/types": "1.16.26",
118
118
  "@arcblock/eslint-config-ts": "^0.3.0",
119
- "@blocklet/payment-types": "1.13.266",
119
+ "@blocklet/payment-types": "1.13.267",
120
120
  "@types/cookie-parser": "^1.4.7",
121
121
  "@types/cors": "^2.8.17",
122
122
  "@types/dotenv-flow": "^3.3.3",
@@ -155,5 +155,5 @@
155
155
  "parser": "typescript"
156
156
  }
157
157
  },
158
- "gitHead": "f6f7ee807560b4b9d01e6654b03cfaf1f523c167"
158
+ "gitHead": "5cbea674669735c0e8c8d8106038106938668c4b"
159
159
  }
@@ -77,10 +77,7 @@ export default function PassportList() {
77
77
  customBodyRenderLite: (_: string, index: number) => {
78
78
  const payLink = data[index].extra?.acquire?.pay;
79
79
  if (payLink) {
80
- if (payLink.startsWith('plink_')) {
81
- return <Link to={`/admin/payments/${payLink}`}>{payLink}</Link>;
82
- }
83
- if (payLink.startsWith('prctbl_')) {
80
+ if (payLink.startsWith('plink_') || payLink.startsWith('prctbl_')) {
84
81
  return <Link to={`/admin/products/${payLink}`}>{payLink}</Link>;
85
82
  }
86
83
  }
@@ -85,10 +85,6 @@ export default function CustomerSubscriptionChangePlan() {
85
85
  return <Alert severity="error">{t('payment.customer.changePlan.subscriptionNotFound')}</Alert>;
86
86
  }
87
87
 
88
- if (!data.table) {
89
- return <Alert severity="error">{t('payment.customer.changePlan.tableNotFound')}</Alert>;
90
- }
91
-
92
88
  const handleSelect = async (priceId: string) => {
93
89
  try {
94
90
  if (state.priceId === priceId) {
@@ -151,11 +147,7 @@ export default function CustomerSubscriptionChangePlan() {
151
147
 
152
148
  // FIXME: support more proration_behavior
153
149
  const result = await api.put(`/api/subscriptions/${id}`, { proration_behavior: 'create_prorations', items });
154
- if (result.data.status === 'active') {
155
- Toast.success(t('payment.customer.changePlan.success'));
156
- setState({ paid: true });
157
- setTimeout(handleBack, 2000);
158
- } else {
150
+ if (result.data.connectAction) {
159
151
  setState({ paying: true });
160
152
  try {
161
153
  setState({ paying: true });
@@ -169,7 +161,10 @@ export default function CustomerSubscriptionChangePlan() {
169
161
  error: t('payment.customer.changePlan.error'),
170
162
  confirm: '',
171
163
  } as any,
172
- extraParams: { invoiceId: result.data.latest_invoice_id, subscriptionId: result.data.id },
164
+ extraParams: {
165
+ invoiceId: result.data.pending_update?.updates?.latest_invoice_id,
166
+ subscriptionId: result.data.id,
167
+ },
173
168
  onSuccess: () => {
174
169
  setState({ paid: true, paying: false });
175
170
  setTimeout(() => {
@@ -191,6 +186,10 @@ export default function CustomerSubscriptionChangePlan() {
191
186
  } finally {
192
187
  setState({ paying: false });
193
188
  }
189
+ } else {
190
+ Toast.success(t('payment.customer.changePlan.success'));
191
+ setState({ paid: true });
192
+ setTimeout(handleBack, 2000);
194
193
  }
195
194
  } catch (err) {
196
195
  console.error(err);
@@ -236,7 +235,7 @@ export default function CustomerSubscriptionChangePlan() {
236
235
  <Stack
237
236
  direction="row"
238
237
  alignItems="center"
239
- sx={{ fontWeight: 'normal', mt: '16px' }}
238
+ sx={{ fontWeight: 'normal', mt: '16px', cursor: 'pointer' }}
240
239
  onClick={() => goBackOrFallback(`/customer/subscription/${data.subscription.id}`)}>
241
240
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
242
241
  <SubscriptionDescription subscription={data.subscription} variant="h5" />
@@ -246,6 +245,7 @@ export default function CustomerSubscriptionChangePlan() {
246
245
  <SectionHeader title={t('payment.customer.changePlan.config')} />
247
246
  <PricingTable mode="select" alignItems="left" interval={interval} table={table} onSelect={handleSelect} />
248
247
  </Stack>
248
+ {!data.table && <Alert severity="error">{t('payment.customer.changePlan.tableNotFound')}</Alert>}
249
249
  {state.priceId && state.total && state.setup && (
250
250
  <Stack direction="column" spacing={3} sx={{ maxWidth: 640 }}>
251
251
  <SectionHeader title={t('payment.customer.changePlan.confirm')} />