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.
Files changed (65) hide show
  1. package/api/src/index.ts +3 -1
  2. package/api/src/integrations/blocklet/user.ts +2 -2
  3. package/api/src/integrations/ethereum/token.ts +4 -5
  4. package/api/src/integrations/stripe/handlers/invoice.ts +31 -26
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
  7. package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
  8. package/api/src/integrations/stripe/resource.ts +30 -1
  9. package/api/src/integrations/stripe/setup.ts +1 -1
  10. package/api/src/libs/auth.ts +7 -6
  11. package/api/src/libs/env.ts +1 -1
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +1 -0
  13. package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
  14. package/api/src/libs/payment.ts +11 -6
  15. package/api/src/libs/refund.ts +1 -1
  16. package/api/src/libs/remote-signer.ts +93 -0
  17. package/api/src/libs/security.ts +1 -1
  18. package/api/src/libs/subscription.ts +4 -7
  19. package/api/src/libs/util.ts +18 -1
  20. package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +17 -9
  21. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +11 -6
  22. package/api/src/queues/payment.ts +2 -2
  23. package/api/src/queues/payout.ts +1 -1
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +1 -1
  26. package/api/src/queues/usage-record.ts +1 -1
  27. package/api/src/queues/vendors/status-check.ts +1 -1
  28. package/api/src/queues/webhook.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +1 -1
  30. package/api/src/routes/checkout-sessions.ts +4 -6
  31. package/api/src/routes/connect/change-payer.ts +148 -0
  32. package/api/src/routes/connect/collect-batch.ts +1 -1
  33. package/api/src/routes/connect/collect.ts +1 -1
  34. package/api/src/routes/connect/pay.ts +1 -1
  35. package/api/src/routes/connect/recharge-account.ts +1 -1
  36. package/api/src/routes/connect/recharge.ts +1 -1
  37. package/api/src/routes/connect/shared.ts +62 -23
  38. package/api/src/routes/customers.ts +1 -1
  39. package/api/src/routes/integrations/stripe.ts +1 -1
  40. package/api/src/routes/invoices.ts +141 -2
  41. package/api/src/routes/meter-events.ts +9 -12
  42. package/api/src/routes/payment-currencies.ts +1 -1
  43. package/api/src/routes/payment-intents.ts +2 -2
  44. package/api/src/routes/payment-links.ts +2 -1
  45. package/api/src/routes/payouts.ts +1 -1
  46. package/api/src/routes/products.ts +1 -0
  47. package/api/src/routes/subscriptions.ts +130 -3
  48. package/api/src/store/models/types.ts +1 -1
  49. package/api/tests/setup.ts +11 -0
  50. package/api/third.d.ts +0 -2
  51. package/blocklet.yml +1 -1
  52. package/jest.config.js +2 -2
  53. package/package.json +26 -26
  54. package/src/components/invoice/table.tsx +2 -2
  55. package/src/components/invoice-pdf/template.tsx +30 -0
  56. package/src/components/subscription/payment-method-info.tsx +222 -0
  57. package/src/global.css +4 -0
  58. package/src/libs/util.ts +1 -1
  59. package/src/locales/en.tsx +13 -0
  60. package/src/locales/zh.tsx +13 -0
  61. package/src/pages/admin/billing/invoices/detail.tsx +5 -3
  62. package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
  63. package/src/pages/admin/overview.tsx +14 -14
  64. package/src/pages/customer/invoice/detail.tsx +59 -17
  65. 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
- appliedDiscounts: [],
275
- discountBreakdown: []
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(invoiceItems, checkoutSession.currency_id, trialInDays > 0 || trialEnd > now, discountConfig);
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 = requiredStake && ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup');
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({ address: paymentCurrency.contract as string, value: staking.licensed.add(staking.metered).toString() });
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 }> {
@@ -1,4 +1,4 @@
1
- import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
1
+ import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
2
2
  import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
@@ -1,4 +1,4 @@
1
- import env from '@blocklet/sdk/lib/env';
1
+ import { env } from '@blocklet/sdk/lib/env';
2
2
  import express, { NextFunction, Request, Response, Router } from 'express';
3
3
  import get from 'lodash/get';
4
4
 
@@ -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
- if (doc.metadata?.stripe_id && (doc.status !== 'paid' || req.query.forceSync)) {
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: !!req.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 where: any = {
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
- where['payload.subscription_id'] = req.query.subscription_id;
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
- where['payload.customer_id'] = customer.id;
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 benefits = await getDonationBenefits(doc);
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;
@@ -137,7 +137,7 @@ export type SimpleCustomField = {
137
137
  };
138
138
 
139
139
  export type DiscountAmount = {
140
- amount: number;
140
+ amount: string;
141
141
  discount?: string;
142
142
  promotion_code?: string;
143
143
  coupon?: string;
@@ -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
@@ -2,8 +2,6 @@ declare module 'vite-plugin-blocklet';
2
2
 
3
3
  declare module '@blocklet/sdk/service/notification';
4
4
 
5
- declare module '@blocklet/sdk/service/auth';
6
-
7
5
  declare module 'express-history-api-fallback';
8
6
 
9
7
  declare module 'express-async-errors';
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.21.16
17
+ version: 1.22.0
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
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: '@blocklet/sdk/lib/util/jest-setup.js',
10
- globalTeardown: '@blocklet/sdk/lib/util/jest-teardown.js',
9
+ globalSetup: '../../tools/jest-setup.js',
10
+ globalTeardown: '../../tools/jest-teardown.js',
11
11
  transform: {
12
12
  '^.+\\.ts?$': 'ts-jest',
13
13
  },