payment-kit 1.13.174 → 1.13.176
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/crons/index.ts +20 -1
- package/api/src/integrations/blockchain/stake.ts +99 -1
- package/api/src/integrations/stripe/handlers/invoice.ts +4 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +63 -12
- package/api/src/libs/audit.ts +3 -3
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/libs/subscription.ts +35 -2
- package/api/src/queues/checkout-session.ts +98 -94
- package/api/src/queues/invoice.ts +23 -13
- package/api/src/queues/notification.ts +13 -11
- package/api/src/queues/payment.ts +27 -21
- package/api/src/queues/refund.ts +5 -5
- package/api/src/queues/subscription.ts +210 -49
- package/api/src/routes/checkout-sessions.ts +8 -40
- package/api/src/routes/connect/change-payment.ts +52 -38
- package/api/src/routes/connect/change-plan.ts +51 -39
- package/api/src/routes/connect/collect-batch.ts +1 -0
- package/api/src/routes/connect/collect.ts +2 -1
- package/api/src/routes/connect/pay.ts +1 -0
- package/api/src/routes/connect/setup.ts +70 -56
- package/api/src/routes/connect/shared.ts +162 -17
- package/api/src/routes/connect/subscribe.ts +60 -54
- package/api/src/routes/invoices.ts +5 -0
- package/api/src/routes/payment-intents.ts +6 -2
- package/api/src/store/models/subscription.ts +1 -6
- package/api/src/store/models/types.ts +5 -1
- package/api/tests/libs/subscription.spec.ts +85 -3
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +2 -1
- package/src/components/customer/link.tsx +22 -8
- package/src/components/event/list.tsx +1 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +12 -1
- package/src/pages/admin/payments/intents/detail.tsx +1 -1
- package/src/pages/admin/products/products/index.tsx +3 -0
- package/src/pages/customer/invoice/detail.tsx +7 -3
- package/src/pages/customer/subscription/detail.tsx +14 -5
- /package/api/src/libs/notification/template/{subscription-cacceled.ts → subscription-canceled.ts} +0 -0
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
/* eslint-disable prettier/prettier */
|
|
3
3
|
import { toTypeInfo } from '@arcblock/did';
|
|
4
|
-
import { toDelegateAddress } from '@arcblock/did-util';
|
|
5
|
-
import {
|
|
4
|
+
import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
|
|
5
|
+
import type { Transaction } from '@ocap/client';
|
|
6
|
+
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
6
7
|
import { fromPublicKey } from '@ocap/wallet';
|
|
7
8
|
import isEmpty from 'lodash/isEmpty';
|
|
8
9
|
|
|
9
10
|
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/blockchain/stake';
|
|
10
11
|
import { blocklet, wallet } from '../../libs/auth';
|
|
11
12
|
import dayjs from '../../libs/dayjs';
|
|
13
|
+
import env from '../../libs/env';
|
|
12
14
|
import logger from '../../libs/logger';
|
|
13
|
-
import { getTokenLimitsForDelegation } from '../../libs/payment';
|
|
15
|
+
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
16
|
+
import { getFastCheckoutAmount, getPriceUintAmountByCurrency, getStatementDescriptor } from '../../libs/session';
|
|
14
17
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
expandSubscriptionItems,
|
|
19
|
+
getSubscriptionCreateSetup,
|
|
20
|
+
getSubscriptionItemPrice,
|
|
21
|
+
getSubscriptionStakeSetup,
|
|
22
|
+
} from '../../libs/subscription';
|
|
23
|
+
import { OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
20
24
|
import type { TLineItemExpanded } from '../../store/models';
|
|
21
25
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
22
26
|
import { Customer } from '../../store/models/customer';
|
|
@@ -29,7 +33,6 @@ import { Price } from '../../store/models/price';
|
|
|
29
33
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
30
34
|
import { Subscription } from '../../store/models/subscription';
|
|
31
35
|
import { SubscriptionItem } from '../../store/models/subscription-item';
|
|
32
|
-
import { OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
33
36
|
|
|
34
37
|
type Result = {
|
|
35
38
|
checkoutSession: CheckoutSession;
|
|
@@ -589,21 +592,23 @@ export async function getDelegationTxClaim({
|
|
|
589
592
|
userPk,
|
|
590
593
|
nonce,
|
|
591
594
|
mode,
|
|
592
|
-
trial,
|
|
593
595
|
data,
|
|
594
596
|
items,
|
|
595
597
|
paymentCurrency,
|
|
596
598
|
paymentMethod,
|
|
599
|
+
trialInDays = 0,
|
|
600
|
+
billingThreshold = 0,
|
|
597
601
|
}: {
|
|
598
602
|
userDid: string;
|
|
599
603
|
userPk: string;
|
|
600
604
|
nonce: string;
|
|
601
|
-
|
|
602
|
-
trial: boolean;
|
|
605
|
+
mode: string;
|
|
603
606
|
data: any;
|
|
604
607
|
items: TLineItemExpanded[];
|
|
605
608
|
paymentCurrency: PaymentCurrency;
|
|
606
609
|
paymentMethod: PaymentMethod;
|
|
610
|
+
trialInDays: number;
|
|
611
|
+
billingThreshold?: number;
|
|
607
612
|
}) {
|
|
608
613
|
const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
|
|
609
614
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
@@ -611,7 +616,8 @@ export async function getDelegationTxClaim({
|
|
|
611
616
|
const tokenRequirements = await getTokenRequirements({
|
|
612
617
|
items,
|
|
613
618
|
mode,
|
|
614
|
-
|
|
619
|
+
trialInDays,
|
|
620
|
+
billingThreshold,
|
|
615
621
|
paymentMethod,
|
|
616
622
|
paymentCurrency,
|
|
617
623
|
});
|
|
@@ -637,26 +643,102 @@ export async function getDelegationTxClaim({
|
|
|
637
643
|
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
638
644
|
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
639
645
|
},
|
|
646
|
+
meta: {
|
|
647
|
+
purpose: 'delegation',
|
|
648
|
+
address,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export async function getStakeTxClaim({
|
|
654
|
+
userDid,
|
|
655
|
+
userPk,
|
|
656
|
+
items,
|
|
657
|
+
subscription,
|
|
658
|
+
paymentCurrency,
|
|
659
|
+
paymentMethod,
|
|
660
|
+
}: {
|
|
661
|
+
userDid: string;
|
|
662
|
+
userPk: string;
|
|
663
|
+
subscription: Subscription;
|
|
664
|
+
items: TLineItemExpanded[];
|
|
665
|
+
paymentCurrency: PaymentCurrency;
|
|
666
|
+
paymentMethod: PaymentMethod;
|
|
667
|
+
}) {
|
|
668
|
+
// create staking amount
|
|
669
|
+
const billingThreshold = fromTokenToUnit(subscription.billing_thresholds?.amount_gte || 0, paymentCurrency.decimal);
|
|
670
|
+
const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, billingThreshold.toString());
|
|
671
|
+
const amount = staking.licensed.add(staking.metered).toString();
|
|
672
|
+
|
|
673
|
+
// create staking data
|
|
674
|
+
const client = paymentMethod.getOcapClient();
|
|
675
|
+
const address = toStakeAddress(userDid, wallet.address);
|
|
676
|
+
const { state } = await client.getStakeState({ address });
|
|
677
|
+
const data = {
|
|
678
|
+
type: 'json',
|
|
679
|
+
value: Object.assign(
|
|
680
|
+
{
|
|
681
|
+
appId: wallet.address,
|
|
682
|
+
},
|
|
683
|
+
JSON.parse(state?.data?.value || '{}'),
|
|
684
|
+
{ [subscription.id]: amount }
|
|
685
|
+
),
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const setup = getSubscriptionCreateSetup(items, paymentCurrency.id, 0, 0);
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
type: 'StakeTx',
|
|
692
|
+
description: 'Sign the staking to continue',
|
|
693
|
+
partialTx: {
|
|
694
|
+
from: userDid,
|
|
695
|
+
pk: userPk,
|
|
696
|
+
itx: {
|
|
697
|
+
address,
|
|
698
|
+
receiver: wallet.address,
|
|
699
|
+
slashers: [wallet.address],
|
|
700
|
+
revokeWaitingPeriod: setup.cycle.duration / 1000, // wait for at least 1 billing cycle
|
|
701
|
+
message: `Stake for subscription on ${env.appName}`,
|
|
702
|
+
inputs: [],
|
|
703
|
+
data,
|
|
704
|
+
},
|
|
705
|
+
signatures: [],
|
|
706
|
+
},
|
|
707
|
+
requirement: {
|
|
708
|
+
tokens: [{ address: paymentCurrency.contract as string, value: amount }],
|
|
709
|
+
},
|
|
710
|
+
nonce: `stake-${subscription.id}`,
|
|
711
|
+
chainInfo: {
|
|
712
|
+
type: paymentMethod.type,
|
|
713
|
+
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
714
|
+
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
715
|
+
},
|
|
716
|
+
meta: {
|
|
717
|
+
purpose: 'staking',
|
|
718
|
+
address,
|
|
719
|
+
},
|
|
640
720
|
};
|
|
641
721
|
}
|
|
642
722
|
|
|
643
723
|
export type TokenRequirementArgs = {
|
|
644
724
|
items: TLineItemExpanded[];
|
|
645
725
|
mode: string;
|
|
646
|
-
includeFreeTrial: boolean;
|
|
647
726
|
paymentMethod: PaymentMethod;
|
|
648
727
|
paymentCurrency: PaymentCurrency;
|
|
728
|
+
trialInDays: number;
|
|
729
|
+
billingThreshold: number;
|
|
649
730
|
};
|
|
650
731
|
|
|
651
732
|
export async function getTokenRequirements({
|
|
652
733
|
items,
|
|
653
734
|
mode,
|
|
654
|
-
includeFreeTrial,
|
|
655
735
|
paymentMethod,
|
|
656
736
|
paymentCurrency,
|
|
737
|
+
trialInDays = 0,
|
|
738
|
+
billingThreshold = 0,
|
|
657
739
|
}: TokenRequirementArgs) {
|
|
658
740
|
const tokenRequirements = [];
|
|
659
|
-
let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!
|
|
741
|
+
let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!trialInDays);
|
|
660
742
|
|
|
661
743
|
// If the app has not staked, we need to add the gas fee to the amount
|
|
662
744
|
if ((await hasStakedForGas(paymentMethod)) === false) {
|
|
@@ -680,10 +762,21 @@ export async function getTokenRequirements({
|
|
|
680
762
|
tokenRequirements.push({ address: paymentCurrency.contract as string, value: amount });
|
|
681
763
|
}
|
|
682
764
|
|
|
765
|
+
// Add stake requirement to token requirement
|
|
766
|
+
const staking = getSubscriptionStakeSetup(
|
|
767
|
+
items,
|
|
768
|
+
paymentCurrency.id,
|
|
769
|
+
fromTokenToUnit(billingThreshold, paymentCurrency.decimal).toString()
|
|
770
|
+
);
|
|
771
|
+
const exist = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
|
|
772
|
+
if (exist) {
|
|
773
|
+
exist.value = new BN(exist.value).add(staking.licensed).add(staking.metered).toString();
|
|
774
|
+
}
|
|
775
|
+
|
|
683
776
|
return tokenRequirements;
|
|
684
777
|
}
|
|
685
778
|
|
|
686
|
-
export async function ensureSubscription(subscriptionId: string): Promise<Result> {
|
|
779
|
+
export async function ensureSubscription(subscriptionId: string): Promise<Result & { customer: Customer }> {
|
|
687
780
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
688
781
|
if (!subscription) {
|
|
689
782
|
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
@@ -722,6 +815,8 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
|
|
|
722
815
|
paymentCurrency,
|
|
723
816
|
// @ts-ignore
|
|
724
817
|
invoice,
|
|
818
|
+
// @ts-ignore
|
|
819
|
+
customer: await Customer.findByPk(subscription.customer_id),
|
|
725
820
|
};
|
|
726
821
|
}
|
|
727
822
|
|
|
@@ -770,6 +865,7 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
770
865
|
setupIntent,
|
|
771
866
|
paymentMethod,
|
|
772
867
|
paymentCurrency,
|
|
868
|
+
customer: await Customer.findByPk(subscription.customer_id),
|
|
773
869
|
};
|
|
774
870
|
}
|
|
775
871
|
|
|
@@ -803,3 +899,52 @@ export async function ensureSubscriptionForCollectBatch(subscriptionId: string,
|
|
|
803
899
|
invoices: detail[currencyId],
|
|
804
900
|
};
|
|
805
901
|
}
|
|
902
|
+
|
|
903
|
+
export async function executeOcapTransactions(
|
|
904
|
+
userDid: string,
|
|
905
|
+
userPk: string,
|
|
906
|
+
claims: any[],
|
|
907
|
+
paymentMethod: PaymentMethod
|
|
908
|
+
) {
|
|
909
|
+
const client = paymentMethod.getOcapClient();
|
|
910
|
+
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
911
|
+
const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
|
|
912
|
+
const transactions = [
|
|
913
|
+
[delegation, 'Delegate'],
|
|
914
|
+
[staking, 'Stake'],
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
const [delegationTxHash, stakingTxHash] = await Promise.all(
|
|
918
|
+
transactions.map(async ([claim, type]) => {
|
|
919
|
+
if (!claim) {
|
|
920
|
+
return '';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
|
|
924
|
+
if (claim.sig) {
|
|
925
|
+
tx.signature = claim.sig;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// @ts-ignore
|
|
929
|
+
const { buffer } = await client[`encode${type}Tx`]({ tx });
|
|
930
|
+
// @ts-ignore
|
|
931
|
+
const txHash = await client[`send${type}Tx`](
|
|
932
|
+
// @ts-ignore
|
|
933
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
934
|
+
getGasPayerExtra(buffer)
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
return txHash;
|
|
938
|
+
})
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
tx_hash: delegationTxHash,
|
|
943
|
+
payer: userDid,
|
|
944
|
+
type: 'delegate',
|
|
945
|
+
staking: {
|
|
946
|
+
tx_hash: stakingTxHash,
|
|
947
|
+
address: toStakeAddress(userDid, wallet.address),
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { toTypeInfo } from '@arcblock/did';
|
|
2
|
-
import type { Transaction } from '@ocap/client';
|
|
3
|
-
import { BN } from '@ocap/util';
|
|
4
|
-
import { fromPublicKey } from '@ocap/wallet';
|
|
5
|
-
|
|
6
1
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
|
-
import
|
|
2
|
+
import logger from '../../libs/logger';
|
|
3
|
+
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
4
|
+
import { getFastCheckoutAmount } from '../../libs/session';
|
|
8
5
|
import { getTxMetadata } from '../../libs/util';
|
|
9
6
|
import { invoiceQueue } from '../../queues/invoice';
|
|
10
7
|
import { addSubscriptionJob } from '../../queues/subscription';
|
|
11
8
|
import type { TLineItemExpanded } from '../../store/models';
|
|
12
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ensureInvoiceForCheckout,
|
|
11
|
+
ensurePaymentIntent,
|
|
12
|
+
executeOcapTransactions,
|
|
13
|
+
getAuthPrincipalClaim,
|
|
14
|
+
getDelegationTxClaim,
|
|
15
|
+
getStakeTxClaim,
|
|
16
|
+
} from './shared';
|
|
13
17
|
|
|
14
18
|
export default {
|
|
15
19
|
action: 'subscription',
|
|
@@ -22,7 +26,7 @@ export default {
|
|
|
22
26
|
},
|
|
23
27
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
24
28
|
const { checkoutSessionId } = extraParams;
|
|
25
|
-
const { checkoutSession, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
|
|
29
|
+
const { checkoutSession, paymentMethod, paymentCurrency, subscription, customer } = await ensurePaymentIntent(
|
|
26
30
|
checkoutSessionId,
|
|
27
31
|
userDid
|
|
28
32
|
);
|
|
@@ -31,28 +35,59 @@ export default {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
if (paymentMethod.type === 'arcblock') {
|
|
38
|
+
const claims: { [type: string]: [string, object] } = {};
|
|
39
|
+
|
|
34
40
|
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
41
|
+
const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
|
|
42
|
+
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
43
|
+
const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, !!trialInDays);
|
|
44
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
45
|
+
paymentMethod,
|
|
46
|
+
paymentCurrency,
|
|
47
|
+
userDid: customer.did,
|
|
48
|
+
amount: fastCheckoutAmount,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// if we can complete purchase without any wallet interaction
|
|
52
|
+
if (delegation.sufficient === false) {
|
|
53
|
+
claims.delegation = [
|
|
54
|
+
'signature',
|
|
55
|
+
await getDelegationTxClaim({
|
|
56
|
+
mode: checkoutSession.mode,
|
|
57
|
+
userDid,
|
|
58
|
+
userPk,
|
|
59
|
+
nonce: checkoutSession.id,
|
|
60
|
+
data: getTxMetadata({ subscriptionId: subscription.id, checkoutSessionId }),
|
|
61
|
+
paymentCurrency,
|
|
62
|
+
paymentMethod,
|
|
63
|
+
trialInDays,
|
|
64
|
+
billingThreshold,
|
|
65
|
+
items,
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
35
69
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
70
|
+
// we always need to stake for the subscription
|
|
71
|
+
claims.staking = [
|
|
72
|
+
'prepareTx',
|
|
73
|
+
await getStakeTxClaim({
|
|
39
74
|
userDid,
|
|
40
75
|
userPk,
|
|
41
|
-
nonce: checkoutSession.id,
|
|
42
|
-
data: getTxMetadata({ subscriptionId: subscription.id, checkoutSessionId }),
|
|
43
76
|
paymentCurrency,
|
|
44
77
|
paymentMethod,
|
|
45
|
-
trial: !!checkoutSession.subscription_data?.trial_period_days,
|
|
46
78
|
items,
|
|
79
|
+
subscription,
|
|
47
80
|
}),
|
|
48
|
-
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
return claims;
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
52
87
|
},
|
|
53
88
|
onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
54
89
|
const { checkoutSessionId } = extraParams;
|
|
55
|
-
const { checkoutSession, customer, paymentMethod,
|
|
90
|
+
const { checkoutSession, customer, paymentMethod, subscription } = await ensurePaymentIntent(
|
|
56
91
|
checkoutSessionId,
|
|
57
92
|
userDid
|
|
58
93
|
);
|
|
@@ -61,17 +96,6 @@ export default {
|
|
|
61
96
|
throw new Error('Subscription for checkoutSession not found');
|
|
62
97
|
}
|
|
63
98
|
|
|
64
|
-
if (checkoutSession.amount_total > '0') {
|
|
65
|
-
const client = paymentMethod.getOcapClient();
|
|
66
|
-
const result = await client.getAccountTokens({ address: userDid, token: paymentCurrency.contract });
|
|
67
|
-
const balance = result.tokens[0]?.balance || '0';
|
|
68
|
-
if (new BN(balance).lt(new BN(checkoutSession.amount_total))) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`Your account ${userDid} does not have enough ${paymentCurrency.symbol} to complete this subscription`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
99
|
if (paymentMethod.type === 'arcblock') {
|
|
76
100
|
await subscription.update({
|
|
77
101
|
payment_settings: {
|
|
@@ -83,37 +107,19 @@ export default {
|
|
|
83
107
|
});
|
|
84
108
|
|
|
85
109
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const claim = claims.find((x) => x.type === 'signature');
|
|
89
|
-
|
|
90
|
-
// execute the delegate tx
|
|
91
|
-
const tx: Partial<Transaction> = client.decodeTx(claim.origin);
|
|
92
|
-
tx.signature = claim.sig;
|
|
93
|
-
|
|
94
|
-
// @ts-ignore
|
|
95
|
-
const { buffer } = await client.encodeDelegateTx({ tx });
|
|
96
|
-
const txHash = await client.sendDelegateTx(
|
|
97
|
-
// @ts-ignore
|
|
98
|
-
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
99
|
-
getGasPayerExtra(buffer)
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
await subscription.update({
|
|
103
|
-
payment_details: {
|
|
104
|
-
arcblock: {
|
|
105
|
-
tx_hash: txHash,
|
|
106
|
-
payer: userDid,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
110
|
+
const paymentDetails = await executeOcapTransactions(userDid, userPk, claims, paymentMethod);
|
|
111
|
+
await subscription.update({ payment_details: { arcblock: paymentDetails } });
|
|
111
112
|
if (invoice) {
|
|
112
|
-
invoiceQueue.
|
|
113
|
+
invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
113
114
|
}
|
|
114
115
|
await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
|
|
116
|
+
logger.info('CheckoutSession updated on subscription done', {
|
|
117
|
+
checkoutSession: checkoutSession.id,
|
|
118
|
+
subscription: subscription.id,
|
|
119
|
+
paymentDetails,
|
|
120
|
+
});
|
|
115
121
|
|
|
116
|
-
return { hash:
|
|
122
|
+
return { hash: paymentDetails.tx_hash };
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
@@ -4,6 +4,7 @@ import Joi from 'joi';
|
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
6
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
7
|
+
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
7
8
|
import { getWhereFromKvQuery } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
9
10
|
import { expandLineItems } from '../libs/session';
|
|
@@ -175,6 +176,10 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
175
176
|
if (doc.status !== 'paid' && doc.metadata?.stripe_id) {
|
|
176
177
|
await syncStripeInvoice(doc);
|
|
177
178
|
}
|
|
179
|
+
if (doc.payment_intent_id) {
|
|
180
|
+
const paymentIntent = await PaymentIntent.findByPk(doc.payment_intent_id);
|
|
181
|
+
await syncStripePayment(paymentIntent!);
|
|
182
|
+
}
|
|
178
183
|
|
|
179
184
|
const json = doc.toJSON();
|
|
180
185
|
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
|
+
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
6
7
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
7
8
|
import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
|
|
8
9
|
import { authenticate } from '../libs/security';
|
|
@@ -175,12 +176,15 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
175
176
|
|
|
176
177
|
if (doc) {
|
|
177
178
|
const shouldSync = doc.status !== 'succeeded' || req.query.sync === '1';
|
|
178
|
-
if (
|
|
179
|
+
if (shouldSync) {
|
|
179
180
|
await syncStripePayment(doc);
|
|
180
181
|
}
|
|
182
|
+
invoice = await Invoice.findByPk(doc.invoice_id);
|
|
183
|
+
if (invoice?.metadata?.stripe_id) {
|
|
184
|
+
await syncStripeInvoice(invoice);
|
|
185
|
+
}
|
|
181
186
|
|
|
182
187
|
checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: doc.id } });
|
|
183
|
-
invoice = await Invoice.findByPk(doc.invoice_id);
|
|
184
188
|
if (invoice && invoice.subscription_id) {
|
|
185
189
|
subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
186
190
|
}
|
|
@@ -59,7 +59,7 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
59
59
|
| 'other',
|
|
60
60
|
string
|
|
61
61
|
>;
|
|
62
|
-
reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed', string>;
|
|
62
|
+
reason: LiteralUnion<'cancellation_requested' | 'payment_disputed' | 'payment_failed' | 'stake_revoked', string>;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
declare billing_cycle_anchor: number;
|
|
@@ -328,11 +328,6 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
// 发射 canceled 事件
|
|
332
|
-
if (model.status === 'canceled' && model.previous('status') !== model.status) {
|
|
333
|
-
createEvent('Subscription', 'customer.subscription.canceled', model, options).catch(console.error);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
331
|
createStatusEvent(
|
|
337
332
|
'Subscription',
|
|
338
333
|
'customer.subscription',
|
|
@@ -260,6 +260,11 @@ export type PaymentDetails = {
|
|
|
260
260
|
arcblock?: {
|
|
261
261
|
tx_hash: string;
|
|
262
262
|
payer: string;
|
|
263
|
+
type?: LiteralUnion<'slash' | 'transfer' | 'delegate', string>;
|
|
264
|
+
staking?: {
|
|
265
|
+
tx_hash: string;
|
|
266
|
+
address: string;
|
|
267
|
+
};
|
|
263
268
|
};
|
|
264
269
|
stripe?: {
|
|
265
270
|
payment_intent_id?: string;
|
|
@@ -434,7 +439,6 @@ export type EventType = LiteralUnion<
|
|
|
434
439
|
| 'customer.subscription.trial_end'
|
|
435
440
|
| 'customer.subscription.started'
|
|
436
441
|
| 'customer.subscription.updated'
|
|
437
|
-
| 'customer.subscription.canceled'
|
|
438
442
|
| 'customer.tax_id.created'
|
|
439
443
|
| 'customer.tax_id.deleted'
|
|
440
444
|
| 'customer.tax_id.updated'
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getMaxRetryCount,
|
|
7
7
|
getMinRetryMail,
|
|
8
8
|
getSubscriptionCreateSetup,
|
|
9
|
+
getSubscriptionStakeSetup,
|
|
9
10
|
shouldCancelSubscription,
|
|
10
11
|
} from '../../src/libs/subscription';
|
|
11
12
|
|
|
@@ -165,7 +166,7 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
165
166
|
expect(result.cycle.duration).toBe(24 * 60 * 60 * 1000);
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
it('should calculate trial period when trialInDays is provided', () => {
|
|
169
|
+
it('should calculate trial period when only trialInDays is provided', () => {
|
|
169
170
|
const items = [
|
|
170
171
|
{
|
|
171
172
|
price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
|
|
@@ -182,7 +183,7 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
182
183
|
);
|
|
183
184
|
});
|
|
184
185
|
|
|
185
|
-
it('should
|
|
186
|
+
it('should trialEnds overwrite trialInDays', () => {
|
|
186
187
|
const items = [
|
|
187
188
|
{
|
|
188
189
|
price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
|
|
@@ -199,7 +200,7 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
199
200
|
);
|
|
200
201
|
});
|
|
201
202
|
|
|
202
|
-
it('should calculate trial period when trialEnds is provided', () => {
|
|
203
|
+
it('should calculate trial period when only trialEnds is provided', () => {
|
|
203
204
|
const items = [
|
|
204
205
|
{
|
|
205
206
|
price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
|
|
@@ -305,3 +306,84 @@ describe('shouldCancelSubscription', () => {
|
|
|
305
306
|
expect(result).toBe(false);
|
|
306
307
|
});
|
|
307
308
|
});
|
|
309
|
+
|
|
310
|
+
describe('getSubscriptionStakeSetup', () => {
|
|
311
|
+
const items: any[] = [
|
|
312
|
+
{
|
|
313
|
+
price: {
|
|
314
|
+
type: 'one_time',
|
|
315
|
+
currency_options: [
|
|
316
|
+
{
|
|
317
|
+
currency_id: 'usd',
|
|
318
|
+
unit_amount: '1',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
quantity: 1,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
price: {
|
|
326
|
+
type: 'recurring',
|
|
327
|
+
currency_options: [
|
|
328
|
+
{
|
|
329
|
+
currency_id: 'usd',
|
|
330
|
+
unit_amount: '1',
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
recurring: {
|
|
334
|
+
interval: 'day',
|
|
335
|
+
interval_count: '1',
|
|
336
|
+
usage_type: 'licensed',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
quantity: 2,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
price: {
|
|
343
|
+
type: 'recurring',
|
|
344
|
+
currency_options: [
|
|
345
|
+
{
|
|
346
|
+
currency_id: 'usd',
|
|
347
|
+
unit_amount: '1',
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
recurring: {
|
|
351
|
+
interval: 'day',
|
|
352
|
+
interval_count: '1',
|
|
353
|
+
usage_type: 'metered',
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
quantity: 2,
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
it('should calculate staking for recurring licensed price type #1', () => {
|
|
361
|
+
const result = getSubscriptionStakeSetup(items, 'usd');
|
|
362
|
+
expect(result.licensed.toString()).toBe('2');
|
|
363
|
+
expect(result.metered.toString()).toBe('2');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should calculate staking for recurring licensed price type #2', () => {
|
|
367
|
+
const result = getSubscriptionStakeSetup(items.slice(0, 2), 'usd');
|
|
368
|
+
expect(result.licensed.toString()).toBe('2');
|
|
369
|
+
expect(result.metered.toString()).toBe('0');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should calculate staking for recurring metered price type when billingThreshold is 0 #1', () => {
|
|
373
|
+
const result = getSubscriptionStakeSetup(items.slice(2, 3), 'usd', '10');
|
|
374
|
+
expect(result.licensed.toString()).toBe('0');
|
|
375
|
+
expect(result.metered.toString()).toBe('10');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should calculate staking for recurring metered price type when billingThreshold is 0 #2', () => {
|
|
379
|
+
const result = getSubscriptionStakeSetup(items, 'usd', '0');
|
|
380
|
+
expect(result.licensed.toString()).toBe('2');
|
|
381
|
+
expect(result.metered.toString()).toBe('2');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should calculate staking for recurring metered price type when billingThreshold is greater than 0', () => {
|
|
385
|
+
const result = getSubscriptionStakeSetup(items, 'usd', '10');
|
|
386
|
+
expect(result.licensed.toString()).toBe('2');
|
|
387
|
+
expect(result.metered.toString()).toBe('10');
|
|
388
|
+
});
|
|
389
|
+
});
|
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.176",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@arcblock/jwt": "^1.18.110",
|
|
51
51
|
"@arcblock/ux": "^2.9.39",
|
|
52
52
|
"@blocklet/logger": "1.16.23",
|
|
53
|
-
"@blocklet/payment-react": "1.13.
|
|
53
|
+
"@blocklet/payment-react": "1.13.176",
|
|
54
54
|
"@blocklet/sdk": "1.16.23",
|
|
55
55
|
"@blocklet/ui-react": "^2.9.39",
|
|
56
56
|
"@blocklet/uploader": "^0.0.74",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@abtnode/types": "1.16.23",
|
|
112
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
113
|
-
"@blocklet/payment-types": "1.13.
|
|
113
|
+
"@blocklet/payment-types": "1.13.176",
|
|
114
114
|
"@types/cookie-parser": "^1.4.6",
|
|
115
115
|
"@types/cors": "^2.8.17",
|
|
116
116
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
"parser": "typescript"
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
|
-
"gitHead": "
|
|
152
|
+
"gitHead": "d39e02f65d5f7a168b3ee3d589b47c4d77c53125"
|
|
153
153
|
}
|