payment-kit 1.13.270 → 1.13.271
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.
- package/api/src/integrations/arcblock/nft.ts +1 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +19 -1
- package/api/src/libs/subscription.ts +69 -3
- package/api/src/routes/pricing-table.ts +1 -1
- package/api/src/routes/subscriptions.ts +208 -155
- package/blocklet.yml +1 -1
- package/package.json +29 -29
- package/src/components/price/form.tsx +1 -1
- package/src/pages/customer/subscription/change-payment.tsx +17 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isEthereumDid, isValid } from '@arcblock/did';
|
|
2
|
-
// import pick from 'lodash/pick';
|
|
3
2
|
import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
|
|
4
3
|
import merge from 'lodash/merge';
|
|
5
4
|
|
|
@@ -59,7 +58,7 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
59
58
|
]);
|
|
60
59
|
|
|
61
60
|
const preMint = preMintFromFactory({
|
|
62
|
-
factory: formatFactoryState(factoryState),
|
|
61
|
+
factory: formatFactoryState(factoryState as any),
|
|
63
62
|
inputs: inputs || {},
|
|
64
63
|
owner: nftOwner,
|
|
65
64
|
issuer: { wallet, name: appState.moniker },
|
|
@@ -2,6 +2,7 @@ import pick from 'lodash/pick';
|
|
|
2
2
|
import type Stripe from 'stripe';
|
|
3
3
|
|
|
4
4
|
import logger from '../../../libs/logger';
|
|
5
|
+
import { finalizeStripeSubscriptionUpdate } from '../../../libs/subscription';
|
|
5
6
|
import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
|
|
6
7
|
|
|
7
8
|
export async function handleStripeSubscriptionSucceed(subscription: Subscription, status: string) {
|
|
@@ -83,7 +84,24 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
|
|
|
83
84
|
if (subscription.status === 'trialing' && event.data.object.status === 'active') {
|
|
84
85
|
fields.push('status');
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
await finalizeStripeSubscriptionUpdate({
|
|
89
|
+
subscription,
|
|
90
|
+
updates: pick(event.data.object, fields),
|
|
91
|
+
items: (event.data.object.items.data || []).map((x: any) => ({
|
|
92
|
+
id: x.metadata?.id,
|
|
93
|
+
price_id: x.price.metadata?.id,
|
|
94
|
+
stripe_id: x.id,
|
|
95
|
+
stripe_price_id: x.price.id,
|
|
96
|
+
stripe_subscription_id: x.subscription,
|
|
97
|
+
metadata: x.metadata,
|
|
98
|
+
quantity: x.quantity,
|
|
99
|
+
billing_thresholds: x.billing_thresholds,
|
|
100
|
+
// discounts: x.discounts,
|
|
101
|
+
// tax_rates: x.tax_rates,
|
|
102
|
+
})),
|
|
103
|
+
});
|
|
104
|
+
|
|
87
105
|
return;
|
|
88
106
|
}
|
|
89
107
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
InvoiceItem,
|
|
13
13
|
Lock,
|
|
14
14
|
PaymentCurrency,
|
|
15
|
+
PaymentMethod,
|
|
15
16
|
Price,
|
|
16
17
|
PriceRecurring,
|
|
17
18
|
Subscription,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
UsageRecord,
|
|
22
23
|
} from '../store/models';
|
|
23
24
|
import dayjs from './dayjs';
|
|
25
|
+
import env from './env';
|
|
24
26
|
import logger from './logger';
|
|
25
27
|
import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
|
|
26
28
|
import { getConnectQueryParam } from './util';
|
|
@@ -228,7 +230,6 @@ export async function createProration(
|
|
|
228
230
|
setup: ReturnType<typeof getSubscriptionCreateSetup>,
|
|
229
231
|
anchor: number
|
|
230
232
|
) {
|
|
231
|
-
// FIXME: should we enforce cycle invoices here?
|
|
232
233
|
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
233
234
|
if (!lastInvoice) {
|
|
234
235
|
throw new Error('Subscription should have latest invoice when create proration');
|
|
@@ -249,9 +250,11 @@ export async function createProration(
|
|
|
249
250
|
|
|
250
251
|
// 2. calculate proration args based on the filtered invoice items
|
|
251
252
|
const precision = 10000;
|
|
252
|
-
const
|
|
253
|
-
const
|
|
253
|
+
const isInvoicePeriodValid = lastInvoice.period_start < lastInvoice.period_end;
|
|
254
|
+
const prorationStart = isInvoicePeriodValid ? lastInvoice.period_start : subscription.current_period_start;
|
|
255
|
+
const prorationEnd = isInvoicePeriodValid ? lastInvoice.period_end : subscription.current_period_end;
|
|
254
256
|
if (anchor > prorationEnd) {
|
|
257
|
+
logger.warn('try to create proration with invalid arguments', { anchor, prorationStart, prorationEnd });
|
|
255
258
|
throw new Error('Subscription proration anchor should not be larger than prorationEnd');
|
|
256
259
|
}
|
|
257
260
|
|
|
@@ -524,6 +527,69 @@ export async function finalizeSubscriptionUpdate({
|
|
|
524
527
|
logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
|
|
525
528
|
}
|
|
526
529
|
|
|
530
|
+
export async function finalizeStripeSubscriptionUpdate({
|
|
531
|
+
subscription,
|
|
532
|
+
items,
|
|
533
|
+
updates,
|
|
534
|
+
}: {
|
|
535
|
+
subscription: Subscription;
|
|
536
|
+
items: any[];
|
|
537
|
+
updates: any;
|
|
538
|
+
}) {
|
|
539
|
+
if (isEmpty(updates)) {
|
|
540
|
+
logger.info('subscription update aborted', { subscription: subscription.id, updates });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (subscription.pending_update) {
|
|
545
|
+
// handle subscription item remove
|
|
546
|
+
// this must be done before update/create
|
|
547
|
+
const localItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
548
|
+
const deletedItems = localItems.filter((x) => items.some((y) => y.id === x.id) === false).map((x) => x.id);
|
|
549
|
+
if (deletedItems.length) {
|
|
550
|
+
await UsageRecord.destroy({ where: { subscription_item_id: deletedItems } });
|
|
551
|
+
logger.info('subscription item usage cleared on update', { subscription: subscription.id, deletedItems });
|
|
552
|
+
await SubscriptionItem.destroy({ where: { id: deletedItems } });
|
|
553
|
+
logger.info('subscription item deleted on update', { subscription: subscription.id, deletedItems });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// handle subscription item update/create
|
|
557
|
+
for (const item of items) {
|
|
558
|
+
if (item.id) {
|
|
559
|
+
// remote item is associated with local item
|
|
560
|
+
await SubscriptionItem.update(pick(item, ['quantity', 'billing_thresholds']), { where: { id: item.id } });
|
|
561
|
+
logger.info('subscription item synced on update', { subscription: subscription.id, item });
|
|
562
|
+
} else {
|
|
563
|
+
// remote item not associated with local item
|
|
564
|
+
const created = await SubscriptionItem.create({
|
|
565
|
+
price_id: item.price_id as string,
|
|
566
|
+
quantity: item.quantity as number,
|
|
567
|
+
livemode: subscription.livemode,
|
|
568
|
+
subscription_id: subscription.id,
|
|
569
|
+
metadata: {
|
|
570
|
+
stripe_id: item.stripe_id,
|
|
571
|
+
stripe_subscription_id: item.stripe_subscription_id,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
logger.info('subscription item mirrored on update', { subscription: subscription.id, item });
|
|
575
|
+
const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
576
|
+
const client = method!.getStripeClient();
|
|
577
|
+
const result = await client.subscriptionItems.update(item.stripe_id, {
|
|
578
|
+
metadata: { appPid: env.appPid, id: created.id },
|
|
579
|
+
});
|
|
580
|
+
logger.info('subscription item related on mirror', { subscription: subscription.id, item, result });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const releaseAt = subscription.current_period_end;
|
|
585
|
+
await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
|
|
586
|
+
logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
logger.info('subscription update finalized', { subscription: subscription.id, updates, items });
|
|
590
|
+
await subscription.update({ ...updates, pending_update: null });
|
|
591
|
+
}
|
|
592
|
+
|
|
527
593
|
export async function onSubscriptionUpdateConnected(subscriptionId: string) {
|
|
528
594
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
529
595
|
if (!subscription) {
|
|
@@ -5,7 +5,7 @@ import Joi from 'joi';
|
|
|
5
5
|
import pick from 'lodash/pick';
|
|
6
6
|
import uniq from 'lodash/uniq';
|
|
7
7
|
|
|
8
|
-
import { ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
8
|
+
import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
9
9
|
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
10
10
|
import dayjs from '../libs/dayjs';
|
|
11
11
|
import logger from '../libs/logger';
|
|
@@ -552,7 +552,7 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
const updatedExpanded = await Price.expand(updatedItems as LineItem[]);
|
|
555
|
-
const newItems:
|
|
555
|
+
const newItems: TLineItemExpanded[] = [...existingExpanded, ...addedExpanded, ...updatedExpanded];
|
|
556
556
|
if (newItems.length === 0) {
|
|
557
557
|
throw new Error('Subscription should have at least one subscription item');
|
|
558
558
|
}
|
|
@@ -675,6 +675,8 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
675
675
|
// handle subscription item changes
|
|
676
676
|
let connectAction = '';
|
|
677
677
|
if (Array.isArray(value.items) && value.items.length > 0) {
|
|
678
|
+
const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
|
|
679
|
+
|
|
678
680
|
if (subscription.isActive() === false) {
|
|
679
681
|
throw new Error('Updating subscription item not allowed for inactive subscriptions');
|
|
680
682
|
}
|
|
@@ -682,10 +684,6 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
682
684
|
throw new Error('Updating subscription item not allowed for scheduled-to-cancel subscriptions');
|
|
683
685
|
}
|
|
684
686
|
|
|
685
|
-
if (paymentMethod.type === 'stripe') {
|
|
686
|
-
throw new Error('Updating subscription item not allowed for subscriptions paid with stripe');
|
|
687
|
-
}
|
|
688
|
-
|
|
689
687
|
const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
|
|
690
688
|
if (locked) {
|
|
691
689
|
throw new Error('Updating subscription item not allowed now until next billing cycle');
|
|
@@ -697,167 +695,232 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
697
695
|
value.items
|
|
698
696
|
);
|
|
699
697
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
|
|
704
|
-
updates.pending_invoice_item_interval = setup.recurring;
|
|
705
|
-
updates.current_period_start = setup.period.start;
|
|
706
|
-
updates.current_period_end = setup.period.end;
|
|
707
|
-
updates.billing_cycle_anchor = setup.cycle.anchor;
|
|
708
|
-
logger.info('subscription updates on reset anchor', { subscription: req.params.id, updates });
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// handle proration
|
|
712
|
-
const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
|
|
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
|
-
}
|
|
698
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
|
|
699
|
+
if (paymentMethod.type === 'stripe' && stripeSubscriptionId) {
|
|
700
|
+
await ensureStripeCustomer(customer, paymentMethod);
|
|
720
701
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
702
|
+
const addedStripeItems = await Promise.all(
|
|
703
|
+
addedItems.map(async (x) => {
|
|
704
|
+
const price = await Price.findByPk(x.price_id);
|
|
705
|
+
const stripePrice = await ensureStripePrice(price!, paymentMethod, paymentCurrency);
|
|
706
|
+
return { price: stripePrice.id, quantity: x.quantity };
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
const updatedStripeItems = await Promise.all(
|
|
710
|
+
updatedItems.map(async (x) => {
|
|
711
|
+
const price = newItems.find((y) => y.price_id === x.price_id);
|
|
712
|
+
const item = await SubscriptionItem.findByPk(x.id);
|
|
713
|
+
return { id: item!.metadata.stripe_id, price: price!.metadata?.stripe_id, quantity: x.quantity };
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
const deletedStripeItems = await Promise.all(
|
|
717
|
+
deletedItems.map(async (x) => {
|
|
718
|
+
const item = await SubscriptionItem.findByPk(x.id);
|
|
719
|
+
return { id: item!.metadata.stripe_id, deleted: true, clear_usage: x.clear_usage };
|
|
720
|
+
})
|
|
726
721
|
);
|
|
727
722
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
metered: false,
|
|
736
|
-
lineItems: newItems,
|
|
737
|
-
applyCredit: false,
|
|
738
|
-
props: {
|
|
739
|
-
status: 'draft',
|
|
740
|
-
livemode: subscription.livemode,
|
|
741
|
-
description: 'Subscription update',
|
|
742
|
-
statement_descriptor: lastInvoice.statement_descriptor,
|
|
743
|
-
period_start: setup.period.start,
|
|
744
|
-
period_end: setup.period.end,
|
|
745
|
-
auto_advance: true,
|
|
746
|
-
billing_reason: 'subscription_update',
|
|
747
|
-
total: setup.amount.setup,
|
|
748
|
-
currency_id: paymentCurrency.id,
|
|
749
|
-
default_payment_method_id: subscription.default_payment_method_id,
|
|
750
|
-
custom_fields: lastInvoice.custom_fields || [],
|
|
751
|
-
footer: lastInvoice.footer || '',
|
|
752
|
-
} as Invoice,
|
|
723
|
+
const stripeItems = [...addedStripeItems, ...updatedStripeItems, ...deletedStripeItems].filter(Boolean);
|
|
724
|
+
logger.info('stripe subscription update attempt', {
|
|
725
|
+
subscription: req.params.id,
|
|
726
|
+
stripeSubscriptionId,
|
|
727
|
+
addedStripeItems,
|
|
728
|
+
updatedStripeItems,
|
|
729
|
+
deletedStripeItems,
|
|
753
730
|
});
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
metadata: {},
|
|
774
|
-
})
|
|
775
|
-
)
|
|
776
|
-
);
|
|
777
|
-
logger.info('subscription proration invoice items created', {
|
|
731
|
+
|
|
732
|
+
await subscription.update({
|
|
733
|
+
pending_update: {
|
|
734
|
+
expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
|
|
735
|
+
updates,
|
|
736
|
+
addedItems,
|
|
737
|
+
deletedItems,
|
|
738
|
+
updatedItems,
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const client = paymentMethod.getStripeClient();
|
|
743
|
+
const result = await client.subscriptions.update(stripeSubscriptionId, {
|
|
744
|
+
proration_behavior: prorationBehavior as any,
|
|
745
|
+
metadata: updates.metadata,
|
|
746
|
+
description: updates.description,
|
|
747
|
+
items: stripeItems,
|
|
748
|
+
});
|
|
749
|
+
logger.info('stripe subscription update done', {
|
|
778
750
|
subscription: req.params.id,
|
|
779
|
-
|
|
751
|
+
stripeSubscriptionId,
|
|
752
|
+
prorationBehavior,
|
|
753
|
+
result,
|
|
780
754
|
});
|
|
755
|
+
} else {
|
|
756
|
+
// update subscription period settings
|
|
757
|
+
// HINT: if we are adding new items, we need to reset the anchor to now
|
|
758
|
+
const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0);
|
|
759
|
+
if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
|
|
760
|
+
updates.pending_invoice_item_interval = setup.recurring;
|
|
761
|
+
updates.current_period_start = setup.period.start;
|
|
762
|
+
updates.current_period_end = setup.period.end;
|
|
763
|
+
updates.billing_cycle_anchor = setup.cycle.anchor;
|
|
764
|
+
logger.info('subscription updates on reset anchor', { subscription: req.params.id, updates });
|
|
765
|
+
}
|
|
781
766
|
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
767
|
+
// handle proration
|
|
768
|
+
if (prorationBehavior === 'create_prorations') {
|
|
769
|
+
// 0. cleanup open invoices
|
|
770
|
+
if (subscription.pending_update?.updates?.latest_invoice_id) {
|
|
771
|
+
await cleanupInvoiceAndItems(subscription.pending_update?.updates?.latest_invoice_id);
|
|
772
|
+
// @ts-ignore
|
|
773
|
+
await subscription.update({ pending_update: null });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// 1. create proration
|
|
777
|
+
const { lastInvoice, due, newCredit, appliedCredit, prorations } = await createProration(
|
|
778
|
+
subscription,
|
|
779
|
+
setup,
|
|
780
|
+
dayjs().unix()
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
// 2. create new invoice: amount according to new subscription items
|
|
784
|
+
// 3. create new invoice items: amount according to new subscription items
|
|
785
|
+
const result = await ensureInvoiceAndItems({
|
|
786
|
+
customer,
|
|
787
|
+
currency: paymentCurrency,
|
|
788
|
+
subscription,
|
|
789
|
+
trialing: false,
|
|
790
|
+
metered: false,
|
|
791
|
+
lineItems: newItems,
|
|
792
|
+
applyCredit: false,
|
|
793
|
+
props: {
|
|
794
|
+
status: 'draft',
|
|
795
|
+
livemode: subscription.livemode,
|
|
796
|
+
description: 'Subscription update',
|
|
797
|
+
statement_descriptor: lastInvoice.statement_descriptor,
|
|
798
|
+
period_start: setup.period.start,
|
|
799
|
+
period_end: setup.period.end,
|
|
800
|
+
auto_advance: true,
|
|
801
|
+
billing_reason: 'subscription_update',
|
|
802
|
+
total: setup.amount.setup,
|
|
803
|
+
currency_id: paymentCurrency.id,
|
|
804
|
+
default_payment_method_id: subscription.default_payment_method_id,
|
|
805
|
+
custom_fields: lastInvoice.custom_fields || [],
|
|
806
|
+
footer: lastInvoice.footer || '',
|
|
807
|
+
} as Invoice,
|
|
808
|
+
});
|
|
809
|
+
const { invoice } = result;
|
|
810
|
+
updates.latest_invoice_id = invoice.id;
|
|
811
|
+
logger.info('subscription update invoice created', { subscription: req.params.id, invoice: invoice.id });
|
|
812
|
+
|
|
813
|
+
// 4. create proration invoice items: amount according to proration amount
|
|
814
|
+
const prorationInvoiceItems = await Promise.all(
|
|
815
|
+
prorations.map((x: any) =>
|
|
816
|
+
InvoiceItem.create({
|
|
817
|
+
...x,
|
|
818
|
+
livemode: subscription.livemode,
|
|
819
|
+
currency_id: subscription.currency_id,
|
|
820
|
+
customer_id: customer.id,
|
|
821
|
+
price_id: x.price_id,
|
|
822
|
+
invoice_id: invoice.id,
|
|
823
|
+
subscription_id: subscription.id,
|
|
824
|
+
subscription_item_id: x.subscription_item_id,
|
|
825
|
+
discountable: false,
|
|
826
|
+
discounts: [],
|
|
827
|
+
discount_amounts: [],
|
|
828
|
+
metadata: {},
|
|
829
|
+
})
|
|
830
|
+
)
|
|
831
|
+
);
|
|
832
|
+
logger.info('subscription proration invoice items created', {
|
|
833
|
+
subscription: req.params.id,
|
|
834
|
+
items: prorationInvoiceItems.map((x) => x.id),
|
|
792
835
|
});
|
|
793
|
-
|
|
836
|
+
|
|
837
|
+
// 5. check do we need to connect
|
|
838
|
+
let hasNext = true;
|
|
839
|
+
if (due === '0') {
|
|
794
840
|
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
841
|
} else {
|
|
801
|
-
|
|
842
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
843
|
+
paymentMethod,
|
|
844
|
+
paymentCurrency,
|
|
845
|
+
userDid: customer.did,
|
|
846
|
+
amount: setup.amount.setup,
|
|
847
|
+
});
|
|
848
|
+
if (delegation.sufficient) {
|
|
849
|
+
hasNext = false;
|
|
850
|
+
} else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
|
|
851
|
+
throw new Error('Subscription update can only be done when you do have connected DID Wallet');
|
|
852
|
+
} else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
|
|
853
|
+
// FIXME: this is not supported at frontend
|
|
854
|
+
connectAction = 'collect';
|
|
855
|
+
} else {
|
|
856
|
+
connectAction = 'change-plan';
|
|
857
|
+
}
|
|
802
858
|
}
|
|
803
|
-
}
|
|
804
859
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
860
|
+
// 6. adjust invoice total
|
|
861
|
+
await invoice.update({
|
|
862
|
+
status: 'open',
|
|
863
|
+
amount_due: due,
|
|
864
|
+
amount_remaining: due,
|
|
865
|
+
});
|
|
811
866
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
867
|
+
// 7. wait for succeed
|
|
868
|
+
if (hasNext) {
|
|
869
|
+
await subscription.update({
|
|
870
|
+
pending_update: {
|
|
871
|
+
expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
|
|
872
|
+
updates,
|
|
873
|
+
appliedCredit,
|
|
874
|
+
newCredit,
|
|
875
|
+
addedItems,
|
|
876
|
+
deletedItems,
|
|
877
|
+
updatedItems,
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
logger.info('subscription update invoice wait for connect', {
|
|
881
|
+
subscription: subscription.id,
|
|
882
|
+
invoice: invoice.id,
|
|
883
|
+
});
|
|
884
|
+
} else {
|
|
885
|
+
await invoiceQueue.pushAndWait({
|
|
886
|
+
id: invoice.id,
|
|
887
|
+
job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
|
|
888
|
+
});
|
|
889
|
+
logger.info('subscription update invoice processed', {
|
|
890
|
+
subscription: subscription.id,
|
|
891
|
+
invoice: invoice.id,
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// check if we have succeeded
|
|
895
|
+
await Promise.all([invoice.reload(), subscription.reload()]);
|
|
896
|
+
|
|
897
|
+
if (invoice.status === 'paid') {
|
|
898
|
+
await subscriptionQueue.delete(subscription.id);
|
|
899
|
+
await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
|
|
900
|
+
} else {
|
|
901
|
+
throw new Error('Subscription update invoice failed to advance');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
await finalizeSubscriptionUpdate({
|
|
905
|
+
subscription,
|
|
906
|
+
customer,
|
|
907
|
+
invoice,
|
|
908
|
+
paymentCurrency,
|
|
818
909
|
appliedCredit,
|
|
819
910
|
newCredit,
|
|
820
911
|
addedItems,
|
|
821
912
|
deletedItems,
|
|
822
913
|
updatedItems,
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
logger.info('subscription update invoice wait for connect', {
|
|
826
|
-
subscription: subscription.id,
|
|
827
|
-
invoice: invoice.id,
|
|
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 });
|
|
835
|
-
|
|
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');
|
|
914
|
+
updates,
|
|
915
|
+
});
|
|
844
916
|
}
|
|
845
|
-
|
|
846
|
-
await finalizeSubscriptionUpdate({
|
|
847
|
-
subscription,
|
|
848
|
-
customer,
|
|
849
|
-
invoice,
|
|
850
|
-
paymentCurrency,
|
|
851
|
-
appliedCredit,
|
|
852
|
-
newCredit,
|
|
853
|
-
addedItems,
|
|
854
|
-
deletedItems,
|
|
855
|
-
updatedItems,
|
|
856
|
-
updates,
|
|
857
|
-
});
|
|
858
917
|
}
|
|
859
918
|
}
|
|
860
919
|
} else if (req.body.billing_cycle_anchor === 'now') {
|
|
920
|
+
if (paymentMethod?.type === 'stripe') {
|
|
921
|
+
return res.status(400).json({ error: 'Update billing_cycle_anchor not supported for stripe subscriptions' });
|
|
922
|
+
}
|
|
923
|
+
|
|
861
924
|
if (subscription.isActive() === false) {
|
|
862
925
|
throw new Error('Updating billing_cycle_anchor not allowed for inactive subscriptions');
|
|
863
926
|
}
|
|
@@ -1007,11 +1070,6 @@ router.get('/:id/change-plan', authPortal, async (req, res) => {
|
|
|
1007
1070
|
return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
|
|
1008
1071
|
}
|
|
1009
1072
|
|
|
1010
|
-
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1011
|
-
if (paymentMethod?.type === 'stripe') {
|
|
1012
|
-
return res.status(400).json({ error: 'Update is not supported for subscriptions paid with stripe' });
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
1073
|
const table = await getUpdateTable(subscription);
|
|
1016
1074
|
return res.json(table);
|
|
1017
1075
|
} catch (err) {
|
|
@@ -1038,11 +1096,6 @@ router.post('/:id/change-plan', authPortal, async (req, res) => {
|
|
|
1038
1096
|
return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
|
|
1039
1097
|
}
|
|
1040
1098
|
|
|
1041
|
-
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1042
|
-
if (paymentMethod?.type === 'stripe') {
|
|
1043
|
-
return res.status(400).json({ error: 'Update is not supported for subscriptions paid with stripe' });
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1099
|
const { error } = updateSchema.validate(req.body);
|
|
1047
1100
|
if (error) {
|
|
1048
1101
|
return res.status(400).json({ error: `Subscription update request invalid: ${error.message}` });
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.271",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -43,32 +43,32 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@abtnode/cron": "1.16.26",
|
|
46
|
-
"@arcblock/did": "^1.18.
|
|
46
|
+
"@arcblock/did": "^1.18.123",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.9.
|
|
49
|
-
"@arcblock/did-util": "^1.18.
|
|
50
|
-
"@arcblock/jwt": "^1.18.
|
|
51
|
-
"@arcblock/ux": "^2.9.
|
|
52
|
-
"@arcblock/validator": "^1.18.
|
|
48
|
+
"@arcblock/did-connect": "^2.9.90",
|
|
49
|
+
"@arcblock/did-util": "^1.18.123",
|
|
50
|
+
"@arcblock/jwt": "^1.18.123",
|
|
51
|
+
"@arcblock/ux": "^2.9.90",
|
|
52
|
+
"@arcblock/validator": "^1.18.123",
|
|
53
53
|
"@blocklet/logger": "1.16.26",
|
|
54
|
-
"@blocklet/payment-react": "1.13.
|
|
54
|
+
"@blocklet/payment-react": "1.13.271",
|
|
55
55
|
"@blocklet/sdk": "1.16.26",
|
|
56
|
-
"@blocklet/ui-react": "^2.9.
|
|
56
|
+
"@blocklet/ui-react": "^2.9.90",
|
|
57
57
|
"@blocklet/uploader": "^0.1.6",
|
|
58
|
-
"@mui/icons-material": "^5.15.
|
|
58
|
+
"@mui/icons-material": "^5.15.19",
|
|
59
59
|
"@mui/lab": "^5.0.0-alpha.170",
|
|
60
|
-
"@mui/material": "^5.15.
|
|
61
|
-
"@mui/styles": "^5.15.
|
|
60
|
+
"@mui/material": "^5.15.19",
|
|
61
|
+
"@mui/styles": "^5.15.19",
|
|
62
62
|
"@mui/system": "^5.15.15",
|
|
63
|
-
"@ocap/asset": "^1.18.
|
|
64
|
-
"@ocap/client": "^1.18.
|
|
65
|
-
"@ocap/mcrypto": "^1.18.
|
|
66
|
-
"@ocap/util": "^1.18.
|
|
67
|
-
"@ocap/wallet": "^1.18.
|
|
63
|
+
"@ocap/asset": "^1.18.123",
|
|
64
|
+
"@ocap/client": "^1.18.123",
|
|
65
|
+
"@ocap/mcrypto": "^1.18.123",
|
|
66
|
+
"@ocap/util": "^1.18.123",
|
|
67
|
+
"@ocap/wallet": "^1.18.123",
|
|
68
68
|
"@react-pdf/renderer": "^3.4.4",
|
|
69
69
|
"@stripe/react-stripe-js": "^2.7.1",
|
|
70
70
|
"@stripe/stripe-js": "^2.4.0",
|
|
71
|
-
"ahooks": "^3.
|
|
71
|
+
"ahooks": "^3.8.0",
|
|
72
72
|
"axios": "^0.27.2",
|
|
73
73
|
"body-parser": "^1.20.2",
|
|
74
74
|
"cls-hooked": "^4.2.2",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"date-fns": "^3.6.0",
|
|
79
79
|
"dayjs": "^1.11.11",
|
|
80
80
|
"dotenv-flow": "^3.3.0",
|
|
81
|
-
"ethers": "^6.
|
|
81
|
+
"ethers": "^6.13.0",
|
|
82
82
|
"express": "^4.19.2",
|
|
83
83
|
"express-async-errors": "^3.1.1",
|
|
84
84
|
"express-history-api-fallback": "^2.2.1",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"react": "^18.3.1",
|
|
99
99
|
"react-dom": "^18.3.1",
|
|
100
100
|
"react-error-boundary": "^4.0.13",
|
|
101
|
-
"react-hook-form": "^7.51.
|
|
101
|
+
"react-hook-form": "^7.51.5",
|
|
102
102
|
"react-international-phone": "^3.1.2",
|
|
103
103
|
"react-router-dom": "^6.23.1",
|
|
104
104
|
"recharts": "^2.12.7",
|
|
@@ -109,22 +109,22 @@
|
|
|
109
109
|
"stripe": "^13.11.0",
|
|
110
110
|
"typewriter-effect": "^2.21.0",
|
|
111
111
|
"ufo": "^1.5.3",
|
|
112
|
-
"umzug": "^3.8.
|
|
112
|
+
"umzug": "^3.8.1",
|
|
113
113
|
"use-bus": "^2.5.2",
|
|
114
114
|
"validator": "^13.12.0"
|
|
115
115
|
},
|
|
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.
|
|
119
|
+
"@blocklet/payment-types": "1.13.271",
|
|
120
120
|
"@types/cookie-parser": "^1.4.7",
|
|
121
121
|
"@types/cors": "^2.8.17",
|
|
122
122
|
"@types/dotenv-flow": "^3.3.3",
|
|
123
123
|
"@types/express": "^4.17.21",
|
|
124
|
-
"@types/node": "^18.19.
|
|
125
|
-
"@types/react": "^18.3.
|
|
124
|
+
"@types/node": "^18.19.34",
|
|
125
|
+
"@types/react": "^18.3.3",
|
|
126
126
|
"@types/react-dom": "^18.3.0",
|
|
127
|
-
"@vitejs/plugin-react": "^4.
|
|
127
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
128
128
|
"bumpp": "^8.2.1",
|
|
129
129
|
"cross-env": "^7.0.3",
|
|
130
130
|
"eslint": "^8.57.0",
|
|
@@ -135,11 +135,11 @@
|
|
|
135
135
|
"npm-run-all": "^4.1.5",
|
|
136
136
|
"prettier": "^2.8.8",
|
|
137
137
|
"prettier-plugin-import-sort": "^0.0.7",
|
|
138
|
-
"ts-jest": "^29.1.
|
|
138
|
+
"ts-jest": "^29.1.4",
|
|
139
139
|
"ts-node": "^10.9.2",
|
|
140
|
-
"type-fest": "^4.
|
|
140
|
+
"type-fest": "^4.19.0",
|
|
141
141
|
"typescript": "^4.9.5",
|
|
142
|
-
"vite": "^5.2.
|
|
142
|
+
"vite": "^5.2.12",
|
|
143
143
|
"vite-plugin-blocklet": "^0.7.9",
|
|
144
144
|
"vite-plugin-node-polyfills": "^0.21.0",
|
|
145
145
|
"vite-plugin-svgr": "^4.2.0",
|
|
@@ -155,5 +155,5 @@
|
|
|
155
155
|
"parser": "typescript"
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
-
"gitHead": "
|
|
158
|
+
"gitHead": "cf476c72b4c54fd56881e03828e4b2c764e76e84"
|
|
159
159
|
}
|
|
@@ -214,7 +214,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
|
|
|
214
214
|
size="small"
|
|
215
215
|
sx={{ width: INPUT_WIDTH }}
|
|
216
216
|
error={!!getFieldState(fieldName).error}
|
|
217
|
-
helperText={getFieldState(fieldName).error?.message}
|
|
217
|
+
helperText={getFieldState(fieldName).error?.message as string}
|
|
218
218
|
InputProps={{
|
|
219
219
|
endAdornment: (
|
|
220
220
|
<InputAdornment position="end">
|
|
@@ -27,7 +27,7 @@ import { useRequest, useSetState } from 'ahooks';
|
|
|
27
27
|
import pWaitFor from 'p-wait-for';
|
|
28
28
|
import { useEffect, useState } from 'react';
|
|
29
29
|
import { Controller, FormProvider, useForm, useFormContext, useWatch } from 'react-hook-form';
|
|
30
|
-
import { useParams } from 'react-router-dom';
|
|
30
|
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
31
31
|
import { joinURL } from 'ufo';
|
|
32
32
|
|
|
33
33
|
import SectionHeader from '../../../components/section/header';
|
|
@@ -75,9 +75,10 @@ const waitForCheckoutComplete = async (sessionId: string) => {
|
|
|
75
75
|
|
|
76
76
|
function CustomerSubscriptionChangePayment({ subscription, customer, onComplete }: Props) {
|
|
77
77
|
const { t } = useLocaleContext();
|
|
78
|
-
const
|
|
78
|
+
const navigate = useNavigate();
|
|
79
|
+
const [searchParams] = useSearchParams();
|
|
80
|
+
const { settings, connect } = usePaymentContext();
|
|
79
81
|
const { control, setValue, handleSubmit } = useFormContext();
|
|
80
|
-
const { connect } = usePaymentContext();
|
|
81
82
|
|
|
82
83
|
const [state, setState] = useSetState<{
|
|
83
84
|
submitting: boolean;
|
|
@@ -115,12 +116,23 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
115
116
|
const method = settings.paymentMethods.find((x: any) => x.id === selectedMethodId) as TPaymentMethod;
|
|
116
117
|
const changed = selectedCurrencyId !== subscription.currency_id;
|
|
117
118
|
|
|
119
|
+
const handleCompleted = async () => {
|
|
120
|
+
await onComplete();
|
|
121
|
+
|
|
122
|
+
const redirectUrl = searchParams.get('redirectUrl');
|
|
123
|
+
if (redirectUrl) {
|
|
124
|
+
window.location.href = redirectUrl;
|
|
125
|
+
} else {
|
|
126
|
+
navigate(`/customer/subscription/${subscription.id}`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
118
130
|
const handleConnected = async () => {
|
|
119
131
|
try {
|
|
120
132
|
await waitForCheckoutComplete(subscription.id);
|
|
121
133
|
setState({ paid: true, paying: false });
|
|
122
134
|
Toast.success(t('payment.customer.changePayment.completed', { time: subscription.current_period_end }));
|
|
123
|
-
await
|
|
135
|
+
await handleCompleted();
|
|
124
136
|
} catch (err) {
|
|
125
137
|
Toast.error(formatError(err));
|
|
126
138
|
} finally {
|
|
@@ -202,7 +214,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
202
214
|
<Stack
|
|
203
215
|
direction="row"
|
|
204
216
|
alignItems="center"
|
|
205
|
-
sx={{ fontWeight: 'normal', mt: 2 }}
|
|
217
|
+
sx={{ fontWeight: 'normal', mt: 2, cursor: 'pointer' }}
|
|
206
218
|
onClick={() => goBackOrFallback(`/customer/subscription/${subscription.id}`)}>
|
|
207
219
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
208
220
|
<SubscriptionDescription subscription={subscription} variant="h5" />
|