payment-kit 1.13.92 → 1.13.93
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/index.ts +2 -0
- package/api/src/libs/audit.ts +28 -34
- package/api/src/libs/payment.ts +2 -11
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/util.ts +8 -5
- package/api/src/routes/checkout-sessions.ts +41 -39
- package/api/src/routes/connect/collect.ts +12 -12
- package/api/src/routes/connect/setup.ts +8 -11
- package/api/src/routes/connect/shared.ts +81 -20
- package/api/src/routes/connect/subscribe.ts +8 -11
- package/api/src/routes/connect/update.ts +134 -0
- package/api/src/routes/pricing-table.ts +9 -121
- package/api/src/routes/subscriptions.ts +416 -141
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/pricing-table.ts +125 -1
- package/api/src/store/models/subscription.ts +4 -0
- package/api/src/store/models/types.ts +8 -0
- package/api/tests/libs/util.spec.ts +6 -6
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/app.tsx +12 -4
- package/src/components/checkout/form/address.tsx +41 -34
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/checkout/pricing-table.tsx +205 -0
- package/src/components/payment-link/product-select.tsx +13 -3
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/actions.tsx +153 -0
- package/src/components/portal/subscription/list.tsx +21 -150
- package/src/components/subscription/metrics.tsx +46 -0
- package/src/contexts/products.tsx +2 -1
- package/src/libs/util.ts +43 -0
- package/src/locales/en.tsx +15 -1
- package/src/locales/zh.tsx +16 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
- package/src/pages/checkout/pricing-table.tsx +9 -158
- package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
- package/src/pages/customer/subscription/update.tsx +281 -0
package/api/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import collectHandlers from './routes/connect/collect';
|
|
|
26
26
|
import payHandlers from './routes/connect/pay';
|
|
27
27
|
import setupHandlers from './routes/connect/setup';
|
|
28
28
|
import subscribeHandlers from './routes/connect/subscribe';
|
|
29
|
+
import updateHandlers from './routes/connect/update';
|
|
29
30
|
import { initialize } from './store/models';
|
|
30
31
|
import { sequelize } from './store/sequelize';
|
|
31
32
|
|
|
@@ -53,6 +54,7 @@ handlers.attach(Object.assign({ app: router }, collectHandlers));
|
|
|
53
54
|
handlers.attach(Object.assign({ app: router }, payHandlers));
|
|
54
55
|
handlers.attach(Object.assign({ app: router }, setupHandlers));
|
|
55
56
|
handlers.attach(Object.assign({ app: router }, subscribeHandlers));
|
|
57
|
+
handlers.attach(Object.assign({ app: router }, updateHandlers));
|
|
56
58
|
|
|
57
59
|
router.use('/api', routes);
|
|
58
60
|
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -16,24 +16,21 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
|
|
|
16
16
|
data.previous_attributes = pick(model._previousDataValues, options.fields);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const event = await Event.create(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
idempotency_key: '',
|
|
31
|
-
},
|
|
32
|
-
metadata: {},
|
|
33
|
-
pending_webhooks: 99, // force all events goto the event queue
|
|
19
|
+
const event = await Event.create({
|
|
20
|
+
type,
|
|
21
|
+
api_version: API_VERSION,
|
|
22
|
+
livemode: !!model.livemode,
|
|
23
|
+
object_id: model.id,
|
|
24
|
+
object_type: scope,
|
|
25
|
+
data,
|
|
26
|
+
request: {
|
|
27
|
+
// FIXME:
|
|
28
|
+
id: '',
|
|
29
|
+
idempotency_key: '',
|
|
34
30
|
},
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
metadata: {},
|
|
32
|
+
pending_webhooks: 99, // force all events goto the event queue
|
|
33
|
+
});
|
|
37
34
|
|
|
38
35
|
events.emit('event.created', { id: event.id });
|
|
39
36
|
events.emit(event.type, data.object);
|
|
@@ -61,24 +58,21 @@ export async function createStatusEvent(
|
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
const suffix = config[data.object.status];
|
|
64
|
-
const event = await Event.create(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
idempotency_key: '',
|
|
76
|
-
},
|
|
77
|
-
metadata: {},
|
|
78
|
-
pending_webhooks: 99, // force all events goto the event queue
|
|
61
|
+
const event = await Event.create({
|
|
62
|
+
type: [prefix, suffix].join('.'),
|
|
63
|
+
api_version: API_VERSION,
|
|
64
|
+
livemode: !!model.livemode,
|
|
65
|
+
object_id: model.id,
|
|
66
|
+
object_type: scope,
|
|
67
|
+
data,
|
|
68
|
+
request: {
|
|
69
|
+
// FIXME:
|
|
70
|
+
id: '',
|
|
71
|
+
idempotency_key: '',
|
|
79
72
|
},
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
metadata: {},
|
|
74
|
+
pending_webhooks: 99, // force all events goto the event queue
|
|
75
|
+
});
|
|
82
76
|
|
|
83
77
|
events.emit('event.created', { id: event.id });
|
|
84
78
|
events.emit(event.type, data.object);
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -8,15 +8,7 @@ import { BN, fromUnitToToken } from '@ocap/util';
|
|
|
8
8
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
9
|
import type { LiteralUnion } from 'type-fest';
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
CheckoutSession,
|
|
13
|
-
Invoice,
|
|
14
|
-
PaymentCurrency,
|
|
15
|
-
PaymentIntent,
|
|
16
|
-
PaymentMethod,
|
|
17
|
-
TCustomer,
|
|
18
|
-
TLineItemExpanded,
|
|
19
|
-
} from '../store/models';
|
|
11
|
+
import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
|
|
20
12
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
21
13
|
import { blocklet, wallet } from './auth';
|
|
22
14
|
import { OCAP_PAYMENT_TX_TYPE } from './util';
|
|
@@ -209,7 +201,7 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
|
|
|
209
201
|
}
|
|
210
202
|
|
|
211
203
|
export async function getTokenLimitsForDelegation(
|
|
212
|
-
|
|
204
|
+
items: TLineItemExpanded[],
|
|
213
205
|
paymentMethod: PaymentMethod,
|
|
214
206
|
paymentCurrency: PaymentCurrency,
|
|
215
207
|
address: string,
|
|
@@ -218,7 +210,6 @@ export async function getTokenLimitsForDelegation(
|
|
|
218
210
|
const client = paymentMethod.getOcapClient();
|
|
219
211
|
const { state } = await client.getDelegateState({ address });
|
|
220
212
|
|
|
221
|
-
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
222
213
|
const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
|
|
223
214
|
const allowance = hasMetered ? '0' : amount;
|
|
224
215
|
|
package/api/src/libs/session.ts
CHANGED
|
@@ -109,7 +109,7 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currencyI
|
|
|
109
109
|
setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
|
|
110
110
|
if (price.type === 'recurring') {
|
|
111
111
|
if (trialInDays === 0) {
|
|
112
|
-
subscription =
|
|
112
|
+
subscription = subscription.add(new BN(unit).mul(new BN(x.quantity)));
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
});
|
package/api/src/libs/util.ts
CHANGED
|
@@ -152,15 +152,18 @@ export function getTxMetadata(extra: Record<string, any> = {}): any {
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
export function
|
|
156
|
-
|
|
155
|
+
export function getDataObjectFromQuery(
|
|
156
|
+
query: Record<string, any> = {},
|
|
157
|
+
prefix: string = 'metadata'
|
|
158
|
+
): Record<string, any> {
|
|
159
|
+
const result: Record<string, any> = {};
|
|
157
160
|
Object.keys(query).forEach((key) => {
|
|
158
|
-
if (key.startsWith(
|
|
159
|
-
|
|
161
|
+
if (key.startsWith(`${prefix}.`) && query[key]) {
|
|
162
|
+
result[key.replace(`${prefix}.`, '')] = query[key];
|
|
160
163
|
}
|
|
161
164
|
});
|
|
162
165
|
|
|
163
|
-
return
|
|
166
|
+
return result;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
// @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
isLineItemAligned,
|
|
34
34
|
} from '../libs/session';
|
|
35
35
|
import { getDaysUntilDue } from '../libs/subscription';
|
|
36
|
-
import { createCodeGenerator, formatMetadata,
|
|
36
|
+
import { createCodeGenerator, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
37
37
|
import { invoiceQueue } from '../queues/invoice';
|
|
38
38
|
import { paymentQueue } from '../queues/payment';
|
|
39
39
|
import { subscriptionQueue } from '../queues/subscription';
|
|
@@ -304,6 +304,13 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
304
304
|
raw.created_via = 'portal';
|
|
305
305
|
raw.currency_id = link.currency_id || req.currency.id;
|
|
306
306
|
raw.payment_link_id = link.id;
|
|
307
|
+
raw.subscription_data = merge(
|
|
308
|
+
{
|
|
309
|
+
description: '',
|
|
310
|
+
trial_period_days: 0,
|
|
311
|
+
},
|
|
312
|
+
getDataObjectFromQuery(req.query, 'subscription_data')
|
|
313
|
+
);
|
|
307
314
|
|
|
308
315
|
if (link.after_completion?.hosted_confirmation?.custom_message) {
|
|
309
316
|
raw.payment_intent_data = {
|
|
@@ -332,7 +339,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
332
339
|
} else {
|
|
333
340
|
raw.metadata = {
|
|
334
341
|
...link.metadata,
|
|
335
|
-
...
|
|
342
|
+
...getDataObjectFromQuery(req.query),
|
|
336
343
|
days_until_due: getDaysUntilDue(req.query),
|
|
337
344
|
passport: await checkPassportForPaymentLink(link),
|
|
338
345
|
preview: '1',
|
|
@@ -341,7 +348,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
341
348
|
} else {
|
|
342
349
|
raw.metadata = {
|
|
343
350
|
...link.metadata,
|
|
344
|
-
...
|
|
351
|
+
...getDataObjectFromQuery(req.query),
|
|
345
352
|
days_until_due: getDaysUntilDue(req.query),
|
|
346
353
|
passport: await checkPassportForPaymentLink(link),
|
|
347
354
|
};
|
|
@@ -629,6 +636,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
629
636
|
pending_setup_intent: setupIntent?.id,
|
|
630
637
|
});
|
|
631
638
|
} else {
|
|
639
|
+
// FIXME: @wangshijun respect all checkoutSession.subscription_data fields
|
|
632
640
|
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays);
|
|
633
641
|
subscription = await Subscription.create({
|
|
634
642
|
livemode: !!checkoutSession.livemode,
|
|
@@ -637,7 +645,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
637
645
|
status: 'incomplete',
|
|
638
646
|
current_period_start: setup.period.start,
|
|
639
647
|
current_period_end: setup.period.end,
|
|
640
|
-
billing_cycle_anchor: setup.cycle.anchor,
|
|
648
|
+
billing_cycle_anchor: checkoutSession.subscription_data?.billing_cycle_anchor || setup.cycle.anchor,
|
|
641
649
|
start_date: dayjs().unix(),
|
|
642
650
|
trail_end: setup.trail.end,
|
|
643
651
|
trail_start: setup.trail.start,
|
|
@@ -651,7 +659,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
651
659
|
default_payment_method_id: paymentMethod.id,
|
|
652
660
|
cancel_at_period_end: false,
|
|
653
661
|
collection_method: 'charge_automatically',
|
|
654
|
-
|
|
662
|
+
description: checkoutSession.subscription_data?.description || '',
|
|
663
|
+
proration_behavior: checkoutSession.subscription_data?.proration_behavior || 'none',
|
|
655
664
|
payment_behavior: 'default_incomplete',
|
|
656
665
|
days_until_due: checkoutSession.metadata?.days_until_due,
|
|
657
666
|
metadata: checkoutSession.metadata as any,
|
|
@@ -688,7 +697,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
688
697
|
}
|
|
689
698
|
}
|
|
690
699
|
|
|
691
|
-
let isPaymentFromBalance = false;
|
|
692
700
|
const fastCheckoutAmount = getFastCheckoutAmount(
|
|
693
701
|
lineItems,
|
|
694
702
|
checkoutSession.mode,
|
|
@@ -709,11 +717,23 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
709
717
|
customer,
|
|
710
718
|
amount: fastCheckoutAmount,
|
|
711
719
|
});
|
|
712
|
-
if
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
720
|
+
// if we can complete purchase without any wallet interaction
|
|
721
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
722
|
+
paymentMethod,
|
|
723
|
+
paymentCurrency,
|
|
724
|
+
userDid: customer.did,
|
|
725
|
+
amount: fastCheckoutAmount,
|
|
726
|
+
});
|
|
716
727
|
|
|
728
|
+
if (checkoutSession.mode === 'payment' && paymentIntent) {
|
|
729
|
+
if (balance.sufficient) {
|
|
730
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
|
|
731
|
+
}
|
|
732
|
+
if (delegation.sufficient) {
|
|
733
|
+
logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
|
|
734
|
+
}
|
|
735
|
+
if (balance.sufficient || delegation.sufficient) {
|
|
736
|
+
await paymentIntent.update({ status: 'requires_capture' });
|
|
717
737
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
718
738
|
if (invoice) {
|
|
719
739
|
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
@@ -724,22 +744,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
724
744
|
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
725
745
|
});
|
|
726
746
|
}
|
|
727
|
-
|
|
728
|
-
isPaymentFromBalance = true;
|
|
729
747
|
}
|
|
730
748
|
}
|
|
731
749
|
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
if (delegation.sufficient) {
|
|
741
|
-
// all subscription payments are done after delegation
|
|
742
|
-
if (checkoutSession.mode === 'subscription' && subscription) {
|
|
750
|
+
// all subscription payments are done after delegation
|
|
751
|
+
if (checkoutSession.mode === 'subscription' && subscription) {
|
|
752
|
+
if (
|
|
753
|
+
delegation.sufficient || // we can pay from delegation
|
|
754
|
+
(balance.sufficient && ['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) // we can pay from balance
|
|
755
|
+
) {
|
|
743
756
|
await subscription.update({ payment_settings: paymentSettings });
|
|
744
757
|
|
|
745
758
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
|
|
@@ -752,28 +765,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
752
765
|
runAt: subscription.trail_end || subscription.current_period_end,
|
|
753
766
|
});
|
|
754
767
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
760
|
-
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
761
|
-
} else {
|
|
762
|
-
await paymentIntent.update({ status: 'requires_capture' });
|
|
763
|
-
paymentQueue.push({
|
|
764
|
-
id: paymentIntent.id,
|
|
765
|
-
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
|
|
771
|
+
if (delegation.sufficient) {
|
|
770
772
|
await setupIntent.update({ status: 'succeeded', ...paymentSettings });
|
|
771
773
|
await subscription.update({
|
|
772
774
|
status: subscription.trail_end ? 'trialing' : 'active',
|
|
773
775
|
payment_settings: paymentSettings,
|
|
774
776
|
});
|
|
775
777
|
await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
|
|
776
|
-
logger.info(`CheckoutSession ${checkoutSession.id} updated on
|
|
778
|
+
logger.info(`CheckoutSession ${checkoutSession.id} updated on setup done ${setupIntent.id}`);
|
|
777
779
|
}
|
|
778
780
|
}
|
|
779
781
|
|
|
@@ -25,17 +25,17 @@ export default {
|
|
|
25
25
|
const { invoiceId } = extraParams;
|
|
26
26
|
const { invoice, paymentIntent, paymentCurrency, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
|
|
27
27
|
|
|
28
|
-
const tokens = [{ address: paymentCurrency.contract as string, value: invoice.amount_due }];
|
|
29
|
-
// @ts-ignore
|
|
30
|
-
const itx: TransferV3Tx = {
|
|
31
|
-
outputs: [{ owner: wallet.address, tokens, assets: [] }],
|
|
32
|
-
data: getTxMetadata({
|
|
33
|
-
paymentIntentId: paymentIntent.id,
|
|
34
|
-
invoiceId,
|
|
35
|
-
}),
|
|
36
|
-
};
|
|
37
|
-
|
|
38
28
|
if (paymentMethod.type === 'arcblock') {
|
|
29
|
+
const tokens = [{ address: paymentCurrency.contract as string, value: invoice.amount_due }];
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const itx: TransferV3Tx = {
|
|
32
|
+
outputs: [{ owner: wallet.address, tokens, assets: [] }],
|
|
33
|
+
data: getTxMetadata({
|
|
34
|
+
paymentIntentId: paymentIntent.id,
|
|
35
|
+
invoiceId,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
39
|
return {
|
|
40
40
|
prepareTx: {
|
|
41
41
|
type: 'TransferV3Tx',
|
|
@@ -93,11 +93,11 @@ export default {
|
|
|
93
93
|
// cleanup the queue
|
|
94
94
|
let exist = await paymentQueue.get(paymentIntent.id);
|
|
95
95
|
if (exist) {
|
|
96
|
-
await paymentQueue.
|
|
96
|
+
await paymentQueue.delete(paymentIntent.id);
|
|
97
97
|
}
|
|
98
98
|
exist = await invoiceQueue.get(invoice.id);
|
|
99
99
|
if (exist) {
|
|
100
|
-
await invoiceQueue.
|
|
100
|
+
await invoiceQueue.delete(invoice.id);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
if (invoice.subscription_id) {
|
|
@@ -32,21 +32,18 @@ export default {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (paymentMethod.type === 'arcblock') {
|
|
35
|
+
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
35
36
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
36
|
-
const amount = getFastCheckoutAmount(
|
|
37
|
-
checkoutSession.line_items as TLineItemExpanded[],
|
|
38
|
-
checkoutSession.mode,
|
|
39
|
-
paymentCurrency.id
|
|
40
|
-
);
|
|
37
|
+
const amount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id);
|
|
41
38
|
|
|
42
|
-
const tokenLimits = await getTokenLimitsForDelegation(
|
|
43
|
-
|
|
39
|
+
const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
|
|
40
|
+
const tokenRequirements = await getTokenRequirements({
|
|
41
|
+
items,
|
|
42
|
+
mode: checkoutSession.mode,
|
|
43
|
+
includeFreeTrial: !!checkoutSession.subscription_data?.trial_period_days,
|
|
44
44
|
paymentMethod,
|
|
45
45
|
paymentCurrency,
|
|
46
|
-
|
|
47
|
-
amount
|
|
48
|
-
);
|
|
49
|
-
const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
|
|
46
|
+
});
|
|
50
47
|
|
|
51
48
|
return {
|
|
52
49
|
signature: {
|
|
@@ -4,7 +4,12 @@ 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 {
|
|
7
|
+
import {
|
|
8
|
+
expandLineItems,
|
|
9
|
+
getFastCheckoutAmount,
|
|
10
|
+
getPriceUintAmountByCurrency,
|
|
11
|
+
getStatementDescriptor,
|
|
12
|
+
} from '../../libs/session';
|
|
8
13
|
import type { TLineItemExpanded } from '../../store/models';
|
|
9
14
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
10
15
|
import { Customer } from '../../store/models/customer';
|
|
@@ -14,6 +19,7 @@ import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
|
14
19
|
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
15
20
|
import { PaymentMethod } from '../../store/models/payment-method';
|
|
16
21
|
import { Price } from '../../store/models/price';
|
|
22
|
+
import { Product } from '../../store/models/product';
|
|
17
23
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
18
24
|
import { Subscription } from '../../store/models/subscription';
|
|
19
25
|
import { SubscriptionItem } from '../../store/models/subscription-item';
|
|
@@ -25,6 +31,7 @@ type Result = {
|
|
|
25
31
|
subscription?: Subscription;
|
|
26
32
|
paymentCurrency: PaymentCurrency;
|
|
27
33
|
paymentMethod: PaymentMethod;
|
|
34
|
+
invoice?: Invoice;
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
export async function ensureCheckoutSession(checkoutSessionId: string) {
|
|
@@ -440,30 +447,30 @@ export async function ensureInvoiceAndItems({
|
|
|
440
447
|
export async function ensureInvoiceForCollect(invoiceId: string) {
|
|
441
448
|
const invoice = await Invoice.findByPk(invoiceId);
|
|
442
449
|
if (!invoice) {
|
|
443
|
-
throw new Error(
|
|
450
|
+
throw new Error(`Invoice ${invoiceId} not found`);
|
|
444
451
|
}
|
|
445
452
|
if (invoice.status === 'paid') {
|
|
446
|
-
throw new Error(
|
|
453
|
+
throw new Error(`Invoice ${invoiceId} already paid`);
|
|
447
454
|
}
|
|
448
455
|
if (invoice.status === 'void') {
|
|
449
|
-
throw new Error(
|
|
456
|
+
throw new Error(`Invoice ${invoiceId} already void`);
|
|
450
457
|
}
|
|
451
458
|
if (invoice.status === 'draft') {
|
|
452
|
-
throw new Error(
|
|
459
|
+
throw new Error(`Invoice ${invoiceId} is draft`);
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
456
463
|
if (!paymentIntent) {
|
|
457
|
-
throw new Error(
|
|
464
|
+
throw new Error(`Payment intent not found for invoice ${invoiceId}`);
|
|
458
465
|
}
|
|
459
466
|
if (paymentIntent.status === 'canceled') {
|
|
460
|
-
throw new Error(
|
|
467
|
+
throw new Error(`Payment intent already canceled for invoice ${invoiceId}`);
|
|
461
468
|
}
|
|
462
469
|
if (paymentIntent.status === 'succeeded') {
|
|
463
|
-
throw new Error(
|
|
470
|
+
throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
|
|
464
471
|
}
|
|
465
472
|
if (paymentIntent.status === 'processing') {
|
|
466
|
-
throw new Error(
|
|
473
|
+
throw new Error(`Payment intent processing for invoice ${invoiceId}`);
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
|
|
@@ -501,18 +508,23 @@ export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
|
|
|
501
508
|
};
|
|
502
509
|
}
|
|
503
510
|
|
|
504
|
-
export
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
export type TokenRequirementArgs = {
|
|
512
|
+
items: TLineItemExpanded[];
|
|
513
|
+
mode: string;
|
|
514
|
+
includeFreeTrial: boolean;
|
|
515
|
+
paymentMethod: PaymentMethod;
|
|
516
|
+
paymentCurrency: PaymentCurrency;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export async function getTokenRequirements({
|
|
520
|
+
items,
|
|
521
|
+
mode,
|
|
522
|
+
includeFreeTrial,
|
|
523
|
+
paymentMethod,
|
|
524
|
+
paymentCurrency,
|
|
525
|
+
}: TokenRequirementArgs) {
|
|
509
526
|
const tokenRequirements = [];
|
|
510
|
-
let amount = getFastCheckoutAmount(
|
|
511
|
-
checkoutSession.line_items as TLineItemExpanded[],
|
|
512
|
-
checkoutSession.mode,
|
|
513
|
-
paymentCurrency.id,
|
|
514
|
-
!!checkoutSession.subscription_data?.trial_period_days
|
|
515
|
-
);
|
|
527
|
+
let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!includeFreeTrial);
|
|
516
528
|
|
|
517
529
|
// If the app has not staked, we need to add the gas fee to the amount
|
|
518
530
|
if ((await hasStakedForGas(paymentMethod)) === false) {
|
|
@@ -538,3 +550,52 @@ export async function getTokenRequirements(
|
|
|
538
550
|
|
|
539
551
|
return tokenRequirements;
|
|
540
552
|
}
|
|
553
|
+
|
|
554
|
+
export async function ensureSubscription(subscriptionId: string): Promise<Result> {
|
|
555
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
556
|
+
if (!subscription) {
|
|
557
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
558
|
+
}
|
|
559
|
+
if (subscription.status !== 'past_due') {
|
|
560
|
+
throw new Error(`Subscription ${subscriptionId} is not in past_due status`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const paymentCurrencyId = subscription.currency_id;
|
|
564
|
+
const paymentMethodId = subscription.default_payment_method_id;
|
|
565
|
+
|
|
566
|
+
const [paymentMethod, paymentCurrency, invoice] = await Promise.all([
|
|
567
|
+
PaymentMethod.findByPk(paymentMethodId),
|
|
568
|
+
PaymentCurrency.findByPk(paymentCurrencyId),
|
|
569
|
+
Invoice.findByPk(subscription.latest_invoice_id),
|
|
570
|
+
]);
|
|
571
|
+
if (!paymentMethod) {
|
|
572
|
+
throw new Error(`Payment method not found for subscription ${subscriptionId}`);
|
|
573
|
+
}
|
|
574
|
+
if (!paymentCurrency) {
|
|
575
|
+
throw new Error(`Payment currency not found for subscription ${subscriptionId}`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
|
|
579
|
+
throw new Error(`Payment method ${paymentMethod.type} should not be here`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const items = (await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } })).map((x) =>
|
|
583
|
+
x.toJSON()
|
|
584
|
+
);
|
|
585
|
+
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
586
|
+
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
587
|
+
// @ts-ignore
|
|
588
|
+
expandLineItems(items, products, prices);
|
|
589
|
+
// @ts-ignore
|
|
590
|
+
subscription.items = await Price.expand(items);
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
// @ts-ignore
|
|
594
|
+
checkoutSession: null,
|
|
595
|
+
subscription,
|
|
596
|
+
paymentMethod,
|
|
597
|
+
paymentCurrency,
|
|
598
|
+
// @ts-ignore
|
|
599
|
+
invoice,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
@@ -34,20 +34,17 @@ export default {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
if (paymentMethod.type === 'arcblock') {
|
|
37
|
+
const items = checkoutSession.line_items as TLineItemExpanded[];
|
|
37
38
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
38
|
-
const amount = getFastCheckoutAmount(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
checkoutSession,
|
|
39
|
+
const amount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id);
|
|
40
|
+
const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
|
|
41
|
+
const tokenRequirements = await getTokenRequirements({
|
|
42
|
+
items,
|
|
43
|
+
mode: checkoutSession.mode,
|
|
44
|
+
includeFreeTrial: !!checkoutSession.subscription_data?.trial_period_days,
|
|
45
45
|
paymentMethod,
|
|
46
46
|
paymentCurrency,
|
|
47
|
-
|
|
48
|
-
amount
|
|
49
|
-
);
|
|
50
|
-
const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
|
|
47
|
+
});
|
|
51
48
|
|
|
52
49
|
return {
|
|
53
50
|
signature: {
|