payment-kit 1.13.72 → 1.13.74
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/{schedule → crons}/base.ts +1 -1
- package/api/src/index.ts +7 -7
- package/api/src/integrations/stripe/handlers/customer.ts +24 -0
- package/api/src/integrations/stripe/handlers/index.ts +4 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/audit.ts +34 -28
- package/api/src/libs/payment.ts +48 -4
- package/api/src/libs/queue/index.ts +18 -1
- package/api/src/libs/queue/store.ts +6 -5
- package/api/src/libs/session.ts +13 -12
- package/api/src/libs/subscription.ts +26 -0
- package/api/src/libs/util.ts +5 -1
- package/api/src/{jobs → queues}/checkout-session.ts +23 -1
- package/api/src/{jobs → queues}/invoice.ts +15 -6
- package/api/src/{jobs → queues}/payment.ts +182 -30
- package/api/src/{jobs → queues}/subscription.ts +36 -104
- package/api/src/{jobs → queues}/webhook.ts +2 -0
- package/api/src/routes/checkout-sessions.ts +72 -26
- package/api/src/routes/connect/collect.ts +2 -2
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/setup.ts +10 -3
- package/api/src/routes/connect/shared.ts +98 -49
- package/api/src/routes/connect/subscribe.ts +10 -4
- package/api/src/routes/pricing-table.ts +2 -0
- package/api/src/routes/subscription-items.ts +1 -1
- package/api/src/routes/subscriptions.ts +434 -13
- package/api/src/store/migrate.ts +0 -1
- package/api/src/store/migrations/20231204-subupdate.ts +50 -0
- package/api/src/store/migrations/20231220-setup-intent.ts +22 -0
- package/api/src/store/models/checkout-session.ts +8 -0
- package/api/src/store/models/customer.ts +52 -15
- package/api/src/store/models/invoice-item.ts +6 -1
- package/api/src/store/models/invoice.ts +41 -22
- package/api/src/store/models/payment-intent.ts +4 -0
- package/api/src/store/models/setup-intent.ts +4 -0
- package/api/src/store/models/subscription-item.ts +0 -4
- package/api/src/store/models/subscription.ts +77 -44
- package/api/src/store/models/types.ts +1 -0
- package/api/src/store/sequelize.ts +6 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/jest.config.js +14 -0
- package/package.json +24 -19
- package/src/components/blockchain/tx.tsx +20 -11
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/invoice/table.tsx +58 -19
- package/src/components/layout/admin.tsx +17 -5
- package/src/components/portal/invoice/list.tsx +12 -8
- package/src/components/portal/subscription/list.tsx +114 -77
- package/src/components/subscription/status.tsx +21 -19
- package/src/global.css +4 -0
- package/src/locales/en.tsx +14 -1
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/customers/customers/detail.tsx +47 -3
- package/src/pages/admin/overview.tsx +21 -1
- package/src/pages/admin/payments/intents/detail.tsx +12 -3
- package/src/pages/customer/invoice.tsx +15 -1
- package/src/pages/customer/subscription/index.tsx +9 -2
- package/tests/api/libs/subscription.spec.ts +45 -0
- /package/api/src/{schedule → crons}/index.ts +0 -0
- /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
- /package/api/src/{jobs → queues}/event.ts +0 -0
- /package/api/src/{jobs → queues}/notification.ts +0 -0
|
@@ -16,12 +16,9 @@ import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
|
16
16
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
17
17
|
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
18
18
|
import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
19
|
-
import { invoiceQueue } from '../jobs/invoice';
|
|
20
|
-
import { paymentQueue } from '../jobs/payment';
|
|
21
|
-
import { subscriptionQueue } from '../jobs/subscription';
|
|
22
19
|
import dayjs from '../libs/dayjs';
|
|
23
20
|
import logger from '../libs/logger';
|
|
24
|
-
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
21
|
+
import { isBalanceSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
|
|
25
22
|
import { authenticate } from '../libs/security';
|
|
26
23
|
import {
|
|
27
24
|
canUpsell,
|
|
@@ -35,11 +32,15 @@ import {
|
|
|
35
32
|
getSupportedPaymentMethods,
|
|
36
33
|
isLineItemAligned,
|
|
37
34
|
} from '../libs/session';
|
|
35
|
+
import { getDaysUntilDue } from '../libs/subscription';
|
|
38
36
|
import { createCodeGenerator, formatMetadata, getMetadataFromQuery } from '../libs/util';
|
|
37
|
+
import { invoiceQueue } from '../queues/invoice';
|
|
38
|
+
import { paymentQueue } from '../queues/payment';
|
|
39
|
+
import { subscriptionQueue } from '../queues/subscription';
|
|
39
40
|
import type { TPriceExpanded, TProductExpanded } from '../store/models';
|
|
40
41
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
41
42
|
import { Customer } from '../store/models/customer';
|
|
42
|
-
import { PaymentCurrency
|
|
43
|
+
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
43
44
|
import { PaymentIntent } from '../store/models/payment-intent';
|
|
44
45
|
import { PaymentLink } from '../store/models/payment-link';
|
|
45
46
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
@@ -186,9 +187,8 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
186
187
|
|
|
187
188
|
export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession) {
|
|
188
189
|
const items = await Price.expand(checkoutSession.line_items);
|
|
189
|
-
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
190
190
|
const includeTrial = !!checkoutSession.subscription_data?.trial_period_days;
|
|
191
|
-
const amount = getCheckoutAmount(items,
|
|
191
|
+
const amount = getCheckoutAmount(items, checkoutSession.currency_id, includeTrial);
|
|
192
192
|
return {
|
|
193
193
|
amount_subtotal: amount.subtotal,
|
|
194
194
|
amount_total: amount.total,
|
|
@@ -333,6 +333,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
333
333
|
raw.metadata = {
|
|
334
334
|
...link.metadata,
|
|
335
335
|
...getMetadataFromQuery(req.query),
|
|
336
|
+
days_until_due: getDaysUntilDue(req.query),
|
|
336
337
|
passport: await checkPassportForPaymentLink(link),
|
|
337
338
|
preview: '1',
|
|
338
339
|
};
|
|
@@ -341,6 +342,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
341
342
|
raw.metadata = {
|
|
342
343
|
...link.metadata,
|
|
343
344
|
...getMetadataFromQuery(req.query),
|
|
345
|
+
days_until_due: getDaysUntilDue(req.query),
|
|
344
346
|
passport: await checkPassportForPaymentLink(link),
|
|
345
347
|
};
|
|
346
348
|
}
|
|
@@ -445,7 +447,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
445
447
|
// always update payment amount in case currency has changed
|
|
446
448
|
const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
447
449
|
const trialInDays = checkoutSession.subscription_data?.trial_period_days || 0;
|
|
448
|
-
const amount = getCheckoutAmount(lineItems, paymentCurrency, !!trialInDays);
|
|
450
|
+
const amount = getCheckoutAmount(lineItems, paymentCurrency.id, !!trialInDays);
|
|
449
451
|
await checkoutSession.update({
|
|
450
452
|
amount_subtotal: amount.subtotal,
|
|
451
453
|
amount_total: amount.total,
|
|
@@ -581,7 +583,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
581
583
|
payment_method_id: paymentMethod.id,
|
|
582
584
|
last_setup_error: null,
|
|
583
585
|
});
|
|
584
|
-
logger.info('
|
|
586
|
+
logger.info('setupIntent reset on checkout session submit', {
|
|
585
587
|
session: checkoutSession.id,
|
|
586
588
|
intent: setupIntent.id,
|
|
587
589
|
});
|
|
@@ -599,13 +601,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
599
601
|
usage: 'off_session',
|
|
600
602
|
metadata: checkoutSession.metadata,
|
|
601
603
|
});
|
|
604
|
+
|
|
605
|
+
// persist setup intent id
|
|
606
|
+
await checkoutSession.update({ setup_intent_id: setupIntent.id });
|
|
602
607
|
logger.info('setupIntent created on checkout session submit', {
|
|
603
608
|
session: checkoutSession.id,
|
|
604
609
|
intent: setupIntent.id,
|
|
605
610
|
});
|
|
606
|
-
|
|
607
|
-
// persist setup intent id
|
|
608
|
-
await checkoutSession.update({ setup_intent_id: setupIntent.id });
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
613
|
|
|
@@ -627,7 +629,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
627
629
|
pending_setup_intent: setupIntent?.id,
|
|
628
630
|
});
|
|
629
631
|
} else {
|
|
630
|
-
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency, trialInDays);
|
|
632
|
+
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays);
|
|
631
633
|
subscription = await Subscription.create({
|
|
632
634
|
livemode: !!checkoutSession.livemode,
|
|
633
635
|
currency_id: paymentCurrency.id,
|
|
@@ -649,6 +651,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
649
651
|
default_payment_method_id: paymentMethod.id,
|
|
650
652
|
cancel_at_period_end: false,
|
|
651
653
|
collection_method: 'charge_automatically',
|
|
654
|
+
proration_behavior: 'none',
|
|
655
|
+
payment_behavior: 'default_incomplete',
|
|
656
|
+
days_until_due: checkoutSession.metadata?.days_until_due,
|
|
652
657
|
metadata: checkoutSession.metadata as any,
|
|
653
658
|
});
|
|
654
659
|
|
|
@@ -678,30 +683,61 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
678
683
|
items: items.map((x) => x.id),
|
|
679
684
|
});
|
|
680
685
|
|
|
681
|
-
// lock prices used by this subscription
|
|
682
|
-
await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.upsell_price_id || x.price_id) } });
|
|
683
|
-
|
|
684
686
|
// persist subscription id
|
|
685
687
|
await checkoutSession.update({ subscription_id: subscription.id });
|
|
686
688
|
}
|
|
687
689
|
}
|
|
688
690
|
|
|
691
|
+
let isPaymentFromBalance = false;
|
|
692
|
+
const fastCheckoutAmount = getFastCheckoutAmount(
|
|
693
|
+
lineItems,
|
|
694
|
+
checkoutSession.mode,
|
|
695
|
+
paymentCurrency.id,
|
|
696
|
+
!!trialInDays
|
|
697
|
+
);
|
|
698
|
+
const paymentSettings = {
|
|
699
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
700
|
+
payment_method_options: {
|
|
701
|
+
arcblock: { payer: customer.did },
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// if we can complete purchase with customer balance
|
|
706
|
+
const balance = isBalanceSufficientForPayment({
|
|
707
|
+
paymentMethod,
|
|
708
|
+
paymentCurrency,
|
|
709
|
+
customer,
|
|
710
|
+
amount: fastCheckoutAmount,
|
|
711
|
+
});
|
|
712
|
+
if (balance.sufficient) {
|
|
713
|
+
if (checkoutSession.mode === 'payment' && paymentIntent) {
|
|
714
|
+
await paymentIntent.update({ status: 'requires_capture' });
|
|
715
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
|
|
716
|
+
|
|
717
|
+
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
718
|
+
if (invoice) {
|
|
719
|
+
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
720
|
+
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
721
|
+
} else {
|
|
722
|
+
paymentQueue.push({
|
|
723
|
+
id: paymentIntent.id,
|
|
724
|
+
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
isPaymentFromBalance = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
689
732
|
// if we can complete purchase without any wallet interaction
|
|
690
|
-
const fastCheckoutAmount = getFastCheckoutAmount(lineItems, checkoutSession.mode, paymentCurrency, !!trialInDays);
|
|
691
733
|
const delegation = await isDelegationSufficientForPayment({
|
|
692
734
|
paymentMethod,
|
|
693
735
|
paymentCurrency,
|
|
694
736
|
userDid: customer.did,
|
|
695
737
|
amount: fastCheckoutAmount,
|
|
696
738
|
});
|
|
697
|
-
if (delegation.sufficient) {
|
|
698
|
-
const paymentSettings = {
|
|
699
|
-
payment_method_types: checkoutSession.payment_method_types,
|
|
700
|
-
payment_method_options: {
|
|
701
|
-
arcblock: { payer: delegation.delegator as string },
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
739
|
|
|
740
|
+
if (delegation.sufficient) {
|
|
705
741
|
// all subscription payments are done after delegation
|
|
706
742
|
if (checkoutSession.mode === 'subscription' && subscription) {
|
|
707
743
|
await subscription.update({ payment_settings: paymentSettings });
|
|
@@ -716,7 +752,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
716
752
|
runAt: subscription.trail_end || subscription.current_period_end,
|
|
717
753
|
});
|
|
718
754
|
}
|
|
719
|
-
if (checkoutSession.mode === 'payment' && paymentIntent) {
|
|
755
|
+
if (checkoutSession.mode === 'payment' && paymentIntent && !isPaymentFromBalance) {
|
|
756
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
|
|
720
757
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
721
758
|
if (invoice) {
|
|
722
759
|
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
@@ -784,7 +821,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
784
821
|
}
|
|
785
822
|
}
|
|
786
823
|
|
|
787
|
-
return res.json({
|
|
824
|
+
return res.json({
|
|
825
|
+
paymentIntent,
|
|
826
|
+
setupIntent,
|
|
827
|
+
stripeContext,
|
|
828
|
+
subscription,
|
|
829
|
+
checkoutSession,
|
|
830
|
+
customer,
|
|
831
|
+
delegation,
|
|
832
|
+
balance,
|
|
833
|
+
});
|
|
788
834
|
} catch (err) {
|
|
789
835
|
console.error(err);
|
|
790
836
|
res.status(500).json({ code: err.code, error: err.message });
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Transaction, TransferV3Tx } from '@ocap/client';
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
3
|
|
|
4
|
-
import { invoiceQueue } from '../../jobs/invoice';
|
|
5
|
-
import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
|
|
6
4
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
5
|
import { wallet } from '../../libs/auth';
|
|
8
6
|
import { events } from '../../libs/event';
|
|
9
7
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
10
8
|
import { getTxMetadata } from '../../libs/util';
|
|
9
|
+
import { invoiceQueue } from '../../queues/invoice';
|
|
10
|
+
import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
|
|
11
11
|
import { Subscription } from '../../store/models';
|
|
12
12
|
import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
|
|
13
13
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Transaction, TransferV3Tx } from '@ocap/client';
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
3
|
|
|
4
|
-
import { handlePaymentSucceed } from '../../jobs/payment';
|
|
5
4
|
import type { CallbackArgs } from '../../libs/auth';
|
|
6
5
|
import { wallet } from '../../libs/auth';
|
|
7
6
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
8
7
|
import { getTxMetadata } from '../../libs/util';
|
|
8
|
+
import { handlePaymentSucceed } from '../../queues/payment';
|
|
9
9
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
10
10
|
|
|
11
11
|
export default {
|
|
@@ -3,12 +3,12 @@ import { toDelegateAddress } from '@arcblock/did-util';
|
|
|
3
3
|
import type { Transaction } from '@ocap/client';
|
|
4
4
|
import { fromPublicKey } from '@ocap/wallet';
|
|
5
5
|
|
|
6
|
-
import { subscriptionQueue } from '../../jobs/subscription';
|
|
7
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
8
7
|
import { wallet } from '../../libs/auth';
|
|
9
8
|
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
10
9
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
11
10
|
import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
|
|
11
|
+
import { subscriptionQueue } from '../../queues/subscription';
|
|
12
12
|
import type { TLineItemExpanded } from '../../store/models';
|
|
13
13
|
import { ensureSetupIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
|
|
14
14
|
|
|
@@ -36,9 +36,16 @@ export default {
|
|
|
36
36
|
const amount = getFastCheckoutAmount(
|
|
37
37
|
checkoutSession.line_items as TLineItemExpanded[],
|
|
38
38
|
checkoutSession.mode,
|
|
39
|
-
paymentCurrency
|
|
39
|
+
paymentCurrency.id
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const tokenLimits = await getTokenLimitsForDelegation(
|
|
43
|
+
checkoutSession,
|
|
44
|
+
paymentMethod,
|
|
45
|
+
paymentCurrency,
|
|
46
|
+
address,
|
|
47
|
+
amount
|
|
40
48
|
);
|
|
41
|
-
const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
|
|
42
49
|
const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
|
|
43
50
|
|
|
44
51
|
return {
|
|
@@ -4,11 +4,11 @@ import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/blockch
|
|
|
4
4
|
import { blocklet } from '../../libs/auth';
|
|
5
5
|
import dayjs from '../../libs/dayjs';
|
|
6
6
|
import logger from '../../libs/logger';
|
|
7
|
-
import { getFastCheckoutAmount, getStatementDescriptor } from '../../libs/session';
|
|
7
|
+
import { getFastCheckoutAmount, getPriceUintAmountByCurrency, getStatementDescriptor } from '../../libs/session';
|
|
8
8
|
import type { TLineItemExpanded } from '../../store/models';
|
|
9
9
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
10
10
|
import { Customer } from '../../store/models/customer';
|
|
11
|
-
import { Invoice } from '../../store/models/invoice';
|
|
11
|
+
import { Invoice, TInvoice } from '../../store/models/invoice';
|
|
12
12
|
import { InvoiceItem } from '../../store/models/invoice-item';
|
|
13
13
|
import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
14
14
|
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
@@ -135,16 +135,16 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
|
|
|
135
135
|
if (checkoutSession.setup_intent_id) {
|
|
136
136
|
setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
|
|
137
137
|
if (!setupIntent) {
|
|
138
|
-
throw new Error('
|
|
138
|
+
throw new Error('Setup intent not found');
|
|
139
139
|
}
|
|
140
140
|
if (setupIntent.status === 'succeeded') {
|
|
141
|
-
throw new Error('
|
|
141
|
+
throw new Error('Setup intent completed');
|
|
142
142
|
}
|
|
143
143
|
if (setupIntent.status === 'canceled') {
|
|
144
|
-
throw new Error('
|
|
144
|
+
throw new Error('Setup intent canceled');
|
|
145
145
|
}
|
|
146
146
|
if (setupIntent.status === 'processing') {
|
|
147
|
-
throw new Error('
|
|
147
|
+
throw new Error('Setup intent processing');
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
customerId = setupIntent.customer_id;
|
|
@@ -245,48 +245,109 @@ export async function ensureInvoiceForCheckout({
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
const { invoice, items } = await ensureInvoiceAndItems({
|
|
249
|
+
customer,
|
|
250
|
+
subscription,
|
|
251
|
+
lineItems: await Price.expand(checkoutSession.line_items, { product: true }),
|
|
252
|
+
trailing: !!checkoutSession.subscription_data?.trial_period_days,
|
|
253
|
+
metered: false,
|
|
254
|
+
props: {
|
|
255
|
+
livemode: checkoutSession.livemode,
|
|
256
|
+
description:
|
|
257
|
+
checkoutSession.invoice_creation?.invoice_data?.description ||
|
|
258
|
+
paymentIntent?.description ||
|
|
259
|
+
'Subscription creation',
|
|
260
|
+
statement_descriptor: paymentIntent?.statement_descriptor || getStatementDescriptor(checkoutSession.line_items),
|
|
261
|
+
period_start: subscription?.current_period_start ?? 0,
|
|
262
|
+
period_end: subscription?.current_period_end ?? 0,
|
|
263
|
+
|
|
264
|
+
auto_advance: !paymentIntent,
|
|
265
|
+
billing_reason: subscription ? 'subscription_create' : 'manual',
|
|
266
|
+
|
|
267
|
+
currency_id: checkoutSession.currency_id,
|
|
268
|
+
payment_intent_id: paymentIntent?.id,
|
|
269
|
+
checkout_session_id: checkoutSession.id,
|
|
270
|
+
|
|
271
|
+
total: checkoutSession.amount_subtotal,
|
|
272
|
+
|
|
273
|
+
default_payment_method_id: (subscription?.default_payment_method_id ||
|
|
274
|
+
paymentIntent?.payment_method_id) as string,
|
|
275
|
+
|
|
276
|
+
custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
|
|
277
|
+
footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
|
|
278
|
+
metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
|
|
279
|
+
} as Invoice,
|
|
280
|
+
});
|
|
281
|
+
logger.info(`Invoice created for checkoutSession ${checkoutSession.id}: ${invoice.id}`);
|
|
282
|
+
|
|
283
|
+
// persist invoice id
|
|
284
|
+
await checkoutSession.update({ invoice_id: invoice.id });
|
|
285
|
+
if (paymentIntent) {
|
|
286
|
+
await paymentIntent.update({ invoice_id: invoice.id });
|
|
287
|
+
}
|
|
288
|
+
if (subscription) {
|
|
289
|
+
await subscription.update({ latest_invoice_id: invoice.id });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { invoice, items };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function ensureInvoiceAndItems({
|
|
296
|
+
customer,
|
|
297
|
+
subscription,
|
|
298
|
+
props,
|
|
299
|
+
lineItems,
|
|
300
|
+
trailing,
|
|
301
|
+
metered,
|
|
302
|
+
}: {
|
|
303
|
+
customer: Customer;
|
|
304
|
+
subscription?: Subscription;
|
|
305
|
+
props: TInvoice;
|
|
306
|
+
lineItems: TLineItemExpanded[];
|
|
307
|
+
trailing: boolean; // do we have trailing
|
|
308
|
+
metered: boolean; // is the quantity metered
|
|
309
|
+
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
|
|
248
310
|
const invoice = await Invoice.create({
|
|
249
|
-
livemode:
|
|
311
|
+
livemode: props.livemode,
|
|
250
312
|
number: await customer.getInvoiceNumber(),
|
|
251
|
-
description:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
period_end: subscription?.current_period_end ?? 0,
|
|
258
|
-
|
|
259
|
-
auto_advance: !paymentIntent,
|
|
313
|
+
description: props.description,
|
|
314
|
+
statement_descriptor: props.statement_descriptor,
|
|
315
|
+
period_start: props.period_start,
|
|
316
|
+
period_end: props.period_end,
|
|
317
|
+
|
|
318
|
+
auto_advance: props.auto_advance,
|
|
260
319
|
paid: false,
|
|
261
320
|
paid_out_of_band: false,
|
|
262
321
|
|
|
263
|
-
status: 'open',
|
|
322
|
+
status: props.status || 'open',
|
|
264
323
|
collection_method: 'charge_automatically',
|
|
265
|
-
billing_reason:
|
|
324
|
+
billing_reason: props.billing_reason,
|
|
266
325
|
|
|
267
|
-
currency_id:
|
|
326
|
+
currency_id: props.currency_id,
|
|
268
327
|
customer_id: customer.id,
|
|
269
|
-
payment_intent_id:
|
|
328
|
+
payment_intent_id: props.payment_intent_id || '',
|
|
270
329
|
subscription_id: subscription?.id,
|
|
271
|
-
checkout_session_id:
|
|
330
|
+
checkout_session_id: props.checkout_session_id || '',
|
|
272
331
|
|
|
273
|
-
subtotal:
|
|
274
|
-
subtotal_excluding_tax:
|
|
332
|
+
subtotal: props.total || '0',
|
|
333
|
+
subtotal_excluding_tax: props.total || '0',
|
|
275
334
|
tax: '0',
|
|
276
|
-
total:
|
|
277
|
-
amount_due:
|
|
335
|
+
total: props.total || '0',
|
|
336
|
+
amount_due: props.total || '0',
|
|
278
337
|
amount_paid: '0',
|
|
279
|
-
amount_remaining:
|
|
338
|
+
amount_remaining: props.total || '0',
|
|
280
339
|
amount_shipping: '0',
|
|
281
340
|
|
|
282
341
|
starting_balance: '0',
|
|
283
342
|
ending_balance: '0',
|
|
343
|
+
starting_token_balance: {},
|
|
344
|
+
ending_token_balance: {},
|
|
284
345
|
|
|
285
346
|
attempt_count: 0,
|
|
286
347
|
attempted: false,
|
|
287
348
|
// next_payment_attempt: undefined,
|
|
288
349
|
|
|
289
|
-
custom_fields:
|
|
350
|
+
custom_fields: props.custom_fields || [],
|
|
290
351
|
customer_address: customer.address,
|
|
291
352
|
customer_email: customer.email,
|
|
292
353
|
customer_name: customer.name,
|
|
@@ -302,31 +363,19 @@ export async function ensureInvoiceForCheckout({
|
|
|
302
363
|
},
|
|
303
364
|
|
|
304
365
|
payment_settings: subscription?.payment_settings,
|
|
305
|
-
default_payment_method_id:
|
|
366
|
+
default_payment_method_id: props.default_payment_method_id,
|
|
306
367
|
|
|
307
368
|
account_country: '',
|
|
308
369
|
account_name: '',
|
|
309
|
-
footer:
|
|
310
|
-
metadata:
|
|
370
|
+
footer: props.footer || '',
|
|
371
|
+
metadata: props.metadata || {},
|
|
311
372
|
});
|
|
312
|
-
logger.info(`Invoice created for checkoutSession ${checkoutSession.id}: ${invoice.id}`);
|
|
313
|
-
|
|
314
|
-
// persist invoice id
|
|
315
|
-
await checkoutSession.update({ invoice_id: invoice.id });
|
|
316
|
-
if (paymentIntent) {
|
|
317
|
-
await paymentIntent.update({ invoice_id: invoice.id });
|
|
318
|
-
}
|
|
319
|
-
if (subscription) {
|
|
320
|
-
await subscription.update({ latest_invoice_id: invoice.id });
|
|
321
|
-
}
|
|
322
373
|
|
|
323
374
|
// create invoice items: for those require payment this time
|
|
324
375
|
const subscriptionItems = subscription
|
|
325
376
|
? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
|
|
326
377
|
: [];
|
|
327
|
-
const lineItems = await Price.expand(checkoutSession.line_items, { product: true });
|
|
328
378
|
|
|
329
|
-
const trailing = !!checkoutSession.subscription_data?.trial_period_days;
|
|
330
379
|
const getLineSetup = (x: TLineItemExpanded) => {
|
|
331
380
|
const price = x.upsell_price || x.price;
|
|
332
381
|
if (price.type === 'recurring' && trailing) {
|
|
@@ -335,14 +384,14 @@ export async function ensureInvoiceForCheckout({
|
|
|
335
384
|
// @ts-ignore
|
|
336
385
|
description: trailing ? `${price.product.name} (trailing)` : price.product.name,
|
|
337
386
|
period: {
|
|
338
|
-
start:
|
|
339
|
-
end:
|
|
387
|
+
start: props.period_start,
|
|
388
|
+
end: props.period_end,
|
|
340
389
|
},
|
|
341
390
|
};
|
|
342
391
|
}
|
|
343
392
|
|
|
344
393
|
return {
|
|
345
|
-
amount: new BN(price.
|
|
394
|
+
amount: new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
|
|
346
395
|
// @ts-ignore
|
|
347
396
|
description: price.product.name,
|
|
348
397
|
period: undefined,
|
|
@@ -355,7 +404,7 @@ export async function ensureInvoiceForCheckout({
|
|
|
355
404
|
const price = x.upsell_price || x.price;
|
|
356
405
|
let { quantity } = x;
|
|
357
406
|
if (price.type === 'recurring') {
|
|
358
|
-
if (price.recurring?.usage_type === 'metered') {
|
|
407
|
+
if (price.recurring?.usage_type === 'metered' && !metered) {
|
|
359
408
|
quantity = 0;
|
|
360
409
|
}
|
|
361
410
|
if (trailing) {
|
|
@@ -364,12 +413,12 @@ export async function ensureInvoiceForCheckout({
|
|
|
364
413
|
}
|
|
365
414
|
|
|
366
415
|
return InvoiceItem.create({
|
|
367
|
-
livemode:
|
|
416
|
+
livemode: !!props.livemode,
|
|
368
417
|
amount: quantity > 0 ? setup.amount : '0',
|
|
369
418
|
quantity,
|
|
370
419
|
description: setup.description,
|
|
371
420
|
period: setup.period,
|
|
372
|
-
currency_id:
|
|
421
|
+
currency_id: props.currency_id,
|
|
373
422
|
customer_id: customer.id,
|
|
374
423
|
price_id: x.price_id,
|
|
375
424
|
invoice_id: invoice.id,
|
|
@@ -461,7 +510,7 @@ export async function getTokenRequirements(
|
|
|
461
510
|
let amount = getFastCheckoutAmount(
|
|
462
511
|
checkoutSession.line_items as TLineItemExpanded[],
|
|
463
512
|
checkoutSession.mode,
|
|
464
|
-
paymentCurrency,
|
|
513
|
+
paymentCurrency.id,
|
|
465
514
|
!!checkoutSession.subscription_data?.trial_period_days
|
|
466
515
|
);
|
|
467
516
|
|
|
@@ -4,13 +4,13 @@ import type { Transaction } from '@ocap/client';
|
|
|
4
4
|
import { BN } from '@ocap/util';
|
|
5
5
|
import { fromPublicKey } from '@ocap/wallet';
|
|
6
6
|
|
|
7
|
-
import { invoiceQueue } from '../../jobs/invoice';
|
|
8
|
-
import { subscriptionQueue } from '../../jobs/subscription';
|
|
9
7
|
import type { CallbackArgs } from '../../libs/auth';
|
|
10
8
|
import { wallet } from '../../libs/auth';
|
|
11
9
|
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
12
10
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
13
11
|
import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
|
|
12
|
+
import { invoiceQueue } from '../../queues/invoice';
|
|
13
|
+
import { subscriptionQueue } from '../../queues/subscription';
|
|
14
14
|
import type { TLineItemExpanded } from '../../store/models';
|
|
15
15
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
|
|
16
16
|
|
|
@@ -38,9 +38,15 @@ export default {
|
|
|
38
38
|
const amount = getFastCheckoutAmount(
|
|
39
39
|
checkoutSession.line_items as TLineItemExpanded[],
|
|
40
40
|
checkoutSession.mode,
|
|
41
|
-
paymentCurrency
|
|
41
|
+
paymentCurrency.id
|
|
42
|
+
);
|
|
43
|
+
const tokenLimits = await getTokenLimitsForDelegation(
|
|
44
|
+
checkoutSession,
|
|
45
|
+
paymentMethod,
|
|
46
|
+
paymentCurrency,
|
|
47
|
+
address,
|
|
48
|
+
amount
|
|
42
49
|
);
|
|
43
|
-
const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
|
|
44
50
|
const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
|
|
45
51
|
|
|
46
52
|
return {
|
|
@@ -9,6 +9,7 @@ import { checkPassportForPricingTable } from '../integrations/blocklet/passport'
|
|
|
9
9
|
import logger from '../libs/logger';
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { isLineItemCurrencyAligned } from '../libs/session';
|
|
12
|
+
import { getDaysUntilDue } from '../libs/subscription';
|
|
12
13
|
import { formatMetadata, getMetadataFromQuery } from '../libs/util';
|
|
13
14
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
14
15
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -340,6 +341,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
|
|
|
340
341
|
metadata: {
|
|
341
342
|
...doc.metadata,
|
|
342
343
|
...getMetadataFromQuery(req.query),
|
|
344
|
+
days_until_due: getDaysUntilDue(req.query),
|
|
343
345
|
passport: await checkPassportForPricingTable(doc),
|
|
344
346
|
pricing_table_id: doc.id,
|
|
345
347
|
},
|
|
@@ -130,7 +130,7 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
130
130
|
const doc = await SubscriptionItem.findByPk(req.params.id);
|
|
131
131
|
|
|
132
132
|
if (!doc) {
|
|
133
|
-
return res.status(404).json({ error: '
|
|
133
|
+
return res.status(404).json({ error: 'subscription item not found' });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
if (req.body.clear_usage) {
|