payment-kit 1.21.16 → 1.22.0
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 +3 -1
- package/api/src/integrations/blocklet/user.ts +2 -2
- package/api/src/integrations/ethereum/token.ts +4 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +31 -26
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
- package/api/src/integrations/stripe/resource.ts +30 -1
- package/api/src/integrations/stripe/setup.ts +1 -1
- package/api/src/libs/auth.ts +7 -6
- package/api/src/libs/env.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +1 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/libs/payment.ts +11 -6
- package/api/src/libs/refund.ts +1 -1
- package/api/src/libs/remote-signer.ts +93 -0
- package/api/src/libs/security.ts +1 -1
- package/api/src/libs/subscription.ts +4 -7
- package/api/src/libs/util.ts +18 -1
- package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +17 -9
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +11 -6
- package/api/src/queues/payment.ts +2 -2
- package/api/src/queues/payout.ts +1 -1
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/queues/vendors/status-check.ts +1 -1
- package/api/src/queues/webhook.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +1 -1
- package/api/src/routes/checkout-sessions.ts +4 -6
- package/api/src/routes/connect/change-payer.ts +148 -0
- package/api/src/routes/connect/collect-batch.ts +1 -1
- package/api/src/routes/connect/collect.ts +1 -1
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +1 -1
- package/api/src/routes/connect/recharge.ts +1 -1
- package/api/src/routes/connect/shared.ts +62 -23
- package/api/src/routes/customers.ts +1 -1
- package/api/src/routes/integrations/stripe.ts +1 -1
- package/api/src/routes/invoices.ts +141 -2
- package/api/src/routes/meter-events.ts +9 -12
- package/api/src/routes/payment-currencies.ts +1 -1
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/payouts.ts +1 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/subscriptions.ts +130 -3
- package/api/src/store/models/types.ts +1 -1
- package/api/tests/setup.ts +11 -0
- package/api/third.d.ts +0 -2
- package/blocklet.yml +1 -1
- package/jest.config.js +2 -2
- package/package.json +26 -26
- package/src/components/invoice/table.tsx +2 -2
- package/src/components/invoice-pdf/template.tsx +30 -0
- package/src/components/subscription/payment-method-info.tsx +222 -0
- package/src/global.css +4 -0
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/admin/billing/invoices/detail.tsx +5 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
- package/src/pages/admin/overview.tsx +14 -14
- package/src/pages/customer/invoice/detail.tsx +59 -17
- package/src/pages/customer/subscription/detail.tsx +21 -2
|
@@ -270,11 +270,12 @@ export async function ensureSetupIntent(checkoutSessionId: string, skipInvoice?:
|
|
|
270
270
|
invoice = await Invoice.findByPk(checkoutSession.invoice_id);
|
|
271
271
|
} else {
|
|
272
272
|
// Get discount information for this checkout session
|
|
273
|
-
let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } =
|
|
274
|
+
{
|
|
275
|
+
appliedDiscounts: [],
|
|
276
|
+
discountBreakdown: [],
|
|
277
|
+
};
|
|
278
|
+
|
|
278
279
|
try {
|
|
279
280
|
discountInfo = await getDiscountRecordsForCheckout({
|
|
280
281
|
checkoutSessionId: checkoutSession.id,
|
|
@@ -462,13 +463,13 @@ export async function ensureInvoiceForCheckout({
|
|
|
462
463
|
const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
|
|
463
464
|
const now = dayjs().unix();
|
|
464
465
|
const invoiceItems = lineItems || (await Price.expand(checkoutSession.line_items, { product: true }));
|
|
465
|
-
|
|
466
|
+
|
|
466
467
|
// Get discount records for this checkout session
|
|
467
|
-
let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } = {
|
|
468
|
-
appliedDiscounts: [],
|
|
469
|
-
discountBreakdown: []
|
|
468
|
+
let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } = {
|
|
469
|
+
appliedDiscounts: [],
|
|
470
|
+
discountBreakdown: [],
|
|
470
471
|
};
|
|
471
|
-
|
|
472
|
+
|
|
472
473
|
// Prepare discount configuration for getCheckoutAmount
|
|
473
474
|
let discountConfig;
|
|
474
475
|
try {
|
|
@@ -476,7 +477,7 @@ export async function ensureInvoiceForCheckout({
|
|
|
476
477
|
checkoutSessionId: checkoutSession.id,
|
|
477
478
|
customerId: customer.id,
|
|
478
479
|
});
|
|
479
|
-
|
|
480
|
+
|
|
480
481
|
// Apply discount if we have discount records
|
|
481
482
|
if (discountInfo.appliedDiscounts.length > 0 && checkoutSession.discounts?.length) {
|
|
482
483
|
const firstDiscount = checkoutSession.discounts[0];
|
|
@@ -495,8 +496,13 @@ export async function ensureInvoiceForCheckout({
|
|
|
495
496
|
error: error.message,
|
|
496
497
|
});
|
|
497
498
|
}
|
|
498
|
-
|
|
499
|
-
const totalAmount = await getCheckoutAmount(
|
|
499
|
+
|
|
500
|
+
const totalAmount = await getCheckoutAmount(
|
|
501
|
+
invoiceItems,
|
|
502
|
+
checkoutSession.currency_id,
|
|
503
|
+
trialInDays > 0 || trialEnd > now,
|
|
504
|
+
discountConfig
|
|
505
|
+
);
|
|
500
506
|
const { invoice, items } = await ensureInvoiceAndItems({
|
|
501
507
|
customer,
|
|
502
508
|
currency: currency as PaymentCurrency,
|
|
@@ -530,16 +536,16 @@ export async function ensureInvoiceForCheckout({
|
|
|
530
536
|
custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
|
|
531
537
|
footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
|
|
532
538
|
metadata,
|
|
533
|
-
|
|
539
|
+
|
|
534
540
|
discounts: discountInfo.appliedDiscounts,
|
|
535
541
|
total_discount_amounts: discountInfo.discountBreakdown,
|
|
536
|
-
|
|
542
|
+
|
|
537
543
|
...(props || {}),
|
|
538
544
|
} as unknown as Invoice,
|
|
539
545
|
});
|
|
540
|
-
|
|
541
|
-
logger.info('Invoice created for checkoutSession', {
|
|
542
|
-
checkoutSessionId: checkoutSession.id,
|
|
546
|
+
|
|
547
|
+
logger.info('Invoice created for checkoutSession', {
|
|
548
|
+
checkoutSessionId: checkoutSession.id,
|
|
543
549
|
invoiceId: invoice.id,
|
|
544
550
|
hasDiscounts: discountInfo.appliedDiscounts.length > 0,
|
|
545
551
|
discountCount: discountInfo.appliedDiscounts.length,
|
|
@@ -696,7 +702,7 @@ export async function ensureAccountRecharge(customerId: string, currencyId: stri
|
|
|
696
702
|
}
|
|
697
703
|
|
|
698
704
|
const receiverAddress = rechargeAddress || customer.did;
|
|
699
|
-
|
|
705
|
+
|
|
700
706
|
return {
|
|
701
707
|
paymentCurrency: paymentCurrency as PaymentCurrency,
|
|
702
708
|
paymentMethod: paymentMethod as PaymentMethod,
|
|
@@ -1084,7 +1090,8 @@ export async function getTokenRequirements({
|
|
|
1084
1090
|
const tokenRequirements: { address: string; value: string }[] = [];
|
|
1085
1091
|
let amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id, trialing: !!trialing });
|
|
1086
1092
|
|
|
1087
|
-
const addStakeRequired =
|
|
1093
|
+
const addStakeRequired =
|
|
1094
|
+
requiredStake && ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup');
|
|
1088
1095
|
|
|
1089
1096
|
if (!addStakeRequired && amount === '0') {
|
|
1090
1097
|
return tokenRequirements;
|
|
@@ -1123,7 +1130,10 @@ export async function getTokenRequirements({
|
|
|
1123
1130
|
if (exist) {
|
|
1124
1131
|
exist.value = new BN(exist.value).add(staking.licensed).add(staking.metered).toString();
|
|
1125
1132
|
} else {
|
|
1126
|
-
tokenRequirements.push({
|
|
1133
|
+
tokenRequirements.push({
|
|
1134
|
+
address: paymentCurrency.contract as string,
|
|
1135
|
+
value: staking.licensed.add(staking.metered).toString(),
|
|
1136
|
+
});
|
|
1127
1137
|
}
|
|
1128
1138
|
}
|
|
1129
1139
|
|
|
@@ -1224,6 +1234,36 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
|
|
|
1224
1234
|
};
|
|
1225
1235
|
}
|
|
1226
1236
|
|
|
1237
|
+
export async function ensurePayerChangeContext(subscriptionId: string) {
|
|
1238
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1239
|
+
if (!subscription) {
|
|
1240
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
1241
|
+
}
|
|
1242
|
+
if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
|
|
1243
|
+
throw new Error(`Subscription ${subscriptionId} is not in a valid status to change payer`);
|
|
1244
|
+
}
|
|
1245
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
1246
|
+
if (!paymentMethod) {
|
|
1247
|
+
throw new Error(`Payment method not found for subscription ${subscriptionId}`);
|
|
1248
|
+
}
|
|
1249
|
+
const payerAddress = getSubscriptionPaymentAddress(subscription, paymentMethod?.type);
|
|
1250
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
1251
|
+
if (!paymentCurrency) {
|
|
1252
|
+
throw new Error(`PaymentCurrency ${subscription.currency_id} not found for subscription ${subscriptionId}`);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// @ts-ignore
|
|
1256
|
+
subscription.items = await expandSubscriptionItems(subscription.id);
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
subscription,
|
|
1260
|
+
paymentCurrency,
|
|
1261
|
+
paymentMethod,
|
|
1262
|
+
customer: await Customer.findByPk(subscription.customer_id),
|
|
1263
|
+
payerAddress,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1227
1267
|
export async function ensureReStakeContext(subscriptionId: string) {
|
|
1228
1268
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
1229
1269
|
if (!subscription) {
|
|
@@ -1378,7 +1418,7 @@ async function executeSingleTransaction(
|
|
|
1378
1418
|
const { buffer } = await client[`encode${type}Tx`]({ tx });
|
|
1379
1419
|
return client[`send${type}Tx`](
|
|
1380
1420
|
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
1381
|
-
getGasPayerExtra(buffer, gasPayerHeaders)
|
|
1421
|
+
await getGasPayerExtra(buffer, gasPayerHeaders)
|
|
1382
1422
|
);
|
|
1383
1423
|
}
|
|
1384
1424
|
|
|
@@ -1501,7 +1541,6 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
|
|
|
1501
1541
|
}
|
|
1502
1542
|
}
|
|
1503
1543
|
|
|
1504
|
-
|
|
1505
1544
|
export async function ensureAutoRechargeAuthorization(
|
|
1506
1545
|
autoRechargeConfigId: string
|
|
1507
1546
|
): Promise<{ autoRechargeConfig: AutoRechargeConfig; paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency }> {
|
|
@@ -8,6 +8,7 @@ import { Op } from 'sequelize';
|
|
|
8
8
|
import { BN } from '@ocap/util';
|
|
9
9
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
10
10
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
11
|
+
import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../integrations/stripe/resource';
|
|
11
12
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
12
13
|
import { authenticate } from '../libs/security';
|
|
13
14
|
import { expandLineItems } from '../libs/session';
|
|
@@ -662,9 +663,11 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
662
663
|
})) as TInvoiceExpanded | null;
|
|
663
664
|
|
|
664
665
|
if (doc) {
|
|
665
|
-
|
|
666
|
+
const shouldSync = req.query.sync === 'true' || !!req.query.forceSync;
|
|
667
|
+
// Sync Stripe invoice when sync=true query parameter is present
|
|
668
|
+
if (doc.metadata?.stripe_id && doc.status !== 'paid') {
|
|
666
669
|
// @ts-ignore
|
|
667
|
-
await syncStripeInvoice(doc);
|
|
670
|
+
await syncStripeInvoice(doc, shouldSync);
|
|
668
671
|
}
|
|
669
672
|
if (doc.payment_intent_id) {
|
|
670
673
|
const paymentIntent = await PaymentIntent.findByPk(doc.payment_intent_id);
|
|
@@ -799,6 +802,142 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
799
802
|
}
|
|
800
803
|
});
|
|
801
804
|
|
|
805
|
+
router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const { invoice_ids, subscription_id, customer_id, currency_id } = req.body;
|
|
808
|
+
|
|
809
|
+
if (!currency_id) {
|
|
810
|
+
return res.status(400).json({ error: 'currency_id is required' });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (!invoice_ids && !subscription_id && !customer_id) {
|
|
814
|
+
return res.status(400).json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let invoices: Invoice[];
|
|
818
|
+
let customer: Customer | null;
|
|
819
|
+
let paymentMethod: PaymentMethod | null = null;
|
|
820
|
+
|
|
821
|
+
if (invoice_ids && Array.isArray(invoice_ids) && invoice_ids.length > 0) {
|
|
822
|
+
invoices = await Invoice.findAll({
|
|
823
|
+
where: {
|
|
824
|
+
id: { [Op.in]: invoice_ids },
|
|
825
|
+
currency_id,
|
|
826
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
827
|
+
},
|
|
828
|
+
include: [
|
|
829
|
+
{ model: Customer, as: 'customer' },
|
|
830
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
831
|
+
],
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (invoices.length === 0) {
|
|
835
|
+
return res.status(404).json({ error: 'No payable invoices found' });
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// @ts-ignore
|
|
839
|
+
customer = invoices[0]?.customer;
|
|
840
|
+
paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
|
|
841
|
+
} else if (subscription_id) {
|
|
842
|
+
const subscription = await Subscription.findByPk(subscription_id, {
|
|
843
|
+
include: [{ model: Customer, as: 'customer' }],
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
if (!subscription) {
|
|
847
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// @ts-ignore
|
|
851
|
+
customer = subscription.customer;
|
|
852
|
+
paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
853
|
+
|
|
854
|
+
invoices = await Invoice.findAll({
|
|
855
|
+
where: {
|
|
856
|
+
subscription_id,
|
|
857
|
+
currency_id,
|
|
858
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
859
|
+
},
|
|
860
|
+
include: [
|
|
861
|
+
{ model: Customer, as: 'customer' },
|
|
862
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
863
|
+
],
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
customer = await Customer.findByPkOrDid(customer_id!);
|
|
867
|
+
if (!customer) {
|
|
868
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
invoices = await Invoice.findAll({
|
|
872
|
+
where: {
|
|
873
|
+
customer_id: customer.id,
|
|
874
|
+
currency_id,
|
|
875
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
876
|
+
},
|
|
877
|
+
include: [
|
|
878
|
+
{ model: Customer, as: 'customer' },
|
|
879
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
880
|
+
],
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (invoices.length === 0) {
|
|
884
|
+
return res.status(404).json({ error: 'No payable invoices found' });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!customer) {
|
|
891
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
895
|
+
return res.status(400).json({ error: 'Not using Stripe payment method' });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (invoices.length === 0) {
|
|
899
|
+
return res.status(400).json({ error: 'No payable invoices found' });
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
await ensureStripeCustomer(customer, paymentMethod);
|
|
903
|
+
|
|
904
|
+
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
905
|
+
|
|
906
|
+
const paymentCurrency = await PaymentCurrency.findByPk(currency_id);
|
|
907
|
+
if (!paymentCurrency) {
|
|
908
|
+
return res.status(404).json({ error: `Payment currency ${currency_id} not found` });
|
|
909
|
+
}
|
|
910
|
+
const totalAmount = invoices.reduce((sum, invoice) => {
|
|
911
|
+
const amount = invoice.amount_remaining || '0';
|
|
912
|
+
return new BN(sum).add(new BN(amount)).toString();
|
|
913
|
+
}, '0');
|
|
914
|
+
|
|
915
|
+
const metadata: any = {
|
|
916
|
+
currency_id,
|
|
917
|
+
customer_id: customer.id,
|
|
918
|
+
invoices: JSON.stringify(invoices.map((inv) => inv.id)),
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const setupIntent = await ensureStripeSetupIntentForInvoicePayment(customer, paymentMethod, metadata);
|
|
922
|
+
|
|
923
|
+
return res.json({
|
|
924
|
+
client_secret: setupIntent.client_secret,
|
|
925
|
+
publishable_key: settings.stripe?.publishable_key,
|
|
926
|
+
setup_intent_id: setupIntent.id,
|
|
927
|
+
invoices: invoices.map((inv) => inv.id),
|
|
928
|
+
amount: totalAmount,
|
|
929
|
+
currency: paymentCurrency,
|
|
930
|
+
customer,
|
|
931
|
+
});
|
|
932
|
+
} catch (err) {
|
|
933
|
+
logger.error('Failed to create setup intent for stripe payment', {
|
|
934
|
+
error: err,
|
|
935
|
+
body: req.body,
|
|
936
|
+
});
|
|
937
|
+
return res.status(400).json({ error: err.message });
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
802
941
|
// eslint-disable-next-line consistent-return
|
|
803
942
|
router.put('/:id', authAdmin, async (req, res) => {
|
|
804
943
|
try {
|
|
@@ -278,7 +278,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
278
278
|
value: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
|
|
279
279
|
},
|
|
280
280
|
identifier: req.body.identifier,
|
|
281
|
-
livemode:
|
|
281
|
+
livemode: meter.livemode,
|
|
282
282
|
processed: false,
|
|
283
283
|
status: 'pending' as MeterEventStatus,
|
|
284
284
|
attempt_count: 0,
|
|
@@ -312,12 +312,15 @@ router.post('/', auth, async (req, res) => {
|
|
|
312
312
|
|
|
313
313
|
router.get('/pending-amount', authMine, async (req, res) => {
|
|
314
314
|
try {
|
|
315
|
-
const
|
|
316
|
-
status: 'requires_action',
|
|
315
|
+
const params: any = {
|
|
316
|
+
status: ['requires_action', 'requires_capture'],
|
|
317
317
|
livemode: !!req.livemode,
|
|
318
318
|
};
|
|
319
319
|
if (req.query.subscription_id) {
|
|
320
|
-
|
|
320
|
+
params.subscriptionId = req.query.subscription_id;
|
|
321
|
+
}
|
|
322
|
+
if (req.query.currency_id) {
|
|
323
|
+
params.currencyId = req.query.currency_id;
|
|
321
324
|
}
|
|
322
325
|
if (req.query.customer_id) {
|
|
323
326
|
if (typeof req.query.customer_id !== 'string') {
|
|
@@ -327,15 +330,9 @@ router.get('/pending-amount', authMine, async (req, res) => {
|
|
|
327
330
|
if (!customer) {
|
|
328
331
|
return res.status(404).json({ error: 'Customer not found' });
|
|
329
332
|
}
|
|
330
|
-
|
|
333
|
+
params.customerId = customer.id;
|
|
331
334
|
}
|
|
332
|
-
const [summary] = await MeterEvent.getPendingAmounts(
|
|
333
|
-
subscriptionId: req.query.subscription_id as string,
|
|
334
|
-
livemode: !!req.livemode,
|
|
335
|
-
currencyId: req.query.currency_id as string,
|
|
336
|
-
status: ['requires_action', 'requires_capture'],
|
|
337
|
-
customerId: req.query.customer_id as string,
|
|
338
|
-
});
|
|
335
|
+
const [summary] = await MeterEvent.getPendingAmounts(params);
|
|
339
336
|
return res.json(summary);
|
|
340
337
|
} catch (err) {
|
|
341
338
|
logger.error('Error getting meter event pending amount', err);
|
|
@@ -5,7 +5,7 @@ import { InferAttributes, Op, WhereOptions } from 'sequelize';
|
|
|
5
5
|
import Joi from 'joi';
|
|
6
6
|
import pick from 'lodash/pick';
|
|
7
7
|
import { getUrl } from '@blocklet/sdk';
|
|
8
|
-
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
8
|
+
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
|
|
9
9
|
import { fetchErc20Meta } from '../integrations/ethereum/token';
|
|
10
10
|
import logger from '../libs/logger';
|
|
11
11
|
import { authenticate } from '../libs/security';
|
|
@@ -362,10 +362,10 @@ router.get('/:id/refundable-amount', authPortal, async (req, res) => {
|
|
|
362
362
|
if (payouts.length > 0) {
|
|
363
363
|
let totalPayoutAmount = new BN('0');
|
|
364
364
|
payouts.forEach((payout) => {
|
|
365
|
-
totalPayoutAmount = totalPayoutAmount.add(new BN(payout.amount));
|
|
365
|
+
totalPayoutAmount = totalPayoutAmount.add(new BN(payout.amount || '0'));
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
result.amount = result.amount.sub(totalPayoutAmount);
|
|
368
|
+
result.amount = new BN(result.amount || '0').sub(totalPayoutAmount).toString();
|
|
369
369
|
}
|
|
370
370
|
res.json(result);
|
|
371
371
|
} else {
|
|
@@ -449,7 +449,8 @@ router.get('/:id/benefits', async (req, res) => {
|
|
|
449
449
|
if (!doc) {
|
|
450
450
|
return res.status(404).json({ error: 'payment link not found' });
|
|
451
451
|
}
|
|
452
|
-
const
|
|
452
|
+
const locale = req.query.locale as string;
|
|
453
|
+
const benefits = await getDonationBenefits(doc, '', locale);
|
|
453
454
|
return res.json(benefits);
|
|
454
455
|
} catch (err) {
|
|
455
456
|
logger.error('Get donation benefits error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
@@ -3,7 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
|
-
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
6
|
+
import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
|
|
7
7
|
import type { WhereOptions } from 'sequelize';
|
|
8
8
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
9
9
|
import { authenticate } from '../libs/security';
|
|
@@ -166,6 +166,7 @@ export async function createProductAndPrices(payload: any) {
|
|
|
166
166
|
// @ts-ignore
|
|
167
167
|
['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
|
|
168
168
|
if (newPrice.custom_unit_amount?.[key]) {
|
|
169
|
+
// @ts-ignore
|
|
169
170
|
newPrice.custom_unit_amount[key] = fromTokenToUnit(
|
|
170
171
|
newPrice.custom_unit_amount[key] as string,
|
|
171
172
|
currency.decimal
|
|
@@ -238,7 +238,7 @@ router.get('/search', auth, async (req, res) => {
|
|
|
238
238
|
|
|
239
239
|
router.get('/:id', authPortal, async (req, res) => {
|
|
240
240
|
try {
|
|
241
|
-
const doc = await Subscription.findOne({
|
|
241
|
+
const doc = (await Subscription.findOne({
|
|
242
242
|
where: { id: req.params.id },
|
|
243
243
|
include: [
|
|
244
244
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
@@ -246,10 +246,15 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
246
246
|
{ model: SubscriptionItem, as: 'items' },
|
|
247
247
|
{ model: Customer, as: 'customer' },
|
|
248
248
|
],
|
|
249
|
-
})
|
|
249
|
+
})) as Subscription & {
|
|
250
|
+
paymentMethod: PaymentMethod;
|
|
251
|
+
paymentCurrency: PaymentCurrency;
|
|
252
|
+
items: SubscriptionItem[];
|
|
253
|
+
customer: Customer;
|
|
254
|
+
};
|
|
250
255
|
|
|
251
256
|
if (doc) {
|
|
252
|
-
const json = doc.toJSON();
|
|
257
|
+
const json: any = doc.toJSON();
|
|
253
258
|
const isConsumesCredit = await doc.isConsumesCredit();
|
|
254
259
|
const serviceType = isConsumesCredit ? 'credit' : 'standard';
|
|
255
260
|
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
@@ -270,9 +275,70 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
270
275
|
logger.error('Failed to fetch subscription discount stats', { error, subscriptionId: json.id });
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
// Get payment method details
|
|
279
|
+
let paymentMethodDetails = null;
|
|
280
|
+
try {
|
|
281
|
+
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
282
|
+
if (paymentMethod?.type === 'stripe' && json.payment_details?.stripe?.subscription_id) {
|
|
283
|
+
const client = paymentMethod.getStripeClient();
|
|
284
|
+
const stripeSubscription = await client.subscriptions.retrieve(json.payment_details.stripe.subscription_id, {
|
|
285
|
+
expand: ['default_payment_method'],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (stripeSubscription.default_payment_method) {
|
|
289
|
+
const paymentMethodId =
|
|
290
|
+
typeof stripeSubscription.default_payment_method === 'string'
|
|
291
|
+
? stripeSubscription.default_payment_method
|
|
292
|
+
: stripeSubscription.default_payment_method.id;
|
|
293
|
+
|
|
294
|
+
const paymentMethodData = await client.paymentMethods.retrieve(paymentMethodId);
|
|
295
|
+
|
|
296
|
+
paymentMethodDetails = {
|
|
297
|
+
id: paymentMethodData.id,
|
|
298
|
+
type: paymentMethodData.type,
|
|
299
|
+
billing_details: paymentMethodData.billing_details,
|
|
300
|
+
} as any;
|
|
301
|
+
|
|
302
|
+
if (paymentMethodData.card) {
|
|
303
|
+
paymentMethodDetails.card = {
|
|
304
|
+
brand: paymentMethodData.card.brand,
|
|
305
|
+
last4: paymentMethodData.card.last4,
|
|
306
|
+
exp_month: paymentMethodData.card.exp_month,
|
|
307
|
+
exp_year: paymentMethodData.card.exp_year,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (paymentMethodData.link) {
|
|
312
|
+
paymentMethodDetails.link = {
|
|
313
|
+
email: paymentMethodData.link.email,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (paymentMethodData.us_bank_account) {
|
|
318
|
+
paymentMethodDetails.us_bank_account = {
|
|
319
|
+
account_type: paymentMethodData.us_bank_account.account_type,
|
|
320
|
+
bank_name: paymentMethodData.us_bank_account.bank_name,
|
|
321
|
+
last4: paymentMethodData.us_bank_account.last4,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (doc.paymentMethod) {
|
|
326
|
+
const payer = getSubscriptionPaymentAddress(doc, doc.paymentMethod.type);
|
|
327
|
+
if (payer) {
|
|
328
|
+
paymentMethodDetails = {
|
|
329
|
+
type: doc.paymentMethod.type,
|
|
330
|
+
payer,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger.error('Failed to fetch payment method details', { error, subscriptionId: json.id });
|
|
336
|
+
}
|
|
337
|
+
|
|
273
338
|
res.json({
|
|
274
339
|
...json,
|
|
275
340
|
discountStats,
|
|
341
|
+
paymentMethodDetails,
|
|
276
342
|
});
|
|
277
343
|
} else {
|
|
278
344
|
res.status(404).json(null);
|
|
@@ -2283,4 +2349,65 @@ router.get('/:id/change-payment/migrate-invoice', auth, async (req, res) => {
|
|
|
2283
2349
|
return res.status(400).json({ error: error.message });
|
|
2284
2350
|
}
|
|
2285
2351
|
});
|
|
2352
|
+
|
|
2353
|
+
router.post('/:id/update-stripe-payment-method', authPortal, async (req, res) => {
|
|
2354
|
+
try {
|
|
2355
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
2356
|
+
if (!subscription) {
|
|
2357
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
|
|
2361
|
+
return res.status(400).json({ error: 'Subscription is not active' });
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
2365
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
2366
|
+
return res.status(400).json({ error: 'Subscription is not using Stripe payment method' });
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
|
|
2370
|
+
if (!stripeSubscriptionId) {
|
|
2371
|
+
return res.status(400).json({ error: 'Stripe subscription not found' });
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
2375
|
+
if (!customer) {
|
|
2376
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
await ensureStripeCustomer(customer, paymentMethod);
|
|
2380
|
+
|
|
2381
|
+
const client = paymentMethod.getStripeClient();
|
|
2382
|
+
const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
|
|
2383
|
+
|
|
2384
|
+
const setupIntent = await client.setupIntents.create({
|
|
2385
|
+
customer: subscription.payment_details?.stripe?.customer_id,
|
|
2386
|
+
payment_method_types: ['card'],
|
|
2387
|
+
usage: 'off_session',
|
|
2388
|
+
metadata: {
|
|
2389
|
+
subscription_id: subscription.id,
|
|
2390
|
+
action: 'update_payment_method',
|
|
2391
|
+
},
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
logger.info('Setup intent created for updating stripe payment method', {
|
|
2395
|
+
subscription: subscription.id,
|
|
2396
|
+
setupIntent: setupIntent.id,
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
return res.json({
|
|
2400
|
+
client_secret: setupIntent.client_secret,
|
|
2401
|
+
publishable_key: settings.stripe?.publishable_key,
|
|
2402
|
+
setup_intent_id: setupIntent.id,
|
|
2403
|
+
});
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
logger.error('Failed to create setup intent for updating payment method', {
|
|
2406
|
+
error: err,
|
|
2407
|
+
subscriptionId: req.params.id,
|
|
2408
|
+
});
|
|
2409
|
+
return res.status(400).json({ error: err.message });
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2286
2413
|
export default router;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { fromRandom } from '@ocap/wallet';
|
|
2
|
+
import { types } from '@ocap/mcrypto';
|
|
3
|
+
|
|
4
|
+
// Create a test wallet
|
|
5
|
+
const wallet = fromRandom({
|
|
6
|
+
role: types.RoleType.ROLE_APPLICATION,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Set missing environment variables that @blocklet/sdk jest-setup.js doesn't provide
|
|
10
|
+
process.env.BLOCKLET_APP_PK = wallet.publicKey;
|
|
11
|
+
process.env.BLOCKLET_APP_EK = wallet.secretKey; // EK (Encryption Key) uses secretKey
|
package/api/third.d.ts
CHANGED
package/blocklet.yml
CHANGED
package/jest.config.js
CHANGED
|
@@ -6,8 +6,8 @@ module.exports = {
|
|
|
6
6
|
coverageDirectory: 'coverage',
|
|
7
7
|
restoreMocks: true,
|
|
8
8
|
clearMocks: true,
|
|
9
|
-
globalSetup: '
|
|
10
|
-
globalTeardown: '
|
|
9
|
+
globalSetup: '../../tools/jest-setup.js',
|
|
10
|
+
globalTeardown: '../../tools/jest-teardown.js',
|
|
11
11
|
transform: {
|
|
12
12
|
'^.+\\.ts?$': 'ts-jest',
|
|
13
13
|
},
|