payment-kit 1.18.56 → 1.19.1

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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +715 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
@@ -38,6 +38,7 @@ import {
38
38
  getOneTimeLineItems,
39
39
  getCheckoutSessionSubscriptionIds,
40
40
  getSubscriptionLineItems,
41
+ isCreditMeteredLineItems,
41
42
  } from '../libs/session';
42
43
  import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../libs/subscription';
43
44
  import {
@@ -78,6 +79,7 @@ import { paymentQueue } from '../queues/payment';
78
79
  import { invoiceQueue } from '../queues/invoice';
79
80
  import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from './connect/shared';
80
81
  import {
82
+ isCreditGrantSufficientForPayment,
81
83
  isCreditSufficientForPayment,
82
84
  isDelegationSufficientForPayment,
83
85
  SufficientForPaymentResult,
@@ -616,6 +618,8 @@ async function processSubscriptionFastCheckout({
616
618
  success: boolean;
617
619
  invoices?: Invoice[];
618
620
  message?: string;
621
+ token?: { address: string; balance: string };
622
+ amount?: string;
619
623
  }> {
620
624
  try {
621
625
  const primarySubscription = subscriptions.find((x) => x.metadata?.is_primary_subscription) || subscriptions[0];
@@ -629,12 +633,17 @@ async function processSubscriptionFastCheckout({
629
633
  .reduce((sum: BN, amt: string) => sum.add(new BN(amt)), new BN('0'))
630
634
  .toString();
631
635
 
636
+ let validAmount = totalAmount;
637
+ if (totalAmount === '0' && paymentCurrency.isCredit()) {
638
+ validAmount = '1';
639
+ }
632
640
  const delegationCheck = await isDelegationSufficientForPayment({
633
641
  paymentMethod,
634
642
  paymentCurrency,
635
643
  userDid: customer.did,
636
- amount: totalAmount,
644
+ amount: validAmount,
637
645
  delegatorAmounts: subscriptionAmounts,
646
+ lineItems,
638
647
  });
639
648
 
640
649
  if (!delegationCheck.sufficient) {
@@ -646,6 +655,8 @@ async function processSubscriptionFastCheckout({
646
655
  return {
647
656
  success: false,
648
657
  message: `Insufficient delegation or insufficient balance: ${delegationCheck.reason}`,
658
+ token: delegationCheck.token,
659
+ amount: totalAmount,
649
660
  };
650
661
  }
651
662
 
@@ -658,6 +669,33 @@ async function processSubscriptionFastCheckout({
658
669
  if (executePayment) {
659
670
  // Update payment settings for all subscriptions
660
671
  await Promise.all(subscriptions.map((sub) => sub.update({ payment_settings: paymentSettings })));
672
+ if (paymentCurrency.isCredit()) {
673
+ // skip invoice creation for credit subscriptions
674
+ checkoutSession.update({
675
+ status: 'complete',
676
+ payment_status: 'paid',
677
+ });
678
+ await Promise.all(
679
+ subscriptions.map(async (sub) => {
680
+ await sub.update({
681
+ payment_settings: paymentSettings,
682
+ status: sub.trial_end ? 'trialing' : 'active',
683
+ payment_details: {
684
+ [paymentMethod.type]: {
685
+ type: 'credit',
686
+ payer: customer.did,
687
+ },
688
+ },
689
+ });
690
+ addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
691
+ })
692
+ );
693
+ return {
694
+ success: true,
695
+ invoices: [],
696
+ token: delegationCheck.token,
697
+ };
698
+ }
661
699
 
662
700
  // Create invoices for all subscriptions
663
701
  const { invoices } = await ensureInvoicesForSubscriptions({
@@ -686,11 +724,15 @@ async function processSubscriptionFastCheckout({
686
724
  return {
687
725
  success: true,
688
726
  invoices,
727
+ token: delegationCheck.token,
728
+ amount: totalAmount,
689
729
  };
690
730
  }
691
731
  return {
692
732
  success: true,
693
733
  invoices: [],
734
+ token: delegationCheck.token,
735
+ amount: totalAmount,
694
736
  };
695
737
  } catch (error) {
696
738
  logger.error('Error processing subscription fast checkout', {
@@ -701,6 +743,8 @@ async function processSubscriptionFastCheckout({
701
743
  return {
702
744
  success: false,
703
745
  message: error.message,
746
+ token: { address: paymentCurrency.id, balance: '0' },
747
+ amount: '0',
704
748
  };
705
749
  }
706
750
  }
@@ -754,7 +798,9 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
754
798
  }
755
799
 
756
800
  // Settings priority: PaymentLink.subscription_data > req.query > environments
757
- const protectedSettings: Partial<SubscriptionData> = {};
801
+ const protectedSettings: Partial<SubscriptionData> = {
802
+ no_stake: isCreditMeteredLineItems(items) ? true : link.subscription_data?.no_stake,
803
+ };
758
804
  if (link.subscription_data?.min_stake_amount) {
759
805
  protectedSettings.min_stake_amount = getMinStakeAmount(link.subscription_data);
760
806
  }
@@ -964,6 +1010,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
964
1010
  }
965
1011
 
966
1012
  const checkoutSession = req.doc as CheckoutSession;
1013
+ logger.info('---checkoutSession---', checkoutSession.line_items);
967
1014
  if (checkoutSession.line_items) {
968
1015
  try {
969
1016
  await validateInventory(checkoutSession.line_items);
@@ -1231,6 +1278,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1231
1278
  let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
1232
1279
  let fastPayInfo = null;
1233
1280
  let delegation: SufficientForPaymentResult | null = null;
1281
+ let creditSufficient = false;
1282
+
1283
+ const canFastPayForSubscription =
1284
+ paymentCurrency.isCredit() ||
1285
+ (checkoutSession.mode === 'subscription' && checkoutSession.subscription_data?.no_stake);
1234
1286
  if (isPayment && paymentIntent && canFastPay) {
1235
1287
  // if we can complete purchase without any wallet interaction
1236
1288
  delegation = await isDelegationSufficientForPayment({
@@ -1246,11 +1298,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1246
1298
  payer: customer.did,
1247
1299
  };
1248
1300
  }
1249
- } else if (
1250
- paymentMethod.type === 'arcblock' &&
1251
- checkoutSession.mode === 'subscription' &&
1252
- checkoutSession.subscription_data?.no_stake
1253
- ) {
1301
+ } else if (paymentMethod.type === 'arcblock' && canFastPayForSubscription) {
1254
1302
  // if we can complete purchase without any wallet interaction
1255
1303
  const result = await processSubscriptionFastCheckout({
1256
1304
  checkoutSession,
@@ -1269,6 +1317,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1269
1317
  logger.warn(`Fast checkout processing failed: ${result.message}`, {
1270
1318
  checkoutSessionId: checkoutSession.id,
1271
1319
  });
1320
+
1321
+ creditSufficient = false;
1272
1322
  } else {
1273
1323
  delegation = {
1274
1324
  sufficient: true,
@@ -1276,9 +1326,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1276
1326
  canFastPay = true;
1277
1327
  fastPayInfo = {
1278
1328
  type: 'delegation',
1279
- amount: fastCheckoutAmount,
1329
+ amount: result?.amount || '0',
1280
1330
  payer: customer.did,
1331
+ token: result?.token,
1281
1332
  };
1333
+ if (paymentCurrency.isCredit()) {
1334
+ creditSufficient = true;
1335
+ }
1282
1336
  }
1283
1337
  }
1284
1338
 
@@ -1411,6 +1465,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1411
1465
  delegation: canFastPay ? delegation : null,
1412
1466
  balance: canFastPay ? balance : null,
1413
1467
  fastPayInfo,
1468
+ creditSufficient,
1414
1469
  });
1415
1470
  } catch (err) {
1416
1471
  logger.error('Error submitting checkout session', {
@@ -1587,12 +1642,78 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
1587
1642
  customer,
1588
1643
  amount: fastCheckoutAmount,
1589
1644
  });
1645
+ // Check if this is a credit payment
1646
+ const isCredit = paymentCurrency.isCredit();
1590
1647
 
1591
1648
  const isPayment = checkoutSession.mode === 'payment';
1592
1649
  let fastPaid = false;
1593
1650
  let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
1594
1651
  let delegation: SufficientForPaymentResult | null = null;
1595
- if (isPayment && paymentIntent && canFastPay) {
1652
+
1653
+ // Handle credit payment directly
1654
+ if (isCredit) {
1655
+ const result = await isCreditGrantSufficientForPayment({
1656
+ paymentMethod,
1657
+ paymentCurrency,
1658
+ userDid: customer.did,
1659
+ amount: fastCheckoutAmount,
1660
+ });
1661
+ if (!result.sufficient) {
1662
+ return res.status(400).json({
1663
+ code: 'CREDIT_INSUFFICIENT',
1664
+ error: result.reason,
1665
+ sufficient: result.sufficient,
1666
+ });
1667
+ }
1668
+ fastPaid = true;
1669
+ // For credit payments, we use the existing subscription fast checkout flow
1670
+ // but skip the actual payment processing
1671
+ if (['setup', 'subscription'].includes(checkoutSession.mode)) {
1672
+ const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
1673
+ const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
1674
+
1675
+ if (checkoutSession.mode === 'setup') {
1676
+ const setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
1677
+ if (!setupIntent) {
1678
+ throw new Error('SetupIntent not found for checkoutSession');
1679
+ }
1680
+ await setupIntent.update({
1681
+ status: 'succeeded',
1682
+ last_setup_error: null,
1683
+ setup_details: {
1684
+ [paymentMethod.type]: {
1685
+ type: 'credit',
1686
+ payer: customer.did,
1687
+ },
1688
+ },
1689
+ ...paymentSettings,
1690
+ });
1691
+ }
1692
+ checkoutSession.update({
1693
+ status: 'complete',
1694
+ payment_status: 'paid',
1695
+ });
1696
+ await Promise.all(
1697
+ subscriptions.map(async (sub) => {
1698
+ await sub.update({
1699
+ payment_settings: paymentSettings,
1700
+ status: sub.trial_end ? 'trialing' : 'active',
1701
+ payment_details: {
1702
+ [paymentMethod.type]: {
1703
+ type: 'credit',
1704
+ payer: customer.did,
1705
+ },
1706
+ },
1707
+ });
1708
+ addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
1709
+ })
1710
+ );
1711
+ delegation = {
1712
+ sufficient: true,
1713
+ };
1714
+ canFastPay = true;
1715
+ }
1716
+ } else if (isPayment && paymentIntent && canFastPay) {
1596
1717
  // if we can complete purchase without any wallet interaction
1597
1718
  delegation = await isDelegationSufficientForPayment({
1598
1719
  paymentMethod,
@@ -1764,6 +1885,33 @@ router.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (req, res) =>
1764
1885
  }
1765
1886
  });
1766
1887
 
1888
+ // adjust quantity
1889
+ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req, res) => {
1890
+ try {
1891
+ const checkoutSession = req.doc as CheckoutSession;
1892
+ const { itemId, quantity } = req.body;
1893
+ if (!checkoutSession.line_items) {
1894
+ return res.status(400).json({ error: 'Line items not found' });
1895
+ }
1896
+
1897
+ const item = checkoutSession.line_items.find((x) => x.price_id === itemId);
1898
+ if (!item) {
1899
+ return res.status(400).json({ error: 'Item not found' });
1900
+ }
1901
+ const items = cloneDeep(checkoutSession.line_items);
1902
+ const targetItem = items.find((x) => x.price_id === itemId);
1903
+ if (targetItem) {
1904
+ targetItem.quantity = quantity;
1905
+ }
1906
+ await validateInventory(items, true);
1907
+ await checkoutSession.update({ line_items: items });
1908
+ const lineItems = await Price.expand(checkoutSession.line_items);
1909
+ res.json({ ...checkoutSession.toJSON(), line_items: lineItems });
1910
+ } catch (err) {
1911
+ logger.error(err);
1912
+ res.status(400).json({ error: err.message });
1913
+ }
1914
+ });
1767
1915
  // eslint-disable-next-line consistent-return
1768
1916
  router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
1769
1917
  const doc = req.doc as CheckoutSession;
@@ -1063,8 +1063,9 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
1063
1063
  if (!subscription) {
1064
1064
  throw new Error(`Subscription not found: ${subscriptionId}`);
1065
1065
  }
1066
- if (subscription.status !== 'past_due') {
1067
- throw new Error(`Subscription ${subscriptionId} is not in past_due status`);
1066
+
1067
+ if (subscription.isActive() === false) {
1068
+ throw new Error(`Subscription ${subscriptionId} is not active`);
1068
1069
  }
1069
1070
 
1070
1071
  const paymentCurrencyId = subscription.currency_id;
@@ -0,0 +1,241 @@
1
+ import { Router } from 'express';
2
+ import Joi from 'joi';
3
+ import { fromTokenToUnit } from '@ocap/util';
4
+
5
+ import { literal, OrderItem } from 'sequelize';
6
+ import pick from 'lodash/pick';
7
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
8
+ import logger from '../libs/logger';
9
+ import { authenticate } from '../libs/security';
10
+ import { CreditGrant, Customer, PaymentCurrency, Price, Subscription } from '../store/models';
11
+ import { createCreditGrant } from '../libs/credit-grant';
12
+ import { getMeterPriceIdsFromSubscription } from '../libs/subscription';
13
+
14
+ const router = Router();
15
+ const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
16
+ const authMine = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'], mine: true });
17
+ const authPortal = authenticate<CreditGrant>({
18
+ component: true,
19
+ roles: ['owner', 'admin'],
20
+ record: {
21
+ // @ts-ignore
22
+ model: CreditGrant,
23
+ field: 'customer_id',
24
+ },
25
+ });
26
+ const creditGrantSchema = Joi.object({
27
+ amount: Joi.number().required(),
28
+ currency_id: Joi.string().max(15).optional(),
29
+ customer_id: Joi.string().max(18).required(),
30
+ name: Joi.string().max(255).optional(),
31
+ category: Joi.string().valid('paid', 'promotional').required(),
32
+ priority: Joi.number().integer().min(0).max(100).default(50),
33
+ effective_at: Joi.number().integer().optional(),
34
+ expires_at: Joi.number().integer().optional(),
35
+ metadata: MetadataSchema,
36
+ applicability_config: Joi.object({
37
+ scope: Joi.object({
38
+ price_type: Joi.string().valid('metered').optional(),
39
+ prices: Joi.array().items(Joi.string()).optional(),
40
+ }).optional(),
41
+ }).optional(),
42
+ });
43
+
44
+ const listSchema = createListParamSchema<{
45
+ customer_id?: string;
46
+ currency_id?: string;
47
+ status?: string;
48
+ livemode?: boolean;
49
+ q?: string;
50
+ }>({
51
+ customer_id: Joi.string().optional(),
52
+ currency_id: Joi.string().optional(),
53
+ status: Joi.string().optional(),
54
+ livemode: Joi.boolean().optional(),
55
+ q: Joi.string().optional(),
56
+ });
57
+
58
+ async function expandScopePrices(creditGrant: CreditGrant) {
59
+ const scope = creditGrant.applicability_config?.scope;
60
+ if (scope && scope.prices) {
61
+ const expandedItems = await Price.expand(
62
+ scope.prices.map((x) => ({ id: x, price_id: x, quantity: 1 })),
63
+ { product: true }
64
+ );
65
+ return expandedItems;
66
+ }
67
+ return [];
68
+ }
69
+
70
+ router.get('/', authMine, async (req, res) => {
71
+ try {
72
+ const { page, pageSize, ...query } = await listSchema.validateAsync(req.query, { stripUnknown: true });
73
+ const where = getWhereFromKvQuery(query.q);
74
+ if (query.customer_id) {
75
+ where.customer_id = query.customer_id;
76
+ }
77
+ if (query.currency_id) {
78
+ where.currency_id = query.currency_id;
79
+ }
80
+ if (query.status) {
81
+ where.status = typeof query.status === 'string' ? query.status.split(',') : query.status;
82
+ }
83
+ if (typeof query.livemode === 'boolean') {
84
+ where.livemode = query.livemode;
85
+ }
86
+
87
+ const order: OrderItem[] = getOrder(req.query);
88
+ // 默认granted 、pending、depleted 排序
89
+ order.unshift([literal("CASE status WHEN 'granted' THEN 1 WHEN 'pending' THEN 2 ELSE 3 END"), 'ASC']);
90
+
91
+ const { rows: list, count } = await CreditGrant.findAndCountAll({
92
+ where,
93
+ order,
94
+ offset: (page - 1) * pageSize,
95
+ limit: pageSize,
96
+ include: [
97
+ { model: Customer, as: 'customer' },
98
+ { model: PaymentCurrency, as: 'paymentCurrency' },
99
+ ],
100
+ });
101
+
102
+ res.json({ count, list, paging: { page, pageSize } });
103
+ } catch (err) {
104
+ logger.error('Error listing credit grants', err);
105
+ res.status(400).json({ error: err.message });
106
+ }
107
+ });
108
+
109
+ router.get('/summary', authMine, async (req, res) => {
110
+ try {
111
+ const customerId = req.query.customer_id;
112
+ if (!customerId) {
113
+ return res.status(400).json({ error: 'customer_id is required' });
114
+ }
115
+
116
+ const customer = await Customer.findByPkOrDid(customerId as string);
117
+ if (!customer) {
118
+ return res.status(404).json({ error: `Customer ${customerId} not found` });
119
+ }
120
+
121
+ const { subscription_id: subscriptionId } = req.query;
122
+ if (subscriptionId && typeof subscriptionId !== 'string') {
123
+ return res.status(400).json({ error: 'subscription_id must be a string' });
124
+ }
125
+
126
+ let priceIds: string[] = [];
127
+ if (subscriptionId) {
128
+ const subscription = await Subscription.findByPk(subscriptionId);
129
+ if (!subscription) {
130
+ return res.status(404).json({ error: 'Subscription not found' });
131
+ }
132
+ priceIds = await getMeterPriceIdsFromSubscription(subscription);
133
+ }
134
+
135
+ const result = await CreditGrant.getEffectiveCreditSummary({
136
+ customerId: customer.id,
137
+ priceIds,
138
+ });
139
+
140
+ return res.json(result);
141
+ } catch (err: any) {
142
+ logger.error('get credit balance failed', { error: err.message, customerId: req.params.customer_id });
143
+ return res.status(400).json({ error: err.message });
144
+ }
145
+ });
146
+
147
+ router.get('/:id', authPortal, async (req, res) => {
148
+ const creditGrant = await CreditGrant.findByPk(req.params.id, {
149
+ include: [
150
+ { model: Customer, as: 'customer' },
151
+ { model: PaymentCurrency, as: 'paymentCurrency' },
152
+ ],
153
+ });
154
+ if (!creditGrant) {
155
+ return res.status(404).json({ error: `Credit grant ${req.params.id} not found` });
156
+ }
157
+ const expandedPrices = await expandScopePrices(creditGrant);
158
+ return res.json({
159
+ ...creditGrant.toJSON(),
160
+ items: expandedPrices,
161
+ });
162
+ });
163
+
164
+ router.post('/', auth, async (req, res) => {
165
+ try {
166
+ const { error } = creditGrantSchema.validate(req.body);
167
+ if (error) {
168
+ return res.status(400).json({ error: `Credit grant create request invalid: ${error.message}` });
169
+ }
170
+
171
+ // 获取币种信息用于金额转换
172
+ const currencyId = req.body.currency_id;
173
+ if (!currencyId) {
174
+ return res.status(400).json({ error: 'currency_id is required' });
175
+ }
176
+
177
+ const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
178
+ if (!paymentCurrency) {
179
+ return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
180
+ }
181
+
182
+ const customer = await Customer.findByPkOrDid(req.body.customer_id);
183
+ if (!customer) {
184
+ return res.status(404).json({ error: `Customer ${req.body.customer_id} not found` });
185
+ }
186
+
187
+ const unitAmount = fromTokenToUnit(req.body.amount, paymentCurrency.decimal).toString();
188
+ let applicabilityConfig = req.body.applicability_config;
189
+ if (!req.body.applicability_config || !req.body.applicability_config.scope?.prices) {
190
+ applicabilityConfig = {
191
+ scope: {
192
+ price_type: 'metered',
193
+ },
194
+ };
195
+ }
196
+
197
+ const creditGrant = await createCreditGrant({
198
+ amount: unitAmount,
199
+ currency_id: currencyId,
200
+ customer_id: req.body.customer_id,
201
+ name: req.body.name,
202
+ category: req.body.category,
203
+ priority: req.body.priority,
204
+ effective_at: req.body.effective_at,
205
+ expires_at: req.body.expires_at,
206
+ applicability_config: applicabilityConfig,
207
+ metadata: req.body.metadata,
208
+ livemode: req.livemode,
209
+ created_via: req.user?.via || 'api',
210
+ created_by: req.user?.did,
211
+ });
212
+
213
+ return res.json({
214
+ ...creditGrant.toJSON(),
215
+ customer,
216
+ paymentCurrency,
217
+ });
218
+ } catch (err: any) {
219
+ logger.error('create credit grant failed', { error: err.message, request: req.body });
220
+ return res.status(400).json({ error: err.message });
221
+ }
222
+ });
223
+
224
+ const updateMetadataSchema = Joi.object({
225
+ metadata: MetadataSchema,
226
+ });
227
+
228
+ router.put('/:id', auth, async (req, res) => {
229
+ const creditGrant = await CreditGrant.findByPk(req.params.id);
230
+ if (!creditGrant) {
231
+ return res.status(404).json({ error: `Credit grant ${req.params.id} not found` });
232
+ }
233
+ const { error } = updateMetadataSchema.validate(pick(req.body, 'metadata'));
234
+ if (error) {
235
+ return res.status(400).json({ error: `Credit grant update request invalid: ${error.message}` });
236
+ }
237
+ await creditGrant.update({ metadata: req.body.metadata });
238
+ return res.json({ success: true });
239
+ });
240
+
241
+ export default router;