payment-kit 1.27.2 → 1.29.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 (241) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +32 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/crons/retry-pending-events.ts +58 -0
  7. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  8. package/api/src/integrations/app-store/client.ts +369 -0
  9. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  10. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  11. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  12. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  13. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  14. package/api/src/integrations/arcblock/nft.ts +6 -2
  15. package/api/src/integrations/arcblock/stake.ts +3 -2
  16. package/api/src/integrations/arcblock/token.ts +4 -4
  17. package/api/src/integrations/blocklet/notification.ts +1 -1
  18. package/api/src/integrations/ethereum/tx.ts +29 -0
  19. package/api/src/integrations/google-play/client.ts +276 -0
  20. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  21. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  22. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  23. package/api/src/integrations/google-play/setup.ts +43 -0
  24. package/api/src/integrations/google-play/verify.ts +251 -0
  25. package/api/src/integrations/iap-reconcile.ts +415 -0
  26. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  27. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  28. package/api/src/integrations/stripe/resource.ts +8 -0
  29. package/api/src/libs/audit.ts +70 -24
  30. package/api/src/libs/auth.ts +49 -2
  31. package/api/src/libs/chain-error.ts +31 -0
  32. package/api/src/libs/entitlement.ts +399 -0
  33. package/api/src/libs/env.ts +2 -0
  34. package/api/src/libs/error.ts +15 -0
  35. package/api/src/libs/event.ts +42 -1
  36. package/api/src/libs/invoice.ts +69 -34
  37. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  38. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  39. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  40. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  41. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  42. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  43. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  44. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  45. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  46. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  47. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  48. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  49. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  50. package/api/src/libs/pagination.ts +14 -9
  51. package/api/src/libs/payment.ts +25 -10
  52. package/api/src/libs/security.ts +51 -0
  53. package/api/src/libs/session.ts +1 -1
  54. package/api/src/libs/subscription.ts +13 -1
  55. package/api/src/libs/timing.ts +35 -0
  56. package/api/src/libs/util.ts +29 -15
  57. package/api/src/libs/wallet-migration.ts +72 -53
  58. package/api/src/queues/auto-recharge.ts +1 -1
  59. package/api/src/queues/credit-consume.ts +94 -12
  60. package/api/src/queues/credit-grant.ts +4 -0
  61. package/api/src/queues/event.ts +39 -21
  62. package/api/src/queues/invoice.ts +1 -0
  63. package/api/src/queues/payment.ts +83 -15
  64. package/api/src/queues/refund.ts +84 -71
  65. package/api/src/queues/subscription.ts +1 -0
  66. package/api/src/queues/webhook.ts +12 -2
  67. package/api/src/routes/checkout-sessions.ts +82 -43
  68. package/api/src/routes/connect/change-payment.ts +2 -0
  69. package/api/src/routes/connect/change-plan.ts +2 -0
  70. package/api/src/routes/connect/pay.ts +12 -3
  71. package/api/src/routes/connect/setup.ts +3 -1
  72. package/api/src/routes/connect/shared.ts +52 -39
  73. package/api/src/routes/connect/subscribe.ts +4 -1
  74. package/api/src/routes/credit-grants.ts +25 -17
  75. package/api/src/routes/donations.ts +2 -2
  76. package/api/src/routes/entitlements.ts +105 -0
  77. package/api/src/routes/events.ts +2 -2
  78. package/api/src/routes/index.ts +12 -2
  79. package/api/src/routes/integrations/app-store.ts +267 -0
  80. package/api/src/routes/integrations/google-play.ts +324 -0
  81. package/api/src/routes/meter-events.ts +16 -6
  82. package/api/src/routes/payment-links.ts +1 -1
  83. package/api/src/routes/payment-methods.ts +131 -1
  84. package/api/src/routes/settings.ts +1 -1
  85. package/api/src/routes/tax-rates.ts +1 -1
  86. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  87. package/api/src/store/models/customer.ts +37 -1
  88. package/api/src/store/models/entitlement-grant.ts +118 -0
  89. package/api/src/store/models/entitlement-product.ts +48 -0
  90. package/api/src/store/models/entitlement.ts +86 -0
  91. package/api/src/store/models/index.ts +9 -0
  92. package/api/src/store/models/invoice.ts +20 -0
  93. package/api/src/store/models/payment-method.ts +66 -1
  94. package/api/src/store/models/price.ts +23 -14
  95. package/api/src/store/models/refund.ts +10 -0
  96. package/api/src/store/models/subscription.ts +14 -0
  97. package/api/src/store/models/types.ts +32 -0
  98. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  99. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  100. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  101. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  102. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  103. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  104. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  105. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  106. package/api/tests/libs/entitlement.spec.ts +347 -0
  107. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  108. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  109. package/api/tests/queues/credit-consume.spec.ts +8 -4
  110. package/api/tests/routes/credit-grants.spec.ts +1 -0
  111. package/blocklet.yml +1 -1
  112. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  113. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  114. package/cloudflare/README.md +499 -0
  115. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  116. package/cloudflare/build.ts +151 -0
  117. package/cloudflare/did-connect-auth.ts +527 -0
  118. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  119. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  120. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  121. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  122. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  123. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  124. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  125. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  126. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  127. package/cloudflare/frontend-shims/session.ts +24 -0
  128. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  129. package/cloudflare/index.html +40 -0
  130. package/cloudflare/migrate-to-d1.js +252 -0
  131. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  132. package/cloudflare/migrations/0002_indexes.sql +75 -0
  133. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  134. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  135. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  136. package/cloudflare/run-build.js +391 -0
  137. package/cloudflare/scripts/test-decrypt.js +102 -0
  138. package/cloudflare/shims/arcblock-ws.ts +20 -0
  139. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  140. package/cloudflare/shims/axios-lite.ts +117 -0
  141. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  142. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  143. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  144. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  145. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  146. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  147. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  148. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  149. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  150. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  151. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  152. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  153. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  154. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  155. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  156. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  157. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  158. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  159. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  160. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  161. package/cloudflare/shims/cookie-parser.ts +3 -0
  162. package/cloudflare/shims/cors.ts +21 -0
  163. package/cloudflare/shims/cron.ts +189 -0
  164. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  165. package/cloudflare/shims/did-space-js.ts +17 -0
  166. package/cloudflare/shims/did-space.ts +11 -0
  167. package/cloudflare/shims/error.ts +18 -0
  168. package/cloudflare/shims/express-compat/index.ts +80 -0
  169. package/cloudflare/shims/express-compat/types.ts +41 -0
  170. package/cloudflare/shims/fastq.ts +105 -0
  171. package/cloudflare/shims/lock.ts +115 -0
  172. package/cloudflare/shims/mime-types.ts +56 -0
  173. package/cloudflare/shims/nedb-storage.ts +9 -0
  174. package/cloudflare/shims/node-child-process.ts +9 -0
  175. package/cloudflare/shims/node-fs.ts +20 -0
  176. package/cloudflare/shims/node-http.ts +13 -0
  177. package/cloudflare/shims/node-https.ts +4 -0
  178. package/cloudflare/shims/node-misc.ts +15 -0
  179. package/cloudflare/shims/node-net.ts +8 -0
  180. package/cloudflare/shims/node-os.ts +14 -0
  181. package/cloudflare/shims/node-tty.ts +8 -0
  182. package/cloudflare/shims/node-zlib.ts +17 -0
  183. package/cloudflare/shims/noop.ts +26 -0
  184. package/cloudflare/shims/payment-vendor.ts +14 -0
  185. package/cloudflare/shims/querystring.ts +12 -0
  186. package/cloudflare/shims/queue.ts +611 -0
  187. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  188. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  189. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  190. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  191. package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
  192. package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
  193. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  194. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  195. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  196. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  197. package/cloudflare/shims/stripe-cf.ts +29 -0
  198. package/cloudflare/shims/ws-lite.ts +103 -0
  199. package/cloudflare/shims/xss.ts +3 -0
  200. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  201. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  202. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  203. package/cloudflare/vite.config.ts +162 -0
  204. package/cloudflare/worker.ts +1608 -0
  205. package/cloudflare/wrangler.json +63 -0
  206. package/cloudflare/wrangler.jsonc +75 -0
  207. package/cloudflare/wrangler.staging.json +67 -0
  208. package/cloudflare/wrangler.toml +28 -0
  209. package/jest.config.js +4 -12
  210. package/package.json +30 -22
  211. package/scripts/seed-google-play.ts +79 -0
  212. package/src/app.tsx +62 -4
  213. package/src/components/customer/link.tsx +9 -13
  214. package/src/components/customer/notification-preference.tsx +3 -2
  215. package/src/components/filter-toolbar.tsx +4 -0
  216. package/src/components/invoice/list.tsx +9 -1
  217. package/src/components/invoice-pdf/utils.ts +2 -1
  218. package/src/components/layout/admin.tsx +39 -5
  219. package/src/components/layout/user-cf.tsx +77 -0
  220. package/src/components/payment-intent/actions.tsx +23 -3
  221. package/src/components/payment-method/app-store.tsx +103 -0
  222. package/src/components/payment-method/form.tsx +7 -1
  223. package/src/components/payment-method/google-play.tsx +85 -0
  224. package/src/components/safe-did-address.tsx +75 -0
  225. package/src/components/subscription/list.tsx +20 -0
  226. package/src/libs/patch-user-card.ts +25 -0
  227. package/src/libs/util.ts +5 -7
  228. package/src/locales/en.tsx +63 -0
  229. package/src/locales/zh.tsx +63 -0
  230. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  231. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  232. package/src/pages/admin/customers/customers/detail.tsx +8 -2
  233. package/src/pages/admin/customers/customers/index.tsx +2 -2
  234. package/src/pages/admin/overview.tsx +3 -1
  235. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  236. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
  237. package/src/pages/customer/subscription/detail.tsx +4 -4
  238. package/tsconfig.api.json +1 -6
  239. package/tsconfig.json +3 -4
  240. package/tsconfig.types.json +2 -1
  241. package/vite.config.ts +6 -1
@@ -2,6 +2,8 @@
2
2
  export default class CustomError extends Error {
3
3
  code: string;
4
4
 
5
+ details?: Record<string, unknown>;
6
+
5
7
  constructor(code = 'GENERIC', ...params: any[]) {
6
8
  super(...params);
7
9
 
@@ -11,6 +13,19 @@ export default class CustomError extends Error {
11
13
 
12
14
  this.code = code;
13
15
  }
16
+
17
+ withDetails(details: Record<string, unknown>) {
18
+ this.details = details;
19
+ return this;
20
+ }
21
+
22
+ toJSON() {
23
+ return {
24
+ code: this.code,
25
+ message: this.message,
26
+ details: this.details,
27
+ };
28
+ }
14
29
  }
15
30
 
16
31
  /**
@@ -9,7 +9,48 @@ interface MyEventType extends EventEmitter {
9
9
  emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
10
10
  }
11
11
 
12
- export const events = new EventEmitter() as MyEventType;
12
+ // eslint-disable-next-line @typescript-eslint/naming-convention
13
+ const _events = new EventEmitter();
14
+
15
+ // Wrap on/once so async listeners auto-register their Promises in CF Workers.
16
+ // EventEmitter.emit() is synchronous and discards async listener return values.
17
+ // In CF Workers, untracked Promises are abandoned when the request/consumer ends.
18
+ // This wrapper ensures async listener work completes via flushPendingJobs().
19
+ // In Blocklet Server (__cfPendingJobs__ doesn't exist), this is a transparent pass-through.
20
+ function wrapAsyncListener(listener: (...args: any[]) => any): (...args: any[]) => any {
21
+ return function (...args: any[]) {
22
+ const result = listener(...args);
23
+ if (result && typeof result.then === 'function') {
24
+ // Context-aware: HTTP uses waitUntil (non-blocking), queue/cron uses pendingJobs (blocking)
25
+ const isHttp = (globalThis as any).__cfHttpContext__;
26
+ if (isHttp) {
27
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
28
+ if (typeof waitUntil === 'function') {
29
+ waitUntil(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
30
+ }
31
+ } else {
32
+ const pending = (globalThis as any).__cfPendingJobs__;
33
+ if (Array.isArray(pending)) {
34
+ pending.push(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
35
+ }
36
+ }
37
+ }
38
+ return result;
39
+ };
40
+ }
41
+
42
+ // eslint-disable-next-line @typescript-eslint/naming-convention
43
+ const _origOn = _events.on.bind(_events);
44
+ // eslint-disable-next-line @typescript-eslint/naming-convention
45
+ const _origOnce = _events.once.bind(_events);
46
+ _events.on = function (eventName: string | symbol, listener: (...args: any[]) => void) {
47
+ return _origOn(eventName, wrapAsyncListener(listener));
48
+ };
49
+ _events.once = function (eventName: string | symbol, listener: (...args: any[]) => void) {
50
+ return _origOnce(eventName, wrapAsyncListener(listener));
51
+ };
52
+
53
+ export const events = _events as MyEventType;
13
54
 
14
55
  export const emitAsync = (event: string, ...args: any[]) => {
15
56
  return new Promise((resolve, reject) => {
@@ -4,7 +4,8 @@ import { withQuery } from 'ufo';
4
4
 
5
5
  import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { Op, type WhereOptions } from 'sequelize';
7
- import { cloneDeep, pick } from 'lodash';
7
+ import cloneDeep from 'lodash/cloneDeep';
8
+ import pick from 'lodash/pick';
8
9
  import {
9
10
  Customer,
10
11
  Invoice,
@@ -78,8 +79,10 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
78
79
  throw new Error(`Invoice not found: ${invoiceId}`);
79
80
  }
80
81
  const json = doc.toJSON();
81
- const products = (await Product.findAll()).map((x) => x.toJSON());
82
- const prices = (await Price.findAll()).map((x) => x.toJSON());
82
+ const [products, prices] = await Promise.all([
83
+ Product.findAll().then((xs) => xs.map((x) => x.toJSON())),
84
+ Price.findAll().then((xs) => xs.map((x) => x.toJSON())),
85
+ ]);
83
86
  // @ts-ignore
84
87
  expandLineItems(json.lines, products, prices);
85
88
  const oneTimePaymentInfo: Array<{
@@ -108,12 +111,18 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
108
111
 
109
112
  export async function getInvoiceShouldPayTotal(invoice: Invoice) {
110
113
  try {
111
- const subscription = await Subscription.findByPk(invoice.subscription_id);
114
+ // Parallel fetch: subscription + paymentCurrency are independent;
115
+ // subscriptionItems + invoiceItems are also independent of each other.
116
+ const [subscription, paymentCurrency, subscriptionItems, invoiceItems] = await Promise.all([
117
+ Subscription.findByPk(invoice.subscription_id),
118
+ PaymentCurrency.findByPk(invoice.currency_id),
119
+ SubscriptionItem.findAll({ where: { subscription_id: invoice.subscription_id } }),
120
+ InvoiceItem.findAll({ where: { invoice_id: invoice.id } }),
121
+ ]);
122
+
112
123
  if (!subscription) {
113
124
  throw new Error(`Subscription not found: ${invoice.subscription_id}`);
114
125
  }
115
-
116
- const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
117
126
  if (!paymentCurrency) {
118
127
  throw new Error(`Payment currency not found: ${invoice.currency_id}`);
119
128
  }
@@ -123,12 +132,6 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
123
132
  throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
124
133
  }
125
134
 
126
- const subscriptionItems = await SubscriptionItem.findAll({
127
- where: { subscription_id: subscription.id },
128
- });
129
- const invoiceItems = await InvoiceItem.findAll({
130
- where: { invoice_id: invoice.id },
131
- });
132
135
  const invoiceQuoteIds = invoiceItems
133
136
  .map((item) => item.metadata?.quote_id)
134
137
  .filter((id): id is string => typeof id === 'string' && id.length > 0);
@@ -577,25 +580,50 @@ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
577
580
  })
578
581
  : itemsData;
579
582
 
583
+ // Pre-fetch all prices and products in bulk to avoid N+1 queries per item
584
+ // eslint-disable-next-line @typescript-eslint/naming-convention
585
+ const _itemPriceIds = enrichedItemsData.map((item) => item.price_id).filter(Boolean);
586
+ // eslint-disable-next-line @typescript-eslint/naming-convention
587
+ const [_allPrices, _taxRateForCustomer] = await Promise.all([
588
+ Price.findAll({ where: { id: _itemPriceIds }, include: [{ model: Product, as: 'product' }] }),
589
+ // Tax rate matching only depends on customer address — same for all items
590
+ customer.address?.country
591
+ ? TaxRate.findMatchingRate({
592
+ country: customer.address.country,
593
+ state: customer.address.state,
594
+ postalCode: customer.address.postal_code,
595
+ livemode,
596
+ }).catch(() => null)
597
+ : Promise.resolve(null),
598
+ ]);
599
+ // eslint-disable-next-line @typescript-eslint/naming-convention
600
+ const _priceMap = new Map(_allPrices.map((p: any) => [p.id, p]));
601
+
580
602
  // create invoice items
581
603
  const items = await Promise.all(
582
604
  enrichedItemsData.map(async (item) => {
583
- // Match tax rate for this specific item
605
+ // Use pre-fetched price/product for tax rate matching
584
606
  let taxRateId: string | undefined;
585
607
  if (customer.address?.country && item.price_id) {
586
608
  try {
587
- const price = await Price.findByPk(item.price_id);
609
+ const price = _priceMap.get(item.price_id);
588
610
  if (price?.product_id) {
589
- const product = await Product.findByPk(price.product_id);
611
+ const { product } = price as any;
590
612
  if (product) {
591
- const taxRate = await TaxRate.findMatchingRate({
592
- country: customer.address.country,
593
- state: customer.address.state,
594
- postalCode: customer.address.postal_code,
595
- taxCode: product.tax_code,
596
- livemode,
597
- });
598
- taxRateId = taxRate?.id;
613
+ // Use pre-fetched tax rate if no product-specific tax_code,
614
+ // otherwise do a specific lookup
615
+ if (!product.tax_code) {
616
+ taxRateId = (_taxRateForCustomer as any)?.id;
617
+ } else {
618
+ const taxRate = await TaxRate.findMatchingRate({
619
+ country: customer.address.country,
620
+ state: customer.address.state,
621
+ postalCode: customer.address.postal_code,
622
+ taxCode: product.tax_code,
623
+ livemode,
624
+ });
625
+ taxRateId = taxRate?.id;
626
+ }
599
627
  }
600
628
  }
601
629
  } catch (error) {
@@ -785,8 +813,10 @@ export async function cleanupInvoiceAndItems(invoiceId: string) {
785
813
  return;
786
814
  }
787
815
 
788
- const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
789
- const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
816
+ const [removedItem, removedInvoice] = await Promise.all([
817
+ InvoiceItem.destroy({ where: { invoice_id: invoiceId } }),
818
+ Invoice.destroy({ where: { id: invoiceId } }),
819
+ ]);
790
820
  logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
791
821
  }
792
822
 
@@ -1345,21 +1375,26 @@ export const migrateSubscriptionPaymentMethodInvoice = async (
1345
1375
  }
1346
1376
  }
1347
1377
 
1348
- // 3. Get old and new payment method/currency
1349
- const oldPaymentCurrency = await PaymentCurrency.findByPk(oldCurrencyId);
1378
+ // 3. Get old and new payment currency in parallel
1379
+ const [oldPaymentCurrency, newPaymentCurrency] = await Promise.all([
1380
+ PaymentCurrency.findByPk(oldCurrencyId),
1381
+ PaymentCurrency.findByPk(newCurrencyId),
1382
+ ]);
1350
1383
  if (!oldPaymentCurrency) {
1351
1384
  throw new Error(`Payment currency ${oldCurrencyId} not found`);
1352
1385
  }
1353
- const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
1354
- if (!oldPaymentMethod) {
1355
- throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
1356
- }
1357
-
1358
- const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
1359
1386
  if (!newPaymentCurrency) {
1360
1387
  throw new Error(`Payment currency ${newCurrencyId} not found`);
1361
1388
  }
1362
- const newPaymentMethod = await PaymentMethod.findByPk(newPaymentCurrency.payment_method_id);
1389
+
1390
+ // Get old and new payment method in parallel
1391
+ const [oldPaymentMethod, newPaymentMethod] = await Promise.all([
1392
+ PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id),
1393
+ PaymentMethod.findByPk(newPaymentCurrency.payment_method_id),
1394
+ ]);
1395
+ if (!oldPaymentMethod) {
1396
+ throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
1397
+ }
1363
1398
  if (!newPaymentMethod) {
1364
1399
  throw new Error(`Payment method for currency ${newCurrencyId} not found`);
1365
1400
  }
@@ -29,9 +29,7 @@ interface CustomerAutoRechargeDailyLimitExceededEmailTemplateContext {
29
29
  customerIndexUrl: string;
30
30
  }
31
31
 
32
- export class CustomerAutoRechargeDailyLimitExceededEmailTemplate
33
- implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext>
34
- {
32
+ export class CustomerAutoRechargeDailyLimitExceededEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext> {
35
33
  options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions;
36
34
 
37
35
  constructor(options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions) {
@@ -46,9 +46,7 @@ interface CustomerAutoRechargeFailedEmailTemplateContext {
46
46
  isSkipped: boolean;
47
47
  }
48
48
 
49
- export class CustomerAutoRechargeFailedEmailTemplate
50
- implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext>
51
- {
49
+ export class CustomerAutoRechargeFailedEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext> {
52
50
  options: CustomerAutoRechargeFailedEmailTemplateOptions;
53
51
 
54
52
  constructor(options: CustomerAutoRechargeFailedEmailTemplateOptions) {
@@ -21,9 +21,7 @@ interface CustomerCreditGrantGrantedEmailTemplateContext {
21
21
  at: string;
22
22
  }
23
23
 
24
- export class CustomerCreditGrantGrantedEmailTemplate
25
- implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext>
26
- {
24
+ export class CustomerCreditGrantGrantedEmailTemplate implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext> {
27
25
  options: CustomerCreditGrantGrantedEmailTemplateOptions;
28
26
 
29
27
  constructor(options: CustomerCreditGrantGrantedEmailTemplateOptions) {
@@ -37,9 +37,7 @@ interface CustomerCreditInsufficientEmailTemplateContext {
37
37
  at: string;
38
38
  }
39
39
 
40
- export class CustomerCreditInsufficientEmailTemplate
41
- implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext>
42
- {
40
+ export class CustomerCreditInsufficientEmailTemplate implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext> {
43
41
  options: CustomerCreditInsufficientEmailTemplateOptions;
44
42
 
45
43
  constructor(options: CustomerCreditInsufficientEmailTemplateOptions) {
@@ -26,9 +26,7 @@ interface CustomerCreditLowBalanceEmailTemplateContext {
26
26
  isCritical: boolean; // true if percentage < 1%
27
27
  rechargeUrl: string | null;
28
28
  }
29
- export class CustomerCreditLowBalanceEmailTemplate
30
- implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext>
31
- {
29
+ export class CustomerCreditLowBalanceEmailTemplate implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext> {
32
30
  // Notification configuration: 10 minute grace period before sending
33
31
  static readonly delay = 10 * 60; // seconds
34
32
 
@@ -43,9 +43,7 @@ interface CustomerRevenueSucceededEmailTemplateContext {
43
43
  * @class CustomerRevenueSucceededEmailTemplate
44
44
  * @implements {BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>}
45
45
  */
46
- export class CustomerRevenueSucceededEmailTemplate
47
- implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>
48
- {
46
+ export class CustomerRevenueSucceededEmailTemplate implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext> {
49
47
  options: CustomerRevenueSucceededEmailTemplateOptions;
50
48
 
51
49
  constructor(options: CustomerRevenueSucceededEmailTemplateOptions) {
@@ -64,9 +64,7 @@ interface CustomerRewardSucceededEmailTemplateContext {
64
64
  * @class CustomerRewardSucceededEmailTemplate
65
65
  * @implements {BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>}
66
66
  */
67
- export class CustomerRewardSucceededEmailTemplate
68
- implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>
69
- {
67
+ export class CustomerRewardSucceededEmailTemplate implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext> {
70
68
  options: CustomerRewardSucceededEmailTemplateOptions;
71
69
 
72
70
  constructor(options: CustomerRewardSucceededEmailTemplateOptions) {
@@ -29,9 +29,7 @@ interface OneTimePaymentRefundSucceededEmailTemplateContext {
29
29
  invoiceNumber?: string;
30
30
  }
31
31
 
32
- export class OneTimePaymentRefundSucceededEmailTemplate
33
- implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext>
34
- {
32
+ export class OneTimePaymentRefundSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext> {
35
33
  options: OneTimePaymentRefundSucceededEmailTemplateOptions;
36
34
 
37
35
  constructor(options: OneTimePaymentRefundSucceededEmailTemplateOptions) {
@@ -37,9 +37,7 @@ interface OneTimePaymentSucceededEmailTemplateContext {
37
37
  * @class OneTimePaymentSucceededEmailTemplate
38
38
  * @implements {BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>}
39
39
  */
40
- export class OneTimePaymentSucceededEmailTemplate
41
- implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>
42
- {
40
+ export class OneTimePaymentSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext> {
43
41
  options: OneTimePaymentSucceededEmailTemplateOptions;
44
42
 
45
43
  constructor(options: OneTimePaymentSucceededEmailTemplateOptions) {
@@ -50,9 +50,7 @@ interface SubscriptionRenewFailedEmailTemplateContext {
50
50
  payer: string;
51
51
  }
52
52
 
53
- export class SubscriptionRenewFailedEmailTemplate
54
- implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext>
55
- {
53
+ export class SubscriptionRenewFailedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext> {
56
54
  options: SubscriptionRenewFailedEmailTemplateOptions;
57
55
 
58
56
  constructor(options: SubscriptionRenewFailedEmailTemplateOptions) {
@@ -27,9 +27,7 @@ interface SubscriptionSlippageExceededEmailTemplateContext {
27
27
  customActions: any[];
28
28
  }
29
29
 
30
- export class SubscriptionSlippageExceededEmailTemplate
31
- implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext>
32
- {
30
+ export class SubscriptionSlippageExceededEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext> {
33
31
  options: SubscriptionSlippageExceededEmailTemplateOptions;
34
32
 
35
33
  constructor(options: SubscriptionSlippageExceededEmailTemplateOptions) {
@@ -29,9 +29,7 @@ interface SubscriptionSlippageWarningEmailTemplateContext {
29
29
  customActions: any[];
30
30
  }
31
31
 
32
- export class SubscriptionSlippageWarningEmailTemplate
33
- implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext>
34
- {
32
+ export class SubscriptionSlippageWarningEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext> {
35
33
  options: SubscriptionSlippageWarningEmailTemplateOptions;
36
34
 
37
35
  constructor(options: SubscriptionSlippageWarningEmailTemplateOptions) {
@@ -68,7 +68,7 @@ export class SubscriptionSucceededEmailTemplate extends BaseSubscriptionEmailTem
68
68
 
69
69
  return Boolean(
70
70
  ['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
71
- (invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
71
+ (invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
72
72
  );
73
73
  },
74
74
  { timeout: 1000 * 10, interval: 1000 }
@@ -140,17 +140,22 @@ function calculateFetchStrategy<T>(
140
140
  return { fetchLimit: Math.max(sourceCount, 1000), fetchOffset: 0 };
141
141
  }
142
142
 
143
- // For database sources with multiple sources, use conservative strategy
143
+ // For database sources with multiple sources, use demand-driven strategy
144
144
  if (sourceMeta?.type === 'database') {
145
145
  if (sources.length > 1) {
146
- // For multi-source scenarios, we need more data to ensure correct merging
147
- // Especially for later pages, estimation can be inaccurate
148
- const bufferMultiplier = Math.max(3, Math.ceil(page * 1.5)); // More aggressive buffer for later pages
149
- const minDataRatio = page <= 2 ? 0.6 : 0.8; // Get more data for later pages
150
- const fetchLimit = Math.min(
151
- sourceCount,
152
- Math.max(pageSize * bufferMultiplier, Math.ceil(sourceCount * minDataRatio))
153
- );
146
+ // Multi-source merge fetches from offset=0 and merge-sorts in memory.
147
+ // We need (offset + pageSize) items after merging. Since we don't know
148
+ // how items interleave between sources, fetch (offset + pageSize) from
149
+ // each source with a buffer for merge uncertainty.
150
+ //
151
+ // Previous approach used sourceCount * 0.6~0.8 which over-fetched
152
+ // massively for early pages (e.g. page 1 fetched 2000+ rows) and still
153
+ // under-fetched for late pages. This demand-driven approach fetches
154
+ // proportionally to the page position — fast for early pages, more data
155
+ // only when needed for later pages.
156
+ const needed = offset + pageSize;
157
+ const buffer = Math.max(pageSize * 3, 50); // generous buffer for interleave uncertainty
158
+ const fetchLimit = Math.min(sourceCount, needed + buffer);
154
159
  return { fetchLimit, fetchOffset: 0 };
155
160
  }
156
161
  // Single database source can use precise offset
@@ -387,20 +387,20 @@ export async function isDelegationSufficientForPayment(args: {
387
387
  delegator,
388
388
  source,
389
389
  });
390
- return { sufficient: false, reason: 'NO_DELEGATION' };
390
+ return { sufficient: false, reason: 'NO_DELEGATION', state };
391
391
  }
392
392
 
393
393
  // have transfer permissions?
394
394
  const grant = (state as DelegateState).ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE)?.value;
395
395
  if (!grant) {
396
- return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION' };
396
+ return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION', state };
397
397
  }
398
398
 
399
399
  // check token limits
400
400
  if (grant.limit && Array.isArray(grant.limit.tokens) && grant.limit.tokens.length > 0) {
401
401
  const tokenLimit = grant.limit.tokens.find((x: any) => x.address === tokenAddress);
402
402
  if (!tokenLimit) {
403
- return { sufficient: false, reason: 'NO_TOKEN_PERMISSION' };
403
+ return { sufficient: false, reason: 'NO_TOKEN_PERMISSION', state };
404
404
  }
405
405
 
406
406
  // FIXME: @wangshijun check other conditions in the token limit: txCount, totalAllowance, validUntil, rateLimit
@@ -411,7 +411,7 @@ export async function isDelegationSufficientForPayment(args: {
411
411
  const allWalletAddresses = [wallet.address, ...migratedFrom];
412
412
  const hasValidTo = allWalletAddresses.some((addr) => tokenLimit.to.includes(addr));
413
413
  if (!hasValidTo) {
414
- return { sufficient: false, reason: 'NO_TRANSFER_TO' };
414
+ return { sufficient: false, reason: 'NO_TRANSFER_TO', state };
415
415
  }
416
416
  }
417
417
 
@@ -426,12 +426,14 @@ export async function isDelegationSufficientForPayment(args: {
426
426
  return {
427
427
  sufficient: false,
428
428
  reason: 'NO_ENOUGH_ALLOWANCE',
429
+ state,
429
430
  };
430
431
  }
431
432
  } else if (totalAmount.gt(allowance)) {
432
433
  return {
433
434
  sufficient: false,
434
435
  reason: 'NO_ENOUGH_ALLOWANCE',
436
+ state,
435
437
  };
436
438
  }
437
439
  }
@@ -441,7 +443,7 @@ export async function isDelegationSufficientForPayment(args: {
441
443
  const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
442
444
  const [token] = tokens;
443
445
  if (!token) {
444
- return { sufficient: false, reason: 'NO_TOKEN' };
446
+ return { sufficient: false, reason: 'NO_TOKEN', state };
445
447
  }
446
448
 
447
449
  if (new BN(token.balance).lt(totalAmount)) {
@@ -449,6 +451,7 @@ export async function isDelegationSufficientForPayment(args: {
449
451
  sufficient: false,
450
452
  reason: 'NO_ENOUGH_TOKEN',
451
453
  token,
454
+ state,
452
455
  requestedAmount: totalAmount.toString(),
453
456
  };
454
457
  }
@@ -602,7 +605,11 @@ export async function getTokenLimitsForDelegation(
602
605
  paymentMethod: PaymentMethod,
603
606
  paymentCurrency: PaymentCurrency,
604
607
  address: string,
605
- amount: string
608
+ amount: string,
609
+ // Optional: if caller already fetched DelegateState (e.g. isDelegationSufficientForPayment),
610
+ // pass it here to skip a redundant chain RPC. Caller MUST use state from the same request
611
+ // to avoid stale-balance issues (see note on delegationCache above).
612
+ delegateState?: DelegateState | null
606
613
  ): Promise<TokenLimit[]> {
607
614
  const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
608
615
  const allowance = hasMetered ? '0' : amount;
@@ -618,8 +625,16 @@ export async function getTokenLimitsForDelegation(
618
625
  };
619
626
 
620
627
  if (paymentMethod.type === 'arcblock') {
621
- const client = paymentMethod.getOcapClient();
622
- const { state } = await client.getDelegateState({ address });
628
+ let state: DelegateState | null | undefined = delegateState;
629
+ if (state === undefined) {
630
+ const client = paymentMethod.getOcapClient();
631
+ // CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
632
+ await client.getContext();
633
+ ({ state } = await client.getDelegateState({ address }));
634
+ logger.info('getTokenLimitsForDelegation: fetched DelegateState from chain', { address });
635
+ } else {
636
+ logger.info('getTokenLimitsForDelegation: reused DelegateState from caller', { address });
637
+ }
623
638
 
624
639
  // If we never delegated before
625
640
  if (!state) {
@@ -630,10 +645,10 @@ export async function getTokenLimitsForDelegation(
630
645
  return [entry];
631
646
  }
632
647
 
633
- const op = state.ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
648
+ const op = state.ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE);
634
649
  if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
635
650
  const tokenLimits = cloneDeep(op.value.limit.tokens);
636
- const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
651
+ const index = op.value.limit.tokens.findIndex((x: any) => x.address === paymentCurrency.contract);
637
652
  // we are updating an existing token limit
638
653
  if (index > -1) {
639
654
  const limit = op.value.limit.tokens[index] as TokenLimit;
@@ -1,5 +1,6 @@
1
1
  import { auth } from '@blocklet/sdk/lib/middlewares';
2
2
  import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
3
+ import { verifyLoginToken } from '@blocklet/sdk/lib/util/verify-session';
3
4
  import { getWallet } from '@blocklet/sdk/lib/wallet';
4
5
  import type { NextFunction, Request, Response } from 'express';
5
6
  import type { Model } from 'sequelize';
@@ -39,6 +40,56 @@ export function authenticate<T extends Model>({
39
40
  ensureLogin,
40
41
  }: PermissionSpec<T>) {
41
42
  return async (req: Request, res: Response, next: NextFunction) => {
43
+ // Dev-only bypass: requires both NODE_ENV=development AND the explicit
44
+ // opt-in env ENABLE_DEV_FAKE_AUTH=1, plus the x-dev-fake-did header on
45
+ // the request. Lets mobile demo clients that don't go through DID Connect
46
+ // (e.g. the local-tunnel backend which bypasses Blocklet Server) still
47
+ // exercise real handlers. Production never sets ENABLE_DEV_FAKE_AUTH, so
48
+ // this branch can't trigger there; dev defaults off, so we don't
49
+ // accidentally regress to fake auth after wiring the real flow.
50
+ if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_FAKE_AUTH === '1') {
51
+ const devDid = req.get('x-dev-fake-did');
52
+ if (devDid) {
53
+ req.user = {
54
+ did: devDid,
55
+ role: 'owner', // satisfies routes that require owner/admin
56
+ provider: 'dev',
57
+ fullName: 'dev-fake-user',
58
+ walletOS: '',
59
+ via: 'dev',
60
+ };
61
+ return next();
62
+ }
63
+ }
64
+
65
+ // Authenticate by Authorization: Bearer <login-token>. The token is a JWT
66
+ // signed with this blocklet's session secret (see @blocklet/sdk session
67
+ // middleware). When clients hit us through a tunnel that bypasses Blocklet
68
+ // Server (so x-user-did is NOT injected), we need to verify the token
69
+ // ourselves. verifyLoginToken does local JWT signature verification, no
70
+ // HTTP callback. On success we forward into the existing x-user-did branch
71
+ // by populating req.headers so the role/mine/record cascade below applies
72
+ // unchanged.
73
+ const authHeader = req.get('authorization');
74
+ if (authHeader && /^Bearer\s+/i.test(authHeader) && !req.headers['x-user-did']) {
75
+ const token = authHeader.replace(/^Bearer\s+/i, '').trim();
76
+ if (token) {
77
+ const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
78
+ if (session?.did) {
79
+ // Some BS versions put a bare base58 address in the JWT, others
80
+ // the canonical `did:abt:…` form. Normalize so downstream code
81
+ // (entitlement self-check, mine: lookups by req.user.did) sees the
82
+ // same shape clients send in `customer_did` query params.
83
+ const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
84
+ req.headers['x-user-did'] = canonicalDid;
85
+ req.headers['x-user-role'] = `blocklet-${session.role || 'user'}`;
86
+ req.headers['x-user-provider'] = session.provider || 'wallet';
87
+ req.headers['x-user-fullname'] = encodeURIComponent(session.fullName || '');
88
+ req.headers['x-user-wallet-os'] = session.walletOS || '';
89
+ }
90
+ }
91
+ }
92
+
42
93
  // authenticate by component call
43
94
  const sig = req.get('x-component-sig');
44
95
  if (component && sig) {
@@ -5,7 +5,7 @@ import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
5
5
  import cloneDeep from 'lodash/cloneDeep';
6
6
  import isEqual from 'lodash/isEqual';
7
7
  import pAll from 'p-all';
8
- import { omit } from 'lodash';
8
+ import omit from 'lodash/omit';
9
9
  import dayjs from './dayjs';
10
10
  import { validCoupon } from './discount/coupon';
11
11
  import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
@@ -1357,8 +1357,20 @@ export async function isSubscriptionOverdraftProtectionEnabled(subscription: Sub
1357
1357
  throw new Error(`PaymentMethod not found in ${subscription.id}`);
1358
1358
  }
1359
1359
 
1360
+ // Overdraft protection is only meaningful for on-chain (arcblock) payment
1361
+ // methods where users stake tokens. IAP / Stripe channels have no notion of
1362
+ // staking; return disabled-default silently rather than throwing into the
1363
+ // outer catch (which would spam the error log on every google_play /
1364
+ // app_store / stripe subscription event).
1360
1365
  if (paymentMethod.type !== 'arcblock') {
1361
- throw new Error(`PaymentMethod type not supported in ${subscription.id}`);
1366
+ return {
1367
+ enabled: false,
1368
+ remaining: '0',
1369
+ used: '0',
1370
+ shouldPay: '0',
1371
+ unused: '0',
1372
+ revokedStake: '0',
1373
+ };
1362
1374
  }
1363
1375
  if (!subscription.overdraft_protection) {
1364
1376
  return {