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
@@ -1,6 +1,7 @@
1
+ // Phase 3 (express→hono) — hono fork of routes/subscriptions.ts
1
2
  /* eslint-disable no-await-in-loop */
3
+ /* eslint-disable consistent-return */
2
4
  import { isValid } from '@arcblock/did';
3
- import { Router } from 'express';
4
5
  import Joi from 'joi';
5
6
  import isObject from 'lodash/isObject';
6
7
  import pick from 'lodash/pick';
@@ -8,22 +9,30 @@ import uniq from 'lodash/uniq';
8
9
 
9
10
  import { literal, Op, OrderItem } from 'sequelize';
10
11
  import { BN, fromTokenToUnit } from '@ocap/util';
11
- import { createEvent } from '../libs/audit';
12
- import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
13
- import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
14
- import dayjs from '../libs/dayjs';
15
- import logger from '../libs/logger';
16
- import { isDelegationSufficientForPayment } from '../libs/payment';
17
- import { authenticate } from '../libs/security';
12
+ import { Hono } from 'hono';
13
+ import { isProduction } from '../../libs/env';
14
+ import { createEvent, reportAuditFailure } from '../../libs/audit';
15
+ import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../../integrations/stripe/resource';
16
+ import {
17
+ createListParamSchema,
18
+ getOrder,
19
+ getWhereFromKvQuery,
20
+ getWhereFromQuery,
21
+ MetadataSchema,
22
+ } from '../../libs/api';
23
+ import dayjs from '../../libs/dayjs';
24
+ import logger from '../../libs/logger';
25
+ import { isDelegationSufficientForPayment } from '../../libs/payment';
26
+ import { authenticate } from '../../middlewares/hono/security';
18
27
  import {
19
28
  expandLineItems,
20
29
  getFastCheckoutAmount,
21
30
  getSubscriptionCreateSetup,
22
31
  isLineItemAligned,
23
32
  SlippageOptions,
24
- } from '../libs/session';
25
- import { getExchangeRateService } from '../libs/exchange-rate/service';
26
- import { getExchangeRateSymbol } from '../libs/exchange-rate/token-address-mapping';
33
+ } from '../../libs/session';
34
+ import { getExchangeRateService } from '../../libs/exchange-rate/service';
35
+ import { getExchangeRateSymbol } from '../../libs/exchange-rate/token-address-mapping';
27
36
  import {
28
37
  checkRemainingStake,
29
38
  createProration,
@@ -36,10 +45,10 @@ import {
36
45
  getSubscriptionUnpaidInvoicesCount,
37
46
  getUpcomingInvoiceAmount,
38
47
  isSubscriptionOverdraftProtectionEnabled,
39
- } from '../libs/subscription';
40
- import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
41
- import { trimDecimals, limitTokenPrecision } from '../libs/math-utils';
42
- import { invoiceQueue } from '../queues/invoice';
48
+ } from '../../libs/subscription';
49
+ import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../../libs/util';
50
+ import { trimDecimals, limitTokenPrecision } from '../../libs/math-utils';
51
+ import { invoiceQueue } from '../../queues/invoice';
43
52
  import {
44
53
  addSubscriptionJob,
45
54
  returnOverdraftProtectionQueue,
@@ -47,38 +56,36 @@ import {
47
56
  slashOverdraftProtectionQueue,
48
57
  slashStakeQueue,
49
58
  subscriptionQueue,
50
- } from '../queues/subscription';
51
- import type { TLineItemExpanded, ChainType } from '../store/models';
52
- import { Customer } from '../store/models/customer';
53
- import { Invoice } from '../store/models/invoice';
54
- import { InvoiceItem } from '../store/models/invoice-item';
55
- import { Lock } from '../store/models/lock';
56
- import { PaymentCurrency } from '../store/models/payment-currency';
57
- import { PaymentIntent } from '../store/models/payment-intent';
58
- import { PaymentMethod } from '../store/models/payment-method';
59
- import { Price } from '../store/models/price';
60
- import { PriceQuote } from '../store/models/price-quote';
61
- import { PricingTable } from '../store/models/pricing-table';
62
- import { Product } from '../store/models/product';
63
- import { SetupIntent } from '../store/models/setup-intent';
64
- import { Subscription, TSubscription } from '../store/models/subscription';
65
- import { SubscriptionItem } from '../store/models/subscription-item';
66
- import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/models/types';
67
- import { UsageRecord } from '../store/models/usage-record';
59
+ } from '../../queues/subscription';
60
+ import type { TLineItemExpanded, ChainType } from '../../store/models';
61
+ import { Customer } from '../../store/models/customer';
62
+ import { Invoice } from '../../store/models/invoice';
63
+ import { InvoiceItem } from '../../store/models/invoice-item';
64
+ import { Lock } from '../../store/models/lock';
65
+ import { PaymentCurrency } from '../../store/models/payment-currency';
66
+ import { PaymentIntent } from '../../store/models/payment-intent';
67
+ import { PaymentMethod } from '../../store/models/payment-method';
68
+ import { Price } from '../../store/models/price';
69
+ import { PriceQuote } from '../../store/models/price-quote';
70
+ import { PricingTable } from '../../store/models/pricing-table';
71
+ import { Product } from '../../store/models/product';
72
+ import { SetupIntent } from '../../store/models/setup-intent';
73
+ import { Subscription, TSubscription } from '../../store/models/subscription';
74
+ import { SubscriptionItem } from '../../store/models/subscription-item';
75
+ import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../../store/models/types';
76
+ import { UsageRecord } from '../../store/models/usage-record';
68
77
  import {
69
78
  cleanupInvoiceAndItems,
70
79
  ensureInvoiceAndItems,
71
80
  migrateSubscriptionPaymentMethodInvoice,
72
- } from '../libs/invoice';
73
- import { createUsageRecordQueryFn } from './usage-records';
74
- import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
75
- import { getTokenByAddress } from '../integrations/arcblock/stake';
76
- import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
77
- import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
78
- import { getSubscriptionDiscountStats } from '../libs/discount/redemption';
79
-
80
- // S1 optimization: Load only the products/prices referenced by a set of subscriptions,
81
- // instead of Product.findAll() + Price.findAll() full table scans.
81
+ } from '../../libs/invoice';
82
+ import { SubscriptionWillCanceledSchedule } from '../../crons/subscription-will-canceled';
83
+ import { getTokenByAddress } from '../../integrations/arcblock/stake';
84
+ import { ensureOverdraftProtectionPrice } from '../../libs/overdraft-protection';
85
+ import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../../libs/constants';
86
+ import { getSubscriptionDiscountStats } from '../../libs/discount/redemption';
87
+
88
+ // S1 optimization: Load only the products/prices referenced by a set of subscriptions
82
89
  async function loadProductsAndPricesForSubscriptions(docs: any[]): Promise<{ products: any[]; prices: any[] }> {
83
90
  const priceIds = uniq(docs.flatMap((x) => (x.items || []).map((i: any) => i.price_id)).filter(Boolean));
84
91
  if (priceIds.length === 0) {
@@ -89,7 +96,6 @@ async function loadProductsAndPricesForSubscriptions(docs: any[]): Promise<{ pro
89
96
  include: [{ model: Product, as: 'product' }],
90
97
  });
91
98
  const pricesJson = prices.map((x) => x.toJSON());
92
- // Derive products from the already-included price.product association
93
99
  const productMap = new Map<string, any>();
94
100
  pricesJson.forEach((p: any) => {
95
101
  if (p.product) {
@@ -99,7 +105,7 @@ async function loadProductsAndPricesForSubscriptions(docs: any[]): Promise<{ pro
99
105
  return { products: Array.from(productMap.values()), prices: pricesJson };
100
106
  }
101
107
 
102
- const router = Router();
108
+ const app = new Hono();
103
109
  const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
104
110
  const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
105
111
  const exchangeRateService = getExchangeRateService();
@@ -268,7 +274,6 @@ const createSchema = Joi.object({
268
274
  .min(1)
269
275
  .max(MAX_SUBSCRIPTION_ITEM_COUNT)
270
276
  .required(),
271
- // Optional: if not provided, subscription won't have automatic billing (for prepaid/gifted subscriptions)
272
277
  default_payment_method_id: Joi.string().optional(),
273
278
  currency_id: Joi.string().optional(),
274
279
  trial_period_days: Joi.number().integer().min(0).optional(),
@@ -281,12 +286,13 @@ const createSchema = Joi.object({
281
286
  collection_method: Joi.string().valid('charge_automatically', 'send_invoice').default('charge_automatically'),
282
287
  proration_behavior: Joi.string().valid('always_invoice', 'create_prorations', 'none').default('none'),
283
288
  service_actions: Joi.array().items(Joi.object()).optional(),
284
- livemode: Joi.boolean().optional(), // Required if no payment_method_id provided
289
+ livemode: Joi.boolean().optional(),
285
290
  });
286
291
 
287
- router.post('/', auth, async (req, res) => {
292
+ app.post('/', auth, async (c) => {
288
293
  try {
289
- const value = await createSchema.validateAsync(req.body);
294
+ const body = c.get('sanitizedBody') ?? {};
295
+ const value = await createSchema.validateAsync(body);
290
296
  const {
291
297
  customer_id: customerId,
292
298
  items,
@@ -304,35 +310,30 @@ router.post('/', auth, async (req, res) => {
304
310
  livemode: livemodeInput,
305
311
  } = value;
306
312
 
307
- // Validate customer exists
308
313
  const customer = await Customer.findByPk(customerId);
309
314
  if (!customer) {
310
- return res.status(404).json({ error: `Customer ${customerId} not found` });
315
+ return c.json({ error: `Customer ${customerId} not found` }, 404);
311
316
  }
312
317
 
313
- // Validate payment method if provided
314
318
  let paymentMethod: PaymentMethod | null = null;
315
319
  if (paymentMethodId) {
316
320
  paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
317
321
  if (!paymentMethod) {
318
- return res.status(404).json({ error: `Payment method ${paymentMethodId} not found` });
322
+ return c.json({ error: `Payment method ${paymentMethodId} not found` }, 404);
319
323
  }
320
324
  }
321
325
 
322
- // If no payment method, automatic billing is disabled (for prepaid/gifted subscriptions)
323
326
  const hasAutomaticBilling = !!paymentMethod;
324
327
 
325
- // Determine livemode
326
328
  let livemode = livemodeInput;
327
329
  if (livemode === undefined) {
328
330
  if (paymentMethod) {
329
331
  livemode = paymentMethod.livemode;
330
332
  } else {
331
- livemode = true; // Default to livemode if not specified
333
+ livemode = true;
332
334
  }
333
335
  }
334
336
 
335
- // Expand and validate line items
336
337
  const priceIds = items.map((item: any) => item.price_id);
337
338
  const prices = await Price.findAll({
338
339
  where: { id: priceIds },
@@ -342,33 +343,33 @@ router.post('/', auth, async (req, res) => {
342
343
  if (prices.length !== priceIds.length) {
343
344
  const foundIds = prices.map((p) => p.id);
344
345
  const missingIds = priceIds.filter((id: string) => !foundIds.includes(id));
345
- return res.status(404).json({ error: `Prices not found: ${missingIds.join(', ')}` });
346
+ return c.json({ error: `Prices not found: ${missingIds.join(', ')}` }, 404);
346
347
  }
347
348
 
348
- // Validate all items are recurring
349
349
  const nonRecurringPrices = prices.filter((p) => p.type !== 'recurring');
350
350
  if (nonRecurringPrices.length > 0) {
351
- return res.status(400).json({
352
- error: `Subscription only supports recurring prices. Non-recurring prices: ${nonRecurringPrices.map((p) => p.id).join(', ')}`,
353
- });
351
+ return c.json(
352
+ {
353
+ error: `Subscription only supports recurring prices. Non-recurring prices: ${nonRecurringPrices.map((p) => p.id).join(', ')}`,
354
+ },
355
+ 400
356
+ );
354
357
  }
355
358
 
356
- // Determine currency from first price if not provided
357
359
  let currencyId = value.currency_id;
358
360
  const firstPrice = prices[0];
359
361
  if (!currencyId && firstPrice) {
360
362
  currencyId = firstPrice.currency_id;
361
363
  }
362
364
  if (!currencyId) {
363
- return res.status(400).json({ error: 'currency_id is required' });
365
+ return c.json({ error: 'currency_id is required' }, 400);
364
366
  }
365
367
 
366
368
  const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
367
369
  if (!paymentCurrency) {
368
- return res.status(404).json({ error: `Payment currency ${currencyId} not found` });
370
+ return c.json({ error: `Payment currency ${currencyId} not found` }, 404);
369
371
  }
370
372
 
371
- // Build line items
372
373
  const lineItems: TLineItemExpanded[] = items.map((item: any) => {
373
374
  const price = prices.find((p) => p.id === item.price_id);
374
375
  return {
@@ -379,15 +380,13 @@ router.post('/', auth, async (req, res) => {
379
380
  };
380
381
  });
381
382
 
382
- // Calculate subscription setup (periods, trial, etc.)
383
383
  const setup = getSubscriptionCreateSetup(lineItems, currencyId, trialPeriodDays, trialEndInput);
384
384
 
385
- // Create subscription
386
385
  const subscription = await Subscription.create({
387
386
  livemode,
388
387
  currency_id: currencyId,
389
388
  customer_id: customerId,
390
- status: 'active', // Direct creation starts as active
389
+ status: 'active',
391
390
  current_period_start: setup.period.start,
392
391
  current_period_end: setup.period.end,
393
392
  billing_cycle_anchor: billingCycleAnchor || setup.cycle.anchor,
@@ -414,7 +413,6 @@ router.post('/', auth, async (req, res) => {
414
413
  service_actions: serviceActions || [],
415
414
  });
416
415
 
417
- // Create subscription items
418
416
  await Promise.all(
419
417
  lineItems.map((item) =>
420
418
  SubscriptionItem.create({
@@ -427,15 +425,13 @@ router.post('/', auth, async (req, res) => {
427
425
  )
428
426
  );
429
427
 
430
- // If has trial period, set status to trialing
431
428
  if (setup.trial.end && setup.trial.end > dayjs().unix()) {
432
429
  await subscription.update({ status: 'trialing' });
433
- createEvent('Subscription', 'customer.subscription.trial_start', subscription).catch(console.error);
430
+ createEvent('Subscription', 'customer.subscription.trial_start', subscription).catch(reportAuditFailure);
434
431
  } else {
435
- createEvent('Subscription', 'customer.subscription.started', subscription).catch(console.error);
432
+ createEvent('Subscription', 'customer.subscription.started', subscription).catch(reportAuditFailure);
436
433
  }
437
434
 
438
- // Schedule subscription cycle job only if has automatic billing
439
435
  if (hasAutomaticBilling) {
440
436
  await addSubscriptionJob(subscription, 'cycle', false, setup.trial.end || setup.period.end);
441
437
  }
@@ -447,7 +443,6 @@ router.post('/', auth, async (req, res) => {
447
443
  hasAutomaticBilling,
448
444
  });
449
445
 
450
- // Return expanded subscription
451
446
  const result = await Subscription.findOne({
452
447
  where: { id: subscription.id },
453
448
  include: [
@@ -463,14 +458,14 @@ router.post('/', auth, async (req, res) => {
463
458
  // @ts-ignore
464
459
  expandLineItems(doc.items, products, allPrices);
465
460
 
466
- return res.json(doc);
461
+ return c.json(doc);
467
462
  } catch (err: any) {
468
463
  logger.error('Failed to create subscription', { error: err.message });
469
- return res.status(400).json({ error: err.message });
464
+ return c.json({ error: err.message }, 400);
470
465
  }
471
466
  });
472
467
 
473
- router.get('/', authMine, async (req, res) => {
468
+ app.get('/', authMine, async (c) => {
474
469
  const {
475
470
  page,
476
471
  pageSize,
@@ -478,7 +473,7 @@ router.get('/', authMine, async (req, res) => {
478
473
  livemode,
479
474
  include_latest_invoice_quote: includeLatestInvoiceQuoteParam = false,
480
475
  ...query
481
- } = await schema.validateAsync(req.query, {
476
+ } = await schema.validateAsync(c.req.query(), {
482
477
  stripUnknown: false,
483
478
  allowUnknown: true,
484
479
  });
@@ -488,19 +483,18 @@ router.get('/', authMine, async (req, res) => {
488
483
  if (status) {
489
484
  where.status = status
490
485
  .split(',')
491
- .map((x) => x.trim())
486
+ .map((x: string) => x.trim())
492
487
  .filter(Boolean);
493
488
  }
494
- if (query.customer_id) {
495
- where.customer_id = query.customer_id;
489
+ if (c.get('customer_id') ?? query.customer_id) {
490
+ where.customer_id = c.get('customer_id') ?? query.customer_id;
496
491
  }
497
492
  if (query.customer_did && isValid(query.customer_did)) {
498
493
  const customer = await Customer.findOne({ where: { did: query.customer_did } });
499
494
  if (customer) {
500
495
  where.customer_id = customer.id;
501
496
  } else {
502
- res.json({ count: 0, list: [], paging: { page, pageSize } });
503
- return;
497
+ return c.json({ count: 0, list: [], paging: { page, pageSize } });
504
498
  }
505
499
  }
506
500
  where.livemode = typeof livemode === 'boolean' ? livemode : true;
@@ -512,7 +506,7 @@ router.get('/', authMine, async (req, res) => {
512
506
  where[key] = query[key];
513
507
  });
514
508
 
515
- const order: OrderItem[] = getOrder(req.query, []);
509
+ const order: OrderItem[] = getOrder(c.req.query(), []);
516
510
 
517
511
  if (query.activeFirst) {
518
512
  order.unshift([
@@ -541,7 +535,6 @@ router.get('/', authMine, async (req, res) => {
541
535
  { model: SubscriptionItem, as: 'items' },
542
536
  { model: Customer, as: 'customer' },
543
537
  ],
544
- // https://github.com/sequelize/sequelize/issues/9481
545
538
  distinct: true,
546
539
  });
547
540
  const docs = list.map((x) => x.toJSON());
@@ -560,13 +553,12 @@ router.get('/', authMine, async (req, res) => {
560
553
  },
561
554
  distinct: true,
562
555
  });
563
- res.json({ count, list: docs, paging: { page, pageSize }, totalCount });
564
- } else {
565
- res.json({ count, list: docs, paging: { page, pageSize } });
556
+ return c.json({ count, list: docs, paging: { page, pageSize }, totalCount });
566
557
  }
558
+ return c.json({ count, list: docs, paging: { page, pageSize } });
567
559
  } catch (err) {
568
560
  logger.error(err);
569
- res.json({ count: 0, list: [], paging: { page, pageSize } });
561
+ return c.json({ count: 0, list: [], paging: { page, pageSize } });
570
562
  }
571
563
  });
572
564
 
@@ -578,7 +570,8 @@ const searchSchema = createListParamSchema<{
578
570
  query: Joi.string(),
579
571
  include_latest_invoice_quote: Joi.boolean().optional(),
580
572
  });
581
- router.get('/search', auth, async (req, res) => {
573
+
574
+ app.get('/search', auth, async (c) => {
582
575
  const {
583
576
  page,
584
577
  pageSize,
@@ -587,7 +580,7 @@ router.get('/search', auth, async (req, res) => {
587
580
  q,
588
581
  o,
589
582
  include_latest_invoice_quote: includeLatestInvoiceQuoteParam = false,
590
- } = await searchSchema.validateAsync(req.query, {
583
+ } = await searchSchema.validateAsync(c.req.query(), {
591
584
  stripUnknown: false,
592
585
  allowUnknown: true,
593
586
  });
@@ -597,10 +590,9 @@ router.get('/search', auth, async (req, res) => {
597
590
  if (typeof livemode === 'boolean') {
598
591
  where.livemode = livemode;
599
592
  }
600
- // fix here https://github.com/blocklet/payment-kit/issues/394
601
593
  const { rows: list, count } = await Subscription.findAndCountAll({
602
594
  where,
603
- order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
595
+ order: getOrder(c.req.query(), [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
604
596
  offset: (page - 1) * pageSize,
605
597
  limit: pageSize,
606
598
  distinct: true,
@@ -619,13 +611,13 @@ router.get('/search', auth, async (req, res) => {
619
611
  if (includeLatestInvoiceQuote) {
620
612
  await attachLatestInvoiceQuotes(docs);
621
613
  }
622
- res.json({ count, list: docs, paging: { page, pageSize } });
614
+ return c.json({ count, list: docs, paging: { page, pageSize } });
623
615
  });
624
616
 
625
- router.get('/:id', authPortal, async (req, res) => {
617
+ app.get('/:id', authPortal, async (c) => {
626
618
  try {
627
619
  const doc = (await Subscription.findOne({
628
- where: { id: req.params.id },
620
+ where: { id: c.req.param('id') },
629
621
  include: [
630
622
  { model: PaymentCurrency, as: 'paymentCurrency' },
631
623
  { model: PaymentMethod, as: 'paymentMethod' },
@@ -649,7 +641,6 @@ router.get('/:id', authPortal, async (req, res) => {
649
641
  // @ts-ignore
650
642
  json.serviceType = serviceType;
651
643
 
652
- // Get discount statistics if subscription has active discounts
653
644
  let discountStats = null;
654
645
  try {
655
646
  const stats = await getSubscriptionDiscountStats(json.id);
@@ -660,7 +651,6 @@ router.get('/:id', authPortal, async (req, res) => {
660
651
  logger.error('Failed to fetch subscription discount stats', { error, subscriptionId: json.id });
661
652
  }
662
653
 
663
- // Get payment method details
664
654
  let paymentMethodDetails = null;
665
655
  try {
666
656
  const paymentMethod = (doc as any).paymentMethod as PaymentMethod | null;
@@ -720,47 +710,48 @@ router.get('/:id', authPortal, async (req, res) => {
720
710
  logger.error('Failed to fetch payment method details', { error, subscriptionId: json.id });
721
711
  }
722
712
 
723
- res.json({
713
+ return c.json({
724
714
  ...json,
725
715
  discountStats,
726
716
  paymentMethodDetails,
727
717
  });
728
- } else {
729
- res.status(404).json(null);
730
718
  }
731
- } catch (err) {
719
+ return c.json(null, 404);
720
+ } catch (err: any) {
732
721
  logger.error(err);
733
- res.status(500).json({ error: `Failed to get subscription: ${err.message}` });
722
+ return c.json({ error: `Failed to get subscription: ${err.message}` }, 500);
734
723
  }
735
724
  });
736
725
 
737
726
  const CommentSchema = Joi.string().max(200).empty('').optional();
738
727
  const SlashStakeSchema = Joi.string().max(200).required();
739
728
 
740
- router.put('/:id/cancel', authPortal, async (req, res) => {
741
- const { error: commentError } = CommentSchema.validate(req.body?.comment);
729
+ app.put('/:id/cancel', authPortal, async (c) => {
730
+ const body = c.get('sanitizedBody') ?? {};
731
+ const { error: commentError } = CommentSchema.validate(body?.comment);
742
732
  if (commentError) {
743
- return res.status(400).json({ error: `comment invalid: ${commentError.message}` });
733
+ return c.json({ error: `comment invalid: ${commentError.message}` }, 400);
744
734
  }
745
735
 
746
- const requestByAdmin = ['owner', 'admin'].includes(req.user?.role as string);
747
- const slashStake = requestByAdmin && req.body?.staking === 'slash';
736
+ const user = c.get('user');
737
+ const requestByAdmin = ['owner', 'admin'].includes(user?.role as string);
738
+ const slashStake = requestByAdmin && body?.staking === 'slash';
748
739
 
749
740
  if (slashStake) {
750
- const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
741
+ const { error: slashReasonError } = SlashStakeSchema.validate(body?.slashReason);
751
742
  if (slashReasonError) {
752
- return res.status(400).json({ error: `slash reason invalid: ${slashReasonError.message}` });
743
+ return c.json({ error: `slash reason invalid: ${slashReasonError.message}` }, 400);
753
744
  }
754
745
  }
755
746
 
756
- const subscription = await Subscription.findByPk(req.params.id);
757
- logger.info('subscription cancel request', { ...req.params, ...req.body });
747
+ const subscription = await Subscription.findByPk(c.req.param('id'));
748
+ logger.info('subscription cancel request', { id: c.req.param('id'), ...body });
758
749
 
759
750
  if (!subscription) {
760
- return res.status(404).json({ error: 'subscription not found' });
751
+ return c.json({ error: 'subscription not found' }, 404);
761
752
  }
762
753
  if (subscription.status === 'canceled') {
763
- return res.status(400).json({ error: 'Subscription already canceled' });
754
+ return c.json({ error: 'Subscription already canceled' }, 400);
764
755
  }
765
756
 
766
757
  const {
@@ -772,20 +763,19 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
772
763
  reason = 'payment_disputed',
773
764
  staking = 'none',
774
765
  slashReason = 'admin slash',
775
- } = req.body;
766
+ } = body;
776
767
  if (at === 'custom' && dayjs(time).unix() < dayjs().unix()) {
777
- return res.status(400).json({ error: 'cancel at must be a future timestamp' });
768
+ return c.json({ error: 'cancel at must be a future timestamp' }, 400);
778
769
  }
779
770
 
780
771
  let canReturnStake = false;
781
- if ((requestByAdmin && staking === 'proration') || req.body?.cancel_from === 'customer') {
772
+ if ((requestByAdmin && staking === 'proration') || body?.cancel_from === 'customer') {
782
773
  canReturnStake = true;
783
774
  }
784
775
  const haveStake = !!subscription.payment_details?.arcblock?.staking?.tx_hash;
785
- // update cancel at
786
776
  const updates: Partial<Subscription> = {
787
777
  cancelation_details: {
788
- comment: comment || `Requested by ${req.user?.role}:${req.user?.did}`,
778
+ comment: comment || `Requested by ${user?.role}:${user?.did}`,
789
779
  reason: reason || 'payment_disputed',
790
780
  feedback: feedback || 'other',
791
781
  return_stake: canReturnStake && haveStake,
@@ -794,7 +784,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
794
784
  },
795
785
  };
796
786
  const now = dayjs().unix() + 3;
797
- if (req.user?.via === 'portal' || req.body?.cancel_from === 'customer') {
787
+ if (user?.via === 'portal' || body?.cancel_from === 'customer') {
798
788
  const inTrialing = subscription.status === 'trialing';
799
789
  updates.cancel_at_period_end = true;
800
790
  updates.cancel_at = subscription.current_period_end;
@@ -813,8 +803,8 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
813
803
  }
814
804
  await addSubscriptionJob(subscription, 'cancel', true, updates.cancel_at);
815
805
  } else {
816
- if (['owner', 'admin'].includes(req.user?.role as string) === false) {
817
- return res.status(403).json({ error: 'Not authorized to perform this action' });
806
+ if (['owner', 'admin'].includes(user?.role as string) === false) {
807
+ return c.json({ error: 'Not authorized to perform this action' }, 403);
818
808
  }
819
809
 
820
810
  if (at === 'now') {
@@ -842,7 +832,11 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
842
832
  if (subscription.payment_details?.stripe?.subscription_id) {
843
833
  const method = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: subscription.livemode } });
844
834
  if (method) {
845
- logger.info('subscription cancel attempt on stripe', { subscription: req.params.id, method: method.id, updates });
835
+ logger.info('subscription cancel attempt on stripe', {
836
+ subscription: c.req.param('id'),
837
+ method: method.id,
838
+ updates,
839
+ });
846
840
  const client = method.getStripeClient();
847
841
  try {
848
842
  if (updates.cancel_at_period_end) {
@@ -855,18 +849,20 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
855
849
  proration_behavior: 'none',
856
850
  });
857
851
  }
858
- logger.info('subscription cancel done on stripe', { subscription: req.params.id, method: method.id, updates });
852
+ logger.info('subscription cancel done on stripe', {
853
+ subscription: c.req.param('id'),
854
+ method: method.id,
855
+ updates,
856
+ });
859
857
  } catch (err) {
860
- // FIXME: how do we handle failure here?
861
- logger.error('subscription cancel failed on stripe', { subscription: req.params.id, updates, error: err });
858
+ logger.error('subscription cancel failed on stripe', { subscription: c.req.param('id'), updates, error: err });
862
859
  }
863
860
  }
864
861
  }
865
862
 
866
- // trigger refund
867
863
  if (updates.cancel_at < subscription.current_period_end && refund !== 'none') {
868
- if (['owner', 'admin'].includes(req.user?.role as string) === false) {
869
- return res.status(403).json({ error: 'Not authorized to refund' });
864
+ if (['owner', 'admin'].includes(user?.role as string) === false) {
865
+ return c.json({ error: 'Not authorized to refund' }, 403);
870
866
  }
871
867
  const result = await getSubscriptionRefundSetup(subscription, updates.cancel_at);
872
868
  if (result.remainingUnused !== '0') {
@@ -874,18 +870,18 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
874
870
  updates.cancelation_details = {
875
871
  ...(updates.cancelation_details || {}),
876
872
  refund,
877
- requested_by: req.user?.did,
873
+ requested_by: user?.did,
878
874
  };
879
875
  logger.info('subscription cancel with refund', {
880
- ...req.params,
881
- ...req.body,
876
+ id: c.req.param('id'),
877
+ ...body,
882
878
  refund,
883
879
  ...pick(result, ['total', 'unused']),
884
880
  });
885
881
  } else {
886
882
  logger.info('subscription cancel no refund', {
887
- ...req.params,
888
- ...req.body,
883
+ id: c.req.param('id'),
884
+ ...body,
889
885
  ...pick(result, ['total', 'unused']),
890
886
  });
891
887
  }
@@ -895,19 +891,19 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
895
891
  logger.info('Update subscription for cancel request successful', {
896
892
  subscriptionId: subscription.id,
897
893
  customerId: subscription.customer_id,
898
- reason: req.body.reason,
894
+ reason: body.reason,
899
895
  cancelAt: subscription.cancel_at,
900
- requestedBy: req.user?.did,
896
+ requestedBy: user?.did,
901
897
  updates,
902
898
  });
903
- return res.json(subscription);
899
+ return c.json(subscription);
904
900
  });
905
901
 
906
- router.get('/:id/recover-info', authPortal, async (req, res) => {
907
- const doc = await Subscription.findByPk(req.params.id);
902
+ app.get('/:id/recover-info', authPortal, async (c) => {
903
+ const doc = await Subscription.findByPk(c.req.param('id'));
908
904
 
909
905
  if (!doc) {
910
- return res.status(404).json({ error: 'Subscription not found' });
906
+ return c.json({ error: 'Subscription not found' }, 404);
911
907
  }
912
908
 
913
909
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
@@ -932,39 +928,38 @@ router.get('/:id/recover-info', authPortal, async (req, res) => {
932
928
  }
933
929
  }
934
930
 
935
- return res.json({
931
+ return c.json({
936
932
  subscription: doc,
937
933
  needStake,
938
934
  revokedStake,
939
935
  });
940
936
  });
941
937
 
942
- router.put('/:id/recover', authPortal, async (req, res) => {
943
- const doc = await Subscription.findByPk(req.params.id);
938
+ app.put('/:id/recover', authPortal, async (c) => {
939
+ const doc = await Subscription.findByPk(c.req.param('id'));
944
940
 
945
941
  if (!doc) {
946
- return res.status(404).json({ error: 'Subscription not found' });
942
+ return c.json({ error: 'Subscription not found' }, 404);
947
943
  }
948
944
  if (!doc.cancel_at_period_end) {
949
- return res.status(400).json({ error: 'Subscription not recoverable from cancellation config' });
945
+ return c.json({ error: 'Subscription not recoverable from cancellation config' }, 400);
950
946
  }
951
947
  if (doc.cancelation_details?.reason === 'payment_failed') {
952
- return res.status(400).json({ error: 'Subscription not recoverable from payment failed' });
948
+ return c.json({ error: 'Subscription not recoverable from payment failed' }, 400);
953
949
  }
954
950
  if (doc.status === 'canceled') {
955
- return res.status(400).json({ error: 'Subscription not recoverable from cancellation' });
951
+ return c.json({ error: 'Subscription not recoverable from cancellation' }, 400);
956
952
  }
957
953
 
958
954
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
959
955
  if (!paymentMethod) {
960
- return res.status(400).json({ error: 'Payment method not found' });
956
+ return c.json({ error: 'Payment method not found' }, 400);
961
957
  }
962
958
  const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
963
959
  if (!paymentCurrency) {
964
- return res.status(400).json({ error: 'Payment currency not found' });
960
+ return c.json({ error: 'Payment currency not found' }, 400);
965
961
  }
966
962
 
967
- // check if need stake
968
963
  if (paymentMethod.type === 'arcblock') {
969
964
  const address = doc.payment_details?.arcblock?.staking?.address;
970
965
  if (address) {
@@ -972,7 +967,7 @@ router.put('/:id/recover', authPortal, async (req, res) => {
972
967
  const { revoked } = await checkRemainingStake(paymentMethod, paymentCurrency, address, '0');
973
968
  const cancelReason = doc.cancelation_details?.reason;
974
969
  if (revoked && revoked.value !== '0' && cancelReason === 'stake_revoked') {
975
- return res.json({
970
+ return c.json({
976
971
  needStake: true,
977
972
  subscription: doc,
978
973
  revoked,
@@ -999,29 +994,29 @@ router.put('/:id/recover', authPortal, async (req, res) => {
999
994
  canceled_at: 0,
1000
995
  });
1001
996
  await new SubscriptionWillCanceledSchedule().deleteScheduleSubscriptionJobs([doc]);
1002
- // reschedule jobs
1003
997
  subscriptionQueue
1004
998
  .delete(`cancel-${doc.id}`)
1005
999
  .then(() => logger.info('subscription cancel job is canceled'))
1006
1000
  .catch((err) => logger.error('subscription cancel job failed to cancel', { error: err }));
1007
1001
  await addSubscriptionJob(doc, 'cycle');
1008
1002
 
1009
- return res.json({ subscription: doc });
1003
+ return c.json({ subscription: doc });
1010
1004
  });
1011
1005
 
1012
- router.put('/:id/pause', auth, async (req, res) => {
1013
- const doc = await Subscription.findByPk(req.params.id);
1006
+ app.put('/:id/pause', auth, async (c) => {
1007
+ const doc = await Subscription.findByPk(c.req.param('id'));
1014
1008
 
1015
1009
  if (!doc) {
1016
- return res.status(404).json({ error: 'subscription not found' });
1010
+ return c.json({ error: 'subscription not found' }, 404);
1017
1011
  }
1018
1012
  if (doc.status === 'paused') {
1019
- return res.status(400).json({ error: 'Subscription already paused' });
1013
+ return c.json({ error: 'Subscription already paused' }, 400);
1020
1014
  }
1021
1015
 
1022
- const { type, resumesAt, behavior } = req.body;
1016
+ const body = c.get('sanitizedBody') ?? {};
1017
+ const { type, resumesAt, behavior } = body;
1023
1018
  if (type === 'custom' && dayjs(resumesAt).unix() < dayjs().unix()) {
1024
- return res.status(400).json({ error: 'resumesAt must be a future timestamp' });
1019
+ return c.json({ error: 'resumesAt must be a future timestamp' }, 400);
1025
1020
  }
1026
1021
 
1027
1022
  const timestamp = type === 'custom' ? dayjs(resumesAt).unix() : 0;
@@ -1045,17 +1040,17 @@ router.put('/:id/pause', auth, async (req, res) => {
1045
1040
  await addSubscriptionJob(doc, 'resume', false, timestamp);
1046
1041
  }
1047
1042
 
1048
- return res.json(doc);
1043
+ return c.json(doc);
1049
1044
  });
1050
1045
 
1051
- router.put('/:id/resume', auth, async (req, res) => {
1052
- const doc = await Subscription.findByPk(req.params.id);
1046
+ app.put('/:id/resume', auth, async (c) => {
1047
+ const doc = await Subscription.findByPk(c.req.param('id'));
1053
1048
 
1054
1049
  if (!doc) {
1055
- return res.status(404).json({ error: 'Subscription not found' });
1050
+ return c.json({ error: 'Subscription not found' }, 404);
1056
1051
  }
1057
1052
  if (doc.status !== 'paused' && doc.pause_collection === null) {
1058
- return res.status(400).json({ error: 'Subscription not paused' });
1053
+ return c.json({ error: 'Subscription not paused' }, 400);
1059
1054
  }
1060
1055
 
1061
1056
  await updateStripeSubscription(doc, { pause_collection: null });
@@ -1066,27 +1061,27 @@ router.put('/:id/resume', auth, async (req, res) => {
1066
1061
  .then(() => logger.info('subscription resume job is canceled'))
1067
1062
  .catch((err) => logger.error('subscription resume job failed to cancel', { error: err }));
1068
1063
 
1069
- return res.json(doc);
1064
+ return c.json(doc);
1070
1065
  });
1071
1066
 
1072
- router.put('/:id/return-stake', authPortal, async (req, res) => {
1073
- const doc = await Subscription.findByPk(req.params.id);
1067
+ app.put('/:id/return-stake', authPortal, async (c) => {
1068
+ const doc = await Subscription.findByPk(c.req.param('id'));
1074
1069
  if (!doc) {
1075
- return res.status(404).json({ error: 'Subscription not found' });
1070
+ return c.json({ error: 'Subscription not found' }, 404);
1076
1071
  }
1077
1072
  if (doc.status !== 'canceled') {
1078
- return res.status(400).json({ error: 'Subscription is not canceled' });
1073
+ return c.json({ error: 'Subscription is not canceled' }, 400);
1079
1074
  }
1080
1075
 
1081
1076
  if (!doc.payment_details?.arcblock?.staking?.tx_hash) {
1082
- return res.status(400).json({ error: 'No staking transaction found in subscription' });
1077
+ return c.json({ error: 'No staking transaction found in subscription' }, 400);
1083
1078
  }
1084
1079
  returnStakeQueue.push({ id: `return-stake-${doc.id}`, job: { subscriptionId: doc.id } });
1085
1080
  logger.info('Subscription return stake job scheduled', {
1086
1081
  jobId: `return-stake-${doc.id}`,
1087
1082
  subscription: doc.id,
1088
1083
  });
1089
- return res.json({ success: true, subscriptionId: doc.id });
1084
+ return c.json({ success: true, subscriptionId: doc.id });
1090
1085
  });
1091
1086
 
1092
1087
  const isValidSubscriptionItemChange = (item: SubscriptionUpdateItem) => {
@@ -1114,28 +1109,23 @@ const isValidSubscriptionItemChange = (item: SubscriptionUpdateItem) => {
1114
1109
  };
1115
1110
 
1116
1111
  const validateSubscriptionUpdateRequest = async (subscription: Subscription, items: SubscriptionUpdateItem[]) => {
1117
- // validate
1118
1112
  items.every(isValidSubscriptionItemChange);
1119
1113
 
1120
- // ensure no duplicate id
1121
1114
  const ids = items.filter((x: any) => x.id).map((x: any) => x.id);
1122
1115
  if (uniq(ids).length !== ids.length) {
1123
1116
  throw new Error('Subscription item should not have duplicate id');
1124
1117
  }
1125
1118
 
1126
- // ensure no duplicate price_id
1127
1119
  const priceIds = items.filter((x: any) => x.price_id).map((x: any) => x.price_id);
1128
1120
  if (uniq(priceIds).length !== priceIds.length) {
1129
1121
  throw new Error('Subscription item should not have duplicate price_id');
1130
1122
  }
1131
1123
 
1132
- // split items into added, deleted
1133
1124
  const existingItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
1134
1125
  const addedItems = items.filter((x: any) => x.price_id && !x.id);
1135
1126
  const deletedItems = items.filter((x: any) => x.deleted && x.id);
1136
1127
  const updatedItems = items.filter((x: any) => !x.deleted && x.id && existingItems.some((i) => i.id === x.id));
1137
1128
 
1138
- // try handle cross-sell with different interval, just replace with new price that have same interval
1139
1129
  let addedExpanded = await Price.expand(addedItems as LineItem[]);
1140
1130
  let existingExpanded = await Price.expand(
1141
1131
  existingItems.filter((x) => deletedItems.some((y) => y.id === x.id) === false).map((x) => x.toJSON())
@@ -1225,7 +1215,6 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
1225
1215
  };
1226
1216
  };
1227
1217
 
1228
- // TODO: @wangshijun forward changes to stripe
1229
1218
  const updateSchema = Joi.object<{
1230
1219
  description?: string;
1231
1220
  metadata?: Record<string, any>;
@@ -1269,24 +1258,23 @@ const updateSchema = Joi.object<{
1269
1258
  )
1270
1259
  .optional(),
1271
1260
  });
1272
- // eslint-disable-next-line consistent-return
1273
- router.put('/:id', authPortal, async (req, res) => {
1274
- logger.debug('subscription update request', { subscription: req.params.id, body: req.body });
1261
+
1262
+ app.put('/:id', authPortal, async (c) => {
1263
+ const body = c.get('sanitizedBody') ?? {};
1264
+ logger.debug('subscription update request', { subscription: c.req.param('id'), body });
1275
1265
  try {
1276
- const { error, value } = updateSchema.validate(req.body);
1266
+ const { error, value } = updateSchema.validate(body);
1277
1267
  if (error) {
1278
- return res.status(400).json({ error: `Subscription update request invalid: ${error.message}` });
1268
+ return c.json({ error: `Subscription update request invalid: ${error.message}` }, 400);
1279
1269
  }
1280
1270
 
1281
- const subscription = await Subscription.findByPk(req.params.id);
1271
+ const subscription = await Subscription.findByPk(c.req.param('id'));
1282
1272
  if (!subscription) {
1283
- return res.status(404).json({ error: 'Subscription not found' });
1273
+ return c.json({ error: 'Subscription not found' }, 404);
1284
1274
  }
1285
1275
 
1286
- // handle updates
1287
1276
  const updates: Partial<TSubscription> = {};
1288
1277
 
1289
- // only metadata + description can be updated when not active
1290
1278
  if (value.metadata) {
1291
1279
  updates.metadata = formatMetadata(value.metadata);
1292
1280
  }
@@ -1298,10 +1286,9 @@ router.put('/:id', authPortal, async (req, res) => {
1298
1286
  }
1299
1287
  if (subscription.isActive() === false) {
1300
1288
  await subscription.update(updates);
1301
- return res.json(subscription);
1289
+ return c.json(subscription);
1302
1290
  }
1303
1291
 
1304
- // other updates are allowed
1305
1292
  if (value.payment_behavior) {
1306
1293
  updates.payment_behavior = value.payment_behavior;
1307
1294
  }
@@ -1323,7 +1310,6 @@ router.put('/:id', authPortal, async (req, res) => {
1323
1310
  throw new Error('Subscription should have customer');
1324
1311
  }
1325
1312
 
1326
- // handle subscription item changes
1327
1313
  let connectAction = '';
1328
1314
  if (Array.isArray(value.items) && value.items.length > 0) {
1329
1315
  const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
@@ -1340,7 +1326,6 @@ router.put('/:id', authPortal, async (req, res) => {
1340
1326
  throw new Error('Updating subscription item not allowed now until next billing cycle');
1341
1327
  }
1342
1328
 
1343
- // validate the request
1344
1329
  const { addedItems, updatedItems, deletedItems, newItems } = await validateSubscriptionUpdateRequest(
1345
1330
  subscription,
1346
1331
  value.items
@@ -1380,7 +1365,7 @@ router.put('/:id', authPortal, async (req, res) => {
1380
1365
 
1381
1366
  const stripeItems = [...addedStripeItems, ...updatedStripeItems, ...deletedStripeItems].filter(Boolean);
1382
1367
  logger.info('stripe subscription update attempt', {
1383
- subscription: req.params.id,
1368
+ subscription: c.req.param('id'),
1384
1369
  stripeSubscriptionId,
1385
1370
  addedStripeItems,
1386
1371
  updatedStripeItems,
@@ -1389,7 +1374,7 @@ router.put('/:id', authPortal, async (req, res) => {
1389
1374
 
1390
1375
  await subscription.update({
1391
1376
  pending_update: {
1392
- expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
1377
+ expires_at: dayjs().unix() + 30 * 60,
1393
1378
  updates,
1394
1379
  addedItems,
1395
1380
  deletedItems,
@@ -1410,25 +1395,17 @@ router.put('/:id', authPortal, async (req, res) => {
1410
1395
  items: stripeItems,
1411
1396
  });
1412
1397
  logger.info('stripe subscription update done', {
1413
- subscription: req.params.id,
1398
+ subscription: c.req.param('id'),
1414
1399
  stripeSubscriptionId,
1415
1400
  prorationBehavior,
1416
1401
  result,
1417
1402
  });
1418
1403
  } else {
1419
- // update subscription period settings
1420
- // HINT: if we are adding new items, we need to reset the anchor to now
1421
- // For change-plan (proration), we need exact amounts, not authorization amounts with slippage buffer.
1422
- // So we don't pass minAcceptableRate or slippage percent here.
1423
- // The custom_amount we set below will be used as-is.
1424
1404
  const slippageOptions: SlippageOptions = {
1425
- percent: 0, // No slippage multiplier for actual payment
1426
- // Don't include minAcceptableRate - it would override custom_amount calculation
1405
+ percent: 0,
1427
1406
  currencyDecimal: paymentCurrency.decimal,
1428
1407
  };
1429
1408
 
1430
- // For dynamic pricing items, calculate custom_amount using current exchange rate
1431
- // This ensures the total is calculated with current rate, not the stale unit_amount
1432
1409
  const hasDynamicPricing = newItems.some((x) => (x.upsell_price || x.price).pricing_type === 'dynamic');
1433
1410
  if (hasDynamicPricing) {
1434
1411
  const currencyPaymentMethod =
@@ -1440,20 +1417,16 @@ router.put('/:id', authPortal, async (req, res) => {
1440
1417
  if (rateResult?.rate) {
1441
1418
  const USD_DECIMALS = 8;
1442
1419
  const currentRate = rateResult.rate;
1443
- // Set custom_amount for each dynamic pricing item
1444
1420
  newItems.forEach((item: any) => {
1445
1421
  const price = item.upsell_price || item.price;
1446
1422
  if (price.pricing_type === 'dynamic' && price.base_amount) {
1447
- // Calculate: base_amount / rate * 10^decimal
1448
- // Use trimDecimals to avoid "too many decimal places" error
1449
1423
  const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
1450
1424
  const rateBN = fromTokenToUnit(trimDecimals(currentRate, USD_DECIMALS), USD_DECIMALS);
1451
1425
  if (rateBN.gt(new BN(0))) {
1452
1426
  const amountBN = baseAmountBN
1453
1427
  .mul(new BN(10).pow(new BN(paymentCurrency.decimal)))
1454
- .add(rateBN.sub(new BN(1))) // Round up
1428
+ .add(rateBN.sub(new BN(1)))
1455
1429
  .div(rateBN);
1456
- // Apply same precision limiting as quote service (10 significant decimal places)
1457
1430
  const totalAmountBN = amountBN.mul(new BN(item.quantity));
1458
1431
  item.custom_amount = limitTokenPrecision(totalAmountBN, paymentCurrency.decimal, 10).toString();
1459
1432
  logger.info('Set custom_amount for dynamic pricing item in subscription update', {
@@ -1476,7 +1449,6 @@ router.put('/:id', authPortal, async (req, res) => {
1476
1449
  }
1477
1450
 
1478
1451
  const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0, 0, slippageOptions);
1479
- // Check if the subscription is currently in trial
1480
1452
  const isInTrial =
1481
1453
  subscription.status === 'trialing' && subscription.trial_end && subscription.trial_end > dayjs().unix();
1482
1454
  if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
@@ -1486,19 +1458,16 @@ router.put('/:id', authPortal, async (req, res) => {
1486
1458
  updates.current_period_end = setup.period.end;
1487
1459
  }
1488
1460
  updates.billing_cycle_anchor = setup.cycle.anchor;
1489
- logger.info('subscription updates on reset anchor', { subscription: req.params.id, updates });
1461
+ logger.info('subscription updates on reset anchor', { subscription: c.req.param('id'), updates });
1490
1462
  }
1491
1463
 
1492
- // handle proration
1493
1464
  if (prorationBehavior === 'create_prorations') {
1494
- // 0. cleanup open invoices
1495
1465
  if (subscription.pending_update?.updates?.latest_invoice_id) {
1496
1466
  await cleanupInvoiceAndItems(subscription.pending_update?.updates?.latest_invoice_id);
1497
1467
  // @ts-ignore
1498
1468
  await subscription.update({ pending_update: null });
1499
1469
  }
1500
1470
 
1501
- // 1. create proration
1502
1471
  const { lastInvoice, due, newCredit, appliedCredit, prorations, total } = await createProration(
1503
1472
  subscription,
1504
1473
  setup,
@@ -1506,7 +1475,6 @@ router.put('/:id', authPortal, async (req, res) => {
1506
1475
  );
1507
1476
 
1508
1477
  if ((total === '0' && isInTrial) || newCredit !== '0') {
1509
- // 0 amount or new credit means no need to create invoice
1510
1478
  await subscription.update(updates);
1511
1479
  await finalizeSubscriptionUpdate({
1512
1480
  subscription,
@@ -1520,11 +1488,10 @@ router.put('/:id', authPortal, async (req, res) => {
1520
1488
  updatedItems,
1521
1489
  updates,
1522
1490
  });
1523
- await createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
1524
- return res.json({ ...subscription.toJSON(), connectAction });
1491
+ await createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(reportAuditFailure);
1492
+ return c.json({ ...subscription.toJSON(), connectAction });
1525
1493
  }
1526
- // 2. create new invoice: amount according to new subscription items
1527
- // 3. create new invoice items: amount according to new subscription items
1494
+
1528
1495
  const result = await ensureInvoiceAndItems({
1529
1496
  customer,
1530
1497
  currency: paymentCurrency,
@@ -1551,9 +1518,8 @@ router.put('/:id', authPortal, async (req, res) => {
1551
1518
  });
1552
1519
  const { invoice } = result;
1553
1520
  updates.latest_invoice_id = invoice.id;
1554
- logger.info('subscription update invoice created', { subscription: req.params.id, invoice: invoice.id });
1521
+ logger.info('subscription update invoice created', { subscription: c.req.param('id'), invoice: invoice.id });
1555
1522
 
1556
- // 4. create proration invoice items: amount according to proration amount
1557
1523
  const prorationInvoiceItems = await Promise.all(
1558
1524
  prorations.map((x: any) =>
1559
1525
  InvoiceItem.create({
@@ -1573,15 +1539,13 @@ router.put('/:id', authPortal, async (req, res) => {
1573
1539
  )
1574
1540
  );
1575
1541
  logger.info('subscription proration invoice items created', {
1576
- subscription: req.params.id,
1542
+ subscription: c.req.param('id'),
1577
1543
  items: prorationInvoiceItems.map((x) => x.id),
1578
1544
  });
1579
1545
 
1580
- // 5. check do we need to connect
1581
1546
  let hasNext = true;
1582
1547
  let needsNewStake = false;
1583
1548
 
1584
- // Check if stake is required and if we need a new one
1585
1549
  const requiresStake = paymentMethod.type === 'arcblock' && !subscription.billing_thresholds?.no_stake;
1586
1550
  if (requiresStake) {
1587
1551
  const existingStakeAddress = subscription.payment_details?.arcblock?.staking?.address;
@@ -1603,8 +1567,6 @@ router.put('/:id', authPortal, async (req, res) => {
1603
1567
  hasNext = false;
1604
1568
  } else {
1605
1569
  const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
1606
- // For change-plan, we should check if delegation is sufficient for the due amount,
1607
- // not the full new plan price. The due amount is what user actually needs to pay.
1608
1570
  const delegation = await isDelegationSufficientForPayment({
1609
1571
  paymentMethod,
1610
1572
  paymentCurrency,
@@ -1622,30 +1584,26 @@ router.put('/:id', authPortal, async (req, res) => {
1622
1584
  needsNewStake,
1623
1585
  });
1624
1586
  if (delegation.sufficient && !needsNewStake) {
1625
- // Both delegation is sufficient and no new stake needed
1626
1587
  hasNext = false;
1627
1588
  } else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
1628
1589
  throw new Error('Subscription update can only be done when you do have connected DID Wallet');
1629
1590
  } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
1630
- // FIXME: this is not supported at frontend
1631
1591
  connectAction = 'collect';
1632
1592
  } else {
1633
1593
  connectAction = 'change-plan';
1634
1594
  }
1635
1595
  }
1636
1596
 
1637
- // 6. adjust invoice total
1638
1597
  await invoice.update({
1639
1598
  status: 'open',
1640
1599
  amount_due: due,
1641
1600
  amount_remaining: due,
1642
1601
  });
1643
1602
 
1644
- // 7. wait for succeed
1645
1603
  if (hasNext) {
1646
1604
  await subscription.update({
1647
1605
  pending_update: {
1648
- expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
1606
+ expires_at: dayjs().unix() + 30 * 60,
1649
1607
  updates,
1650
1608
  appliedCredit,
1651
1609
  newCredit,
@@ -1678,7 +1636,6 @@ router.put('/:id', authPortal, async (req, res) => {
1678
1636
  invoice: invoice.id,
1679
1637
  });
1680
1638
 
1681
- // check if we have succeeded
1682
1639
  await Promise.all([invoice.reload(), subscription.reload()]);
1683
1640
 
1684
1641
  if (invoice.status === 'paid') {
@@ -1703,9 +1660,9 @@ router.put('/:id', authPortal, async (req, res) => {
1703
1660
  }
1704
1661
  }
1705
1662
  }
1706
- } else if (req.body.billing_cycle_anchor === 'now') {
1663
+ } else if (body.billing_cycle_anchor === 'now') {
1707
1664
  if (paymentMethod?.type === 'stripe') {
1708
- return res.status(400).json({ error: 'Update billing_cycle_anchor not supported for stripe subscriptions' });
1665
+ return c.json({ error: 'Update billing_cycle_anchor not supported for stripe subscriptions' }, 400);
1709
1666
  }
1710
1667
 
1711
1668
  if (subscription.isActive() === false) {
@@ -1715,7 +1672,6 @@ router.put('/:id', authPortal, async (req, res) => {
1715
1672
  throw new Error('Updating billing_cycle_anchor not allowed for scheduled-to-cancel subscriptions');
1716
1673
  }
1717
1674
 
1718
- // FIXME: handle billing cycle anchor change without any item change
1719
1675
  await subscription.update(updates);
1720
1676
  } else {
1721
1677
  const now = dayjs().unix();
@@ -1787,15 +1743,14 @@ router.put('/:id', authPortal, async (req, res) => {
1787
1743
  updatedFields: Object.keys(updates),
1788
1744
  newStatus: subscription.status,
1789
1745
  });
1790
- return res.json({ ...subscription.toJSON(), connectAction });
1791
- } catch (err) {
1746
+ return c.json({ ...subscription.toJSON(), connectAction });
1747
+ } catch (err: any) {
1792
1748
  logger.error(err);
1793
- return res.status(500).json({ code: err.code, error: err.message });
1749
+ return c.json({ code: err.code, error: err.message }, 500);
1794
1750
  }
1795
1751
  });
1796
1752
 
1797
1753
  const getUpdateTable = async (subscription: Subscription) => {
1798
- // If we are from a pricing table
1799
1754
  if (subscription.metadata.pricing_table_id) {
1800
1755
  const table = await PricingTable.findByPk(subscription.metadata.pricing_table_id);
1801
1756
  if (table) {
@@ -1804,7 +1759,6 @@ const getUpdateTable = async (subscription: Subscription) => {
1804
1759
  }
1805
1760
  }
1806
1761
 
1807
- // if we are from upsell
1808
1762
  const items = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
1809
1763
  if (items.length === 1) {
1810
1764
  const expanded = await Price!.expand(
@@ -1813,14 +1767,12 @@ const getUpdateTable = async (subscription: Subscription) => {
1813
1767
  );
1814
1768
  const exist = expanded.find((x) => x.price.type === 'recurring' && x.price.upsell?.upsells_to_id);
1815
1769
  if (exist && exist.price.upsell?.upsells_to) {
1816
- // Fake a pricing table here
1817
1770
  return {
1818
1771
  active: true,
1819
1772
  livemode: subscription.livemode,
1820
1773
  name: 'Fake Pricing Table',
1821
1774
  items: [
1822
1775
  {
1823
- // Current item should always be the first
1824
1776
  ...PricingTable.formatItem({
1825
1777
  price_id: exist.price.id,
1826
1778
  product_id: exist.price.product_id,
@@ -1829,7 +1781,6 @@ const getUpdateTable = async (subscription: Subscription) => {
1829
1781
  product: exist.price.product,
1830
1782
  },
1831
1783
  {
1832
- // Upsell item comes next
1833
1784
  ...PricingTable.formatItem({
1834
1785
  price_id: exist.price.upsell.upsells_to_id,
1835
1786
  product_id: exist.price.product_id,
@@ -1846,69 +1797,65 @@ const getUpdateTable = async (subscription: Subscription) => {
1846
1797
  };
1847
1798
 
1848
1799
  // Check that the subscription is upgradable
1849
- router.get('/:id/change-plan', authPortal, async (req, res) => {
1800
+ app.get('/:id/change-plan', authPortal, async (c) => {
1850
1801
  try {
1851
- const subscription = await Subscription.findByPk(req.params.id);
1802
+ const subscription = await Subscription.findByPk(c.req.param('id'));
1852
1803
  if (!subscription) {
1853
- return res.status(404).json({ error: 'Subscription not found' });
1804
+ return c.json({ error: 'Subscription not found' }, 404);
1854
1805
  }
1855
1806
  if (subscription.isActive() === false) {
1856
- return res.status(400).json({ error: 'Subscription is not active' });
1807
+ return c.json({ error: 'Subscription is not active' }, 400);
1857
1808
  }
1858
1809
  if (subscription.isScheduledToCancel()) {
1859
- return res.status(400).json({ error: 'Subscription is scheduled to cancel' });
1810
+ return c.json({ error: 'Subscription is scheduled to cancel' }, 400);
1860
1811
  }
1861
1812
  const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
1862
1813
  if (locked) {
1863
- return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1814
+ return c.json({ error: 'Subscription plan change is not allowed until next billing cycle' }, 400);
1864
1815
  }
1865
1816
 
1866
1817
  const table = await getUpdateTable(subscription);
1867
- return res.json(table);
1818
+ return c.json(table);
1868
1819
  } catch (err) {
1869
1820
  logger.error(err);
1870
- return res.json(null);
1821
+ return c.json(null);
1871
1822
  }
1872
1823
  });
1873
1824
 
1874
1825
  // Simulate subscription plan change
1875
- router.post('/:id/change-plan', authPortal, async (req, res) => {
1826
+ app.post('/:id/change-plan', authPortal, async (c) => {
1876
1827
  try {
1877
- const subscription = await Subscription.findByPk(req.params.id);
1828
+ const subscription = await Subscription.findByPk(c.req.param('id'));
1878
1829
  if (!subscription) {
1879
- return res.status(404).json({ error: 'Subscription not found' });
1830
+ return c.json({ error: 'Subscription not found' }, 404);
1880
1831
  }
1881
1832
  if (subscription.isActive() === false) {
1882
- return res.status(400).json({ error: 'Subscription is not active' });
1833
+ return c.json({ error: 'Subscription is not active' }, 400);
1883
1834
  }
1884
1835
  if (subscription.isScheduledToCancel()) {
1885
- return res.status(400).json({ error: 'Subscription is scheduled to cancel' });
1836
+ return c.json({ error: 'Subscription is scheduled to cancel' }, 400);
1886
1837
  }
1887
1838
  const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
1888
1839
  if (locked) {
1889
- return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1840
+ return c.json({ error: 'Subscription plan change is not allowed until next billing cycle' }, 400);
1890
1841
  }
1891
1842
 
1892
- const { error } = updateSchema.validate(req.body);
1843
+ const body = c.get('sanitizedBody') ?? {};
1844
+ const { error } = updateSchema.validate(body);
1893
1845
  if (error) {
1894
- return res.status(400).json({ error: `Subscription update request invalid: ${error.message}` });
1846
+ return c.json({ error: `Subscription update request invalid: ${error.message}` }, 400);
1895
1847
  }
1896
1848
 
1897
- // handle subscription item changes
1898
- if (!Array.isArray(req.body.items) || !req.body.items.length) {
1899
- return res.status(400).json({ error: 'Subscription update request invalid: items are empty' });
1849
+ if (!Array.isArray(body.items) || !body.items.length) {
1850
+ return c.json({ error: 'Subscription update request invalid: items are empty' }, 400);
1900
1851
  }
1901
1852
 
1902
- // validate the request
1903
- const { newItems } = await validateSubscriptionUpdateRequest(subscription, req.body.items);
1853
+ const { newItems } = await validateSubscriptionUpdateRequest(subscription, body.items);
1904
1854
 
1905
- // do the simulation
1906
- // Note: For dynamic pricing, the actual amount is calculated by frontend using current exchange rate
1907
- // Backend only provides the base structure, frontend handles display with real-time rates
1908
1855
  const setup = getSubscriptionCreateSetup(newItems, subscription.currency_id, 0);
1909
1856
  const result = await createProration(subscription, setup, dayjs().unix());
1910
1857
 
1911
- return res.json({
1858
+ return c.json({
1912
1859
  setup,
1913
1860
  total: result.total,
1914
1861
  newCredit: result.newCredit,
@@ -1918,21 +1865,21 @@ router.post('/:id/change-plan', authPortal, async (req, res) => {
1918
1865
  prorations: result.prorations,
1919
1866
  items: newItems,
1920
1867
  });
1921
- } catch (err) {
1868
+ } catch (err: any) {
1922
1869
  logger.error(err);
1923
- return res.status(400).json({ error: err.message });
1870
+ return c.json({ error: err.message }, 400);
1924
1871
  }
1925
1872
  });
1926
1873
 
1927
1874
  // Simulate subscription cancel
1928
- router.get('/:id/proration', authPortal, async (req, res) => {
1875
+ app.get('/:id/proration', authPortal, async (c) => {
1929
1876
  try {
1930
- const subscription = await Subscription.findByPk(req.params.id);
1877
+ const subscription = await Subscription.findByPk(c.req.param('id'));
1931
1878
  if (!subscription) {
1932
- return res.status(404).json({ error: 'Subscription not found' });
1879
+ return c.json({ error: 'Subscription not found' }, 404);
1933
1880
  }
1934
1881
  if (subscription.isActive() === false) {
1935
- return res.status(400).json({ error: 'Subscription is not active' });
1882
+ return c.json({ error: 'Subscription is not active' }, 400);
1936
1883
  }
1937
1884
 
1938
1885
  const invoice = await Invoice.findOne({
@@ -1944,18 +1891,19 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1944
1891
  order: [['created_at', 'DESC']],
1945
1892
  });
1946
1893
 
1947
- const anchor = req.query.time ? dayjs(req.query.time as any).unix() : dayjs().unix();
1894
+ const timeQuery = c.req.query('time');
1895
+ const anchor = timeQuery ? dayjs(timeQuery as any).unix() : dayjs().unix();
1948
1896
  const result = await getSubscriptionRefundSetup(subscription, anchor, invoice?.currency_id);
1949
1897
  if (result.remaining === '0') {
1950
- return res.json(null);
1898
+ return c.json(null);
1951
1899
  }
1952
1900
  const paymentCurrency = await PaymentCurrency.findByPk(result.lastInvoice?.currency_id);
1953
1901
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency?.payment_method_id);
1954
1902
  if (paymentMethod?.type === 'stripe') {
1955
- return res.status(400).json({ error: 'Not supported for subscriptions paid with stripe' });
1903
+ return c.json({ error: 'Not supported for subscriptions paid with stripe' }, 400);
1956
1904
  }
1957
1905
 
1958
- return res.json({
1906
+ return c.json({
1959
1907
  total: result.remaining,
1960
1908
  latest: invoice?.total,
1961
1909
  unused: result.remainingUnused,
@@ -1963,67 +1911,68 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1963
1911
  prorations: result.prorations,
1964
1912
  paymentCurrency,
1965
1913
  });
1966
- } catch (err) {
1914
+ } catch (err: any) {
1967
1915
  logger.error(err);
1968
- return res.status(400).json({ error: err.message });
1916
+ return c.json({ error: err.message }, 400);
1969
1917
  }
1970
1918
  });
1971
1919
 
1972
1920
  // Simulate stake return when subscription is canceled
1973
- router.get('/:id/staking', authPortal, async (req, res) => {
1921
+ app.get('/:id/staking', authPortal, async (c) => {
1974
1922
  try {
1975
- const subscription = await Subscription.findByPk(req.params.id);
1923
+ const subscription = await Subscription.findByPk(c.req.param('id'));
1976
1924
  if (!subscription) {
1977
- return res.status(404).json({ error: 'Subscription not found' });
1925
+ return c.json({ error: 'Subscription not found' }, 404);
1978
1926
  }
1979
1927
 
1980
1928
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1981
1929
  if (paymentMethod?.type !== 'arcblock') {
1982
- return res
1983
- .status(400)
1984
- .json({ error: `Stake return not supported for subscription with payment method ${paymentMethod?.type}` });
1930
+ return c.json(
1931
+ { error: `Stake return not supported for subscription with payment method ${paymentMethod?.type}` },
1932
+ 400
1933
+ );
1985
1934
  }
1986
1935
  const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
1987
1936
  if (!address) {
1988
- return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
1937
+ return c.json({ error: 'Staking not found on subscription payment detail' }, 400);
1989
1938
  }
1990
1939
  const returnResult = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
1991
1940
  const slashResult = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
1992
- return res.json({
1941
+ return c.json({
1993
1942
  return_amount: returnResult.return_amount,
1994
1943
  total: returnResult.total,
1995
1944
  slash_amount: slashResult.return_amount,
1996
1945
  });
1997
- } catch (err) {
1946
+ } catch (err: any) {
1998
1947
  logger.error('subscription staking simulation failed', { error: err });
1999
1948
  logger.error(err);
2000
- return res.status(400).json({ error: err.message });
1949
+ return c.json({ error: err.message }, 400);
2001
1950
  }
2002
1951
  });
2003
1952
 
2004
1953
  // Check payment change status
2005
- router.get('/:id/change-payment', authPortal, async (req, res) => {
2006
- const subscription = await Subscription.findByPk(req.params.id);
1954
+ app.get('/:id/change-payment', authPortal, async (c) => {
1955
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2007
1956
  if (!subscription) {
2008
- return res.status(404).json({ error: 'Subscription not found' });
1957
+ return c.json({ error: 'Subscription not found' }, 404);
2009
1958
  }
2010
1959
  const context = subscription.metadata.changePayment || {};
2011
1960
  if (!context.setup_intent_id) {
2012
- return res.status(404).json({ error: 'Subscription change payment context not found' });
1961
+ return c.json({ error: 'Subscription change payment context not found' }, 404);
2013
1962
  }
2014
1963
 
2015
1964
  const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
2016
- return res.json({ subscription, setupIntent });
1965
+ return c.json({ subscription, setupIntent });
2017
1966
  });
2018
1967
 
2019
- router.get('/:id/exchange-rate', authPortal, async (req, res) => {
1968
+ app.get('/:id/exchange-rate', authPortal, async (c) => {
2020
1969
  try {
2021
- const subscription = await Subscription.findByPk(req.params.id);
1970
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2022
1971
  if (!subscription) {
2023
- return res.status(404).json({ error: 'Subscription not found' });
1972
+ return c.json({ error: 'Subscription not found' }, 404);
2024
1973
  }
2025
1974
 
2026
- const currencyId = (req.query.currency_id as string) || subscription.currency_id;
1975
+ const currencyId = c.req.query('currency_id') || subscription.currency_id;
2027
1976
  const paymentCurrency = (await PaymentCurrency.findByPk(currencyId, {
2028
1977
  include: [
2029
1978
  {
@@ -2034,24 +1983,24 @@ router.get('/:id/exchange-rate', authPortal, async (req, res) => {
2034
1983
  })) as PaymentCurrency & { payment_method: PaymentMethod };
2035
1984
 
2036
1985
  if (!paymentCurrency) {
2037
- return res.status(400).json({ error: 'Currency not found' });
1986
+ return c.json({ error: 'Currency not found' }, 400);
2038
1987
  }
2039
1988
 
2040
1989
  const paymentMethod =
2041
1990
  paymentCurrency.payment_method || (await PaymentMethod.findByPk(paymentCurrency.payment_method_id));
2042
1991
  if (!paymentMethod) {
2043
- return res.status(400).json({ error: 'Payment method not found' });
1992
+ return c.json({ error: 'Payment method not found' }, 400);
2044
1993
  }
2045
1994
 
2046
1995
  if (paymentMethod.type === 'stripe') {
2047
- return res.status(400).json({ error: 'Stripe currency does not require exchange rate.' });
1996
+ return c.json({ error: 'Stripe currency does not require exchange rate.' }, 400);
2048
1997
  }
2049
1998
 
2050
1999
  const rateSymbol = getExchangeRateSymbol(paymentCurrency.symbol, paymentMethod.type as any);
2051
2000
  const rateResult = await exchangeRateService.getRate(rateSymbol);
2052
2001
  const serverNow = Date.now();
2053
2002
 
2054
- return res.json({
2003
+ return c.json({
2055
2004
  server_now: serverNow,
2056
2005
  rate: rateResult.rate,
2057
2006
  timestamp_ms: rateResult.timestamp_ms,
@@ -2067,32 +2016,31 @@ router.get('/:id/exchange-rate', authPortal, async (req, res) => {
2067
2016
  });
2068
2017
  } catch (err: any) {
2069
2018
  logger.error('Failed to fetch exchange rate for subscription change payment', {
2070
- subscriptionId: req.params.id,
2019
+ subscriptionId: c.req.param('id'),
2071
2020
  error: err.message,
2072
2021
  });
2073
- return res.status(400).json({ error: err.message });
2022
+ return c.json({ error: err.message }, 400);
2074
2023
  }
2075
2024
  });
2076
2025
 
2077
- router.put('/:id/slippage', authPortal, async (req, res) => {
2026
+ app.put('/:id/slippage', authPortal, async (c) => {
2078
2027
  try {
2079
- const subscription = await Subscription.findByPk(req.params.id);
2028
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2080
2029
  if (!subscription) {
2081
- return res.status(404).json({ error: 'Subscription not found' });
2030
+ return c.json({ error: 'Subscription not found' }, 404);
2082
2031
  }
2083
2032
 
2084
- const { slippage_percent: slippagePercent } = req.body;
2085
- const rawConfig = req.body?.slippage_config || req.body?.slippage || null;
2033
+ const body = c.get('sanitizedBody') ?? {};
2034
+ const { slippage_percent: slippagePercent } = body;
2035
+ const rawConfig = body?.slippage_config || body?.slippage || null;
2086
2036
  const normalizePercent = (value: any) => {
2087
2037
  const normalized = typeof value === 'string' ? Number(value) : value;
2088
- // Only validate that it's a non-negative finite number, no upper limit
2089
2038
  if (!Number.isFinite(normalized) || normalized < 0) {
2090
2039
  return null;
2091
2040
  }
2092
2041
  return normalized;
2093
2042
  };
2094
2043
 
2095
- // Helper: get current exchange rate for subscription currency
2096
2044
  const getCurrentRate = async (): Promise<{ rate: string; baseCurrency: string } | null> => {
2097
2045
  const currency = (await PaymentCurrency.findByPk(subscription.currency_id, {
2098
2046
  include: [{ model: PaymentMethod, as: 'payment_method' }],
@@ -2106,13 +2054,11 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2106
2054
  return { rate: rateResult.rate, baseCurrency: 'USD' };
2107
2055
  };
2108
2056
 
2109
- // Helper: calculate min_acceptable_rate from percent
2110
2057
  const calcMinRateFromPercent = (percent: number, currentRate: string): string => {
2111
2058
  const rateNum = Number(currentRate);
2112
2059
  if (!Number.isFinite(rateNum) || rateNum <= 0) {
2113
2060
  return '0';
2114
2061
  }
2115
- // min_acceptable_rate = current_rate / (1 + percent/100)
2116
2062
  const minRate = rateNum / (1 + percent / 100);
2117
2063
  return minRate.toFixed(8);
2118
2064
  };
@@ -2122,12 +2068,10 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2122
2068
  const mode = rawConfig.mode === 'rate' ? 'rate' : 'percent';
2123
2069
  const minRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
2124
2070
 
2125
- // For rate mode, min_acceptable_rate is required; percent is derived
2126
2071
  if (mode === 'rate') {
2127
2072
  if (minRate === undefined || minRate === null || minRate === '') {
2128
- return res.status(400).json({ error: 'min_acceptable_rate is required for rate mode' });
2073
+ return c.json({ error: 'min_acceptable_rate is required for rate mode' }, 400);
2129
2074
  }
2130
- // Accept any non-negative percent value for rate mode (calculated from rate)
2131
2075
  const percent = normalizePercent(rawConfig.percent);
2132
2076
  const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency ?? 'USD';
2133
2077
  const rateInfo = await getCurrentRate();
@@ -2140,21 +2084,17 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2140
2084
  updated_at_ms: Date.now(),
2141
2085
  };
2142
2086
  } else {
2143
- // Percent mode: validate percent
2144
2087
  const percent = normalizePercent(rawConfig.percent);
2145
2088
  if (percent === null) {
2146
- return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
2089
+ return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
2147
2090
  }
2148
2091
  const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency ?? 'USD';
2149
- // Use min_acceptable_rate from frontend if provided, otherwise calculate it
2150
2092
  const frontendMinRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
2151
2093
  let minAcceptableRate: string | undefined;
2152
2094
  let rateAtConfigTime: string | undefined;
2153
2095
  if (frontendMinRate) {
2154
- // Frontend already calculated it - use directly
2155
2096
  minAcceptableRate = String(frontendMinRate);
2156
2097
  } else {
2157
- // Frontend didn't provide - calculate using same algorithm
2158
2098
  const rateInfo = await getCurrentRate();
2159
2099
  if (rateInfo) {
2160
2100
  minAcceptableRate = calcMinRateFromPercent(percent, rateInfo.rate);
@@ -2173,7 +2113,7 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2173
2113
  } else if (slippagePercent !== undefined && slippagePercent !== null) {
2174
2114
  const value = normalizePercent(slippagePercent);
2175
2115
  if (value === null) {
2176
- return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
2116
+ return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
2177
2117
  }
2178
2118
  const rateInfo = await getCurrentRate();
2179
2119
  const minAcceptableRate = rateInfo ? calcMinRateFromPercent(value, rateInfo.rate) : undefined;
@@ -2186,7 +2126,7 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2186
2126
  updated_at_ms: Date.now(),
2187
2127
  };
2188
2128
  } else {
2189
- return res.status(400).json({ error: 'slippage config is required' });
2129
+ return c.json({ error: 'slippage config is required' }, 400);
2190
2130
  }
2191
2131
 
2192
2132
  await subscription.update({ slippage_config: config });
@@ -2195,7 +2135,6 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2195
2135
  slippageConfig: config,
2196
2136
  });
2197
2137
 
2198
- // Check if authorization is sufficient with the new slippage config
2199
2138
  let delegationWarning: {
2200
2139
  sufficient: boolean;
2201
2140
  reason?: string;
@@ -2208,18 +2147,15 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2208
2147
  include: [{ model: PaymentMethod, as: 'payment_method' }],
2209
2148
  })) as PaymentCurrency & { payment_method: PaymentMethod };
2210
2149
 
2211
- // Only check delegation for non-Stripe payment methods
2212
2150
  if (paymentCurrency && paymentCurrency.payment_method?.type !== 'stripe') {
2213
2151
  const paymentMethod = paymentCurrency.payment_method;
2214
2152
  const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
2215
2153
 
2216
- // Get subscription items and expand them
2217
2154
  const subscriptionItems = await SubscriptionItem.findAll({
2218
2155
  where: { subscription_id: subscription.id },
2219
2156
  });
2220
2157
  const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
2221
2158
 
2222
- // Calculate the required amount with the new slippage
2223
2159
  const requiredAmount = await getFastCheckoutAmount({
2224
2160
  items: lineItems,
2225
2161
  mode: 'subscription',
@@ -2227,7 +2163,6 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2227
2163
  trialing: subscription.status === 'trialing',
2228
2164
  });
2229
2165
 
2230
- // Check if delegation is sufficient
2231
2166
  const delegation = await isDelegationSufficientForPayment({
2232
2167
  paymentMethod,
2233
2168
  paymentCurrency,
@@ -2251,73 +2186,70 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2251
2186
  }
2252
2187
  }
2253
2188
  } catch (delegationError: any) {
2254
- // Don't fail the slippage update if delegation check fails
2255
2189
  logger.warn('Failed to check delegation after slippage update', {
2256
2190
  subscriptionId: subscription.id,
2257
2191
  error: delegationError.message,
2258
2192
  });
2259
2193
  }
2260
2194
 
2261
- return res.json({
2195
+ return c.json({
2262
2196
  ...subscription.toJSON(),
2263
2197
  ...(delegationWarning ? { delegation_warning: delegationWarning } : {}),
2264
2198
  });
2265
2199
  } catch (err: any) {
2266
2200
  logger.error('Failed to update subscription slippage', {
2267
- subscriptionId: req.params.id,
2201
+ subscriptionId: c.req.param('id'),
2268
2202
  error: err.message,
2269
2203
  });
2270
- return res.status(500).json({ error: err.message });
2204
+ return c.json({ error: err.message }, 500);
2271
2205
  }
2272
2206
  });
2273
2207
 
2274
2208
  // Prepare setupIntent for payment change
2275
- router.post('/:id/change-payment', authPortal, async (req, res) => {
2209
+ app.post('/:id/change-payment', authPortal, async (c) => {
2276
2210
  try {
2277
- const subscription = await Subscription.findByPk(req.params.id);
2211
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2278
2212
  if (!subscription) {
2279
- return res.status(404).json({ error: `Subscription ${req.params.id} not found when change payment` });
2213
+ return c.json({ error: `Subscription ${c.req.param('id')} not found when change payment` }, 404);
2280
2214
  }
2281
2215
  if (['active', 'trialing', 'past_due'].includes(subscription.status) === false) {
2282
- return res.status(400).json({ error: `Subscription ${req.params.id} not active when change payment` });
2216
+ return c.json({ error: `Subscription ${c.req.param('id')} not active when change payment` }, 400);
2283
2217
  }
2284
- const paymentCurrency = await PaymentCurrency.findByPk(req.body.payment_currency);
2218
+
2219
+ const body = c.get('sanitizedBody') ?? {};
2220
+ const paymentCurrency = await PaymentCurrency.findByPk(body.payment_currency);
2285
2221
  if (!paymentCurrency) {
2286
- return res
2287
- .status(400)
2288
- .json({ error: `Payment currency ${req.body.payment_currency} not found when change payment` });
2222
+ return c.json({ error: `Payment currency ${body.payment_currency} not found when change payment` }, 400);
2289
2223
  }
2290
2224
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
2291
2225
  if (!paymentMethod) {
2292
- return res
2293
- .status(400)
2294
- .json({ error: `Payment method ${paymentCurrency.payment_method_id} not found when change payment` });
2226
+ return c.json(
2227
+ { error: `Payment method ${paymentCurrency.payment_method_id} not found when change payment` },
2228
+ 400
2229
+ );
2295
2230
  }
2296
2231
 
2297
2232
  const customer = await Customer.findByPk(subscription.customer_id);
2298
2233
  if (paymentMethod.type === 'stripe') {
2299
- await customer?.update({ address: Object.assign({}, customer.address, req.body.billing_address) });
2234
+ await customer?.update({ address: Object.assign({}, customer.address, body.billing_address) });
2300
2235
  }
2301
2236
 
2302
2237
  if (subscription.currency_id === paymentCurrency.id) {
2303
- return res.status(400).json({ error: 'Payment currency not changed when change payment' });
2238
+ return c.json({ error: 'Payment currency not changed when change payment' }, 400);
2304
2239
  }
2305
2240
 
2306
2241
  const previousPaymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
2307
2242
  if (previousPaymentMethod?.type === 'stripe') {
2308
2243
  if (!subscription.payment_details?.stripe?.subscription_id) {
2309
- return res.status(400).json({ error: 'Can not change from stripe without stripe subscription id' });
2244
+ return c.json({ error: 'Can not change from stripe without stripe subscription id' }, 400);
2310
2245
  }
2311
2246
  }
2312
2247
 
2313
- // ensure setupIntent
2314
2248
  const context = subscription.metadata.changePayment || {};
2315
2249
  let setupIntent: SetupIntent | null = null;
2316
2250
  if (context.setup_intent_id) {
2317
- // should be cleared after success
2318
2251
  setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
2319
2252
  }
2320
- // Reuse existing setupIntent if not succeeded
2321
2253
  if (setupIntent && setupIntent.status !== 'succeeded') {
2322
2254
  await setupIntent.update({
2323
2255
  status: 'requires_capture',
@@ -2352,7 +2284,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2352
2284
  },
2353
2285
  });
2354
2286
 
2355
- // persist setup intent id
2356
2287
  await subscription.update({
2357
2288
  metadata: { ...subscription.metadata, changePayment: { setup_intent_id: setupIntent.id } },
2358
2289
  });
@@ -2363,7 +2294,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2363
2294
  });
2364
2295
  }
2365
2296
 
2366
- // if we can complete purchase without any wallet interaction
2367
2297
  const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
2368
2298
  const lineItems = await Price.expand(
2369
2299
  subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity }))
@@ -2393,7 +2323,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2393
2323
  publishable_key: settings.stripe?.publishable_key,
2394
2324
  status: exist.status,
2395
2325
  };
2396
- return res.json({
2326
+ return c.json({
2397
2327
  setupIntent,
2398
2328
  stripeContext,
2399
2329
  subscription,
@@ -2434,7 +2364,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2434
2364
  } else {
2435
2365
  const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
2436
2366
 
2437
- // changing from crypto to stripe: create/resume stripe subscription, pause crypto subscription
2438
2367
  const stripeSubscription = await ensureStripeSubscription(
2439
2368
  subscription,
2440
2369
  paymentMethod,
@@ -2477,10 +2406,8 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2477
2406
  });
2478
2407
  }
2479
2408
  } else {
2480
- // changing from crypto to crypto: just update the subscription
2481
2409
  const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
2482
2410
 
2483
- // Calculate required amount considering slippage_config for dynamic pricing
2484
2411
  const slippageConfig = subscription?.slippage_config;
2485
2412
  let requiredAmount: string;
2486
2413
  if (slippageConfig?.min_acceptable_rate) {
@@ -2535,8 +2462,6 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2535
2462
  });
2536
2463
  }
2537
2464
 
2538
- // NOTE: this should only happen when local subscription is updated
2539
- // changing from stripe to crypto: pause stripe subscription
2540
2465
  if (previousPaymentMethod!.type === 'stripe' && setupIntent.status === 'succeeded') {
2541
2466
  const client = await previousPaymentMethod?.getStripeClient();
2542
2467
  const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
@@ -2553,25 +2478,25 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2553
2478
  }
2554
2479
  }
2555
2480
 
2556
- return res.json({
2481
+ return c.json({
2557
2482
  setupIntent,
2558
2483
  stripeContext,
2559
2484
  subscription,
2560
2485
  customer,
2561
2486
  delegation,
2562
2487
  });
2563
- } catch (err) {
2488
+ } catch (err: any) {
2564
2489
  logger.error(err);
2565
- return res.status(500).json({ code: err.code, error: err.message });
2490
+ return c.json({ code: err.code, error: err.message }, 500);
2566
2491
  }
2567
2492
  });
2493
+
2568
2494
  // FIXME: this should be removed in future
2569
2495
  // Clean up subscriptions that have invalid invoices and payments
2570
- router.delete('/cleanup', auth, async (req, res) => {
2571
- const status = String(req.query.status || 'uncollectible');
2496
+ app.delete('/cleanup', auth, async (c) => {
2497
+ const status = String(c.req.query('status') || 'uncollectible');
2572
2498
  if (['open', 'uncollectible'].includes(status) === false) {
2573
- res.json({ error: 'status must be either open or uncollectible' });
2574
- return;
2499
+ return c.json({ error: 'status must be either open or uncollectible' });
2575
2500
  }
2576
2501
 
2577
2502
  const subscriptions = await Subscription.findAll();
@@ -2609,19 +2534,19 @@ router.delete('/cleanup', auth, async (req, res) => {
2609
2534
  })
2610
2535
  );
2611
2536
 
2612
- res.json(canceled);
2537
+ return c.json(canceled);
2613
2538
  });
2614
2539
 
2615
2540
  // Delete subscription and all related data
2616
- router.delete('/:id', auth, async (req, res) => {
2617
- if (process.env.BLOCKLET_MODE === 'production') {
2618
- return res.status(404).json({ error: 'Subscription delete not allowed in production' });
2541
+ app.delete('/:id', auth, async (c) => {
2542
+ if (isProduction()) {
2543
+ return c.json({ error: 'Subscription delete not allowed in production' }, 404);
2619
2544
  }
2620
2545
 
2621
- const doc = await Subscription.findByPk(req.params.id);
2546
+ const doc = await Subscription.findByPk(c.req.param('id'));
2622
2547
 
2623
2548
  if (!doc) {
2624
- return res.status(404).json({ error: 'Subscription not found' });
2549
+ return c.json({ error: 'Subscription not found' }, 404);
2625
2550
  }
2626
2551
 
2627
2552
  await InvoiceItem.destroy({ where: { subscription_id: doc.id } });
@@ -2631,29 +2556,68 @@ router.delete('/:id', auth, async (req, res) => {
2631
2556
  await SubscriptionItem.destroy({ where: { subscription_id: doc.id } });
2632
2557
  await doc.destroy();
2633
2558
  logger.info('Subscription deleted successfully', {
2634
- subscriptionId: req.params.id,
2559
+ subscriptionId: c.req.param('id'),
2635
2560
  deletedRelatedRecords: {
2636
2561
  invoiceItems: await InvoiceItem.count({ where: { subscription_id: doc.id } }),
2637
2562
  invoices: await Invoice.count({ where: { subscription_id: doc.id } }),
2638
2563
  usageRecords: await UsageRecord.count({ where: { subscription_item_id: items.map((x) => x.id) } }),
2639
2564
  subscriptionItems: items.length,
2640
2565
  },
2641
- requestedBy: req.user?.did,
2566
+ requestedBy: c.get('user')?.did,
2642
2567
  });
2643
- return res.json(doc);
2568
+ return c.json(doc);
2644
2569
  });
2645
2570
 
2646
- // Get usage records
2647
- router.get('/:id/usage-records', authPortal, (req, res) => {
2648
- createUsageRecordQueryFn(req.doc)(req, res);
2571
+ // Get usage records — inline since usage-records.ts is not yet a hono module
2572
+ const UsageRecordScheme = Joi.object({
2573
+ subscription_item_id: Joi.string().required(),
2574
+ start: Joi.number().optional(),
2575
+ end: Joi.number().optional(),
2576
+ livemode: Joi.boolean().empty('').optional(),
2577
+ q: Joi.string().empty('').optional(),
2578
+ o: Joi.string().empty('').optional(),
2579
+ }).unknown(true);
2580
+
2581
+ app.get('/:id/usage-records', authPortal, async (c) => {
2582
+ const doc = c.get('doc') as Subscription | undefined;
2583
+ const { error, value: query } = UsageRecordScheme.validate(c.req.query(), { stripUnknown: true });
2584
+ if (error) {
2585
+ return c.json({ error: `usage record request query invalid: ${error.message}` }, 400);
2586
+ }
2587
+ try {
2588
+ const item = await SubscriptionItem.findByPk(query.subscription_item_id);
2589
+ if (!item) {
2590
+ return c.json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` }, 400);
2591
+ }
2592
+ const subscription = doc || (await Subscription.findByPk(item.subscription_id));
2593
+ if (!subscription) {
2594
+ return c.json({ error: `Subscription not found: ${item.subscription_id}` }, 400);
2595
+ }
2596
+
2597
+ const { rows: list, count } = await UsageRecord.findAndCountAll({
2598
+ where: {
2599
+ subscription_item_id: query.subscription_item_id,
2600
+ timestamp: {
2601
+ [Op.gt]: query.start || subscription.current_period_start,
2602
+ [Op.lte]: query.end || subscription.current_period_end,
2603
+ },
2604
+ },
2605
+ order: [['created_at', 'ASC']],
2606
+ });
2607
+
2608
+ return c.json({ count, list });
2609
+ } catch (err) {
2610
+ logger.error(err);
2611
+ return c.json({ count: 0, list: [] });
2612
+ }
2649
2613
  });
2650
2614
 
2651
2615
  // Get invoice summary
2652
- router.get('/:id/summary', authPortal, async (req, res) => {
2616
+ app.get('/:id/summary', authPortal, async (c) => {
2653
2617
  try {
2654
- const subscription = await Subscription.findByPk(req.params.id);
2618
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2655
2619
  if (!subscription) {
2656
- return res.status(404).json({ error: 'Subscription not found' });
2620
+ return c.json({ error: 'Subscription not found' }, 404);
2657
2621
  }
2658
2622
 
2659
2623
  const [summary] = await Invoice.getUncollectibleAmount({
@@ -2661,96 +2625,95 @@ router.get('/:id/summary', authPortal, async (req, res) => {
2661
2625
  currencyId: subscription.currency_id,
2662
2626
  customerId: subscription.customer_id,
2663
2627
  });
2664
- return res.json(summary);
2665
- } catch (err) {
2628
+ return c.json(summary);
2629
+ } catch (err: any) {
2666
2630
  logger.error(err);
2667
- return res.status(400).json({ error: err.message });
2631
+ return c.json({ error: err.message }, 400);
2668
2632
  }
2669
2633
  });
2670
2634
 
2671
2635
  // Get upcoming invoice amount
2672
- router.get('/:id/upcoming', authPortal, async (req, res) => {
2636
+ app.get('/:id/upcoming', authPortal, async (c) => {
2673
2637
  try {
2674
- const result = await getUpcomingInvoiceAmount(req.params.id as string);
2675
- return res.json(result);
2676
- } catch (err) {
2638
+ const result = await getUpcomingInvoiceAmount(c.req.param('id') as string);
2639
+ return c.json(result);
2640
+ } catch (err: any) {
2677
2641
  logger.error(err);
2678
- return res.json({ error: err.message });
2642
+ return c.json({ error: err.message });
2679
2643
  }
2680
2644
  });
2681
2645
 
2682
- router.get('/:id/cycle-amount', authPortal, async (req, res) => {
2683
- const subscription = await Subscription.findByPk(req.params.id);
2646
+ app.get('/:id/cycle-amount', authPortal, async (c) => {
2647
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2684
2648
  if (!subscription) {
2685
- return res.status(404).json({ error: 'Subscription not found' });
2649
+ return c.json({ error: 'Subscription not found' }, 404);
2686
2650
  }
2687
2651
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
2688
2652
  if (!currency) {
2689
- return res.status(404).json({ error: 'Currency not found' });
2653
+ return c.json({ error: 'Currency not found' }, 404);
2690
2654
  }
2691
2655
  try {
2692
- // get upcoming invoice
2693
2656
  const result = await getUpcomingInvoiceAmount(subscription.id);
2694
- // get past invoices
2695
2657
  const pastMaxAmount = await getPastInvoicesAmount(subscription.id, 'max');
2696
2658
 
2697
- // return max amount
2698
2659
  const nextAmount = new BN(result.amount === '0' ? result.minExpectedAmount : result.amount).toString();
2699
2660
 
2700
2661
  const maxAmount = new BN(pastMaxAmount.amount).lte(new BN(nextAmount)) ? nextAmount : pastMaxAmount.amount;
2701
2662
 
2702
- if (req.query?.overdraftProtection) {
2663
+ if (c.req.query('overdraftProtection')) {
2703
2664
  const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
2704
2665
  const invoicePrice = (price?.currency_options || []).find(
2705
2666
  (x: any) => x.currency_id === subscription?.currency_id
2706
2667
  );
2707
2668
  const gas = invoicePrice?.unit_amount;
2708
- return res.json({
2669
+ return c.json({
2709
2670
  amount: new BN(maxAmount).add(new BN(gas)).toString(),
2710
2671
  gas,
2711
2672
  currency,
2712
2673
  });
2713
2674
  }
2714
- return res.json({
2675
+ return c.json({
2715
2676
  amount: maxAmount,
2716
2677
  currency,
2717
2678
  });
2718
- } catch (err) {
2679
+ } catch (err: any) {
2719
2680
  logger.error(err);
2720
- return res.status(400).json({ error: err.message });
2681
+ return c.json({ error: err.message }, 400);
2721
2682
  }
2722
2683
  });
2723
2684
 
2724
2685
  // slash stake
2725
- router.put('/:id/slash-stake', auth, async (req, res) => {
2726
- const { error: slashReasonError } = SlashStakeSchema.validate(req.body?.slashReason);
2686
+ app.put('/:id/slash-stake', auth, async (c) => {
2687
+ const body = c.get('sanitizedBody') ?? {};
2688
+ const { error: slashReasonError } = SlashStakeSchema.validate(body?.slashReason);
2727
2689
  if (slashReasonError) {
2728
- return res.status(400).json({ error: `slash reason invalid: ${slashReasonError.message}` });
2690
+ return c.json({ error: `slash reason invalid: ${slashReasonError.message}` }, 400);
2729
2691
  }
2730
- const subscription = await Subscription.findByPk(req.params.id);
2692
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2731
2693
  if (!subscription) {
2732
- return res.status(404).json({ error: 'Subscription not found' });
2694
+ return c.json({ error: 'Subscription not found' }, 404);
2733
2695
  }
2734
2696
 
2735
2697
  if (subscription.status !== 'canceled') {
2736
- return res.status(400).json({ error: `Subscription for ${subscription.id} not canceled` });
2698
+ return c.json({ error: `Subscription for ${subscription.id} not canceled` }, 400);
2737
2699
  }
2738
2700
 
2739
2701
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
2740
2702
  if (paymentMethod?.type !== 'arcblock') {
2741
- return res
2742
- .status(400)
2743
- .json({ error: `Stake slash not supported for subscription with payment method ${paymentMethod?.type}` });
2703
+ return c.json(
2704
+ { error: `Stake slash not supported for subscription with payment method ${paymentMethod?.type}` },
2705
+ 400
2706
+ );
2744
2707
  }
2745
2708
  const address = subscription?.payment_details?.arcblock?.staking?.address ?? undefined;
2746
2709
  if (!address) {
2747
- return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
2710
+ return c.json({ error: 'Staking not found on subscription payment detail' }, 400);
2748
2711
  }
2749
2712
  try {
2750
2713
  logger.warn('Stake slash initiated', {
2751
2714
  subscriptionId: subscription.id,
2752
- slashReason: req.body.slashReason,
2753
- requestedBy: req.user?.did,
2715
+ slashReason: body.slashReason,
2716
+ requestedBy: c.get('user')?.did,
2754
2717
  });
2755
2718
 
2756
2719
  await subscription.update({
@@ -2758,7 +2721,7 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
2758
2721
  cancelation_details: {
2759
2722
  ...subscription.cancelation_details,
2760
2723
  slash_stake: true,
2761
- slash_reason: req.body.slashReason,
2724
+ slash_reason: body.slashReason,
2762
2725
  },
2763
2726
  });
2764
2727
  const result = await slashStakeQueue.pushAndWait({
@@ -2768,41 +2731,41 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
2768
2731
  logger.info('Stake slash scheduled successfully', {
2769
2732
  subscriptionId: subscription.id,
2770
2733
  result,
2771
- slashReason: req.body.slashReason,
2772
- requestedBy: req.user?.did,
2734
+ slashReason: body.slashReason,
2735
+ requestedBy: c.get('user')?.did,
2773
2736
  stakingAddress: address,
2774
2737
  });
2775
- return res.json(result);
2776
- } catch (err) {
2738
+ return c.json(result);
2739
+ } catch (err: any) {
2777
2740
  logger.error('subscription slash stake failed', { subscription: subscription.id, error: err });
2778
- return res.status(400).json({ error: err.message });
2741
+ return c.json({ error: err.message }, 400);
2779
2742
  }
2780
2743
  });
2781
2744
 
2782
2745
  // get payer token
2783
- router.get('/:id/payer-token', authMine, async (req, res) => {
2784
- const subscription = await Subscription.findByPk(req.params.id);
2746
+ app.get('/:id/payer-token', authMine, async (c) => {
2747
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2785
2748
  if (!subscription) {
2786
- return res.status(400).json({ error: `Subscription(${req.params.id}) not found` });
2749
+ return c.json({ error: `Subscription(${c.req.param('id')}) not found` }, 400);
2787
2750
  }
2788
2751
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
2789
2752
  if (!paymentMethod) {
2790
- return res.status(400).json({ error: `Payment method(${subscription.default_payment_method_id}) not found` });
2753
+ return c.json({ error: `Payment method(${subscription.default_payment_method_id}) not found` }, 400);
2791
2754
  }
2792
2755
 
2793
2756
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
2794
2757
  if (!paymentCurrency) {
2795
- return res.status(400).json({ error: `Payment currency(${subscription.currency_id}) not found` });
2758
+ return c.json({ error: `Payment currency(${subscription.currency_id}) not found` }, 400);
2796
2759
  }
2797
2760
 
2798
2761
  // @ts-ignore
2799
2762
  const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
2800
2763
  if (!paymentAddress && CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
2801
- return res.status(400).json({ error: `Payer not found on subscription payment detail: ${subscription.id}` });
2764
+ return c.json({ error: `Payer not found on subscription payment detail: ${subscription.id}` }, 400);
2802
2765
  }
2803
2766
 
2804
2767
  const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
2805
- return res.json({ token, paymentAddress });
2768
+ return c.json({ token, paymentAddress });
2806
2769
  });
2807
2770
 
2808
2771
  const rechargeSchema = createListParamSchema<{
@@ -2814,12 +2777,13 @@ const rechargeSchema = createListParamSchema<{
2814
2777
  customer_did: Joi.string().empty(''),
2815
2778
  currency_id: Joi.string().empty(''),
2816
2779
  });
2817
- router.get('/:id/recharge', authMine, async (req, res) => {
2818
- const subscription = await Subscription.findByPk(req.params.id);
2780
+
2781
+ app.get('/:id/recharge', authMine, async (c) => {
2782
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2819
2783
  if (!subscription) {
2820
- return res.status(404).json({ error: `Subscription(${req.params.id}) not found` });
2784
+ return c.json({ error: `Subscription(${c.req.param('id')}) not found` }, 404);
2821
2785
  }
2822
- const { page, pageSize, ...query } = await rechargeSchema.validateAsync(req.query, {
2786
+ const { page, pageSize, ...query } = await rechargeSchema.validateAsync(c.req.query(), {
2823
2787
  stripUnknown: false,
2824
2788
  allowUnknown: true,
2825
2789
  });
@@ -2835,31 +2799,32 @@ router.get('/:id/recharge', authMine, async (req, res) => {
2835
2799
  },
2836
2800
  offset: (page - 1) * pageSize,
2837
2801
  limit: pageSize,
2838
- order: getOrder(req.query, [['created_at', 'DESC']]),
2802
+ order: getOrder(c.req.query(), [['created_at', 'DESC']]),
2839
2803
  include: [
2840
2804
  { model: PaymentCurrency, as: 'paymentCurrency' },
2841
2805
  { model: PaymentMethod, as: 'paymentMethod' },
2842
2806
  ],
2843
2807
  });
2844
2808
 
2845
- return res.json({ count, list: invoices, subscription, paging: { page, pageSize } });
2846
- } catch (err) {
2809
+ return c.json({ count, list: invoices, subscription, paging: { page, pageSize } });
2810
+ } catch (err: any) {
2847
2811
  logger.error(err);
2848
- return res.status(400).json({ error: err.message });
2812
+ return c.json({ error: err.message }, 400);
2849
2813
  }
2850
2814
  });
2851
2815
 
2852
- router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
2816
+ app.get('/:id/overdue/invoices', authPortal, async (c) => {
2853
2817
  try {
2854
- const subscription = await Subscription.findByPk(req.params.id, {
2818
+ const subscription = await Subscription.findByPk(c.req.param('id'), {
2855
2819
  include: [{ model: Customer, as: 'customer' }],
2856
2820
  });
2857
2821
  if (!subscription) {
2858
- return res.status(404).json({ error: 'Subscription not found' });
2822
+ return c.json({ error: 'Subscription not found' }, 404);
2859
2823
  }
2824
+ const user = c.get('user');
2860
2825
  // @ts-ignore
2861
- if (subscription.customer?.did !== req.user?.did && !['admin', 'owner'].includes(req.user?.role)) {
2862
- return res.status(403).json({ error: 'You are not allowed to access this subscription' });
2826
+ if (subscription.customer?.did !== user?.did && !['admin', 'owner'].includes(user?.role)) {
2827
+ return c.json({ error: 'You are not allowed to access this subscription' }, 403);
2863
2828
  }
2864
2829
  const { rows: invoices, count } = await Invoice.findAndCountAll({
2865
2830
  where: {
@@ -2873,7 +2838,7 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
2873
2838
  ],
2874
2839
  });
2875
2840
  if (count === 0) {
2876
- return res.json({ subscription, invoices: [], summary: null });
2841
+ return c.json({ subscription, invoices: [], summary: null });
2877
2842
  }
2878
2843
  const summary: Record<string, { amount: string; currency: PaymentCurrency; method: PaymentMethod }> = {};
2879
2844
  invoices.forEach((invoice) => {
@@ -2894,27 +2859,27 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
2894
2859
  .toString();
2895
2860
  }
2896
2861
  });
2897
- return res.json({
2862
+ return c.json({
2898
2863
  subscription,
2899
2864
  summary,
2900
2865
  invoices,
2901
2866
  });
2902
- } catch (err) {
2867
+ } catch (err: any) {
2903
2868
  logger.error(err);
2904
- return res.status(400).json({ error: err.message });
2869
+ return c.json({ error: err.message }, 400);
2905
2870
  }
2906
2871
  });
2907
2872
 
2908
- router.get('/:id/delegation', authPortal, async (req, res) => {
2873
+ app.get('/:id/delegation', authPortal, async (c) => {
2909
2874
  try {
2910
- const subscription = (await Subscription.findByPk(req.params.id, {
2875
+ const subscription = (await Subscription.findByPk(c.req.param('id'), {
2911
2876
  include: [
2912
2877
  { model: PaymentCurrency, as: 'paymentCurrency' },
2913
2878
  { model: PaymentMethod, as: 'paymentMethod' },
2914
2879
  ],
2915
2880
  })) as (Subscription & { paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency }) | null;
2916
2881
  if (!subscription) {
2917
- return res.status(404).json({ error: 'Subscription not found' });
2882
+ return c.json({ error: 'Subscription not found' }, 404);
2918
2883
  }
2919
2884
  const payer = getSubscriptionPaymentAddress(subscription, subscription.paymentMethod?.type);
2920
2885
  const delegator = await isDelegationSufficientForPayment({
@@ -2935,28 +2900,28 @@ router.get('/:id/delegation', authPortal, async (req, res) => {
2935
2900
  'NO_ENOUGH_TOKEN',
2936
2901
  ].includes(delegator?.reason || '')
2937
2902
  ) {
2938
- return res.json(delegator);
2903
+ return c.json(delegator);
2939
2904
  }
2940
- return res.json(null);
2905
+ return c.json(null);
2941
2906
  } catch (err) {
2942
2907
  logger.error(err);
2943
- return res.json(null);
2908
+ return c.json(null);
2944
2909
  }
2945
2910
  });
2946
2911
 
2947
- router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
2948
- const subscription = await Subscription.findByPk(req.params.id);
2912
+ app.get('/:id/overdraft-protection', authPortal, async (c) => {
2913
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2949
2914
  if (!subscription) {
2950
- return res.status(404).json({ error: 'Subscription not found' });
2915
+ return c.json({ error: 'Subscription not found' }, 404);
2951
2916
  }
2952
2917
  try {
2953
2918
  const { enabled, remaining, unused, used, shouldPay } =
2954
2919
  await isSubscriptionOverdraftProtectionEnabled(subscription);
2955
- const upcoming = await getUpcomingInvoiceAmount(req.params.id as string);
2920
+ const upcoming = await getUpcomingInvoiceAmount(c.req.param('id') as string);
2956
2921
  const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
2957
2922
  const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
2958
2923
  const gas = invoicePrice?.unit_amount;
2959
- return res.json({
2924
+ return c.json({
2960
2925
  enabled,
2961
2926
  remaining,
2962
2927
  unused,
@@ -2965,9 +2930,9 @@ router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
2965
2930
  gas,
2966
2931
  shouldPay,
2967
2932
  });
2968
- } catch (err) {
2933
+ } catch (err: any) {
2969
2934
  logger.error(err);
2970
- return res.status(400).json({ error: err.message });
2935
+ return c.json({ error: err.message }, 400);
2971
2936
  }
2972
2937
  });
2973
2938
 
@@ -2976,36 +2941,37 @@ const overdraftProtectionSchema = Joi.object({
2976
2941
  enabled: Joi.boolean().required(),
2977
2942
  return_stake: Joi.boolean().empty(false).optional(),
2978
2943
  }).unknown(true);
2944
+
2979
2945
  // 订阅保护
2980
- router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
2946
+ app.post('/:id/overdraft-protection', authPortal, async (c) => {
2981
2947
  try {
2948
+ const body = c.get('sanitizedBody') ?? {};
2982
2949
  const {
2983
2950
  error: overdraftProtectionError,
2984
2951
  value: { amount, return_stake: returnStake, enabled },
2985
- } = overdraftProtectionSchema.validate(req.body);
2952
+ } = overdraftProtectionSchema.validate(body);
2986
2953
  if (overdraftProtectionError) {
2987
- return res.status(400).json({ error: `Overdraft protection invalid: ${overdraftProtectionError.message}` });
2954
+ return c.json({ error: `Overdraft protection invalid: ${overdraftProtectionError.message}` }, 400);
2988
2955
  }
2989
2956
 
2990
- const subscription = await Subscription.findByPk(req.params.id);
2957
+ const subscription = await Subscription.findByPk(c.req.param('id'));
2991
2958
  if (!subscription) {
2992
- return res.status(404).json({ error: 'Subscription not found' });
2959
+ return c.json({ error: 'Subscription not found' }, 404);
2993
2960
  }
2994
2961
  const previousOverdraftProtection = {
2995
2962
  enabled: subscription.overdraft_protection?.enabled || false,
2996
2963
  payment_method_id: subscription.overdraft_protection?.payment_method_id || null,
2997
2964
  payment_details: subscription.overdraft_protection?.payment_details || null,
2998
2965
  };
2999
- const customer = await Customer.findByPkOrDid(req.user?.did as string);
2966
+ const customer = await Customer.findByPkOrDid(c.get('user')?.did as string);
3000
2967
  if (!customer) {
3001
- return res.status(404).json({ error: 'Customer not found' });
2968
+ return c.json({ error: 'Customer not found' }, 404);
3002
2969
  }
3003
2970
  const { remaining, used, unused } = await isSubscriptionOverdraftProtectionEnabled(subscription);
3004
2971
  if (unused === '0' && !amount && enabled) {
3005
- return res.status(400).json({ error: 'Please add stake to enable SubGuard™' });
2972
+ return c.json({ error: 'Please add stake to enable SubGuard™' }, 400);
3006
2973
  }
3007
2974
  if (returnStake && remaining !== '0' && !enabled) {
3008
- // disable overdraft protection
3009
2975
  await subscription.update({
3010
2976
  // @ts-ignore
3011
2977
  overdraft_protection: {
@@ -3019,22 +2985,21 @@ router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
3019
2985
  });
3020
2986
  logger.info('Return overdraft protection stake scheduled', {
3021
2987
  subscriptionId: subscription.id,
3022
- requestedBy: req.user?.did,
2988
+ requestedBy: c.get('user')?.did,
3023
2989
  });
3024
- return res.json({
2990
+ return c.json({
3025
2991
  open: false,
3026
2992
  overdraft_protection: subscription.overdraft_protection,
3027
2993
  });
3028
2994
  }
3029
2995
  if (remaining !== '0' && used !== '0' && !enabled) {
3030
- // slash stake
3031
2996
  slashOverdraftProtectionQueue.push({
3032
2997
  id: `slash-overdraft-protection-${subscription.id}`,
3033
2998
  job: { subscriptionId: subscription.id },
3034
2999
  });
3035
3000
  logger.info('Slash overdraft protection stake scheduled', {
3036
3001
  subscriptionId: subscription.id,
3037
- requestedBy: req.user?.did,
3002
+ requestedBy: c.get('user')?.did,
3038
3003
  });
3039
3004
  }
3040
3005
  await subscription.update({
@@ -3045,47 +3010,46 @@ router.post('/:id/overdraft-protection', authPortal, async (req, res) => {
3045
3010
  },
3046
3011
  });
3047
3012
  if (enabled && Number(amount) > 0) {
3048
- return res.json({
3013
+ return c.json({
3049
3014
  open: true,
3050
3015
  amount,
3051
3016
  overdraft_protection: subscription.overdraft_protection,
3052
3017
  });
3053
3018
  }
3054
3019
  if (enabled) {
3055
- // release the exhausted lock, so that the notification can be sent again if overdraft protection exhausted
3056
3020
  await Lock.release(`${subscription.id}-${subscription.currency_id}-overdraft-protection-exhausted`);
3057
3021
  }
3058
- return res.json({
3022
+ return c.json({
3059
3023
  open: false,
3060
3024
  overdraft_protection: subscription.overdraft_protection,
3061
3025
  });
3062
- } catch (err) {
3026
+ } catch (err: any) {
3063
3027
  logger.error(err);
3064
- return res.status(400).json({ error: err.message });
3028
+ return c.json({ error: err.message }, 400);
3065
3029
  }
3066
3030
  });
3067
3031
 
3068
- router.get('/:id/unpaid-invoices', authPortal, async (req, res) => {
3069
- const subscription = await Subscription.findByPk(req.params.id);
3032
+ app.get('/:id/unpaid-invoices', authPortal, async (c) => {
3033
+ const subscription = await Subscription.findByPk(c.req.param('id'));
3070
3034
  if (!subscription) {
3071
- return res.status(404).json({ error: 'Subscription not found' });
3035
+ return c.json({ error: 'Subscription not found' }, 404);
3072
3036
  }
3073
3037
  const count = await getSubscriptionUnpaidInvoicesCount(subscription);
3074
- return res.json({ count });
3038
+ return c.json({ count });
3075
3039
  });
3076
3040
 
3077
- router.get('/:id/change-payment/migrate-invoice', auth, async (req, res) => {
3078
- const subscription = await Subscription.findByPk(req.params.id);
3041
+ app.get('/:id/change-payment/migrate-invoice', auth, async (c) => {
3042
+ const subscription = await Subscription.findByPk(c.req.param('id'));
3079
3043
  if (!subscription) {
3080
- return res.status(404).json({ error: 'Subscription not found' });
3044
+ return c.json({ error: 'Subscription not found' }, 404);
3081
3045
  }
3082
3046
  const context = subscription.metadata.changePayment || {};
3083
3047
  if (!context.setup_intent_id) {
3084
- return res.status(404).json({ error: 'Subscription change payment context not found' });
3048
+ return c.json({ error: 'Subscription change payment context not found' }, 404);
3085
3049
  }
3086
3050
  const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
3087
3051
  if (!setupIntent) {
3088
- return res.status(404).json({ error: 'Setup intent not found' });
3052
+ return c.json({ error: 'Setup intent not found' }, 404);
3089
3053
  }
3090
3054
  try {
3091
3055
  const migrationResult = await migrateSubscriptionPaymentMethodInvoice(
@@ -3093,37 +3057,37 @@ router.get('/:id/change-payment/migrate-invoice', auth, async (req, res) => {
3093
3057
  setupIntent.metadata?.from_currency,
3094
3058
  setupIntent.metadata?.to_currency
3095
3059
  );
3096
- return res.json(migrationResult);
3097
- } catch (error) {
3060
+ return c.json(migrationResult);
3061
+ } catch (error: any) {
3098
3062
  logger.error(error);
3099
- return res.status(400).json({ error: error.message });
3063
+ return c.json({ error: error.message }, 400);
3100
3064
  }
3101
3065
  });
3102
3066
 
3103
- router.post('/:id/update-stripe-payment-method', authPortal, async (req, res) => {
3067
+ app.post('/:id/update-stripe-payment-method', authPortal, async (c) => {
3104
3068
  try {
3105
- const subscription = await Subscription.findByPk(req.params.id);
3069
+ const subscription = await Subscription.findByPk(c.req.param('id'));
3106
3070
  if (!subscription) {
3107
- return res.status(404).json({ error: 'Subscription not found' });
3071
+ return c.json({ error: 'Subscription not found' }, 404);
3108
3072
  }
3109
3073
 
3110
3074
  if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
3111
- return res.status(400).json({ error: 'Subscription is not active' });
3075
+ return c.json({ error: 'Subscription is not active' }, 400);
3112
3076
  }
3113
3077
 
3114
3078
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
3115
3079
  if (!paymentMethod || paymentMethod.type !== 'stripe') {
3116
- return res.status(400).json({ error: 'Subscription is not using Stripe payment method' });
3080
+ return c.json({ error: 'Subscription is not using Stripe payment method' }, 400);
3117
3081
  }
3118
3082
 
3119
3083
  const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
3120
3084
  if (!stripeSubscriptionId) {
3121
- return res.status(400).json({ error: 'Stripe subscription not found' });
3085
+ return c.json({ error: 'Stripe subscription not found' }, 400);
3122
3086
  }
3123
3087
 
3124
3088
  const customer = await Customer.findByPk(subscription.customer_id);
3125
3089
  if (!customer) {
3126
- return res.status(404).json({ error: 'Customer not found' });
3090
+ return c.json({ error: 'Customer not found' }, 404);
3127
3091
  }
3128
3092
 
3129
3093
  await ensureStripeCustomer(customer, paymentMethod);
@@ -3146,63 +3110,51 @@ router.post('/:id/update-stripe-payment-method', authPortal, async (req, res) =>
3146
3110
  setupIntent: setupIntent.id,
3147
3111
  });
3148
3112
 
3149
- return res.json({
3113
+ return c.json({
3150
3114
  client_secret: setupIntent.client_secret,
3151
3115
  publishable_key: settings.stripe?.publishable_key,
3152
3116
  setup_intent_id: setupIntent.id,
3153
3117
  });
3154
- } catch (err) {
3118
+ } catch (err: any) {
3155
3119
  logger.error('Failed to create setup intent for updating payment method', {
3156
3120
  error: err,
3157
- subscriptionId: req.params.id,
3121
+ subscriptionId: c.req.param('id'),
3158
3122
  });
3159
- return res.status(400).json({ error: err.message });
3123
+ return c.json({ error: err.message }, 400);
3160
3124
  }
3161
3125
  });
3162
3126
 
3163
3127
  /**
3164
3128
  * Fix subscription after payment method migration from Stripe to non-Stripe
3165
- * This API is used to fix legacy subscriptions that were migrated but the migration
3166
- * was incomplete (missing payment_settings/payment_details updates)
3167
- *
3168
- * This API will:
3169
- * 1. Pause the Stripe subscription (if not already paused)
3170
- * 2. Update payment_settings to use arcblock
3171
- * 3. Update payment_details to add arcblock info
3172
- * 4. Recalculate cancel_at if needed
3173
3129
  */
3174
- router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3130
+ app.put('/:id/fix-stripe-migration', auth, async (c) => {
3175
3131
  try {
3176
- const subscription = await Subscription.findByPk(req.params.id);
3132
+ const subscription = await Subscription.findByPk(c.req.param('id'));
3177
3133
  if (!subscription) {
3178
- return res.status(404).json({ error: 'Subscription not found' });
3134
+ return c.json({ error: 'Subscription not found' }, 404);
3179
3135
  }
3180
3136
 
3181
3137
  const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
3182
3138
  if (!stripeSubscriptionId) {
3183
- return res.status(400).json({ error: 'Subscription does not have Stripe subscription_id' });
3139
+ return c.json({ error: 'Subscription does not have Stripe subscription_id' }, 400);
3184
3140
  }
3185
3141
 
3186
- // Get customer for payer DID
3187
3142
  const customer = await Customer.findByPk(subscription.customer_id);
3188
3143
  if (!customer) {
3189
- return res.status(404).json({ error: 'Customer not found' });
3144
+ return c.json({ error: 'Customer not found' }, 404);
3190
3145
  }
3191
3146
 
3192
- // Find arcblock payment method
3193
3147
  const arcblockMethod = await PaymentMethod.findOne({
3194
3148
  where: { type: 'arcblock', livemode: subscription.livemode },
3195
3149
  });
3196
3150
  if (!arcblockMethod) {
3197
- return res.status(400).json({ error: 'ArcBlock payment method not found' });
3151
+ return c.json({ error: 'ArcBlock payment method not found' }, 400);
3198
3152
  }
3199
3153
 
3200
- // Find Stripe payment method to pause subscription
3201
3154
  const stripeMethod = await PaymentMethod.findOne({
3202
3155
  where: { type: 'stripe', livemode: subscription.livemode },
3203
3156
  });
3204
3157
 
3205
- // 1. Pause Stripe subscription if not already paused
3206
3158
  let stripePaused = false;
3207
3159
  if (stripeMethod) {
3208
3160
  try {
@@ -3232,9 +3184,9 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3232
3184
  }
3233
3185
  }
3234
3186
 
3187
+ const body = c.get('sanitizedBody') ?? {};
3235
3188
  const updates: Partial<TSubscription> = {};
3236
3189
 
3237
- // 2. Update payment_settings to use arcblock (matching change-payment behavior)
3238
3190
  updates.payment_settings = {
3239
3191
  payment_method_types: ['arcblock'],
3240
3192
  payment_method_options: {
@@ -3242,7 +3194,6 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3242
3194
  },
3243
3195
  };
3244
3196
 
3245
- // 3. Update payment_details.arcblock.payer if not already present
3246
3197
  if (!subscription.payment_details?.arcblock?.payer) {
3247
3198
  const existingArcblock = subscription.payment_details?.arcblock;
3248
3199
  updates.payment_details = {
@@ -3256,15 +3207,13 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3256
3207
  };
3257
3208
  }
3258
3209
 
3259
- // 4. Update default_payment_method_id to arcblock
3260
3210
  updates.default_payment_method_id = arcblockMethod.id;
3261
3211
 
3262
- // 5. Recalculate cancel_at if subscription has cancelation_details but no cancel_at
3263
- if (req.body.recalculate_cancel_at !== false) {
3212
+ if (body.recalculate_cancel_at !== false) {
3264
3213
  if (subscription.cancelation_details && !subscription.cancel_at) {
3265
3214
  const daysUntilCancel = subscription.days_until_cancel || 0;
3266
3215
  if (daysUntilCancel > 0) {
3267
- const dueUnit = 24 * 60 * 60; // 1 day in seconds
3216
+ const dueUnit = 24 * 60 * 60;
3268
3217
  updates.cancel_at = subscription.current_period_start + daysUntilCancel * dueUnit;
3269
3218
  } else {
3270
3219
  updates.cancel_at_period_end = true;
@@ -3273,9 +3222,8 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3273
3222
  }
3274
3223
  }
3275
3224
 
3276
- // Allow manual override of cancel_at
3277
- if (typeof req.body.cancel_at === 'number') {
3278
- updates.cancel_at = req.body.cancel_at;
3225
+ if (typeof body.cancel_at === 'number') {
3226
+ updates.cancel_at = body.cancel_at;
3279
3227
  }
3280
3228
 
3281
3229
  await subscription.update(updates);
@@ -3291,10 +3239,9 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3291
3239
  stripeSubscriptionId,
3292
3240
  });
3293
3241
 
3294
- // Reload subscription to return updated data
3295
3242
  await subscription.reload();
3296
3243
 
3297
- return res.json({
3244
+ return c.json({
3298
3245
  success: true,
3299
3246
  stripePaused,
3300
3247
  subscription: pick(subscription, [
@@ -3312,13 +3259,13 @@ router.put('/:id/fix-stripe-migration', auth, async (req, res) => {
3312
3259
  'current_period_end',
3313
3260
  ]),
3314
3261
  });
3315
- } catch (err) {
3262
+ } catch (err: any) {
3316
3263
  logger.error('Failed to fix subscription stripe migration', {
3317
3264
  error: err,
3318
- subscriptionId: req.params.id,
3265
+ subscriptionId: c.req.param('id'),
3319
3266
  });
3320
- return res.status(400).json({ error: err.message });
3267
+ return c.json({ error: err.message }, 400);
3321
3268
  }
3322
3269
  });
3323
3270
 
3324
- export default router;
3271
+ export default app;