payment-kit 1.29.1 → 1.29.3

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 (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -11,6 +11,9 @@ const REPRESENTATIVE_TOKENS = ['ABT', 'ETH'];
11
11
  interface HealthCheckJob {
12
12
  type: 'health_check';
13
13
  timestamp: number;
14
+ /** D6: the tenant this check belongs to — carried in the payload so the
15
+ * re-schedule survives outside any ALS context (multi mode). */
16
+ instance_did?: string;
14
17
  }
15
18
 
16
19
  interface HealthCheckResult {
@@ -201,43 +204,59 @@ export const exchangeRateHealthQueue = createQueue<HealthCheckJob>({
201
204
  // Schedule health checks every 6 hours
202
205
  const HEALTH_CHECK_INTERVAL = 6 * 60 * 60; // 6 hours in seconds
203
206
 
207
+ // D6: every scheduled health-check job carries its tenant in the PAYLOAD so the
208
+ // re-schedule (on 'finished') preserves it WITHOUT relying on the ALS context —
209
+ // the 'finished' listener fires outside any withTenant() scope. In multi mode a
210
+ // tenant-less push would throw TENANT_CONTEXT_MISSING; here `instance_did` is set
211
+ // on the job, so injectJobTenant honors it instead of reading the (absent) ALS.
212
+ let scheduleListenerBound = false;
213
+
214
+ function scheduleNextHealthCheck(instanceDid?: string): void {
215
+ const now = Math.floor(Date.now() / 1000);
216
+ const nextRun = now + HEALTH_CHECK_INTERVAL;
217
+
218
+ exchangeRateHealthQueue.push({
219
+ job: {
220
+ type: 'health_check',
221
+ timestamp: Date.now(),
222
+ // carry the tenant in the payload (multi mode has no ALS context here)
223
+ ...(instanceDid ? { instance_did: instanceDid } : {}),
224
+ } as HealthCheckJob,
225
+ runAt: nextRun,
226
+ persist: true,
227
+ });
228
+
229
+ logger.info('Scheduled next exchange rate health check', {
230
+ nextRunAt: new Date(nextRun * 1000).toISOString(),
231
+ intervalHours: HEALTH_CHECK_INTERVAL / 3600,
232
+ instanceDid,
233
+ });
234
+ }
235
+
204
236
  /**
205
- * Initialize and schedule periodic health checks
237
+ * Schedule periodic health checks for a tenant. The initial push AND every
238
+ * re-schedule carry `instance_did` in the job payload, so the cancel-then-replay
239
+ * recovery (D2) and the post-restart redispatch stay under the correct tenant.
206
240
  */
207
- export function scheduleHealthChecks(): void {
208
- const scheduleNext = () => {
209
- const now = Math.floor(Date.now() / 1000);
210
- const nextRun = now + HEALTH_CHECK_INTERVAL;
211
-
212
- exchangeRateHealthQueue.push({
213
- job: {
214
- type: 'health_check',
215
- timestamp: Date.now(),
216
- },
217
- runAt: nextRun,
218
- persist: true,
241
+ export function scheduleHealthChecks(instanceDid?: string): void {
242
+ scheduleNextHealthCheck(instanceDid);
243
+
244
+ // bind the re-schedule listener ONCE (idempotent across bootstrapTenant calls);
245
+ // it reads the finished job's own instance_did to re-schedule under that tenant.
246
+ if (!scheduleListenerBound) {
247
+ scheduleListenerBound = true;
248
+ exchangeRateHealthQueue.on('finished', (data: { job?: HealthCheckJob }) => {
249
+ scheduleNextHealthCheck(data?.job?.instance_did);
219
250
  });
220
-
221
- logger.info('Scheduled next exchange rate health check', {
222
- nextRunAt: new Date(nextRun * 1000).toISOString(),
223
- intervalHours: HEALTH_CHECK_INTERVAL / 3600,
224
- });
225
- };
226
-
227
- // Schedule initial check
228
- scheduleNext();
229
-
230
- // Listen for completed checks and schedule next one
231
- exchangeRateHealthQueue.on('finished', () => {
232
- scheduleNext();
233
- });
251
+ }
234
252
 
235
253
  logger.info('Exchange rate health check scheduling initialized', {
236
254
  intervalHours: HEALTH_CHECK_INTERVAL / 3600,
255
+ instanceDid,
237
256
  });
238
257
  }
239
258
 
240
- // Export for explicit initialization in index.ts
241
- export function startExchangeRateHealthQueue(): void {
242
- scheduleHealthChecks();
259
+ // Export for explicit initialization (single mode / per-tenant bootstrap).
260
+ export function startExchangeRateHealthQueue(instanceDid?: string): void {
261
+ scheduleHealthChecks(instanceDid);
243
262
  }
@@ -1,10 +1,11 @@
1
1
  import { Op } from 'sequelize';
2
+ import { systemFindAll, systemFindByPk } from '../store/scoped';
2
3
 
3
4
  import { getInvoiceShouldPayTotal, handleOverdraftProtectionInvoiceAfterPayment } from '../libs/invoice';
4
- import { createEvent } from '../libs/audit';
5
+ import { createEvent, reportAuditFailure } from '../libs/audit';
5
6
  import dayjs from '../libs/dayjs';
6
7
  import logger from '../libs/logger';
7
- import createQueue from '../libs/queue';
8
+ import createQueue, { assertJobObjectTenant } from '../libs/queue';
8
9
  import { PaymentMethod, Invoice, InvoiceItem } from '../store/models';
9
10
  import { CheckoutSession } from '../store/models/checkout-session';
10
11
  import { PaymentIntent } from '../store/models/payment-intent';
@@ -29,11 +30,12 @@ type InvoiceJob = {
29
30
  export const handleInvoice = async (job: InvoiceJob) => {
30
31
  logger.info('handle invoice', job);
31
32
 
32
- const invoice = await Invoice.findByPk(job.invoiceId);
33
+ const invoice = await systemFindByPk(Invoice, job.invoiceId);
33
34
  if (!invoice) {
34
35
  logger.warn('invoice not found', { invoiceId: job.invoiceId });
35
36
  return;
36
37
  }
38
+ assertJobObjectTenant(invoice);
37
39
  if (invoice.status !== 'open') {
38
40
  logger.warn('invoice not open', { invoiceId: job.invoiceId });
39
41
  return;
@@ -63,7 +65,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
63
65
  logger.info('Invoice updated to paid status', { invoiceId: invoice.id });
64
66
 
65
67
  if (invoice.subscription_id) {
66
- const subscription = await Subscription.findByPk(invoice.subscription_id);
68
+ const subscription = await systemFindByPk(Subscription, invoice.subscription_id);
67
69
  if (subscription) {
68
70
  if (subscription.status === 'incomplete') {
69
71
  await subscription.start();
@@ -78,17 +80,17 @@ export const handleInvoice = async (job: InvoiceJob) => {
78
80
  }
79
81
  } else {
80
82
  if (invoice.billing_reason === 'subscription_cycle') {
81
- createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
83
+ createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(reportAuditFailure);
82
84
  }
83
85
  if (invoice.billing_reason === 'subscription_update') {
84
- createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
86
+ createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(reportAuditFailure);
85
87
  }
86
88
  }
87
89
  }
88
90
  }
89
91
 
90
92
  if (invoice.checkout_session_id) {
91
- const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
93
+ const checkoutSession = await systemFindByPk(CheckoutSession, invoice.checkout_session_id);
92
94
  if (checkoutSession && checkoutSession.status === 'open') {
93
95
  await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
94
96
  logger.info('invoice checkout session updated', { invoice: invoice.id, checkoutSession: checkoutSession.id });
@@ -96,7 +98,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
96
98
  }
97
99
 
98
100
  if (invoice.payment_intent_id) {
99
- const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
101
+ const paymentIntent = await systemFindByPk(PaymentIntent, invoice.payment_intent_id);
100
102
  if (
101
103
  paymentIntent &&
102
104
  ['requires_action', 'requires_capture', 'requires_payment_method'].includes(paymentIntent.status)
@@ -119,7 +121,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
119
121
  invoiceId: job.invoiceId,
120
122
  paymentIntent: invoice.payment_intent_id,
121
123
  });
122
- paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
124
+ paymentIntent = await systemFindByPk(PaymentIntent, invoice.payment_intent_id);
123
125
  if (paymentIntent && paymentIntent.isImmutable() === false) {
124
126
  await paymentIntent.update({ status: 'requires_capture', customer_id: invoice.customer_id });
125
127
  logger.info('PaymentIntent updated for invoice', {
@@ -166,7 +168,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
166
168
  });
167
169
 
168
170
  if (invoice.checkout_session_id) {
169
- const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
171
+ const checkoutSession = await systemFindByPk(CheckoutSession, invoice.checkout_session_id);
170
172
  if (checkoutSession && checkoutSession.status === 'open') {
171
173
  await checkoutSession.update({ payment_intent_id: paymentIntent.id });
172
174
  logger.info('PaymentIntent attached to invoice checkout session', {
@@ -212,14 +214,14 @@ export const invoiceQueue = createQueue<InvoiceJob>({
212
214
  });
213
215
 
214
216
  export const startInvoiceQueue = async () => {
215
- const lock = getLock('startInvoiceQueue');
217
+ const lock = getLock('startInvoiceQueue', { scope: 'global' });
216
218
  if (lock.locked) {
217
219
  return;
218
220
  }
219
221
 
220
222
  try {
221
223
  await lock.acquire();
222
- const invoices = await Invoice.findAll({
224
+ const invoices = await systemFindAll(Invoice, {
223
225
  where: {
224
226
  status: 'open',
225
227
  collection_method: 'charge_automatically',
@@ -259,11 +261,12 @@ invoiceQueue.on('failed', ({ id, job, error }) => {
259
261
  });
260
262
 
261
263
  events.on('invoice.paid', async ({ id: invoiceId }) => {
262
- const invoice = await Invoice.findByPk(invoiceId);
264
+ const invoice = await systemFindByPk(Invoice, invoiceId);
263
265
  if (!invoice) {
264
266
  logger.error('Invoice not found for paid event', { invoiceId });
265
267
  return;
266
268
  }
269
+ assertJobObjectTenant(invoice);
267
270
  logger.info('Processing paid invoice', { invoiceId, billingReason: invoice.billing_reason });
268
271
 
269
272
  const checkBillingReason = ['subscription_cycle', 'subscription_cancel'];
@@ -286,7 +289,7 @@ events.on('invoice.paid', async ({ id: invoiceId }) => {
286
289
  // Separate listener to mark quotes as paid - independent of other invoice.paid handlers
287
290
  events.on('invoice.paid', async ({ id: invoiceId }) => {
288
291
  try {
289
- const invoiceItems = await InvoiceItem.findAll({
292
+ const invoiceItems = await systemFindAll(InvoiceItem, {
290
293
  where: { invoice_id: invoiceId },
291
294
  });
292
295
 
@@ -304,7 +307,7 @@ events.on('invoice.paid', async ({ id: invoiceId }) => {
304
307
  await Promise.all(
305
308
  quoteIds.map(async (quoteId) => {
306
309
  try {
307
- const quote = await PriceQuote.findByPk(quoteId);
310
+ const quote = await systemFindByPk(PriceQuote, quoteId);
308
311
  if (quote && quote.status !== 'paid') {
309
312
  await quote.update({ invoice_id: invoiceId });
310
313
  await quoteService.markAsPaid(quoteId);
@@ -1,5 +1,6 @@
1
1
  import { Op } from 'sequelize';
2
2
  import debounce from 'lodash/debounce';
3
+ import { systemFindByPk } from '../store/scoped';
3
4
  /* eslint-disable @typescript-eslint/indent */
4
5
  import { events } from '../libs/event';
5
6
  import logger from '../libs/logger';
@@ -62,7 +63,7 @@ import {
62
63
  SubscriptionStakeSlashSucceededEmailTemplate,
63
64
  SubscriptionStakeSlashSucceededEmailTemplateOptions,
64
65
  } from '../libs/notification/template/subscription-stake-slash-succeeded';
65
- import createQueue from '../libs/queue';
66
+ import createQueue, { assertJobObjectTenant } from '../libs/queue';
66
67
  import {
67
68
  CheckoutSession,
68
69
  EventType,
@@ -263,7 +264,8 @@ async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseE
263
264
  }
264
265
  if (job.type === 'refund.succeeded') {
265
266
  const { refundId } = job.options as { refundId: string };
266
- const refund = await Refund.findByPk(refundId);
267
+ const refund = await systemFindByPk(Refund, refundId);
268
+ if (refund) assertJobObjectTenant(refund);
267
269
  if (refund?.subscription_id) {
268
270
  return new SubscriptionRefundSucceededEmailTemplate(
269
271
  job.options as SubscriptionRefundSucceededEmailTemplateOptions
@@ -570,7 +572,8 @@ export async function startNotificationQueue() {
570
572
  // 一次性购买成功
571
573
  events.on('checkout.session.completed', async (checkoutSession: CheckoutSession) => {
572
574
  if (checkoutSession.mode === 'payment') {
573
- const paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
575
+ const paymentLink = await systemFindByPk(PaymentLink, checkoutSession.payment_link_id);
576
+ if (paymentLink) assertJobObjectTenant(paymentLink);
574
577
  if (paymentLink?.submit_type === 'donate') {
575
578
  addNotificationJob('customer.reward.succeeded', { checkoutSessionId: checkoutSession.id }, [
576
579
  checkoutSession.id,
@@ -583,7 +586,8 @@ export async function startNotificationQueue() {
583
586
  });
584
587
 
585
588
  events.on('customer.subscription.renewed', async (subscription: Subscription) => {
586
- const customer = await Customer.findByPk(subscription.customer_id);
589
+ const customer = await systemFindByPk(Customer, subscription.customer_id);
590
+ if (customer) assertJobObjectTenant(customer);
587
591
  const preference = customer?.preference?.notification;
588
592
  if (!subscription.latest_invoice_id) {
589
593
  logger.warn('Missing latest_invoice_id for subscription', { subscriptionId: subscription.id });
@@ -619,7 +623,8 @@ export async function startNotificationQueue() {
619
623
  });
620
624
 
621
625
  events.on('customer.subscription.renew_failed', async (subscription: Subscription, { invoiceId }) => {
622
- const invoice = await Invoice.findByPk(invoiceId || subscription.latest_invoice_id);
626
+ const invoice = await systemFindByPk(Invoice, invoiceId || subscription.latest_invoice_id);
627
+ if (invoice) assertJobObjectTenant(invoice);
623
628
 
624
629
  logger.info('events.on', 'customer.subscription.renew_failed', {
625
630
  subscriptionId: subscription.id,
@@ -731,7 +736,8 @@ export async function startNotificationQueue() {
731
736
  events.on(
732
737
  'customer.auto_recharge.failed',
733
738
  async (customer: Customer, { autoRechargeConfigId, invoiceId, paymentIntentId }) => {
734
- const invoice = await Invoice.findByPk(invoiceId);
739
+ const invoice = await systemFindByPk(Invoice, invoiceId);
740
+ if (invoice) assertJobObjectTenant(invoice);
735
741
  logger.info('addNotificationJob:customer.auto_recharge.failed', autoRechargeConfigId);
736
742
  if (invoice && autoRechargeConfigId && invoice.metadata?.auto_recharge_failed_reason) {
737
743
  addNotificationJob(
@@ -819,7 +825,8 @@ export async function startNotificationQueue() {
819
825
 
820
826
  // Reuse customer.auto_recharge.failed notification with skipped reason
821
827
  // This ensures users are notified when auto-recharge cannot proceed
822
- const customer = await Customer.findByPk(data.customer_id);
828
+ const customer = await systemFindByPk(Customer, data.customer_id);
829
+ if (customer) assertJobObjectTenant(customer);
823
830
  if (customer) {
824
831
  addNotificationJob(
825
832
  'customer.auto_recharge.failed',
@@ -1,10 +1,13 @@
1
1
  import isEmpty from 'lodash/isEmpty';
2
-
3
2
  import { BN } from '@ocap/util';
3
+ import { isCfWorker } from '../libs/env';
4
+ import { systemFindAll, systemFindByPk, systemFindOne } from '../store/scoped';
5
+
4
6
  import { ensureStakedForGas } from '../integrations/arcblock/stake';
5
7
  import { transferErc20FromUser } from '../integrations/ethereum/token';
6
- import { createEvent } from '../libs/audit';
8
+ import { createEvent, reportAuditFailure } from '../libs/audit';
7
9
  import { blocklet, wallet } from '../libs/auth';
10
+ import { withTenant } from '../libs/context';
8
11
  import dayjs from '../libs/dayjs';
9
12
  import CustomError from '../libs/error';
10
13
  import { events } from '../libs/event';
@@ -41,7 +44,7 @@ import { notificationQueue } from './notification';
41
44
  import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
42
45
  import { AutoRechargeConfig, Lock, MeterEvent } from '../store/models';
43
46
  import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
44
- import createQueue from '../libs/queue';
47
+ import createQueue, { assertJobObjectTenant } from '../libs/queue';
45
48
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
46
49
  import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup, SlippageOptions } from '../libs/session';
47
50
  import { syncStripeSubscriptionAfterRecovery } from '../integrations/stripe/handlers/subscription';
@@ -170,11 +173,11 @@ export async function updateSubscriptionOnPaymentSuccess(
170
173
  // Trigger renewal and upgrade events
171
174
  if (triggerRenew && (!invoice || invoice.billing_reason !== 'subscription_update')) {
172
175
  if (!invoice || invoice.billing_reason === 'subscription_cycle' || paymentIntent?.capture_method === 'manual') {
173
- createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
176
+ createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(reportAuditFailure);
174
177
  }
175
178
  }
176
179
  if (invoice?.billing_reason === 'subscription_update') {
177
- createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
180
+ createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(reportAuditFailure);
178
181
  }
179
182
  }
180
183
 
@@ -232,11 +235,11 @@ export async function handlePastDueSubscriptionRecovery(
232
235
  }
233
236
 
234
237
  // Reset billing cycle
235
- const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
238
+ const subscriptionItems = await systemFindAll(SubscriptionItem, { where: { subscription_id: subscription.id } });
236
239
  const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
237
240
  // Use the subscription's slippage config if available, otherwise use default 0.5%
238
241
  const slippageConfig = subscription.slippage_config;
239
- const currency = await PaymentCurrency.findByPk(subscription.currency_id);
242
+ const currency = await systemFindByPk(PaymentCurrency, subscription.currency_id);
240
243
  const slippageOptions: SlippageOptions = {
241
244
  percent: slippageConfig?.percent ?? 0.5,
242
245
  minAcceptableRate: slippageConfig?.min_acceptable_rate,
@@ -255,7 +258,7 @@ export async function handlePastDueSubscriptionRecovery(
255
258
  cancelation_details: null,
256
259
  });
257
260
 
258
- createEvent('Subscription', 'customer.subscription.recovered', subscription).catch(console.error);
261
+ createEvent('Subscription', 'customer.subscription.recovered', subscription).catch(reportAuditFailure);
259
262
  const recoveryReason =
260
263
  subscription.cancelation_details?.reason === 'insufficient_credit' ? 'credit replenished' : 'payment done';
261
264
  logger.info(
@@ -405,20 +408,20 @@ export const handlePaymentSucceed = async (
405
408
 
406
409
  let invoice;
407
410
  if (paymentIntent.invoice_id) {
408
- invoice = await Invoice.findByPk(paymentIntent.invoice_id);
411
+ invoice = await systemFindByPk(Invoice, paymentIntent.invoice_id);
409
412
  }
410
413
 
411
414
  // Handle checkout session when no invoice exists
412
415
  if (!invoice && !slashStake) {
413
- const checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: paymentIntent.id } });
416
+ const checkoutSession = await systemFindOne(CheckoutSession, { where: { payment_intent_id: paymentIntent.id } });
414
417
  if (checkoutSession) {
415
418
  if (
416
- (globalThis as any).__CF_ENV__ &&
419
+ isCfWorker() &&
417
420
  checkoutSession.mode === 'payment' &&
418
421
  checkoutSession.invoice_creation?.enabled &&
419
422
  !checkoutSession.invoice_id
420
423
  ) {
421
- const customer = await Customer.findByPk(checkoutSession.customer_id);
424
+ const customer = await systemFindByPk(Customer, checkoutSession.customer_id);
422
425
  if (customer) {
423
426
  try {
424
427
  const result = await ensureInvoiceForCheckout({
@@ -480,7 +483,7 @@ export const handlePaymentSucceed = async (
480
483
  }
481
484
  // Update subscription status
482
485
  if (invoice && invoice.subscription_id && !slashStake) {
483
- const subscription = await Subscription.findByPk(invoice.subscription_id);
486
+ const subscription = await systemFindByPk(Subscription, invoice.subscription_id);
484
487
  if (subscription) {
485
488
  await updateSubscriptionOnPaymentSuccess(paymentIntent, subscription, invoice, triggerRenew);
486
489
  }
@@ -488,7 +491,7 @@ export const handlePaymentSucceed = async (
488
491
 
489
492
  // Update checkout session
490
493
  if (invoice && invoice.checkout_session_id && !slashStake) {
491
- const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
494
+ const checkoutSession = await systemFindByPk(CheckoutSession, invoice.checkout_session_id);
492
495
  if (checkoutSession) {
493
496
  await updateCheckoutSessionOnPaymentSuccess(paymentIntent, checkoutSession, invoice);
494
497
  }
@@ -508,7 +511,7 @@ export const doOverdraftProtection = async (
508
511
  });
509
512
  return;
510
513
  }
511
- const existProtectedInvoice = await Invoice.findOne({
514
+ const existProtectedInvoice = await systemFindOne(Invoice, {
512
515
  where: {
513
516
  subscription_id: subscription.id,
514
517
  billing_reason: 'overdraft_protection',
@@ -593,10 +596,11 @@ export const handlePaymentFailed = async (
593
596
  if (!invoice.subscription_id) {
594
597
  return updates.retry;
595
598
  }
596
- const subscription = await Subscription.findByPk(invoice.subscription_id);
599
+ const subscription = await systemFindByPk(Subscription, invoice.subscription_id);
597
600
  if (!subscription) {
598
601
  return updates.retry;
599
602
  }
603
+ assertJobObjectTenant(subscription);
600
604
 
601
605
  const { interval } = subscription.pending_invoice_item_interval;
602
606
  updates.retry.minRetryMail = getMinRetryMail(interval);
@@ -636,7 +640,7 @@ export const handlePaymentFailed = async (
636
640
  return updates.terminate;
637
641
  }
638
642
  // check overdraft protection, if protected, no need to check due
639
- const customer = await Customer.findByPk(invoice.customer_id);
643
+ const customer = await systemFindByPk(Customer, invoice.customer_id);
640
644
 
641
645
  const { enabled: enableOverdraftProtection, unused: unusedAmount } =
642
646
  await isSubscriptionOverdraftProtectionEnabled(subscription);
@@ -649,7 +653,9 @@ export const handlePaymentFailed = async (
649
653
  const isLock = await Lock.isLocked(lockKey);
650
654
  if (!isLock) {
651
655
  await Lock.acquire(lockKey, subscription.current_period_end);
652
- createEvent('Subscription', 'subscription.overdraft_protection.exhausted', subscription).catch(console.error);
656
+ createEvent('Subscription', 'subscription.overdraft_protection.exhausted', subscription).catch(
657
+ reportAuditFailure
658
+ );
653
659
  }
654
660
  }
655
661
 
@@ -735,7 +741,10 @@ export const handlePaymentFailed = async (
735
741
  });
736
742
 
737
743
  if (invoice.billing_reason === 'auto_recharge' && invoice.metadata?.recharge_config?.recharge_id) {
738
- const autoRechargeConfig = await AutoRechargeConfig.findByPk(invoice.metadata?.recharge_config?.recharge_id);
744
+ const autoRechargeConfig = await systemFindByPk(
745
+ AutoRechargeConfig,
746
+ invoice.metadata?.recharge_config?.recharge_id
747
+ );
739
748
  if (autoRechargeConfig && autoRechargeConfig.enabled) {
740
749
  autoRechargeConfig.update({
741
750
  enabled: false,
@@ -760,7 +769,7 @@ const handleStakeSlash = async (
760
769
  customer: Customer,
761
770
  paymentCurrency: PaymentCurrency
762
771
  ) => {
763
- const subscription = await Subscription.findByPk(invoice.subscription_id);
772
+ const subscription = await systemFindByPk(Subscription, invoice.subscription_id);
764
773
  if (!subscription) {
765
774
  logger.warn('Stake slashing skipped because Subscription not found', {
766
775
  subscription: invoice.subscription_id,
@@ -769,6 +778,7 @@ const handleStakeSlash = async (
769
778
  });
770
779
  return;
771
780
  }
781
+ assertJobObjectTenant(subscription);
772
782
  if (!subscription.cancelation_details?.slash_stake || subscription.status !== 'canceled') {
773
783
  logger.warn('Stake slashing skipped because subscription not canceled or slash_stake is false', {
774
784
  subscription: invoice.subscription_id,
@@ -871,22 +881,24 @@ const handleStakeSlash = async (
871
881
  export const handlePayment = async (job: PaymentJob) => {
872
882
  logger.info('handle payment', job);
873
883
 
874
- const paymentIntent = await PaymentIntent.findByPk(job.paymentIntentId);
884
+ const paymentIntent = await systemFindByPk(PaymentIntent, job.paymentIntentId);
875
885
  if (!paymentIntent) {
876
886
  logger.warn('PaymentIntent not found', { id: job.paymentIntentId });
877
887
  return;
878
888
  }
889
+ assertJobObjectTenant(paymentIntent);
879
890
 
880
891
  if (['requires_capture', 'processing'].includes(paymentIntent.status) === false) {
881
892
  logger.warn('PaymentIntent status not expected', { id: paymentIntent.id, status: paymentIntent.status });
882
893
  return;
883
894
  }
884
895
 
885
- const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
896
+ const paymentMethod = await systemFindByPk(PaymentMethod, paymentIntent.payment_method_id);
886
897
  if (!paymentMethod) {
887
898
  logger.warn('PaymentMethod not found', { id: paymentIntent.payment_method_id });
888
899
  return;
889
900
  }
901
+ assertJobObjectTenant(paymentMethod);
890
902
 
891
903
  const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentIntent.payment_method_id);
892
904
  if (supportAutoCharge === false) {
@@ -894,26 +906,28 @@ export const handlePayment = async (job: PaymentJob) => {
894
906
  return;
895
907
  }
896
908
 
897
- const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
909
+ const paymentCurrency = await systemFindByPk(PaymentCurrency, paymentIntent.currency_id);
898
910
  if (!paymentCurrency) {
899
911
  logger.warn('PaymentCurrency not found', { id: paymentIntent.currency_id });
900
912
  return;
901
913
  }
914
+ assertJobObjectTenant(paymentCurrency);
902
915
 
903
916
  if (paymentCurrency.isCredit()) {
904
917
  logger.info('PaymentIntent capture skipped because paymentCurrency is credit', { id: paymentIntent.id });
905
918
  return;
906
919
  }
907
920
 
908
- const customer = await Customer.findByPk(paymentIntent.customer_id);
921
+ const customer = await systemFindByPk(Customer, paymentIntent.customer_id);
909
922
  if (!customer) {
910
923
  logger.warn('Customer not found', { id: paymentIntent.customer_id });
911
924
  return;
912
925
  }
926
+ assertJobObjectTenant(customer);
913
927
 
914
- const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
928
+ const invoice = await systemFindByPk(Invoice, paymentIntent.invoice_id);
915
929
  // Fetch subscription early for migration fallback support and overdraft protection check
916
- const subscription = invoice?.subscription_id ? await Subscription.findByPk(invoice.subscription_id) : null;
930
+ const subscription = invoice?.subscription_id ? await systemFindByPk(Subscription, invoice.subscription_id) : null;
917
931
 
918
932
  if (invoice?.status === 'void') {
919
933
  await paymentIntent.update({
@@ -1359,23 +1373,36 @@ export const paymentQueue = createQueue<PaymentJob>({
1359
1373
  });
1360
1374
 
1361
1375
  export const startPaymentQueue = async () => {
1362
- const payments = await PaymentIntent.findAll({
1376
+ const payments = await systemFindAll(PaymentIntent, {
1363
1377
  where: {
1364
1378
  status: ['requires_capture', 'processing'],
1365
1379
  capture_method: 'automatic',
1366
1380
  },
1367
1381
  });
1368
1382
 
1369
- payments.forEach(async (x) => {
1370
- const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.payment_method_id);
1371
- if (supportAutoCharge === false) {
1372
- return;
1373
- }
1374
- const exist = await paymentQueue.get(x.id);
1375
- if (!exist) {
1376
- paymentQueue.push({ id: x.id, job: { paymentIntentId: x.id } });
1383
+ // Each pending intent belongs to a tenant: the scoped supportAutoCharge query
1384
+ // and the queue push (push stamps the job tenant via getInstanceDid) must run in
1385
+ // that tenant's context — multi mode fails closed otherwise. for-of + await,
1386
+ // NOT forEach(async): a fire-and-forget rejection here becomes an
1387
+ // unhandledRejection → FATAL on boot. Per-record try/catch isolates one bad row.
1388
+ for (const x of payments) {
1389
+ const dispatch = async () => {
1390
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.payment_method_id);
1391
+ if (supportAutoCharge === false) {
1392
+ return;
1393
+ }
1394
+ const exist = await paymentQueue.get(x.id);
1395
+ if (!exist) {
1396
+ paymentQueue.push({ id: x.id, job: { paymentIntentId: x.id } });
1397
+ }
1398
+ };
1399
+ try {
1400
+ // eslint-disable-next-line no-await-in-loop
1401
+ await (x.instance_did ? withTenant(x.instance_did, dispatch) : dispatch());
1402
+ } catch (error) {
1403
+ logger.error('startPaymentQueue: re-queue failed', { id: x.id, error });
1377
1404
  }
1378
- });
1405
+ }
1379
1406
  };
1380
1407
 
1381
1408
  paymentQueue.on('failed', ({ id, job, error }) => {
@@ -1384,7 +1411,7 @@ paymentQueue.on('failed', ({ id, job, error }) => {
1384
1411
 
1385
1412
  // Do gas stake as soon as possible after payment succeed
1386
1413
  events.on('payment_intent.succeeded', async (paymentIntent: PaymentIntent) => {
1387
- const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
1414
+ const paymentMethod = await systemFindByPk(PaymentMethod, paymentIntent.payment_method_id);
1388
1415
  if (paymentMethod && paymentMethod.type === 'arcblock') {
1389
1416
  ensureStakedForGas();
1390
1417
  }