payment-kit 1.19.8 → 1.19.10
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 +15 -10
- package/api/src/libs/env.ts +4 -0
- package/api/src/libs/notification/index.ts +28 -1
- package/api/src/libs/payment.ts +17 -5
- package/api/src/libs/setting.ts +20 -1
- package/api/src/locales/zh.ts +3 -3
- package/api/src/routes/checkout-sessions.ts +70 -33
- package/api/src/routes/connect/change-payment.ts +2 -2
- package/api/src/routes/connect/change-plan.ts +2 -2
- package/api/src/routes/connect/setup.ts +6 -8
- package/api/src/routes/connect/shared.ts +142 -157
- package/api/src/routes/connect/subscribe.ts +21 -6
- package/api/src/routes/meters.ts +1 -0
- package/api/src/routes/payment-currencies.ts +11 -2
- package/api/src/routes/subscriptions.ts +8 -3
- package/blocklet.yml +1 -1
- package/package.json +9 -8
- package/src/components/customer/credit-overview.tsx +11 -8
- package/src/components/payment-link/product-select.tsx +14 -1
- package/src/components/pricing-table/product-item.tsx +8 -1
- package/src/contexts/products.tsx +6 -1
- package/src/pages/admin/products/links/detail.tsx +14 -1
- package/src/pages/admin/products/prices/actions.tsx +4 -0
- package/src/pages/admin/products/prices/detail.tsx +2 -0
- package/src/pages/admin/products/products/create.tsx +1 -1
- package/src/pages/admin/products/products/detail.tsx +3 -0
- package/src/pages/admin/products/products/index.tsx +3 -1
|
@@ -13,14 +13,14 @@ import { encodeApproveItx } from '../../integrations/ethereum/token';
|
|
|
13
13
|
import { blocklet, ethWallet, wallet } from '../../libs/auth';
|
|
14
14
|
import logger from '../../libs/logger';
|
|
15
15
|
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
16
|
-
import {
|
|
17
|
-
getCheckoutAmount,
|
|
18
|
-
getCheckoutSessionSubscriptionIds,
|
|
19
|
-
getFastCheckoutAmount,
|
|
20
|
-
getStatementDescriptor,
|
|
21
|
-
getSubscriptionCreateSetup,
|
|
16
|
+
import {
|
|
17
|
+
getCheckoutAmount,
|
|
18
|
+
getCheckoutSessionSubscriptionIds,
|
|
19
|
+
getFastCheckoutAmount,
|
|
20
|
+
getStatementDescriptor,
|
|
21
|
+
getSubscriptionCreateSetup,
|
|
22
22
|
isDonationCheckoutSession,
|
|
23
|
-
getSubscriptionLineItems
|
|
23
|
+
getSubscriptionLineItems,
|
|
24
24
|
} from '../../libs/session';
|
|
25
25
|
import {
|
|
26
26
|
expandSubscriptionItems,
|
|
@@ -68,7 +68,11 @@ export async function ensureCheckoutSession(checkoutSessionId: string) {
|
|
|
68
68
|
return checkoutSession;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export async function ensurePaymentIntent(
|
|
71
|
+
export async function ensurePaymentIntent(
|
|
72
|
+
checkoutSessionId: string,
|
|
73
|
+
userDid?: string,
|
|
74
|
+
skipCustomer?: boolean
|
|
75
|
+
): Promise<Result> {
|
|
72
76
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
73
77
|
|
|
74
78
|
let paymentCurrencyId;
|
|
@@ -100,11 +104,8 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
100
104
|
let primarySubscription: Subscription | null = null;
|
|
101
105
|
if (subscriptionIds.length > 0) {
|
|
102
106
|
// @ts-ignore
|
|
103
|
-
subscriptions = await Promise.all(
|
|
104
|
-
|
|
105
|
-
.map(id => Subscription.findByPk(id))
|
|
106
|
-
);
|
|
107
|
-
|
|
107
|
+
subscriptions = await Promise.all(subscriptionIds.filter(Boolean).map((id) => Subscription.findByPk(id)));
|
|
108
|
+
|
|
108
109
|
subscriptions = subscriptions.filter(Boolean);
|
|
109
110
|
for (const subscription of subscriptions) {
|
|
110
111
|
if (subscription && subscription.status !== 'incomplete') {
|
|
@@ -119,81 +120,69 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
let customer = null;
|
|
122
|
-
|
|
123
|
+
|
|
123
124
|
if (!skipCustomer) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
125
|
+
// 检查是否为打赏场景
|
|
126
|
+
const isDonation = isDonationCheckoutSession(checkoutSession);
|
|
127
|
+
|
|
128
|
+
// if donation, create customer if not exists
|
|
129
|
+
if (isDonation && !checkoutSession.customer_id && userDid) {
|
|
130
|
+
customer = await Customer.findByPkOrDid(userDid);
|
|
131
|
+
if (!customer) {
|
|
132
|
+
const { user } = await blocklet.getUser(userDid);
|
|
133
|
+
if (user) {
|
|
134
|
+
customer = await Customer.create({
|
|
135
|
+
did: userDid,
|
|
136
|
+
email: user.email,
|
|
137
|
+
name: user.fullName || userDid,
|
|
138
|
+
description: user.remark,
|
|
139
|
+
metadata: { fromDonation: true },
|
|
140
|
+
livemode: checkoutSession.livemode,
|
|
141
|
+
phone: user.phone,
|
|
142
|
+
address: Customer.formatAddressFromUser(user),
|
|
143
|
+
delinquent: false,
|
|
144
|
+
balance: '0',
|
|
145
|
+
next_invoice_sequence: 1,
|
|
146
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
147
|
+
});
|
|
148
|
+
logger.info('Customer created for donation', { userDid, customerId: customer.id });
|
|
149
|
+
} else {
|
|
150
|
+
customer = await Customer.create({
|
|
151
|
+
did: userDid,
|
|
152
|
+
email: '',
|
|
153
|
+
name: 'anonymous',
|
|
154
|
+
description: 'Anonymous customer',
|
|
155
|
+
metadata: { fromDonation: true, anonymous: true },
|
|
156
|
+
livemode: checkoutSession.livemode,
|
|
157
|
+
phone: '',
|
|
158
|
+
delinquent: false,
|
|
159
|
+
balance: '0',
|
|
160
|
+
next_invoice_sequence: 1,
|
|
161
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
162
164
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
|
|
166
|
+
if (customer) {
|
|
167
|
+
await checkoutSession.update({ customer_id: customer.id, customer_did: customer.did });
|
|
168
|
+
if (paymentIntent) {
|
|
169
|
+
await paymentIntent.update({ customer_id: customer.id });
|
|
170
|
+
}
|
|
171
|
+
logger.info('Customer associated with donation', {
|
|
172
|
+
userDid,
|
|
173
|
+
customerId: customer.id,
|
|
174
|
+
checkoutSessionId: checkoutSession.id,
|
|
175
|
+
paymentIntentId: paymentIntent?.id,
|
|
176
|
+
});
|
|
169
177
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
checkoutSessionId: checkoutSession.id,
|
|
174
|
-
paymentIntentId: paymentIntent?.id
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
// 非打赏场景或已有客户ID的情况
|
|
179
|
-
customer = await Customer.findByPk(checkoutSession.customer_id);
|
|
180
|
-
}
|
|
181
|
-
if (!customer) {
|
|
182
|
-
throw new Error('Customer not found');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (userDid && !isDonation) {
|
|
186
|
-
const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
|
|
187
|
-
if (!user) {
|
|
188
|
-
throw new Error('Seems you have not connected to this app before');
|
|
178
|
+
} else {
|
|
179
|
+
// 非打赏场景或已有客户ID的情况
|
|
180
|
+
customer = await Customer.findByPk(checkoutSession.customer_id);
|
|
189
181
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error('This is not your payment intent');
|
|
182
|
+
if (!customer) {
|
|
183
|
+
throw new Error('Customer not found');
|
|
193
184
|
}
|
|
194
185
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
186
|
|
|
198
187
|
const [paymentMethod, paymentCurrency] = await Promise.all([
|
|
199
188
|
PaymentMethod.findByPk(paymentMethodId),
|
|
@@ -223,7 +212,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
223
212
|
};
|
|
224
213
|
}
|
|
225
214
|
|
|
226
|
-
export async function ensureSetupIntent(checkoutSessionId: string
|
|
215
|
+
export async function ensureSetupIntent(checkoutSessionId: string) {
|
|
227
216
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
228
217
|
|
|
229
218
|
if (!checkoutSession.setup_intent_id) {
|
|
@@ -256,16 +245,6 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
|
|
|
256
245
|
if (!customer) {
|
|
257
246
|
throw new Error('Customer not found for checkoutSession');
|
|
258
247
|
}
|
|
259
|
-
if (userDid) {
|
|
260
|
-
const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
|
|
261
|
-
if (!user) {
|
|
262
|
-
throw new Error('Seems you have not connected to this app before');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (customer.did !== user.did) {
|
|
266
|
-
throw new Error('This is not your setupIntent');
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
248
|
|
|
270
249
|
const [paymentMethod, paymentCurrency] = await Promise.all([
|
|
271
250
|
PaymentMethod.findByPk(subscription.default_payment_method_id),
|
|
@@ -344,8 +323,13 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
|
|
|
344
323
|
export async function ensureSubscriptionDelegation(subscriptionId: string) {
|
|
345
324
|
const subscription = (await Subscription.findOne({
|
|
346
325
|
where: { id: subscriptionId },
|
|
347
|
-
include: [
|
|
348
|
-
|
|
326
|
+
include: [
|
|
327
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
328
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
329
|
+
],
|
|
330
|
+
})) as
|
|
331
|
+
| (Subscription & { paymentCurrency: PaymentCurrency; paymentMethod: PaymentMethod; items?: TLineItemExpanded[] })
|
|
332
|
+
| null;
|
|
349
333
|
if (!subscription) {
|
|
350
334
|
throw new Error('Subscription not found');
|
|
351
335
|
}
|
|
@@ -380,7 +364,6 @@ export async function ensureInvoiceForCheckout({
|
|
|
380
364
|
subscriptions,
|
|
381
365
|
lineItems,
|
|
382
366
|
}: Args): Promise<{ invoice: Invoice | null; items: InvoiceItem[] }> {
|
|
383
|
-
|
|
384
367
|
const isGroupInvoice = subscriptions && subscriptions.length > 1;
|
|
385
368
|
// invoices are optional when checkout session is in payment mode
|
|
386
369
|
if (checkoutSession.mode === 'payment' && !checkoutSession.invoice_creation?.enabled) {
|
|
@@ -426,9 +409,7 @@ export async function ensureInvoiceForCheckout({
|
|
|
426
409
|
client.invoices
|
|
427
410
|
.voidInvoice(invoice.metadata.stripe_id)
|
|
428
411
|
.then(() => {
|
|
429
|
-
logger.info(
|
|
430
|
-
`Invoice marked void on stripe for checkout session ${checkoutSession.id}: ${existingInvoice}`
|
|
431
|
-
);
|
|
412
|
+
logger.info(`Invoice marked void on stripe for checkout session ${checkoutSession.id}: ${existingInvoice}`);
|
|
432
413
|
})
|
|
433
414
|
.catch((err) => {
|
|
434
415
|
logger.error(
|
|
@@ -441,16 +422,16 @@ export async function ensureInvoiceForCheckout({
|
|
|
441
422
|
}
|
|
442
423
|
|
|
443
424
|
const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
|
|
444
|
-
|
|
425
|
+
|
|
445
426
|
const metadata = {
|
|
446
427
|
...(checkoutSession.invoice_creation?.invoice_data?.metadata || {}),
|
|
447
428
|
};
|
|
448
|
-
|
|
429
|
+
|
|
449
430
|
if (isGroupInvoice) {
|
|
450
|
-
metadata.subscription_ids = subscriptions.map(sub => sub.id);
|
|
431
|
+
metadata.subscription_ids = subscriptions.map((sub) => sub.id);
|
|
451
432
|
metadata.is_group_invoice = true;
|
|
452
433
|
}
|
|
453
|
-
|
|
434
|
+
|
|
454
435
|
const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
|
|
455
436
|
const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
456
437
|
const now = dayjs().unix();
|
|
@@ -507,14 +488,13 @@ export async function ensureInvoiceForCheckout({
|
|
|
507
488
|
return { invoice, items };
|
|
508
489
|
}
|
|
509
490
|
|
|
510
|
-
|
|
511
491
|
export async function ensureInvoicesForSubscriptions({
|
|
512
492
|
checkoutSession,
|
|
513
493
|
customer,
|
|
514
494
|
subscriptions,
|
|
515
495
|
invoiceProps,
|
|
516
|
-
}: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
|
|
517
|
-
invoices: Invoice[];
|
|
496
|
+
}: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
|
|
497
|
+
invoices: Invoice[];
|
|
518
498
|
}> {
|
|
519
499
|
if (!subscriptions?.length) {
|
|
520
500
|
logger.warn('No subscriptions provided for invoice creation');
|
|
@@ -523,15 +503,16 @@ export async function ensureInvoicesForSubscriptions({
|
|
|
523
503
|
|
|
524
504
|
const lineItems = await Price.expand(checkoutSession.line_items, { product: true });
|
|
525
505
|
|
|
526
|
-
const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
|
|
506
|
+
const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
|
|
507
|
+
subscriptions[0]) as Subscription;
|
|
527
508
|
const invoices = await Promise.all(
|
|
528
509
|
subscriptions.map(async (subscription) => {
|
|
529
510
|
const subItems = await getSubscriptionLineItems(subscription, lineItems, primarySubscription);
|
|
530
|
-
const { invoice } = await ensureInvoiceForCheckout({
|
|
531
|
-
checkoutSession,
|
|
532
|
-
customer,
|
|
533
|
-
subscription,
|
|
534
|
-
subscriptions,
|
|
511
|
+
const { invoice } = await ensureInvoiceForCheckout({
|
|
512
|
+
checkoutSession,
|
|
513
|
+
customer,
|
|
514
|
+
subscription,
|
|
515
|
+
subscriptions,
|
|
535
516
|
lineItems: subItems,
|
|
536
517
|
props: invoiceProps,
|
|
537
518
|
});
|
|
@@ -540,10 +521,10 @@ export async function ensureInvoicesForSubscriptions({
|
|
|
540
521
|
);
|
|
541
522
|
|
|
542
523
|
const createdInvoices = invoices.filter(Boolean);
|
|
543
|
-
|
|
524
|
+
|
|
544
525
|
logger.info(`Created ${createdInvoices.length} invoices for subscriptions`, {
|
|
545
526
|
checkoutSessionId: checkoutSession.id,
|
|
546
|
-
invoiceIds: createdInvoices.map(inv => inv?.id)
|
|
527
|
+
invoiceIds: createdInvoices.map((inv) => inv?.id),
|
|
547
528
|
});
|
|
548
529
|
|
|
549
530
|
return { invoices: createdInvoices as Invoice[] };
|
|
@@ -625,7 +606,7 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
|
|
|
625
606
|
paymentMethod: paymentMethod as PaymentMethod,
|
|
626
607
|
receiverAddress,
|
|
627
608
|
subscription,
|
|
628
|
-
customer
|
|
609
|
+
customer,
|
|
629
610
|
};
|
|
630
611
|
}
|
|
631
612
|
|
|
@@ -715,7 +696,7 @@ export async function getDelegationTxClaim({
|
|
|
715
696
|
billingThreshold,
|
|
716
697
|
paymentMethod,
|
|
717
698
|
paymentCurrency,
|
|
718
|
-
requiredStake
|
|
699
|
+
requiredStake,
|
|
719
700
|
});
|
|
720
701
|
if (mode === 'delegation') {
|
|
721
702
|
tokenRequirements = [];
|
|
@@ -788,7 +769,11 @@ export async function getDelegationTxClaim({
|
|
|
788
769
|
throw new Error(`getDelegationTxClaim: Payment method ${paymentMethod.type} not supported`);
|
|
789
770
|
}
|
|
790
771
|
|
|
791
|
-
export function getStakeAmount(
|
|
772
|
+
export function getStakeAmount(
|
|
773
|
+
subscription: Subscription,
|
|
774
|
+
paymentCurrency: PaymentCurrency,
|
|
775
|
+
items: TLineItemExpanded[]
|
|
776
|
+
) {
|
|
792
777
|
const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
793
778
|
const minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
|
|
794
779
|
const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
|
|
@@ -816,29 +801,29 @@ export async function getStakeTxClaim({
|
|
|
816
801
|
}) {
|
|
817
802
|
let billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
818
803
|
let minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
|
|
819
|
-
|
|
804
|
+
|
|
820
805
|
const hasGrouping = subscriptions && subscriptions.length > 1;
|
|
821
806
|
if (hasGrouping) {
|
|
822
|
-
const primarySubscription = subscriptions[0] as Subscription;
|
|
807
|
+
const primarySubscription = subscriptions[0] as Subscription;
|
|
823
808
|
// use the settings of the primary subscription, not the scattered staking
|
|
824
809
|
billingThreshold = Number(primarySubscription.billing_thresholds?.amount_gte || 0);
|
|
825
810
|
minStakeAmount = Number(primarySubscription.billing_thresholds?.stake_gte || 0);
|
|
826
|
-
|
|
811
|
+
|
|
827
812
|
logger.info('Using primary subscription for staking', {
|
|
828
813
|
primarySubscriptionId: primarySubscription.id,
|
|
829
814
|
billingThreshold,
|
|
830
815
|
minStakeAmount,
|
|
831
|
-
allSubscriptionIds: subscriptions.map(s => s.id)
|
|
816
|
+
allSubscriptionIds: subscriptions.map((s) => s.id),
|
|
832
817
|
});
|
|
833
818
|
}
|
|
834
|
-
|
|
819
|
+
|
|
835
820
|
const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
|
|
836
821
|
const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
|
|
837
822
|
const amount = staking.licensed.add(staking.metered).toString();
|
|
838
|
-
|
|
823
|
+
|
|
839
824
|
logger.info('getStakeTxClaim', {
|
|
840
825
|
subscriptionId: subscription.id,
|
|
841
|
-
allSubscriptions: subscriptions?.map(s => s.id) || [],
|
|
826
|
+
allSubscriptions: subscriptions?.map((s) => s.id) || [],
|
|
842
827
|
billingThreshold,
|
|
843
828
|
minStakeAmount,
|
|
844
829
|
threshold: threshold.toString(),
|
|
@@ -849,11 +834,9 @@ export async function getStakeTxClaim({
|
|
|
849
834
|
if (paymentMethod.type === 'arcblock') {
|
|
850
835
|
// create staking data
|
|
851
836
|
const client = paymentMethod.getOcapClient();
|
|
852
|
-
|
|
853
|
-
const stakeId = hasGrouping
|
|
854
|
-
|
|
855
|
-
: subscription.id;
|
|
856
|
-
|
|
837
|
+
|
|
838
|
+
const stakeId = hasGrouping ? `stake-group-${subscription.id}` : subscription.id;
|
|
839
|
+
|
|
857
840
|
const address = await getCustomerStakeAddress(userDid, stakeId);
|
|
858
841
|
const { state } = await client.getStakeState({ address });
|
|
859
842
|
const data = {
|
|
@@ -862,10 +845,12 @@ export async function getStakeTxClaim({
|
|
|
862
845
|
{
|
|
863
846
|
appId: wallet.address,
|
|
864
847
|
subscriptionId: subscription.id,
|
|
865
|
-
...(hasGrouping
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
848
|
+
...(hasGrouping
|
|
849
|
+
? {
|
|
850
|
+
subscriptionGroup: true,
|
|
851
|
+
subscriptionIds: subscriptions.map((s) => s.id),
|
|
852
|
+
}
|
|
853
|
+
: {}),
|
|
869
854
|
},
|
|
870
855
|
JSON.parse(state?.data?.value || '{}')
|
|
871
856
|
),
|
|
@@ -902,11 +887,13 @@ export async function getStakeTxClaim({
|
|
|
902
887
|
meta: {
|
|
903
888
|
purpose: 'staking',
|
|
904
889
|
address,
|
|
905
|
-
...(hasGrouping
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
890
|
+
...(hasGrouping
|
|
891
|
+
? {
|
|
892
|
+
subscriptionGroup: true,
|
|
893
|
+
primarySubscriptionId: subscription.id,
|
|
894
|
+
allSubscriptionIds: subscriptions.map((s) => s.id),
|
|
895
|
+
}
|
|
896
|
+
: {}),
|
|
910
897
|
},
|
|
911
898
|
chainInfo: {
|
|
912
899
|
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
@@ -1015,7 +1002,7 @@ export async function getTokenRequirements({
|
|
|
1015
1002
|
paymentCurrency,
|
|
1016
1003
|
trialing = false,
|
|
1017
1004
|
billingThreshold = 0,
|
|
1018
|
-
requiredStake
|
|
1005
|
+
requiredStake,
|
|
1019
1006
|
}: TokenRequirementArgs) {
|
|
1020
1007
|
const tokenRequirements = [];
|
|
1021
1008
|
let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!trialing);
|
|
@@ -1197,7 +1184,7 @@ export async function ensureReStakeContext(subscriptionId: string) {
|
|
|
1197
1184
|
};
|
|
1198
1185
|
}
|
|
1199
1186
|
export async function ensureSubscriptionForCollectBatch(
|
|
1200
|
-
subscriptionId?: string,
|
|
1187
|
+
subscriptionId?: string,
|
|
1201
1188
|
currencyId?: string,
|
|
1202
1189
|
customerId?: string
|
|
1203
1190
|
) {
|
|
@@ -1258,13 +1245,13 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
|
|
|
1258
1245
|
if (!subscription) {
|
|
1259
1246
|
throw new Error(`Subscription ${subscriptionId} not found when prepare SubGuard`);
|
|
1260
1247
|
}
|
|
1261
|
-
|
|
1248
|
+
// @ts-ignore
|
|
1262
1249
|
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
1263
1250
|
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
1264
1251
|
if (!paymentCurrency) {
|
|
1265
1252
|
throw new Error(`PaymentCurrency ${subscription.currency_id} not found when prepare SubGuard`);
|
|
1266
1253
|
}
|
|
1267
|
-
|
|
1254
|
+
|
|
1268
1255
|
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
1269
1256
|
|
|
1270
1257
|
if (!paymentMethod) {
|
|
@@ -1288,7 +1275,6 @@ export async function ensureSubscriptionForOverdraftProtection(subscriptionId: s
|
|
|
1288
1275
|
};
|
|
1289
1276
|
}
|
|
1290
1277
|
|
|
1291
|
-
|
|
1292
1278
|
async function executeSingleTransaction(
|
|
1293
1279
|
client: any,
|
|
1294
1280
|
claim: any,
|
|
@@ -1323,13 +1309,12 @@ export async function executeOcapTransactions(
|
|
|
1323
1309
|
) {
|
|
1324
1310
|
const client = paymentMethod.getOcapClient();
|
|
1325
1311
|
logger.info('start executeOcapTransactions', { userDid, claims });
|
|
1326
|
-
|
|
1327
|
-
const delegation = claims.find(x => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
1328
|
-
const staking = claims.find(x => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
|
|
1329
|
-
|
|
1330
|
-
const stakingAmount =
|
|
1331
|
-
(x: any) => x?.address === paymentCurrencyContract
|
|
1332
|
-
)?.value || '0';
|
|
1312
|
+
|
|
1313
|
+
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
1314
|
+
const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');
|
|
1315
|
+
|
|
1316
|
+
const stakingAmount =
|
|
1317
|
+
staking?.requirement?.tokens?.find((x: any) => x?.address === paymentCurrencyContract)?.value || '0';
|
|
1333
1318
|
|
|
1334
1319
|
try {
|
|
1335
1320
|
const getHeaders = (index: number): Record<string, string> => {
|
|
@@ -1341,17 +1326,17 @@ export async function executeOcapTransactions(
|
|
|
1341
1326
|
const req = requestSource[headerIndex];
|
|
1342
1327
|
return req ? client.pickGasPayerHeaders(req) : {};
|
|
1343
1328
|
}
|
|
1344
|
-
|
|
1329
|
+
|
|
1345
1330
|
if (requestSource && typeof requestSource === 'object') {
|
|
1346
1331
|
return client.pickGasPayerHeaders(requestSource);
|
|
1347
1332
|
}
|
|
1348
|
-
|
|
1333
|
+
|
|
1349
1334
|
return {};
|
|
1350
1335
|
};
|
|
1351
1336
|
|
|
1352
1337
|
const transactions = [
|
|
1353
1338
|
{ claim: delegation, type: 'Delegate' },
|
|
1354
|
-
{ claim: staking, type: 'Stake' }
|
|
1339
|
+
{ claim: staking, type: 'Stake' },
|
|
1355
1340
|
];
|
|
1356
1341
|
|
|
1357
1342
|
const txHashes = [];
|
|
@@ -1377,7 +1362,7 @@ export async function executeOcapTransactions(
|
|
|
1377
1362
|
type: 'delegate',
|
|
1378
1363
|
staking: {
|
|
1379
1364
|
tx_hash: stakingTxHash,
|
|
1380
|
-
address:
|
|
1365
|
+
address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
|
|
1381
1366
|
},
|
|
1382
1367
|
stakingAmount,
|
|
1383
1368
|
};
|
|
@@ -1413,7 +1398,7 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
|
|
|
1413
1398
|
const toMethod = await PaymentMethod.findByPk(toMethodId);
|
|
1414
1399
|
if (toMethod?.type === 'stripe') {
|
|
1415
1400
|
// resume stripe
|
|
1416
|
-
const client =
|
|
1401
|
+
const client = toMethod?.getStripeClient();
|
|
1417
1402
|
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
|
|
1418
1403
|
if (client && stripeSubscriptionId) {
|
|
1419
1404
|
const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
|
|
@@ -1443,7 +1428,7 @@ export async function returnStakeForCanceledSubscription(subscriptionId: string)
|
|
|
1443
1428
|
if (subscription.status !== 'canceled') {
|
|
1444
1429
|
throw new Error(`Subscription ${subscriptionId} is not canceled`);
|
|
1445
1430
|
}
|
|
1446
|
-
|
|
1431
|
+
|
|
1447
1432
|
if (!subscription.payment_details?.arcblock?.staking?.tx_hash) {
|
|
1448
1433
|
throw new Error(`No staking transaction found in subscription ${subscriptionId}`);
|
|
1449
1434
|
}
|
|
@@ -1455,4 +1440,4 @@ export async function returnStakeForCanceledSubscription(subscriptionId: string)
|
|
|
1455
1440
|
} catch (err) {
|
|
1456
1441
|
logger.error('returnStakeForCanceledSubscription failed', { error: err, subscriptionId });
|
|
1457
1442
|
}
|
|
1458
|
-
}
|
|
1443
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pAll from 'p-all';
|
|
1
2
|
import { broadcastEvmTransaction, executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
2
3
|
import type { CallbackArgs } from '../../libs/auth';
|
|
3
4
|
import dayjs from '../../libs/dayjs';
|
|
@@ -19,13 +20,24 @@ import {
|
|
|
19
20
|
} from './shared';
|
|
20
21
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
21
22
|
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
23
|
+
import { updateDataConcurrency } from '../../libs/env';
|
|
22
24
|
|
|
23
25
|
const updateInvoices = async (invoices: Invoice[], update: Partial<Invoice>) => {
|
|
24
|
-
await
|
|
26
|
+
await pAll(
|
|
27
|
+
invoices.map((invoice) => async () => {
|
|
28
|
+
await invoice.update(update);
|
|
29
|
+
}),
|
|
30
|
+
{ concurrency: updateDataConcurrency }
|
|
31
|
+
);
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
const updateSubscriptions = async (subscriptions: Subscription[], update: Partial<Subscription>) => {
|
|
28
|
-
await
|
|
35
|
+
await pAll(
|
|
36
|
+
subscriptions.map((subscription) => async () => {
|
|
37
|
+
await subscription.update(update);
|
|
38
|
+
}),
|
|
39
|
+
{ concurrency: updateDataConcurrency }
|
|
40
|
+
);
|
|
29
41
|
};
|
|
30
42
|
|
|
31
43
|
export default {
|
|
@@ -46,7 +58,6 @@ export default {
|
|
|
46
58
|
paymentMethod,
|
|
47
59
|
paymentCurrency,
|
|
48
60
|
subscriptions,
|
|
49
|
-
customer,
|
|
50
61
|
subscription: primarySubscription,
|
|
51
62
|
} = await ensurePaymentIntent(checkoutSessionId, connectedDid || sessionUserDid || userDid);
|
|
52
63
|
if (!subscriptions || subscriptions.length === 0) {
|
|
@@ -71,7 +82,7 @@ export default {
|
|
|
71
82
|
const delegation = await isDelegationSufficientForPayment({
|
|
72
83
|
paymentMethod,
|
|
73
84
|
paymentCurrency,
|
|
74
|
-
userDid
|
|
85
|
+
userDid,
|
|
75
86
|
amount: fastCheckoutAmount,
|
|
76
87
|
});
|
|
77
88
|
|
|
@@ -208,8 +219,11 @@ export default {
|
|
|
208
219
|
}
|
|
209
220
|
}
|
|
210
221
|
|
|
211
|
-
await
|
|
212
|
-
subscriptions.map((subscription) =>
|
|
222
|
+
await pAll(
|
|
223
|
+
subscriptions.map((subscription) => async () => {
|
|
224
|
+
await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
|
|
225
|
+
}),
|
|
226
|
+
{ concurrency: updateDataConcurrency }
|
|
213
227
|
);
|
|
214
228
|
|
|
215
229
|
logger.info('CheckoutSession updated with multiple subscriptions', {
|
|
@@ -233,6 +247,7 @@ export default {
|
|
|
233
247
|
paymentCurrency,
|
|
234
248
|
userDid,
|
|
235
249
|
amount: fastCheckoutAmount,
|
|
250
|
+
skipUserCheck: true,
|
|
236
251
|
});
|
|
237
252
|
|
|
238
253
|
if (tokenBalanceCheck.sufficient === false) {
|
package/api/src/routes/meters.ts
CHANGED
|
@@ -306,6 +306,8 @@ const updateCurrencySchema = Joi.object({
|
|
|
306
306
|
name: Joi.string().empty('').max(32).optional(),
|
|
307
307
|
description: Joi.string().empty('').max(255).optional(),
|
|
308
308
|
logo: Joi.string().empty('').optional(),
|
|
309
|
+
metadata: Joi.object().optional(),
|
|
310
|
+
symbol: Joi.string().empty('').optional(),
|
|
309
311
|
}).unknown(true);
|
|
310
312
|
router.put('/:id', auth, async (req, res) => {
|
|
311
313
|
const { id } = req.params;
|
|
@@ -329,11 +331,18 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
329
331
|
return res.status(400).json({ error: 'Payment method not found' });
|
|
330
332
|
}
|
|
331
333
|
|
|
332
|
-
const
|
|
334
|
+
const updates: Partial<TPaymentCurrency> = {
|
|
333
335
|
name: raw.name || currency.name,
|
|
334
336
|
description: raw.description || currency.description,
|
|
335
337
|
logo: raw.logo || method.logo,
|
|
336
|
-
|
|
338
|
+
metadata: raw.metadata || currency.metadata,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (currency.type === 'credit') {
|
|
342
|
+
updates.symbol = raw.symbol || currency.symbol;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const updatedCurrency = await currency.update(updates);
|
|
337
346
|
return res.json(updatedCurrency);
|
|
338
347
|
});
|
|
339
348
|
|