payment-kit 1.29.0 → 1.29.2

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 (312) 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 +36 -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 +27 -24
  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/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  236. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  237. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  238. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  239. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  240. package/cloudflare/queue-runtime-mode.ts +13 -0
  241. package/cloudflare/run-build.js +31 -56
  242. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  243. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  244. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  245. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  246. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  247. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  248. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  249. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  250. package/cloudflare/shims/cron.ts +38 -158
  251. package/cloudflare/shims/events.ts +124 -0
  252. package/cloudflare/shims/fastq.ts +15 -1
  253. package/cloudflare/shims/nedb-storage.ts +16 -8
  254. package/cloudflare/shims/node-fetch.ts +35 -0
  255. package/cloudflare/shims/xss.ts +8 -0
  256. package/cloudflare/tenant-middleware.ts +36 -0
  257. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  258. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  259. package/cloudflare/worker.ts +204 -433
  260. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  261. package/jest.config.js +3 -1
  262. package/package.json +33 -38
  263. package/scripts/core-env-whitelist.json +1 -0
  264. package/scripts/e2e-12b-runtime.ts +149 -0
  265. package/scripts/e2e-core-config.ts +125 -0
  266. package/scripts/e2e-d1-tenancy.ts +116 -0
  267. package/scripts/e2e-d2-cron-queue.ts +139 -0
  268. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  269. package/scripts/e2e-hono-s2.ts +125 -0
  270. package/scripts/e2e-hono-s3e.ts +135 -0
  271. package/scripts/e2e-hono-s4.ts +114 -0
  272. package/scripts/e2e-migration-contract.ts +100 -0
  273. package/scripts/e2e-s0.ts +61 -0
  274. package/scripts/e2e-s1.ts +107 -0
  275. package/scripts/e2e-s2.ts +178 -0
  276. package/scripts/e2e-s3.ts +110 -0
  277. package/scripts/e2e-s4.ts +191 -0
  278. package/scripts/e2e-s5.ts +139 -0
  279. package/scripts/e2e-s6.ts +127 -0
  280. package/scripts/e2e-tenant-model.ts +119 -0
  281. package/scripts/e2e-tenant-worker.ts +199 -0
  282. package/scripts/gen-sql-migrations.js +46 -0
  283. package/scripts/phase8-codemod.js +219 -0
  284. package/scripts/phase9a-env-getters-codemod.js +82 -0
  285. package/scripts/scan-core-env.js +109 -0
  286. package/scripts/scan-tenant-queries.js +235 -0
  287. package/scripts/schema-drift-guard.ts +210 -0
  288. package/scripts/tenant-scan-whitelist.json +1 -0
  289. package/src/env.d.ts +13 -1
  290. package/tsconfig.json +1 -1
  291. package/api/src/libs/did-space.ts +0 -235
  292. package/api/src/libs/middleware.ts +0 -50
  293. package/api/src/libs/security.ts +0 -192
  294. package/api/src/queues/space.ts +0 -662
  295. package/api/src/routes/credit-tokens.ts +0 -38
  296. package/api/src/routes/exchange-rates.ts +0 -87
  297. package/api/src/routes/index.ts +0 -142
  298. package/api/src/routes/integrations/stripe.ts +0 -61
  299. package/api/src/routes/meters.ts +0 -274
  300. package/api/src/routes/passports.ts +0 -68
  301. package/api/src/routes/redirect.ts +0 -20
  302. package/api/src/routes/tool.ts +0 -65
  303. package/api/src/routes/webhook-endpoints.ts +0 -126
  304. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  305. package/cloudflare/shims/did-space-js.ts +0 -17
  306. package/cloudflare/shims/did-space.ts +0 -11
  307. package/cloudflare/shims/express-compat/index.ts +0 -80
  308. package/cloudflare/shims/express-compat/types.ts +0 -41
  309. package/cloudflare/shims/lock.ts +0 -115
  310. package/cloudflare/shims/queue.ts +0 -611
  311. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  312. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,9 +1,11 @@
1
+ // Phase 3 (express→hono) — hono fork of routes/checkout-sessions.ts. Sub-app with
2
+ // routes relative to /api/checkout-sessions (mounted via mountResourceGroup). The
3
+ // business logic is unchanged; only the express plumbing becomes hono:
4
+ // req.body → c.get('sanitizedBody') ?? {}; res.status(n).json(x) → c.json(x, n).
1
5
  /* eslint-disable consistent-return */
2
6
  import { isValid } from '@arcblock/did';
3
7
  import { getUrl } from '@blocklet/sdk/lib/component';
4
- import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
5
8
  import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
6
- import { NextFunction, Request, Response, Router } from 'express';
7
9
  import Joi from 'joi';
8
10
  import cloneDeep from 'lodash/cloneDeep';
9
11
  import merge from 'lodash/merge';
@@ -13,22 +15,25 @@ import sortBy from 'lodash/sortBy';
13
15
  import uniq from 'lodash/uniq';
14
16
  import type { WhereOptions } from 'sequelize';
15
17
  import { Op } from 'sequelize';
18
+ import type { MiddlewareHandler } from 'hono';
19
+ import { Hono } from 'hono';
16
20
 
17
21
  import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
18
22
  import pAll from 'p-all';
19
23
  import { withQuery } from 'ufo';
20
- import { MetadataSchema } from '../libs/api';
21
- import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
22
- import dayjs from '../libs/dayjs';
23
- import logger from '../libs/logger';
24
- import { trimDecimals } from '../libs/math-utils';
25
- import { authenticate } from '../libs/security';
24
+ import { paymentRateVolatilityThreshold, updateDataConcurrency, stopAcceptingOrders } from '../../libs/env';
25
+ import { MetadataSchema } from '../../libs/api';
26
+ import { checkPassportForPaymentLink } from '../../integrations/blocklet/passport';
27
+ import dayjs from '../../libs/dayjs';
28
+ import logger from '../../libs/logger';
29
+ import { trimDecimals } from '../../libs/math-utils';
30
+ import { authenticate } from '../../middlewares/hono/security';
26
31
  import {
27
32
  buildSlippageSnapshot,
28
33
  DEFAULT_SLIPPAGE_PERCENT,
29
34
  isRateBelowMinAcceptableRate,
30
35
  normalizeSlippageConfigFromMetadata,
31
- } from '../libs/slippage';
36
+ } from '../../libs/slippage';
32
37
  import {
33
38
  canPayWithDelegation,
34
39
  canUpsell,
@@ -54,8 +59,8 @@ import {
54
59
  processCheckoutSessionDiscounts,
55
60
  getCheckoutSessionAmounts,
56
61
  enrichCheckoutSessionWithQuotes,
57
- } from '../libs/session';
58
- import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../libs/subscription';
62
+ } from '../../libs/session';
63
+ import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../../libs/subscription';
59
64
  import {
60
65
  CHECKOUT_SESSION_TTL,
61
66
  formatAmountPrecisionLimit,
@@ -65,7 +70,7 @@ import {
65
70
  getUserOrAppInfo,
66
71
  hasObjectChanged,
67
72
  isUserInBlocklist,
68
- } from '../libs/util';
73
+ } from '../../libs/util';
69
74
  import {
70
75
  PaymentBeneficiary,
71
76
  SetupIntent,
@@ -80,54 +85,54 @@ import {
80
85
  Coupon,
81
86
  PromotionCode,
82
87
  Discount,
83
- } from '../store/models';
84
- import type { ChainType } from '../store/models/types';
85
- import { CheckoutSession } from '../store/models/checkout-session';
86
- import { Customer } from '../store/models/customer';
87
- import { PaymentCurrency } from '../store/models/payment-currency';
88
- import { PaymentIntent } from '../store/models/payment-intent';
89
- import { PaymentLink } from '../store/models/payment-link';
90
- import { PaymentMethod } from '../store/models/payment-method';
91
- import { Price } from '../store/models/price';
92
- import { PriceQuote } from '../store/models/price-quote';
93
- import { Product } from '../store/models/product';
88
+ } from '../../store/models';
89
+ import type { ChainType } from '../../store/models/types';
90
+ import { CheckoutSession } from '../../store/models/checkout-session';
91
+ import { Customer } from '../../store/models/customer';
92
+ import { PaymentCurrency } from '../../store/models/payment-currency';
93
+ import { PaymentIntent } from '../../store/models/payment-intent';
94
+ import { PaymentLink } from '../../store/models/payment-link';
95
+ import { PaymentMethod } from '../../store/models/payment-method';
96
+ import { Price } from '../../store/models/price';
97
+ import { PriceQuote } from '../../store/models/price-quote';
98
+ import { Product } from '../../store/models/product';
94
99
  import {
95
100
  ensureStripePaymentIntent,
96
101
  ensureStripeSetupIntentForCheckoutSession,
97
102
  ensureStripeSubscription,
98
- } from '../integrations/stripe/resource';
99
- import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
100
- import { paymentQueue } from '../queues/payment';
101
- import { invoiceQueue } from '../queues/invoice';
102
- import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from './connect/shared';
103
+ } from '../../integrations/stripe/resource';
104
+ import { handleStripePaymentSucceed } from '../../integrations/stripe/handlers/payment-intent';
105
+ import { paymentQueue } from '../../queues/payment';
106
+ import { invoiceQueue } from '../../queues/invoice';
107
+ import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from '../connect/shared';
103
108
  import {
104
109
  isCreditGrantSufficientForPayment,
105
110
  isCreditSufficientForPayment,
106
111
  isDelegationSufficientForPayment,
107
112
  SufficientForPaymentResult,
108
- } from '../libs/payment';
109
- import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
110
- import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
111
- import { blocklet } from '../libs/auth';
112
- import { addSubscriptionJob } from '../queues/subscription';
113
- import { updateDataConcurrency, stopAcceptingOrders } from '../libs/env';
113
+ } from '../../libs/payment';
114
+ import { handleStripeSubscriptionSucceed } from '../../integrations/stripe/handlers/subscription';
115
+ import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../../libs/constants';
116
+ import { blocklet } from '../../libs/auth';
117
+ import { addSubscriptionJob } from '../../queues/subscription';
114
118
  import {
115
119
  expandLineItemsWithCouponInfo,
116
120
  expandDiscountsWithDetails,
117
121
  checkPromotionCodeEligibility,
118
122
  createDiscountRecordsForCheckout,
119
123
  updateSubscriptionDiscountReferences,
120
- } from '../libs/discount/coupon';
121
- import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '../libs/discount/discount';
122
- import { formatToShortUrl } from '../libs/url';
123
- import { destroyExistingInvoice } from '../libs/invoice';
124
- import { getApproveFunction } from '../integrations/ethereum/contract';
125
- import { getQuoteService } from '../libs/quote-service';
126
- import { getExchangeRateService } from '../libs/exchange-rate/service';
127
- import { getExchangeRateSymbol } from '../libs/exchange-rate/token-address-mapping';
128
- import { sequelize } from '../store/sequelize';
129
-
130
- const router = Router();
124
+ } from '../../libs/discount/coupon';
125
+ import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '../../libs/discount/discount';
126
+ import { formatToShortUrl } from '../../libs/url';
127
+ import { destroyExistingInvoice } from '../../libs/invoice';
128
+ import { getApproveFunction } from '../../integrations/ethereum/contract';
129
+ import { getQuoteService } from '../../libs/quote-service';
130
+ import { getExchangeRateService } from '../../libs/exchange-rate/service';
131
+ import { getExchangeRateSymbol } from '../../libs/exchange-rate/token-address-mapping';
132
+ import { sequelize } from '../../store/sequelize';
133
+ import { sessionMiddleware } from '../../middlewares/hono/session';
134
+
135
+ const app = new Hono();
131
136
 
132
137
  const user = sessionMiddleware({ accessKey: true });
133
138
  const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
@@ -137,7 +142,7 @@ const exchangeRateService = getExchangeRateService();
137
142
  const DEFAULT_RATE_VOLATILITY_THRESHOLD = 0.1;
138
143
 
139
144
  const getRateVolatilityThreshold = () => {
140
- const raw = Number(process.env.PAYMENT_RATE_VOLATILITY_THRESHOLD || DEFAULT_RATE_VOLATILITY_THRESHOLD);
145
+ const raw = Number(paymentRateVolatilityThreshold() || DEFAULT_RATE_VOLATILITY_THRESHOLD);
141
146
  if (!Number.isFinite(raw) || raw <= 0) {
142
147
  return DEFAULT_RATE_VOLATILITY_THRESHOLD;
143
148
  }
@@ -249,7 +254,7 @@ export async function validateInventory(line_items: LineItem[], includePendingQu
249
254
  throw new Error(`Can not exceed available quantity for price: ${priceId}`);
250
255
  }
251
256
  });
252
- await pAll(checks, { concurrency: updateDataConcurrency });
257
+ await pAll(checks, { concurrency: updateDataConcurrency() });
253
258
  }
254
259
 
255
260
  export async function validatePaymentSettings(paymentMethodId: string, paymentCurrencyId: string) {
@@ -875,22 +880,22 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
875
880
  });
876
881
  };
877
882
 
878
- export async function ensureCheckoutSessionOpen(req: Request, res: Response, next: NextFunction) {
879
- const doc = await CheckoutSession.findByPk(req.params.id);
883
+ // Hono MiddlewareHandler version of ensureCheckoutSessionOpen
884
+ export const ensureCheckoutSessionOpen: MiddlewareHandler = async (c, next) => {
885
+ const id = c.req.param('id');
886
+ const doc = await CheckoutSession.findByPk(id);
880
887
  if (!doc) {
881
- return res.status(404).json({ code: 'CHECKOUT_NOT_FOUND', error: 'Checkout session not found' });
888
+ return c.json({ code: 'CHECKOUT_NOT_FOUND', error: 'Checkout session not found' }, 404);
882
889
  }
883
890
  if (doc.status === 'complete') {
884
- return res.status(403).json({ code: 'CHECKOUT_COMPLETED', error: 'Checkout session completed' });
891
+ return c.json({ code: 'CHECKOUT_COMPLETED', error: 'Checkout session completed' }, 403);
885
892
  }
886
893
  if (doc.status === 'expired') {
887
- return res.status(403).json({ code: 'CHECKOUT_EXPIRED', error: 'Checkout session already expired' });
894
+ return c.json({ code: 'CHECKOUT_EXPIRED', error: 'Checkout session already expired' }, 403);
888
895
  }
889
-
890
- req.doc = doc;
891
-
892
- next();
893
- }
896
+ c.set('doc', doc);
897
+ await next();
898
+ };
894
899
 
895
900
  const getBeneficiaryName = async (beneficiary: PaymentBeneficiary) => {
896
901
  if (!beneficiary) return '';
@@ -1008,7 +1013,7 @@ async function processSubscriptionFastCheckout({
1008
1013
  });
1009
1014
  return amount;
1010
1015
  }),
1011
- { concurrency: updateDataConcurrency }
1016
+ { concurrency: updateDataConcurrency() }
1012
1017
  );
1013
1018
  const totalAmount = subscriptionAmounts
1014
1019
  .reduce((sum: BN, amt: string) => sum.add(new BN(amt)), new BN('0'))
@@ -1056,7 +1061,7 @@ async function processSubscriptionFastCheckout({
1056
1061
  payment_details: { [paymentMethod.type]: { payer: customer.did } },
1057
1062
  });
1058
1063
  }),
1059
- { concurrency: updateDataConcurrency }
1064
+ { concurrency: updateDataConcurrency() }
1060
1065
  );
1061
1066
  if (paymentCurrency.isCredit()) {
1062
1067
  // skip invoice creation for credit subscriptions
@@ -1079,7 +1084,7 @@ async function processSubscriptionFastCheckout({
1079
1084
  });
1080
1085
  await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
1081
1086
  }),
1082
- { concurrency: updateDataConcurrency }
1087
+ { concurrency: updateDataConcurrency() }
1083
1088
  );
1084
1089
  return {
1085
1090
  success: true,
@@ -1102,14 +1107,14 @@ async function processSubscriptionFastCheckout({
1102
1107
  return invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
1103
1108
  }
1104
1109
  }),
1105
- { concurrency: updateDataConcurrency }
1110
+ { concurrency: updateDataConcurrency() }
1106
1111
  );
1107
1112
  // Add subscription cycle jobs
1108
1113
  await pAll(
1109
1114
  subscriptions.map((sub) => async () => {
1110
1115
  await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
1111
1116
  }),
1112
- { concurrency: updateDataConcurrency }
1117
+ { concurrency: updateDataConcurrency() }
1113
1118
  );
1114
1119
 
1115
1120
  logger.info('Created and queued invoices for fast checkout with subscriptions', {
@@ -1146,21 +1151,22 @@ async function processSubscriptionFastCheckout({
1146
1151
  }
1147
1152
 
1148
1153
  // create checkout session
1149
- router.post('/', authLogin, async (req, res) => {
1154
+ app.post('/', authLogin, async (c) => {
1150
1155
  try {
1151
- const raw: Partial<CheckoutSession> = await formatCheckoutSession(req.body);
1152
- raw.livemode = !!req.livemode;
1153
- raw.created_via = req.user?.via as string;
1156
+ const body = c.get('sanitizedBody') ?? {};
1157
+ const raw: Partial<CheckoutSession> = await formatCheckoutSession(body);
1158
+ raw.livemode = !!c.get('livemode');
1159
+ raw.created_via = c.get('user')?.via as string;
1154
1160
 
1155
1161
  // Customer permission validation and createMine handling
1156
- const { create_mine: createMine } = req.body;
1157
- const currentUserDid = req.user?.did;
1162
+ const { create_mine: createMine } = body;
1163
+ const currentUserDid = c.get('user')?.did;
1158
1164
 
1159
- const isAdmin = ['owner', 'admin'].includes(req.user?.role as string);
1165
+ const isAdmin = ['owner', 'admin'].includes(c.get('user')?.role as string);
1160
1166
  // Handle createMine parameter
1161
1167
  if (createMine === true) {
1162
1168
  if (!currentUserDid) {
1163
- return res.status(400).json({ error: 'User not authenticated, cannot create checkout session for self' });
1169
+ return c.json({ error: 'User not authenticated, cannot create checkout session for self' }, 400);
1164
1170
  }
1165
1171
 
1166
1172
  const currentCustomer = await Customer.findOne({ where: { did: currentUserDid } });
@@ -1173,7 +1179,7 @@ router.post('/', authLogin, async (req, res) => {
1173
1179
  createdBy: currentUserDid,
1174
1180
  };
1175
1181
  } else if (!isAdmin) {
1176
- return res.status(403).json({ error: 'Not authorized to perform this action' });
1182
+ return c.json({ error: 'Not authorized to perform this action' }, 403);
1177
1183
  }
1178
1184
 
1179
1185
  if (raw.line_items) {
@@ -1181,24 +1187,24 @@ router.post('/', authLogin, async (req, res) => {
1181
1187
  await validateInventory(raw.line_items, true);
1182
1188
  } catch (err) {
1183
1189
  logger.error('validateInventory failed', { error: err, line_items: raw.line_items });
1184
- return res.status(400).json({ error: err.message });
1190
+ return c.json({ error: err.message }, 400);
1185
1191
  }
1186
1192
  }
1187
1193
 
1188
1194
  // Process discounts before creating checkout session
1189
1195
  let processedDiscounts: any[] = [];
1190
- if (req.body.discounts && Array.isArray(req.body.discounts)) {
1196
+ if (body.discounts && Array.isArray(body.discounts)) {
1191
1197
  if (!isAdmin) {
1192
- return res.status(403).json({ error: 'Not allowed to apply discounts' });
1198
+ return c.json({ error: 'Not allowed to apply discounts' }, 403);
1193
1199
  }
1194
1200
  try {
1195
- processedDiscounts = await processCheckoutSessionDiscounts(raw as any, req.body.discounts);
1201
+ processedDiscounts = await processCheckoutSessionDiscounts(raw as any, body.discounts);
1196
1202
  } catch (discountError) {
1197
1203
  logger.error('Discount processing failed during checkout session creation', {
1198
1204
  error: discountError.message,
1199
- discounts: req.body.discounts,
1205
+ discounts: body.discounts,
1200
1206
  });
1201
- return res.status(400).json({ error: discountError.message });
1207
+ return c.json({ error: discountError.message }, 400);
1202
1208
  }
1203
1209
  }
1204
1210
 
@@ -1212,7 +1218,7 @@ router.post('/', authLogin, async (req, res) => {
1212
1218
  try {
1213
1219
  await prefetchQuotesForCheckoutSession(doc as CheckoutSession);
1214
1220
  } catch (error: any) {
1215
- return res.status(400).json({ error: error.message });
1221
+ return c.json({ error: error.message }, 400);
1216
1222
  }
1217
1223
 
1218
1224
  let url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
@@ -1220,24 +1226,23 @@ router.post('/', authLogin, async (req, res) => {
1220
1226
  url = withQuery(url, getConnectQueryParam({ userDid: currentUserDid }));
1221
1227
  }
1222
1228
 
1223
- res.json({ ...doc.toJSON(), url });
1229
+ return c.json({ ...doc.toJSON(), url });
1224
1230
  } catch (error) {
1225
- logger.error('Create checkout session failed', { error: error.message, body: req.body });
1226
- res.status(500).json({ error: error.message });
1231
+ logger.error('Create checkout session failed', { error: error.message, body: c.get('sanitizedBody') ?? {} });
1232
+ return c.json({ error: error.message }, 500);
1227
1233
  }
1228
1234
  });
1229
1235
 
1230
- export async function startCheckoutSessionFromPaymentLink(id: string, req: Request, res: Response) {
1231
- const { metadata, needShortUrl = false } = req.body;
1236
+ export async function startCheckoutSessionFromPaymentLink(id: string, c: any) {
1237
+ const body = c.get('sanitizedBody') ?? {};
1238
+ const { metadata, needShortUrl = false } = body;
1232
1239
  try {
1233
1240
  const link = await PaymentLink.findByPk(id);
1234
1241
  if (!link) {
1235
- res.status(400).json({ error: 'Payment link not found, please contact the source of the payment link.' });
1236
- return;
1242
+ return c.json({ error: 'Payment link not found, please contact the source of the payment link.' }, 400);
1237
1243
  }
1238
1244
  if (!link.active) {
1239
- res.status(400).json({ error: 'Payment link archived, we can not create new checkout session.' });
1240
- return;
1245
+ return c.json({ error: 'Payment link archived, we can not create new checkout session.' }, 400);
1241
1246
  }
1242
1247
 
1243
1248
  const items = await Price.expand(link.line_items, { upsell: true });
@@ -1249,10 +1254,9 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1249
1254
  raw.livemode = link.livemode;
1250
1255
  raw.created_via = 'portal';
1251
1256
  raw.submit_type = link.submit_type;
1252
- raw.currency_id = link.currency_id || req.baseCurrency.id;
1257
+ raw.currency_id = link.currency_id || c.get('baseCurrency').id;
1253
1258
  if (!raw.currency_id) {
1254
- res.status(400).json({ error: 'Currency not found in payment link' });
1255
- return;
1259
+ return c.json({ error: 'Currency not found in payment link' }, 400);
1256
1260
  }
1257
1261
  raw.payment_link_id = link.id;
1258
1262
 
@@ -1271,11 +1275,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1271
1275
  if (link.subscription_data?.billing_threshold_amount) {
1272
1276
  protectedSettings.billing_threshold_amount = getBillingThreshold(link.subscription_data);
1273
1277
  }
1274
- raw.subscription_data = merge(
1275
- link.subscription_data,
1276
- getDataObjectFromQuery(req.query, 'subscription_data'),
1277
- protectedSettings
1278
- );
1278
+ raw.subscription_data = merge(link.subscription_data, getDataObjectFromQuery(c.req.query()), protectedSettings);
1279
1279
 
1280
1280
  if (link.after_completion?.hosted_confirmation?.custom_message) {
1281
1281
  raw.payment_intent_data = {
@@ -1317,17 +1317,17 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1317
1317
  raw.success_url = link.after_completion?.redirect?.url;
1318
1318
  }
1319
1319
 
1320
- if (req.query.redirect) {
1321
- raw.success_url = req.query.redirect as string;
1322
- raw.cancel_url = req.query.redirect as string;
1320
+ if (c.req.query('redirect')) {
1321
+ raw.success_url = c.req.query('redirect') as string;
1322
+ raw.cancel_url = c.req.query('redirect') as string;
1323
1323
  }
1324
1324
 
1325
1325
  try {
1326
- if (req.query.nft_mint_factory && isValid(req.query.nft_mint_factory as string)) {
1326
+ if (c.req.query('nft_mint_factory') && isValid(c.req.query('nft_mint_factory') as string)) {
1327
1327
  raw.nft_mint_settings = {
1328
1328
  enabled: true,
1329
1329
  behavior: 'per_checkout_session',
1330
- factory: req.query.nft_mint_factory as string,
1330
+ factory: c.req.query('nft_mint_factory') as string,
1331
1331
  };
1332
1332
  raw.nft_mint_status = 'pending';
1333
1333
  logger.info('use nft_mint_settings from query when checkout from payment link', { v: raw.nft_mint_settings });
@@ -1339,17 +1339,17 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1339
1339
  }
1340
1340
 
1341
1341
  let doc;
1342
- if (req.query.preview === '1') {
1342
+ if (c.req.query('preview') === '1') {
1343
1343
  doc = await CheckoutSession.findOne({ where: { payment_link_id: link.id, metadata: { preview: '1' } } });
1344
1344
  if (doc) {
1345
1345
  await doc.update(omit(raw, ['metadata']));
1346
1346
  } else {
1347
1347
  raw.metadata = {
1348
1348
  ...link.metadata,
1349
- ...getDataObjectFromQuery(req.query),
1349
+ ...getDataObjectFromQuery(c.req.query()),
1350
1350
  ...metadata,
1351
- days_until_due: getDaysUntilDue(req.query),
1352
- days_until_cancel: getDaysUntilCancel(req.query),
1351
+ days_until_due: getDaysUntilDue(c.req.query()),
1352
+ days_until_cancel: getDaysUntilCancel(c.req.query()),
1353
1353
  passport: await checkPassportForPaymentLink(link),
1354
1354
  preview: '1',
1355
1355
  };
@@ -1357,10 +1357,10 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1357
1357
  } else {
1358
1358
  raw.metadata = {
1359
1359
  ...link.metadata,
1360
- ...getDataObjectFromQuery(req.query),
1360
+ ...getDataObjectFromQuery(c.req.query()),
1361
1361
  ...metadata,
1362
- days_until_due: getDaysUntilDue(req.query),
1363
- days_until_cancel: getDaysUntilCancel(req.query),
1362
+ days_until_due: getDaysUntilDue(c.req.query()),
1363
+ days_until_cancel: getDaysUntilCancel(c.req.query()),
1364
1364
  passport: await checkPassportForPaymentLink(link),
1365
1365
  };
1366
1366
  }
@@ -1371,7 +1371,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1371
1371
 
1372
1372
  doc.line_items = items;
1373
1373
 
1374
- if (req.query.upsell === '1') {
1374
+ if (c.req.query('upsell') === '1') {
1375
1375
  // add upsell to line items
1376
1376
  const updatedItems = cloneDeep(doc.line_items);
1377
1377
  updatedItems.forEach((item: any) => {
@@ -1422,7 +1422,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1422
1422
  paymentUrl = await formatToShortUrl({ url: paymentUrl, validUntil, maxVisits });
1423
1423
  }
1424
1424
 
1425
- res.json({
1425
+ return c.json({
1426
1426
  paymentUrl,
1427
1427
  checkoutSession: doc.toJSON(),
1428
1428
  quotes, // Include quotes information for frontend
@@ -1431,201 +1431,36 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
1431
1431
  paymentMethods: await getPaymentMethods(doc),
1432
1432
  paymentLink: link,
1433
1433
  paymentIntent: null,
1434
- customer: req.user ? await Customer.findOne({ where: { did: req.user.did } }) : null,
1434
+ customer: c.get('user') ? await Customer.findOne({ where: { did: c.get('user').did } }) : null,
1435
1435
  });
1436
1436
  } catch (err) {
1437
1437
  logger.error(err);
1438
1438
  if (err instanceof CustomError) {
1439
- res.status(getStatusFromError(err)).json({ error: formatError(err) });
1440
- } else {
1441
- res.status(500).json({ error: err.message });
1439
+ return c.json({ error: formatError(err) }, getStatusFromError(err));
1442
1440
  }
1441
+ return c.json({ error: err.message }, 500);
1443
1442
  }
1444
1443
  }
1445
1444
 
1446
1445
  // start checkout session from payment link
1447
- router.post('/start/:id', user, async (req, res) => {
1446
+ app.post('/start/:id', user, (c) => {
1448
1447
  logger.info('Starting checkout session from payment link', {
1449
- paymentLinkId: req.params.id,
1450
- userId: req.user?.did,
1448
+ paymentLinkId: c.req.param('id'),
1449
+ userId: c.get('user')?.did,
1451
1450
  });
1452
- await startCheckoutSessionFromPaymentLink(req.params.id as string, req, res);
1453
- });
1454
-
1455
- // for Node.js SDK
1456
- router.get('/:id', auth, async (req, res) => {
1457
- const doc = await CheckoutSession.findByPk(req.params.id);
1458
-
1459
- if (doc) {
1460
- // @ts-ignore
1461
- doc.line_items = await Price.expand(doc.line_items, { upsell: true });
1462
- doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
1463
- }
1464
-
1465
- if (doc) {
1466
- res.json(doc?.toJSON());
1467
- } else {
1468
- res.status(404).json(null);
1469
- }
1470
- });
1471
-
1472
- // abort stripe subscription(s) created during an incomplete checkout session
1473
- router.post('/:id/abort-stripe', user, ensureCheckoutSessionOpen, async (req, res) => {
1474
- try {
1475
- const checkoutSession = req.doc as CheckoutSession;
1476
-
1477
- if (checkoutSession.status === 'complete') {
1478
- return res.status(400).json({ error: 'Checkout session already completed' });
1479
- }
1480
-
1481
- // cancel stripe subscriptions if any
1482
- const canceledSubscriptions: string[] = [];
1483
- if (['subscription', 'setup'].includes(checkoutSession.mode)) {
1484
- const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
1485
- const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
1486
-
1487
- const cancelOps = subscriptions.map(async (sub) => {
1488
- const stripeSubId = sub.payment_details?.stripe?.subscription_id;
1489
- if (!stripeSubId) {
1490
- return null;
1491
- }
1492
- const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
1493
- if (!method || method.type !== 'stripe') {
1494
- return null;
1495
- }
1496
- const client = method.getStripeClient();
1497
- try {
1498
- await client.subscriptions.cancel(stripeSubId);
1499
- await sub.update({
1500
- payment_details: omit(sub.payment_details || {}, 'stripe'),
1501
- payment_settings: {
1502
- payment_method_options: omit(sub.payment_settings?.payment_method_options || {}, 'stripe'),
1503
- payment_method_types: sub.payment_settings?.payment_method_types || [],
1504
- },
1505
- });
1506
- canceledSubscriptions.push(sub.id);
1507
- } catch (err) {
1508
- logger.error('Failed to cancel stripe subscription for checkout abort', {
1509
- checkoutSessionId: checkoutSession.id,
1510
- subscriptionId: sub.id,
1511
- error: err.message,
1512
- });
1513
- }
1514
- return sub.id;
1515
- });
1516
- await Promise.all(cancelOps);
1517
- }
1518
-
1519
- // remove related invoice if created
1520
- try {
1521
- const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
1522
- if (existInvoice) {
1523
- await destroyExistingInvoice(existInvoice);
1524
- }
1525
- } catch (error: any) {
1526
- logger.error('Failed to destroy invoice on checkout abort', {
1527
- checkoutSessionId: checkoutSession.id,
1528
- error: error.message,
1529
- });
1530
- }
1531
- res.json({ checkoutSessionId: checkoutSession.id, canceledSubscriptions });
1532
- } catch (err: any) {
1533
- logger.error('Error aborting stripe for checkout session', {
1534
- sessionId: req.params.id,
1535
- error: err.message,
1536
- stack: err.stack,
1537
- });
1538
- res.status(500).json({ error: err.message });
1539
- }
1540
- });
1541
-
1542
- // Skip payment method for $0 subscription — user chose "Skip, bind later"
1543
- // Keeps the subscription but sets cancel_at_period_end so it won't renew without a payment method
1544
- router.post('/:id/skip-payment-method', user, ensureCheckoutSessionOpen, async (req, res) => {
1545
- try {
1546
- if (!req.user) {
1547
- return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
1548
- }
1549
-
1550
- const checkoutSession = req.doc as CheckoutSession;
1551
-
1552
- if (!['subscription', 'setup'].includes(checkoutSession.mode)) {
1553
- return res.status(400).json({ error: 'Skip payment method is only supported for subscriptions' });
1554
- }
1555
-
1556
- const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
1557
- const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
1558
-
1559
- if (!subscriptions.length) {
1560
- return res.status(400).json({ error: 'No subscriptions found for this checkout session' });
1561
- }
1562
-
1563
- // Cancel Stripe setup intents and activate subscriptions concurrently
1564
- await Promise.all(
1565
- subscriptions.map(async (sub) => {
1566
- const stripeSubId = sub.payment_details?.stripe?.subscription_id;
1567
- if (stripeSubId) {
1568
- const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
1569
- if (method?.type === 'stripe') {
1570
- const client = method.getStripeClient();
1571
- try {
1572
- const stripeSub = await client.subscriptions.retrieve(stripeSubId, {
1573
- expand: ['pending_setup_intent'],
1574
- });
1575
- if (stripeSub.pending_setup_intent && typeof stripeSub.pending_setup_intent !== 'string') {
1576
- await client.setupIntents.cancel(stripeSub.pending_setup_intent.id);
1577
- }
1578
- await client.subscriptions.update(stripeSubId, { cancel_at_period_end: true });
1579
- } catch (err: any) {
1580
- logger.error('Failed to update Stripe subscription for skip-payment-method', {
1581
- checkoutSessionId: checkoutSession.id,
1582
- subscriptionId: sub.id,
1583
- stripeSubId,
1584
- error: err.message,
1585
- });
1586
- }
1587
- }
1588
- }
1589
-
1590
- // Activate the local subscription with cancel_at_period_end
1591
- await sub.update({
1592
- status: sub.trial_end && sub.trial_end > Date.now() / 1000 ? 'trialing' : 'active',
1593
- cancel_at_period_end: true,
1594
- });
1595
- await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
1596
- })
1597
- );
1598
-
1599
- // Complete the checkout session
1600
- await checkoutSession.update({
1601
- status: 'complete',
1602
- payment_status: 'no_payment_required',
1603
- });
1604
-
1605
- return res.json({
1606
- checkoutSession: { id: checkoutSession.id, status: 'complete' },
1607
- skipped: true,
1608
- });
1609
- } catch (err: any) {
1610
- logger.error('Error in skip-payment-method', {
1611
- sessionId: req.params.id,
1612
- error: err.message,
1613
- stack: err.stack,
1614
- });
1615
- res.status(500).json({ error: err.message });
1616
- }
1451
+ return startCheckoutSessionFromPaymentLink(c.req.param('id') as string, c);
1617
1452
  });
1618
1453
 
1619
- // for checkout page
1620
1454
  // Lightweight status-only endpoint for polling.
1621
1455
  // Returns only status fields — no includes, no joins, single D1 query.
1622
1456
  // Used by waitForCheckoutComplete to minimize D1 load during payment processing.
1623
- router.get('/status/:id', user, async (req, res) => {
1624
- const doc = await CheckoutSession.findByPk(req.params.id, {
1457
+ // Static prefix /status/:id registered before /:id to avoid shadowing.
1458
+ app.get('/status/:id', user, async (c) => {
1459
+ const doc = await CheckoutSession.findByPk(c.req.param('id'), {
1625
1460
  attributes: ['id', 'status', 'payment_status', 'payment_intent_id'],
1626
1461
  });
1627
1462
  if (!doc) {
1628
- return res.status(404).json({ error: 'Checkout session not found' });
1463
+ return c.json({ error: 'Checkout session not found' }, 404);
1629
1464
  }
1630
1465
 
1631
1466
  const piStatus = doc.payment_intent_id
@@ -1634,18 +1469,18 @@ router.get('/status/:id', user, async (req, res) => {
1634
1469
  }).then((pi) => (pi ? { status: pi.status, last_payment_error: pi.last_payment_error } : null))
1635
1470
  : null;
1636
1471
 
1637
- return res.json({
1472
+ return c.json({
1638
1473
  checkoutSession: { status: doc.status, payment_status: doc.payment_status },
1639
1474
  paymentIntent: piStatus,
1640
1475
  });
1641
1476
  });
1642
1477
 
1643
- router.get('/retrieve/:id', user, async (req, res) => {
1644
- const doc = await CheckoutSession.findByPk(req.params.id);
1478
+ // Static prefix /retrieve/:id registered before /:id to avoid shadowing.
1479
+ app.get('/retrieve/:id', user, async (c) => {
1480
+ const doc = await CheckoutSession.findByPk(c.req.param('id'));
1645
1481
 
1646
1482
  if (!doc) {
1647
- res.status(404).json({ error: 'Checkout session not found, you may have incorrectly copied the link.' });
1648
- return;
1483
+ return c.json({ error: 'Checkout session not found, you may have incorrectly copied the link.' }, 404);
1649
1484
  }
1650
1485
 
1651
1486
  // Check if subscription status needs updating (conditional write — must happen before reads)
@@ -1705,7 +1540,7 @@ router.get('/retrieve/:id', user, async (req, res) => {
1705
1540
 
1706
1541
  // Handle dynamic pricing: create or reuse quotes
1707
1542
  const isCheckoutConfirmed = doc.payment_status === 'paid' || doc.status === 'complete' || !!doc.subscription_id;
1708
- const forceRefresh = req.query.forceRefresh === '1' || req.query.forceRefresh === 'true';
1543
+ const forceRefresh = c.req.query('forceRefresh') === '1' || c.req.query('forceRefresh') === 'true';
1709
1544
 
1710
1545
  // Start PaymentIntent fetch early — runs in parallel with quote processing (save ~85ms D1 RTT)
1711
1546
  const paymentIntentPromise = doc.payment_intent_id
@@ -1796,10 +1631,10 @@ router.get('/retrieve/:id', user, async (req, res) => {
1796
1631
  const [paymentMethods, paymentLink, customer] = await Promise.all([
1797
1632
  getPaymentMethods(doc),
1798
1633
  doc.payment_link_id ? PaymentLink.findByPk(doc.payment_link_id) : null,
1799
- req.user ? Customer.findOne({ where: { did: req.user.did } }) : null,
1634
+ c.get('user') ? Customer.findOne({ where: { did: c.get('user').did } }) : null,
1800
1635
  ]);
1801
1636
 
1802
- res.json({
1637
+ return c.json({
1803
1638
  checkoutSession: {
1804
1639
  ...doc.toJSON(),
1805
1640
  discounts: enhancedDiscounts,
@@ -1812,12 +1647,69 @@ router.get('/retrieve/:id', user, async (req, res) => {
1812
1647
  paymentLink,
1813
1648
  paymentIntent,
1814
1649
  customer,
1815
- ...(stopAcceptingOrders ? { stopAcceptingOrders: true } : {}),
1650
+ ...(stopAcceptingOrders() ? { stopAcceptingOrders: true } : {}),
1816
1651
  });
1817
1652
  });
1818
1653
 
1819
- // Fetch latest exchange rate for checkout session (for UI display/monitoring only)
1820
- // Also renews active Quote's expires_at if it's about to expire
1654
+ // for Node.js SDK
1655
+ // Static prefix /broker-status/:id registered before /:id to avoid shadowing.
1656
+ app.get('/broker-status/:id', user, async (c) => {
1657
+ const { needShortUrl = false } = c.req.query();
1658
+ const doc = await CheckoutSession.findByPk(c.req.param('id'));
1659
+
1660
+ if (!doc) {
1661
+ return c.json({
1662
+ checkoutSession: {},
1663
+ paymentLink: null,
1664
+ });
1665
+ }
1666
+
1667
+ // @ts-ignore
1668
+ doc.line_items = await Price.expand(doc.line_items, { upsell: true });
1669
+
1670
+ const hasVendorConfig = doc.line_items?.some((item: any) => !!item?.price?.product?.vendor_config?.length);
1671
+
1672
+ if (!hasVendorConfig || doc.payment_status === 'unpaid' || doc.fulfillment_status === 'cancelled') {
1673
+ return c.json({
1674
+ checkoutSession: {},
1675
+ paymentLink: null,
1676
+ });
1677
+ }
1678
+
1679
+ const paymentUrl = getUrl(`/checkout/pay/${doc.id}`);
1680
+ const paymentLink = needShortUrl
1681
+ ? await formatToShortUrl({
1682
+ url: paymentUrl,
1683
+ validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
1684
+ maxVisits: 5,
1685
+ })
1686
+ : paymentUrl;
1687
+
1688
+ return c.json({
1689
+ checkoutSession: {
1690
+ ...doc.toJSON(),
1691
+ line_items: doc.line_items, // Override with expanded line_items (includes price object)
1692
+ },
1693
+ paymentLink,
1694
+ });
1695
+ });
1696
+
1697
+ // for Node.js SDK
1698
+ app.get('/:id', auth, async (c) => {
1699
+ const doc = await CheckoutSession.findByPk(c.req.param('id'));
1700
+
1701
+ if (doc) {
1702
+ // @ts-ignore
1703
+ doc.line_items = await Price.expand(doc.line_items, { upsell: true });
1704
+ doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
1705
+ }
1706
+
1707
+ if (doc) {
1708
+ return c.json(doc?.toJSON());
1709
+ }
1710
+ return c.json(null, 404);
1711
+ });
1712
+
1821
1713
  /**
1822
1714
  * Exchange Rate Endpoint (Final Freeze Architecture)
1823
1715
  *
@@ -1826,23 +1718,25 @@ router.get('/retrieve/:id', user, async (req, res) => {
1826
1718
  *
1827
1719
  * @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
1828
1720
  */
1829
- router.get('/:id/exchange-rate', user, async (req, res) => {
1830
- const doc = await CheckoutSession.findByPk(req.params.id);
1721
+ // Fetch latest exchange rate for checkout session (for UI display/monitoring only)
1722
+ // Also renews active Quote's expires_at if it's about to expire
1723
+ app.get('/:id/exchange-rate', user, async (c) => {
1724
+ const doc = await CheckoutSession.findByPk(c.req.param('id'));
1831
1725
  if (!doc) {
1832
- return res.status(404).json({ error: 'Checkout session not found, you may have incorrectly copied the link.' });
1726
+ return c.json({ error: 'Checkout session not found, you may have incorrectly copied the link.' }, 404);
1833
1727
  }
1834
1728
 
1835
- const currencyId = (req.query.currency_id as string) || doc.currency_id;
1729
+ const currencyId = (c.req.query('currency_id') as string) || doc.currency_id;
1836
1730
  const paymentCurrency = (await PaymentCurrency.findByPk(currencyId, {
1837
1731
  include: [{ model: PaymentMethod, as: 'payment_method' }],
1838
1732
  })) as (PaymentCurrency & { payment_method: PaymentMethod }) | null;
1839
1733
 
1840
1734
  if (!paymentCurrency) {
1841
- return res.status(400).json({ error: 'Payment currency not found' });
1735
+ return c.json({ error: 'Payment currency not found' }, 400);
1842
1736
  }
1843
1737
 
1844
1738
  if (paymentCurrency.payment_method?.type === 'stripe') {
1845
- return res.status(400).json({ error: 'Exchange rate is not supported for stripe payments' });
1739
+ return c.json({ error: 'Exchange rate is not supported for stripe payments' }, 400);
1846
1740
  }
1847
1741
 
1848
1742
  const serverNow = Math.floor(Date.now() / 1000);
@@ -1851,7 +1745,7 @@ router.get('/:id/exchange-rate', user, async (req, res) => {
1851
1745
  const rateResult = await fetchCurrentExchangeRate(paymentCurrency, paymentCurrency.payment_method);
1852
1746
 
1853
1747
  // Final Freeze: Only return rate snapshot, no quote data
1854
- return res.json({
1748
+ return c.json({
1855
1749
  server_now: serverNow,
1856
1750
  rate: rateResult.rate,
1857
1751
  timestamp_ms: rateResult.timestamp_ms,
@@ -1876,55 +1770,14 @@ router.get('/:id/exchange-rate', user, async (req, res) => {
1876
1770
  });
1877
1771
 
1878
1772
  // Return 503 with RATE_UNAVAILABLE code
1879
- return res.status(503).json({
1880
- code: 'RATE_UNAVAILABLE',
1881
- error: error?.message || 'Exchange rate unavailable',
1882
- });
1883
- }
1884
- });
1885
-
1886
- // for checkout page
1887
- router.get('/broker-status/:id', user, async (req, res) => {
1888
- const { needShortUrl = false } = req.query;
1889
- const doc = await CheckoutSession.findByPk(req.params.id);
1890
-
1891
- if (!doc) {
1892
- res.json({
1893
- checkoutSession: {},
1894
- paymentLink: null,
1895
- });
1896
- return;
1897
- }
1898
-
1899
- // @ts-ignore
1900
- doc.line_items = await Price.expand(doc.line_items, { upsell: true });
1901
-
1902
- const hasVendorConfig = doc.line_items?.some((item: any) => !!item?.price?.product?.vendor_config?.length);
1903
-
1904
- if (!hasVendorConfig || doc.payment_status === 'unpaid' || doc.fulfillment_status === 'cancelled') {
1905
- res.json({
1906
- checkoutSession: {},
1907
- paymentLink: null,
1908
- });
1909
- return;
1773
+ return c.json(
1774
+ {
1775
+ code: 'RATE_UNAVAILABLE',
1776
+ error: error?.message || 'Exchange rate unavailable',
1777
+ },
1778
+ 503
1779
+ );
1910
1780
  }
1911
-
1912
- const paymentUrl = getUrl(`/checkout/pay/${doc.id}`);
1913
- const paymentLink = needShortUrl
1914
- ? await formatToShortUrl({
1915
- url: paymentUrl,
1916
- validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
1917
- maxVisits: 5,
1918
- })
1919
- : paymentUrl;
1920
-
1921
- res.json({
1922
- checkoutSession: {
1923
- ...doc.toJSON(),
1924
- line_items: doc.line_items, // Override with expanded line_items (includes price object)
1925
- },
1926
- paymentLink,
1927
- });
1928
1781
  });
1929
1782
 
1930
1783
  async function checkVendorConfig(items: TLineItemExpanded[]) {
@@ -2666,20 +2519,24 @@ async function prefetchQuotesForCheckoutSession(checkoutSession: CheckoutSession
2666
2519
  }
2667
2520
 
2668
2521
  // submit order
2669
- router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2522
+ app.put('/:id/submit', user, ensureCheckoutSessionOpen, async (c) => {
2670
2523
  let consumedQuotes: PriceQuote[] = [];
2671
- const checkoutSession = req.doc as CheckoutSession;
2524
+ const checkoutSession = c.get('doc') as CheckoutSession;
2525
+ const body = c.get('sanitizedBody') ?? {};
2672
2526
  try {
2673
- if (!req.user) {
2674
- return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
2527
+ if (!c.get('user')) {
2528
+ return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
2675
2529
  }
2676
2530
 
2677
2531
  // Check if system is accepting new orders
2678
- if (stopAcceptingOrders) {
2679
- return res.status(422).json({
2680
- code: 'STOP_ACCEPTING_ORDERS',
2681
- error: 'We are not accepting new orders at this time. Please try again later.',
2682
- });
2532
+ if (stopAcceptingOrders()) {
2533
+ return c.json(
2534
+ {
2535
+ code: 'STOP_ACCEPTING_ORDERS',
2536
+ error: 'We are not accepting new orders at this time. Please try again later.',
2537
+ },
2538
+ 422
2539
+ );
2683
2540
  }
2684
2541
 
2685
2542
  // Idempotency check: If already complete/paid, return existing state
@@ -2695,28 +2552,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2695
2552
  const quoteIds = (existingPaymentIntent?.metadata as any)?.quoteIds || [];
2696
2553
  const quotes = quoteIds.length > 0 ? await PriceQuote.findAll({ where: { id: quoteIds } }) : [];
2697
2554
 
2698
- return res.status(200).json({
2699
- checkoutSession,
2700
- paymentIntent: existingPaymentIntent,
2701
- invoice: existingInvoice,
2702
- quotes: quotes.map((q) => ({
2703
- id: q.id,
2704
- status: q.status,
2705
- quoted_amount: q.quoted_amount,
2706
- expires_at: q.expires_at,
2707
- })),
2708
- message: 'Checkout session already completed',
2709
- });
2555
+ return c.json(
2556
+ {
2557
+ checkoutSession,
2558
+ paymentIntent: existingPaymentIntent,
2559
+ invoice: existingInvoice,
2560
+ quotes: quotes.map((q) => ({
2561
+ id: q.id,
2562
+ status: q.status,
2563
+ quoted_amount: q.quoted_amount,
2564
+ expires_at: q.expires_at,
2565
+ })),
2566
+ message: 'Checkout session already completed',
2567
+ },
2568
+ 200
2569
+ );
2710
2570
  }
2711
2571
 
2712
- const hasVendorConfig = await checkVendorConfig(req.doc.line_items);
2572
+ const hasVendorConfig = await checkVendorConfig((c.get('doc') as CheckoutSession).line_items as any);
2713
2573
  if (hasVendorConfig) {
2714
- const { user: userDetail } = await blocklet.getUser(req.user.did, { enableConnectedAccount: true });
2574
+ const { user: userDetail } = await blocklet.getUser(c.get('user').did, { enableConnectedAccount: true });
2715
2575
  if (!userDetail?.sourceAppPid) {
2716
- return res.status(403).json({
2717
- code: 'UNIFIED_APP_REQUIRED',
2718
- error: 'This action requires a unified account. Please switch accounts and try again.',
2719
- });
2576
+ return c.json(
2577
+ {
2578
+ code: 'UNIFIED_APP_REQUIRED',
2579
+ error: 'This action requires a unified account. Please switch accounts and try again.',
2580
+ },
2581
+ 403
2582
+ );
2720
2583
  }
2721
2584
  }
2722
2585
 
@@ -2729,7 +2592,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2729
2592
  line_items: checkoutSession.line_items,
2730
2593
  checkoutSessionId: checkoutSession.id,
2731
2594
  });
2732
- return res.status(400).json({ error: err.message });
2595
+ return c.json({ error: (err as any).message }, 400);
2733
2596
  }
2734
2597
  }
2735
2598
  // validate cross sell
@@ -2738,20 +2601,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2738
2601
  const result = await getCrossSellItem(checkoutSession);
2739
2602
  // @ts-ignore
2740
2603
  if (result.id) {
2741
- return res
2742
- .status(400)
2743
- .json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
2604
+ return c.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' }, 400);
2744
2605
  }
2745
2606
  }
2746
2607
  }
2747
2608
 
2748
2609
  const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
2749
- req.body.payment_method,
2750
- req.body.payment_currency
2610
+ (body as any).payment_method,
2611
+ (body as any).payment_currency
2751
2612
  );
2752
2613
  await checkoutSession.update({
2753
2614
  currency_id: paymentCurrency.id,
2754
- metadata: { ...checkoutSession.metadata, preferred_locale: req.locale },
2615
+ metadata: { ...checkoutSession.metadata, preferred_locale: c.get('locale') },
2755
2616
  });
2756
2617
 
2757
2618
  const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
@@ -2763,13 +2624,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2763
2624
  const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > submitNow;
2764
2625
 
2765
2626
  // Check if using Final Freeze flow (idempotency_key provided)
2766
- const useFinalFreezeFlow = !!req.body.idempotency_key;
2627
+ const useFinalFreezeFlow = !!(body as any).idempotency_key;
2767
2628
 
2768
2629
  if (useFinalFreezeFlow) {
2769
2630
  // Final Freeze: Create quotes at submit time with dual-layer validation
2770
2631
  logger.info('Using Final Freeze quote creation flow', {
2771
2632
  sessionId: checkoutSession.id,
2772
- idempotencyKey: req.body.idempotency_key,
2633
+ idempotencyKey: (body as any).idempotency_key,
2773
2634
  isTrialing,
2774
2635
  });
2775
2636
 
@@ -2777,38 +2638,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2777
2638
  checkoutSession,
2778
2639
  lineItems: expandedLineItems,
2779
2640
  paymentCurrencyId: paymentCurrency.id,
2780
- idempotencyKey: req.body.idempotency_key,
2781
- previewRate: req.body.preview_rate,
2782
- priceConfirmed: req.body.price_confirmed === true,
2641
+ idempotencyKey: (body as any).idempotency_key,
2642
+ previewRate: (body as any).preview_rate,
2643
+ priceConfirmed: (body as any).price_confirmed === true,
2783
2644
  trialing: isTrialing,
2784
2645
  });
2785
2646
 
2786
2647
  // Handle validation errors (PRICE_UNAVAILABLE, PRICE_UNSTABLE, PRICE_CHANGED, RATE_BELOW_SLIPPAGE_LIMIT)
2787
2648
  if (quoteResult.validationError) {
2788
2649
  const { code, message, changePercent, currentRate, minAcceptableRate } = quoteResult.validationError;
2789
- // RATE_BELOW_SLIPPAGE_LIMIT: 400 - user needs to update slippage settings
2790
- // PRICE_CHANGED: 409 - price changed, needs confirmation
2791
- // PRICE_UNAVAILABLE/PRICE_UNSTABLE: 503 - service temporarily unavailable
2792
2650
  let statusCode = 503;
2793
2651
  if (code === 'RATE_BELOW_SLIPPAGE_LIMIT') {
2794
2652
  statusCode = 400;
2795
2653
  } else if (code === 'PRICE_CHANGED') {
2796
2654
  statusCode = 409;
2797
2655
  }
2798
- return res.status(statusCode).json({
2799
- code,
2800
- error: message,
2801
- change_percent: changePercent,
2802
- // Include extra data for PRICE_CHANGED to help frontend display confirmation dialog
2803
- ...(code === 'PRICE_CHANGED' && {
2804
- requires_confirmation: true,
2805
- }),
2806
- // Include rate info for RATE_BELOW_SLIPPAGE_LIMIT to help frontend display update dialog
2807
- ...(code === 'RATE_BELOW_SLIPPAGE_LIMIT' && {
2808
- current_rate: currentRate,
2809
- min_acceptable_rate: minAcceptableRate,
2810
- }),
2811
- });
2656
+ return c.json(
2657
+ {
2658
+ code,
2659
+ error: message,
2660
+ change_percent: changePercent,
2661
+ ...(code === 'PRICE_CHANGED' && { requires_confirmation: true }),
2662
+ ...(code === 'RATE_BELOW_SLIPPAGE_LIMIT' && {
2663
+ current_rate: currentRate,
2664
+ min_acceptable_rate: minAcceptableRate,
2665
+ }),
2666
+ },
2667
+ statusCode as any
2668
+ );
2812
2669
  }
2813
2670
 
2814
2671
  consumedQuotes = quoteResult.consumedQuotes;
@@ -2824,7 +2681,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2824
2681
  checkoutSession,
2825
2682
  lineItems: expandedLineItems,
2826
2683
  paymentCurrencyId: paymentCurrency.id,
2827
- quotesInput: req.body.quotes,
2684
+ quotesInput: (body as any).quotes,
2828
2685
  strict: true,
2829
2686
  });
2830
2687
  consumedQuotes = quoteResult.consumedQuotes;
@@ -2834,7 +2691,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2834
2691
  }
2835
2692
  } catch (err) {
2836
2693
  if (err instanceof CustomError) {
2837
- const errCode = getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID';
2694
+ const errCode = getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID';
2838
2695
  if (
2839
2696
  [
2840
2697
  'QUOTE_AMOUNT_MISMATCH',
@@ -2844,41 +2701,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2844
2701
  'QUOTE_LOCK_EXPIRED',
2845
2702
  ].includes(String(errCode))
2846
2703
  ) {
2847
- return res.status(409).json({ code: String(errCode), error: err.message });
2704
+ return c.json({ code: String(errCode), error: (err as any).message }, 409);
2848
2705
  }
2849
- return res.status(400).json({ code: String(errCode), error: err.message });
2706
+ return c.json({ code: String(errCode), error: (err as any).message }, 400);
2850
2707
  }
2851
2708
  throw err;
2852
2709
  }
2853
2710
  }
2854
2711
 
2855
- // Save quote_id and quote_currency_id to CheckoutSession.line_items for future reference
2856
- // Preserve all existing fields in line_items, but update quote-related fields
2857
- // Quote data is the single source of truth in PriceQuote table
2858
- // Also update metadata.slippage with min_acceptable_rate for subscription creation
2859
2712
  const quoteWithMinRate = consumedQuotes.find((q) => q.min_acceptable_rate);
2860
2713
  let updatedMetadata = checkoutSession.metadata;
2861
2714
 
2862
2715
  if (quoteWithMinRate) {
2863
- // Use min_acceptable_rate from quote
2864
2716
  updatedMetadata = {
2865
2717
  ...(checkoutSession.metadata || {}),
2866
2718
  slippage: {
2867
2719
  ...((checkoutSession.metadata as any)?.slippage || {}),
2868
- mode: 'rate', // Use 'rate' mode so buildSlippageConfig will include min_acceptable_rate
2720
+ mode: 'rate',
2869
2721
  min_acceptable_rate: quoteWithMinRate.min_acceptable_rate,
2870
2722
  percent: quoteWithMinRate.slippage_percent ?? 0.5,
2871
2723
  updated_at_ms: Date.now(),
2872
2724
  },
2873
2725
  };
2874
2726
  } else {
2875
- // No quote (e.g., trial period) - calculate min_acceptable_rate from current rate if needed
2876
2727
  const existingSlippage = (checkoutSession.metadata as any)?.slippage;
2877
2728
  const hasDynamicPricing = expandedLineItems.some(
2878
2729
  (item) => ((item.upsell_price || item.price) as any)?.pricing_type === 'dynamic'
2879
2730
  );
2880
2731
 
2881
- // If dynamic pricing and no min_acceptable_rate yet, calculate from current rate
2882
2732
  if (hasDynamicPricing && existingSlippage && !existingSlippage.min_acceptable_rate) {
2883
2733
  try {
2884
2734
  const rateResult = await fetchCurrentExchangeRate(paymentCurrency, paymentMethod);
@@ -2886,14 +2736,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2886
2736
  const rate = Number(rateResult.rate);
2887
2737
  const percent = existingSlippage.percent ?? DEFAULT_SLIPPAGE_PERCENT;
2888
2738
  if (Number.isFinite(rate) && rate > 0) {
2889
- // min_rate = rate / (1 + percent/100)
2890
2739
  const minRate = rate / (1 + percent / 100);
2891
2740
  updatedMetadata = {
2892
2741
  ...(checkoutSession.metadata || {}),
2893
2742
  slippage: {
2894
2743
  ...existingSlippage,
2895
2744
  min_acceptable_rate: minRate.toFixed(8).replace(/\.?0+$/, ''),
2896
- base_currency: 'USD', // Exchange rates are always USD-based
2745
+ base_currency: 'USD',
2897
2746
  updated_at_ms: Date.now(),
2898
2747
  },
2899
2748
  };
@@ -2910,7 +2759,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2910
2759
  sessionId: checkoutSession.id,
2911
2760
  error: error.message,
2912
2761
  });
2913
- // Continue without min_acceptable_rate - not a fatal error
2914
2762
  }
2915
2763
  }
2916
2764
  }
@@ -2921,16 +2769,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2921
2769
  const baseItem = checkoutSession.line_items.find((li: any) => li.price_id === item.price_id);
2922
2770
  const quoteId = (item as any).quote_id || (baseItem as any)?.quote_id;
2923
2771
  const quoteCurrencyId = (item as any).quote_currency_id || (baseItem as any)?.quote_currency_id;
2924
- const result: any = {
2925
- ...baseItem, // Preserve all original fields
2926
- };
2772
+ const result: any = { ...baseItem };
2927
2773
  if (quoteId) {
2928
- result.quote_id = String(quoteId); // Ensure quote_id is string
2929
- // Also save quote_currency_id so backend can validate custom_amount against currency
2774
+ result.quote_id = String(quoteId);
2930
2775
  if (quoteCurrencyId) {
2931
2776
  result.quote_currency_id = quoteCurrencyId;
2932
2777
  }
2933
- // Update custom_amount from the new quote if available
2934
2778
  if ((item as any).custom_amount) {
2935
2779
  result.custom_amount = (item as any).custom_amount;
2936
2780
  }
@@ -2946,16 +2790,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2946
2790
  savedMinAcceptableRate: quoteWithMinRate?.min_acceptable_rate,
2947
2791
  });
2948
2792
 
2949
- let customer = await Customer.findOne({ where: { did: req.user.did } });
2793
+ let customer = await Customer.findOne({ where: { did: c.get('user').did } });
2950
2794
  if (!customer) {
2951
- const { user: userInfo } = await blocklet.getUser(req.user.did);
2795
+ const { user: userInfo } = await blocklet.getUser(c.get('user').did);
2952
2796
  customer = await Customer.create({
2953
2797
  livemode: !!checkoutSession.livemode,
2954
- did: req.user.did,
2955
- name: req.body.customer_name,
2956
- email: req.body.customer_email || userInfo?.email || '',
2957
- phone: req.body.customer_phone || userInfo?.phone || '',
2958
- address: req.body.billing_address || Customer.formatAddressFromUser(userInfo),
2798
+ did: c.get('user').did,
2799
+ name: (body as any).customer_name,
2800
+ email: (body as any).customer_email || userInfo?.email || '',
2801
+ phone: (body as any).customer_phone || userInfo?.phone || '',
2802
+ address: (body as any).billing_address || Customer.formatAddressFromUser(userInfo),
2959
2803
  description: userInfo?.remark || '',
2960
2804
  metadata: {},
2961
2805
  balance: '0',
@@ -2963,7 +2807,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2963
2807
  delinquent: false,
2964
2808
  invoice_prefix: Customer.getInvoicePrefix(),
2965
2809
  });
2966
- logger.info('customer created on checkout session submit', { did: req.user.did, id: customer.id });
2810
+ logger.info('customer created on checkout session submit', { did: c.get('user').did, id: customer.id });
2967
2811
  try {
2968
2812
  await blocklet.updateUserAddress(
2969
2813
  {
@@ -2972,48 +2816,39 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
2972
2816
  },
2973
2817
  {
2974
2818
  headers: {
2975
- cookie: req.headers.cookie || '',
2819
+ cookie: c.req.header('cookie') || '',
2976
2820
  },
2977
2821
  }
2978
2822
  );
2979
- logger.info('updateUserAddress success', {
2980
- did: customer.did,
2981
- });
2823
+ logger.info('updateUserAddress success', { did: customer.did });
2982
2824
  } catch (err) {
2983
- logger.error('updateUserAddress failed', {
2984
- error: err,
2985
- customerId: customer.id,
2986
- });
2825
+ logger.error('updateUserAddress failed', { error: err, customerId: customer.id });
2987
2826
  }
2988
2827
  } else {
2989
2828
  const updates: Record<string, any> = {};
2990
2829
  if (checkoutSession.customer_update?.name) {
2991
- if (req.body.customer_name) {
2992
- updates.name = req.body.customer_name;
2830
+ if ((body as any).customer_name) {
2831
+ updates.name = (body as any).customer_name;
2993
2832
  }
2994
- if (req.body.customer_email) {
2995
- updates.email = req.body.customer_email;
2833
+ if ((body as any).customer_email) {
2834
+ updates.email = (body as any).customer_email;
2996
2835
  }
2997
- if (req.body.customer_phone) {
2998
- updates.phone = req.body.customer_phone;
2836
+ if ((body as any).customer_phone) {
2837
+ updates.phone = (body as any).customer_phone;
2999
2838
  }
3000
2839
  }
3001
2840
  if (checkoutSession.customer_update?.address) {
3002
- updates.address = Customer.formatUpdateAddress(req.body.billing_address, customer);
2841
+ updates.address = Customer.formatUpdateAddress((body as any).billing_address, customer);
3003
2842
  }
3004
2843
  if (!customer.invoice_prefix) {
3005
2844
  updates.invoice_prefix = Customer.getInvoicePrefix();
3006
2845
  }
3007
2846
 
3008
2847
  if (!hasObjectChanged(updates, customer, { deepCompare: ['address'] })) {
3009
- logger.info('customer update skipped (no changes)', {
3010
- did: customer.did,
3011
- });
2848
+ logger.info('customer update skipped (no changes)', { did: customer.did });
3012
2849
  } else {
3013
2850
  await customer.update(updates);
3014
- logger.info('customer updated', {
3015
- did: customer.did,
3016
- });
2851
+ logger.info('customer updated', { did: customer.did });
3017
2852
 
3018
2853
  try {
3019
2854
  await blocklet.updateUserAddress(
@@ -3025,18 +2860,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3025
2860
  },
3026
2861
  {
3027
2862
  headers: {
3028
- cookie: req.headers.cookie || '',
2863
+ cookie: c.req.header('cookie') || '',
3029
2864
  },
3030
2865
  }
3031
2866
  );
3032
- logger.info('updateUserAddress success', {
3033
- did: customer.did,
3034
- });
2867
+ logger.info('updateUserAddress success', { did: customer.did });
3035
2868
  } catch (err) {
3036
- logger.error('updateUserAddress failed', {
3037
- error: err,
3038
- customerId: customer.id,
3039
- });
2869
+ logger.error('updateUserAddress failed', { error: err, customerId: customer.id });
3040
2870
  }
3041
2871
  }
3042
2872
  }
@@ -3044,57 +2874,55 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3044
2874
  // check if customer can make new purchase
3045
2875
  const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
3046
2876
  if (!canMakeNewPurchase) {
3047
- return res.status(403).json({
3048
- code: 'CUSTOMER_LIMITED',
3049
- error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
3050
- });
2877
+ return c.json(
2878
+ {
2879
+ code: 'CUSTOMER_LIMITED',
2880
+ error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
2881
+ },
2882
+ 403
2883
+ );
3051
2884
  }
3052
2885
 
3053
2886
  // check if user is in blocklist
3054
2887
  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
3055
- const inBlock = await isUserInBlocklist(req.user.did, paymentMethod);
2888
+ const inBlock = await isUserInBlocklist(c.get('user').did, paymentMethod);
3056
2889
  if (inBlock) {
3057
- return res
3058
- .status(403)
3059
- .json({ code: 'PAYMENT_RESTRICTED', error: 'Unable to process your purchase at this time' });
2890
+ return c.json({ code: 'PAYMENT_RESTRICTED', error: 'Unable to process your purchase at this time' }, 403);
3060
2891
  }
3061
2892
  }
3062
2893
 
3063
- await checkoutSession.update({ customer_id: customer.id, customer_did: req.user.did });
2894
+ await checkoutSession.update({ customer_id: customer.id, customer_did: c.get('user').did });
3064
2895
 
3065
- // Verify and recalculate amount with current discounts
3066
2896
  const { lineItems, trialInDays, trialEnd, now } = await calculateAndUpdateAmount(
3067
2897
  checkoutSession,
3068
2898
  paymentCurrency.id,
3069
2899
  true,
3070
- {
3071
- lineItemsOverride: lineItemsWithQuotes,
3072
- }
2900
+ { lineItemsOverride: lineItemsWithQuotes }
3073
2901
  );
3074
2902
 
3075
- // Validate payment amounts meet minimum requirements
3076
2903
  if (paymentMethod.type === 'stripe') {
3077
2904
  const result = await validatePaymentAmounts(lineItems, paymentCurrency, checkoutSession);
3078
2905
  if (!result.valid) {
3079
- return res.status(400).json({ error: result.error });
2906
+ return c.json({ error: result.error }, 400);
3080
2907
  }
3081
2908
  }
3082
2909
 
3083
- // Validate checkout session ownership if it was created for a specific customer
3084
2910
  if (checkoutSession.customer_did && checkoutSession.metadata?.createdBy) {
3085
2911
  const createdByDid = checkoutSession.metadata.createdBy;
3086
- if (createdByDid !== req.user.did) {
3087
- return res.status(403).json({
3088
- error:
3089
- "It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
3090
- });
2912
+ if (createdByDid !== c.get('user').did) {
2913
+ return c.json(
2914
+ {
2915
+ error:
2916
+ "It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
2917
+ },
2918
+ 403
2919
+ );
3091
2920
  }
3092
2921
  }
3093
2922
 
3094
- // Validate promotion codes are still valid during submission
3095
2923
  await validatePromotionCodesOnSubmit(checkoutSession, {
3096
2924
  lineItems,
3097
- customerId: req.user.did,
2925
+ customerId: c.get('user').did,
3098
2926
  amount: checkoutSession.amount_subtotal,
3099
2927
  });
3100
2928
 
@@ -3106,12 +2934,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3106
2934
  }
3107
2935
  } catch (err) {
3108
2936
  logger.error('Failed to destroy exist invoice', {
3109
- error: err.message,
2937
+ error: (err as any).message,
3110
2938
  checkoutSessionId: checkoutSession.id,
3111
2939
  });
3112
2940
  }
3113
2941
 
3114
- // create or update payment intent
3115
2942
  let paymentIntent: PaymentIntent | null = null;
3116
2943
  if (checkoutSession.mode === 'payment') {
3117
2944
  const result = await createOrUpdatePaymentIntent(
@@ -3126,13 +2953,12 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3126
2953
  );
3127
2954
  paymentIntent = result.paymentIntent;
3128
2955
 
3129
- // Create discount records for payment mode immediately after payment intent creation
3130
2956
  if (checkoutSession.discounts?.length) {
3131
2957
  try {
3132
2958
  const discountResult = await createDiscountRecordsForCheckout({
3133
2959
  checkoutSession,
3134
2960
  customerId: customer.id,
3135
- subscriptionIds: [], // No subscriptions for payment mode
2961
+ subscriptionIds: [],
3136
2962
  });
3137
2963
 
3138
2964
  logger.info('Created discount records for checkout session payment', {
@@ -3145,7 +2971,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3145
2971
  logger.error('Failed to create discount records for checkout session payment', {
3146
2972
  checkoutSessionId: checkoutSession.id,
3147
2973
  paymentIntentId: paymentIntent?.id,
3148
- error: discountError.message,
2974
+ error: (discountError as any).message,
3149
2975
  });
3150
2976
  }
3151
2977
  }
@@ -3156,7 +2982,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3156
2982
  }
3157
2983
  }
3158
2984
 
3159
- // SetupIntent processing
3160
2985
  let setupIntent: SetupIntent | null = null;
3161
2986
  if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
3162
2987
  if (checkoutSession.setup_intent_id) {
@@ -3165,10 +2990,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3165
2990
 
3166
2991
  if (setupIntent) {
3167
2992
  if (setupIntent.isImmutable()) {
3168
- return res.status(403).json({ code: 'SETUP_SUCCEEDED', error: 'Checkout session setup completed' });
2993
+ return c.json({ code: 'SETUP_SUCCEEDED', error: 'Checkout session setup completed' }, 403);
3169
2994
  }
3170
2995
  if (setupIntent.status === 'processing') {
3171
- return res.status(403).json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' });
2996
+ return c.json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' }, 403);
3172
2997
  }
3173
2998
  await setupIntent.update({
3174
2999
  status: 'requires_capture',
@@ -3197,7 +3022,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3197
3022
  metadata: checkoutSession.metadata,
3198
3023
  });
3199
3024
 
3200
- // persist setup intent id
3201
3025
  await checkoutSession.update({ setup_intent_id: setupIntent.id });
3202
3026
  logger.info('setupIntent created on checkout session submit', {
3203
3027
  session: checkoutSession.id,
@@ -3206,7 +3030,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3206
3030
  }
3207
3031
  }
3208
3032
 
3209
- // subscription processing
3210
3033
  let subscription: Subscription | null = null;
3211
3034
  let subscriptions: Subscription[] = [];
3212
3035
 
@@ -3245,7 +3068,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3245
3068
  enableSubscriptionGrouping: checkoutSession.enable_subscription_grouping,
3246
3069
  });
3247
3070
 
3248
- // Create discount records immediately after subscriptions are created
3249
3071
  if (checkoutSession.discounts?.length) {
3250
3072
  try {
3251
3073
  const discountResult = await createDiscountRecordsForCheckout({
@@ -3254,7 +3076,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3254
3076
  subscriptionIds: subscriptions.map((s) => s.id),
3255
3077
  });
3256
3078
 
3257
- // Update subscription discount_id references
3258
3079
  if (discountResult.subscriptionsUpdated.length > 0) {
3259
3080
  try {
3260
3081
  const subscriptionUpdateResult = await updateSubscriptionDiscountReferences({
@@ -3270,7 +3091,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3270
3091
  logger.error('Failed to update subscription discount_id references', {
3271
3092
  checkoutSessionId: checkoutSession.id,
3272
3093
  subscriptionsUpdated: discountResult.subscriptionsUpdated,
3273
- error: subscriptionUpdateError.message,
3094
+ error: (subscriptionUpdateError as any).message,
3274
3095
  });
3275
3096
  throw subscriptionUpdateError;
3276
3097
  }
@@ -3285,16 +3106,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3285
3106
  } catch (discountError) {
3286
3107
  logger.error('Failed to create discount records for checkout session', {
3287
3108
  checkoutSessionId: checkoutSession.id,
3288
- error: discountError.message,
3109
+ error: (discountError as any).message,
3289
3110
  });
3290
- return res
3291
- .status(400)
3292
- .json({ error: 'Failed to create discount records for checkout session, please try again later' });
3111
+ return c.json(
3112
+ { error: 'Failed to create discount records for checkout session, please try again later' },
3113
+ 400
3114
+ );
3293
3115
  }
3294
3116
  }
3295
3117
  }
3296
3118
 
3297
- // Prepare discount configuration
3298
3119
  let discountConfig;
3299
3120
  if (checkoutSession.discounts && checkoutSession.discounts.length > 0) {
3300
3121
  const firstDiscount = checkoutSession.discounts[0];
@@ -3307,7 +3128,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3307
3128
  }
3308
3129
  }
3309
3130
 
3310
- // Calculate actual current payment amount (excludes future renewals)
3311
3131
  const trialing = trialInDays > 0 || trialEnd > now;
3312
3132
  const { total: currentPaymentTotal } = await getCheckoutAmount(
3313
3133
  lineItems,
@@ -3332,7 +3152,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3332
3152
  },
3333
3153
  };
3334
3154
 
3335
- // if we can complete purchase with customer balance
3336
3155
  const balance = isCreditSufficientForPayment({
3337
3156
  paymentMethod,
3338
3157
  paymentCurrency,
@@ -3350,7 +3169,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3350
3169
  paymentCurrency.isCredit() ||
3351
3170
  (checkoutSession.mode === 'subscription' && checkoutSession.subscription_data?.no_stake);
3352
3171
  if (isPayment && paymentIntent && canFastPay) {
3353
- // if we can complete purchase without any wallet interaction
3354
3172
  delegation = await isDelegationSufficientForPayment({
3355
3173
  paymentMethod,
3356
3174
  paymentCurrency,
@@ -3365,7 +3183,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3365
3183
  };
3366
3184
  }
3367
3185
  } else if (paymentMethod.type === 'arcblock' && canFastPayForSubscription) {
3368
- // if we can complete purchase without any wallet interaction
3369
3186
  const result = await processSubscriptionFastCheckout({
3370
3187
  checkoutSession,
3371
3188
  customer,
@@ -3383,12 +3200,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3383
3200
  logger.warn(`Fast checkout processing failed: ${result.message}`, {
3384
3201
  checkoutSessionId: checkoutSession.id,
3385
3202
  });
3386
-
3387
3203
  creditSufficient = false;
3388
3204
  } else {
3389
- delegation = {
3390
- sufficient: true,
3391
- };
3205
+ delegation = { sufficient: true };
3392
3206
  canFastPay = true;
3393
3207
  fastPayInfo = {
3394
3208
  type: 'delegation',
@@ -3428,10 +3242,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3428
3242
  const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
3429
3243
  subscriptions[0]) as Subscription;
3430
3244
  if (!stripeContext) {
3431
- stripeContext = {
3432
- type: 'subscription',
3433
- stripe_subscriptions: '',
3434
- };
3245
+ stripeContext = { type: 'subscription', stripe_subscriptions: '' };
3435
3246
  }
3436
3247
  stripeContext.stripe_subscriptions = '';
3437
3248
  await pAll(
@@ -3486,12 +3297,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3486
3297
  status: stripeSubscription.status,
3487
3298
  };
3488
3299
  }
3489
- return {
3490
- stripeSubscription,
3491
- subscription: sub,
3492
- };
3300
+ return { stripeSubscription, subscription: sub };
3493
3301
  }),
3494
- { concurrency: updateDataConcurrency }
3302
+ { concurrency: updateDataConcurrency() }
3495
3303
  );
3496
3304
  if (subscriptions.length > 1) {
3497
3305
  stripeContext.has_multiple_subscriptions = true;
@@ -3514,14 +3322,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3514
3322
  }
3515
3323
 
3516
3324
  logger.info('Checkout session submitted successfully', {
3517
- sessionId: req.params.id,
3325
+ sessionId: c.req.param('id'),
3518
3326
  paymentIntentId: paymentIntent?.id,
3519
3327
  setupIntentId: setupIntent?.id,
3520
3328
  subscriptionId: subscription?.id,
3521
3329
  customerId: customer?.id,
3522
3330
  });
3523
3331
 
3524
- return res.json({
3332
+ return c.json({
3525
3333
  paymentIntent,
3526
3334
  setupIntent,
3527
3335
  stripeContext,
@@ -3543,50 +3351,44 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3543
3351
  quoteService.markAsPaymentFailed(q.id).catch((error) =>
3544
3352
  logger.error('Failed to mark quote as payment failed', {
3545
3353
  quoteId: q.id,
3546
- sessionId: req.params.id,
3547
- error: error.message,
3354
+ sessionId: c.req.param('id'),
3355
+ error: (error as any).message,
3548
3356
  })
3549
3357
  )
3550
3358
  )
3551
3359
  );
3552
3360
  }
3553
3361
  if (err instanceof CustomError) {
3554
- if (err.code === 'RATE_UNAVAILABLE') {
3555
- return res.status(503).json({ code: err.code, error: err.message });
3362
+ if ((err as any).code === 'RATE_UNAVAILABLE') {
3363
+ return c.json({ code: (err as any).code, error: (err as any).message }, 503);
3556
3364
  }
3557
- if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
3558
- return res.status(409).json({ code: String(err.code), error: err.message });
3365
+ if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
3366
+ return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
3559
3367
  }
3560
3368
  }
3561
3369
  logger.error('Error submitting checkout session', {
3562
- sessionId: req.params.id,
3370
+ sessionId: c.req.param('id'),
3563
3371
  error: err,
3564
- stack: err.stack,
3372
+ stack: (err as any).stack,
3565
3373
  });
3566
- res.status(500).json({ code: err.code, error: err.message });
3374
+ return c.json({ code: (err as any).code, error: (err as any).message }, 500);
3567
3375
  }
3568
3376
  });
3569
3377
 
3570
3378
  // 打赏(不强制登录)
3571
- router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) => {
3379
+ app.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (c) => {
3572
3380
  let consumedQuotes: PriceQuote[] = [];
3381
+ const body = c.get('sanitizedBody') ?? {};
3573
3382
  try {
3574
- const checkoutSession = req.doc as CheckoutSession;
3383
+ const checkoutSession = c.get('doc') as CheckoutSession;
3575
3384
  if (!isDonationCheckoutSession(checkoutSession)) {
3576
- return res.status(400).json({
3577
- code: 'INVALID_DONATION',
3578
- error: 'This endpoint is only for donations',
3579
- });
3385
+ return c.json({ code: 'INVALID_DONATION', error: 'This endpoint is only for donations' }, 400);
3580
3386
  }
3581
3387
 
3582
3388
  if (checkoutSession.mode !== 'payment') {
3583
- return res.status(400).json({
3584
- code: 'INVALID_MODE',
3585
- error: 'This endpoint is only for payment mode donations',
3586
- });
3389
+ return c.json({ code: 'INVALID_MODE', error: 'This endpoint is only for payment mode donations' }, 400);
3587
3390
  }
3588
3391
 
3589
- // validate inventory
3590
3392
  if (checkoutSession.line_items) {
3591
3393
  try {
3592
3394
  await validateInventory(checkoutSession.line_items);
@@ -3596,14 +3398,13 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3596
3398
  line_items: checkoutSession.line_items,
3597
3399
  checkoutSessionId: checkoutSession.id,
3598
3400
  });
3599
- return res.status(400).json({ error: err.message });
3401
+ return c.json({ error: (err as any).message }, 400);
3600
3402
  }
3601
3403
  }
3602
3404
 
3603
- // validate payment settings
3604
3405
  const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
3605
- req.body.payment_method,
3606
- req.body.payment_currency
3406
+ (body as any).payment_method,
3407
+ (body as any).payment_currency
3607
3408
  );
3608
3409
  await checkoutSession.update({ currency_id: paymentCurrency.id });
3609
3410
 
@@ -3614,7 +3415,7 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3614
3415
  checkoutSession,
3615
3416
  lineItems: expandedLineItems,
3616
3417
  paymentCurrencyId: paymentCurrency.id,
3617
- quotesInput: req.body.quotes,
3418
+ quotesInput: (body as any).quotes,
3618
3419
  strict: true,
3619
3420
  });
3620
3421
  consumedQuotes = quoteResult.consumedQuotes;
@@ -3624,7 +3425,7 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3624
3425
  }
3625
3426
  } catch (err) {
3626
3427
  if (err instanceof CustomError) {
3627
- const errCode = String(getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID');
3428
+ const errCode = String(getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID');
3628
3429
  if (
3629
3430
  [
3630
3431
  'QUOTE_AMOUNT_MISMATCH',
@@ -3634,14 +3435,13 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3634
3435
  'QUOTE_LOCK_EXPIRED',
3635
3436
  ].includes(errCode)
3636
3437
  ) {
3637
- return res.status(409).json({ code: errCode, error: err.message });
3438
+ return c.json({ code: errCode, error: (err as any).message }, 409);
3638
3439
  }
3639
- return res.status(400).json({ code: errCode, error: err.message });
3440
+ return c.json({ code: errCode, error: (err as any).message }, 400);
3640
3441
  }
3641
3442
  throw err;
3642
3443
  }
3643
3444
 
3644
- // calculate amount and update checkout session
3645
3445
  const { lineItems } = await calculateAndUpdateAmount(checkoutSession, paymentCurrency.id, false, {
3646
3446
  lineItemsOverride: lineItemsWithQuotes,
3647
3447
  });
@@ -3661,12 +3461,12 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3661
3461
  await quoteService.markQuotesAsPaidByInvoice(paymentIntent.invoice_id);
3662
3462
  }
3663
3463
 
3664
- return res.json({
3464
+ return c.json({
3665
3465
  paymentIntent,
3666
3466
  checkoutSession,
3667
3467
  paymentMethod,
3668
3468
  paymentCurrency,
3669
- formData: req.body,
3469
+ formData: body,
3670
3470
  });
3671
3471
  } catch (err) {
3672
3472
  if (consumedQuotes.length) {
@@ -3676,45 +3476,49 @@ router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) =>
3676
3476
  quoteService.markAsPaymentFailed(q.id).catch((error) =>
3677
3477
  logger.error('Failed to mark quote as payment failed', {
3678
3478
  quoteId: q.id,
3679
- sessionId: req.params.id,
3680
- error: error.message,
3479
+ sessionId: c.req.param('id'),
3480
+ error: (error as any).message,
3681
3481
  })
3682
3482
  )
3683
3483
  )
3684
3484
  );
3685
3485
  }
3686
3486
  if (err instanceof CustomError) {
3687
- if (err.code === 'RATE_UNAVAILABLE') {
3688
- return res.status(503).json({ code: err.code, error: err.message });
3487
+ if ((err as any).code === 'RATE_UNAVAILABLE') {
3488
+ return c.json({ code: (err as any).code, error: (err as any).message }, 503);
3689
3489
  }
3690
- if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
3691
- return res.status(409).json({ code: String(err.code), error: err.message });
3490
+ if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
3491
+ return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
3692
3492
  }
3693
3493
  }
3694
3494
  logger.error('Error processing donation submission', {
3695
- sessionId: req.params.id,
3696
- error: err.message,
3697
- stack: err.stack,
3495
+ sessionId: c.req.param('id'),
3496
+ error: (err as any).message,
3497
+ stack: (err as any).stack,
3698
3498
  });
3699
- res.status(400).json({ code: err.code, error: err.message });
3499
+ return c.json({ code: (err as any).code, error: (err as any).message }, 400);
3700
3500
  }
3701
3501
  });
3702
3502
 
3703
- router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async (req, res) => {
3503
+ app.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async (c) => {
3704
3504
  let consumedQuotes: PriceQuote[] = [];
3505
+ const body = c.get('sanitizedBody') ?? {};
3705
3506
  try {
3706
- if (!req.user) {
3707
- return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
3507
+ if (!c.get('user')) {
3508
+ return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
3708
3509
  }
3709
3510
 
3710
- if (stopAcceptingOrders) {
3711
- return res.status(422).json({
3712
- code: 'STOP_ACCEPTING_ORDERS',
3713
- error: 'We are not accepting new orders at this time. Please try again later.',
3714
- });
3511
+ if (stopAcceptingOrders()) {
3512
+ return c.json(
3513
+ {
3514
+ code: 'STOP_ACCEPTING_ORDERS',
3515
+ error: 'We are not accepting new orders at this time. Please try again later.',
3516
+ },
3517
+ 422
3518
+ );
3715
3519
  }
3716
3520
 
3717
- const checkoutSession = req.doc as CheckoutSession;
3521
+ const checkoutSession = c.get('doc') as CheckoutSession;
3718
3522
 
3719
3523
  if (checkoutSession.line_items) {
3720
3524
  try {
@@ -3725,56 +3529,57 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3725
3529
  line_items: checkoutSession.line_items,
3726
3530
  checkoutSessionId: checkoutSession.id,
3727
3531
  });
3728
- return res.status(400).json({ error: err.message });
3532
+ return c.json({ error: (err as any).message }, 400);
3729
3533
  }
3730
3534
  }
3731
- // validate cross sell
3732
3535
  if (checkoutSession.cross_sell_behavior === 'required') {
3733
3536
  if (checkoutSession.line_items.some((x) => x.cross_sell) === false) {
3734
3537
  const result = await getCrossSellItem(checkoutSession);
3735
3538
  // @ts-ignore
3736
3539
  if (result.id) {
3737
- return res
3738
- .status(400)
3739
- .json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' });
3540
+ return c.json({ code: 'REQUIRE_CROSS_SELL', error: 'Please select cross sell product to continue' }, 400);
3740
3541
  }
3741
3542
  }
3742
3543
  }
3743
3544
 
3744
- // Parallel: load currency + customer (independent lookups)
3745
3545
  const [paymentCurrency, customer] = await Promise.all([
3746
3546
  PaymentCurrency.findByPk(checkoutSession.currency_id),
3747
- Customer.findByPkOrDid(req.user.did),
3547
+ Customer.findByPkOrDid(c.get('user').did),
3748
3548
  ]);
3749
3549
  if (!paymentCurrency) {
3750
- return res.status(400).json({ error: 'Payment currency not found' });
3550
+ return c.json({ error: 'Payment currency not found' }, 400);
3751
3551
  }
3752
3552
  if (!customer) {
3753
- return res.status(400).json({ error: '' });
3553
+ return c.json({ error: '' }, 400);
3754
3554
  }
3755
3555
 
3756
3556
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
3757
3557
  if (!paymentMethod) {
3758
- return res.status(400).json({ error: 'Payment method not found' });
3558
+ return c.json({ error: 'Payment method not found' }, 400);
3759
3559
  }
3760
3560
 
3761
- // Validate checkout session ownership if it was created for a specific customer
3762
3561
  if (checkoutSession.customer_id && checkoutSession.metadata?.createdBy) {
3763
3562
  const createdByDid = checkoutSession.metadata.createdBy;
3764
- if (createdByDid !== req.user.did) {
3765
- return res.status(403).json({
3766
- error:
3767
- "It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
3768
- });
3563
+ if (createdByDid !== c.get('user').did) {
3564
+ return c.json(
3565
+ {
3566
+ error:
3567
+ "It's not allowed to submit checkout sessions created by other users, please create your own checkout session",
3568
+ },
3569
+ 403
3570
+ );
3769
3571
  }
3770
3572
  }
3771
3573
 
3772
3574
  const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
3773
3575
  if (!canMakeNewPurchase) {
3774
- return res.status(403).json({
3775
- code: 'CUSTOMER_LIMITED',
3776
- error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
3777
- });
3576
+ return c.json(
3577
+ {
3578
+ code: 'CUSTOMER_LIMITED',
3579
+ error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
3580
+ },
3581
+ 403
3582
+ );
3778
3583
  }
3779
3584
 
3780
3585
  const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
@@ -3784,7 +3589,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3784
3589
  checkoutSession,
3785
3590
  lineItems: expandedLineItems,
3786
3591
  paymentCurrencyId: paymentCurrency.id,
3787
- quotesInput: req.body.quotes,
3592
+ quotesInput: (body as any).quotes,
3788
3593
  strict: true,
3789
3594
  });
3790
3595
  consumedQuotes = quoteResult.consumedQuotes;
@@ -3794,7 +3599,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3794
3599
  }
3795
3600
  } catch (err) {
3796
3601
  if (err instanceof CustomError) {
3797
- const errCode = String(getQuoteErrorCode(err) || err.code || 'QUOTE_INVALID');
3602
+ const errCode = String(getQuoteErrorCode(err) || (err as any).code || 'QUOTE_INVALID');
3798
3603
  if (
3799
3604
  [
3800
3605
  'QUOTE_AMOUNT_MISMATCH',
@@ -3804,9 +3609,9 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3804
3609
  'QUOTE_LOCK_EXPIRED',
3805
3610
  ].includes(errCode)
3806
3611
  ) {
3807
- return res.status(409).json({ code: errCode, error: err.message });
3612
+ return c.json({ code: errCode, error: (err as any).message }, 409);
3808
3613
  }
3809
- return res.status(400).json({ code: errCode, error: err.message });
3614
+ return c.json({ code: errCode, error: (err as any).message }, 400);
3810
3615
  }
3811
3616
  throw err;
3812
3617
  }
@@ -3815,9 +3620,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3815
3620
  checkoutSession,
3816
3621
  paymentCurrency.id,
3817
3622
  true,
3818
- {
3819
- lineItemsOverride: lineItemsWithQuotes,
3820
- }
3623
+ { lineItemsOverride: lineItemsWithQuotes }
3821
3624
  );
3822
3625
 
3823
3626
  let paymentIntent: PaymentIntent | null = null;
@@ -3862,15 +3665,12 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3862
3665
  customer,
3863
3666
  amount: fastCheckoutAmount,
3864
3667
  });
3865
- // Check if this is a credit payment
3866
3668
  const isCredit = paymentCurrency.isCredit();
3867
-
3868
3669
  const isPayment = checkoutSession.mode === 'payment';
3869
3670
  let fastPaid = false;
3870
3671
  let canFastPay = isPayment && (await canPayWithDelegation(paymentIntent?.beneficiaries || [], paymentMethod));
3871
3672
  let delegation: SufficientForPaymentResult | null = null;
3872
3673
 
3873
- // Handle credit payment directly
3874
3674
  if (isCredit) {
3875
3675
  const result = await isCreditGrantSufficientForPayment({
3876
3676
  paymentMethod,
@@ -3879,15 +3679,16 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3879
3679
  amount: fastCheckoutAmount,
3880
3680
  });
3881
3681
  if (!result.sufficient) {
3882
- return res.status(400).json({
3883
- code: 'CREDIT_INSUFFICIENT',
3884
- error: result.reason,
3885
- sufficient: result.sufficient,
3886
- });
3682
+ return c.json(
3683
+ {
3684
+ code: 'CREDIT_INSUFFICIENT',
3685
+ error: result.reason,
3686
+ sufficient: result.sufficient,
3687
+ },
3688
+ 400
3689
+ );
3887
3690
  }
3888
3691
  fastPaid = true;
3889
- // For credit payments, we use the existing subscription fast checkout flow
3890
- // but skip the actual payment processing
3891
3692
  if (['setup', 'subscription'].includes(checkoutSession.mode)) {
3892
3693
  const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
3893
3694
  const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
@@ -3928,11 +3729,9 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3928
3729
  });
3929
3730
  await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
3930
3731
  }),
3931
- { concurrency: updateDataConcurrency }
3732
+ { concurrency: updateDataConcurrency() }
3932
3733
  );
3933
- delegation = {
3934
- sufficient: true,
3935
- };
3734
+ delegation = { sufficient: true };
3936
3735
  canFastPay = true;
3937
3736
  }
3938
3737
  } else if (isPayment && paymentIntent && canFastPay) {
@@ -3969,7 +3768,6 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3969
3768
  ) {
3970
3769
  const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
3971
3770
  const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
3972
- // if we can complete purchase without any wallet interaction
3973
3771
  const result = await processSubscriptionFastCheckout({
3974
3772
  checkoutSession,
3975
3773
  customer,
@@ -3987,13 +3785,11 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3987
3785
  });
3988
3786
  } else {
3989
3787
  fastPaid = true;
3990
- delegation = {
3991
- sufficient: true,
3992
- };
3788
+ delegation = { sufficient: true };
3993
3789
  canFastPay = true;
3994
3790
  }
3995
3791
  } else if (paymentMethod.type !== 'arcblock') {
3996
- return res.status(400).json({ error: 'Payment method not supported for fast checkout' });
3792
+ return c.json({ error: 'Payment method not supported for fast checkout' }, 400);
3997
3793
  }
3998
3794
 
3999
3795
  if (paymentIntent?.status === 'succeeded' && paymentIntent?.invoice_id) {
@@ -4001,12 +3797,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
4001
3797
  await quoteService.markQuotesAsPaidByInvoice(paymentIntent.invoice_id);
4002
3798
  }
4003
3799
 
4004
- return res.json({
4005
- paymentIntent,
4006
- checkoutSession,
4007
- customer,
4008
- fastPaid,
4009
- });
3800
+ return c.json({ paymentIntent, checkoutSession, customer, fastPaid });
4010
3801
  } catch (err) {
4011
3802
  if (consumedQuotes.length) {
4012
3803
  const quoteService = getQuoteService();
@@ -4015,139 +3806,132 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
4015
3806
  quoteService.markAsPaymentFailed(q.id).catch((error) =>
4016
3807
  logger.error('Failed to mark quote as payment failed', {
4017
3808
  quoteId: q.id,
4018
- sessionId: req.params.id,
4019
- error: error.message,
3809
+ sessionId: c.req.param('id'),
3810
+ error: (error as any).message,
4020
3811
  })
4021
3812
  )
4022
3813
  )
4023
3814
  );
4024
3815
  }
4025
3816
  if (err instanceof CustomError) {
4026
- if (err.code === 'RATE_UNAVAILABLE') {
4027
- return res.status(503).json({ code: err.code, error: err.message });
3817
+ if ((err as any).code === 'RATE_UNAVAILABLE') {
3818
+ return c.json({ code: (err as any).code, error: (err as any).message }, 503);
4028
3819
  }
4029
- if (['QUOTE_LOCK_EXPIRED'].includes(String(err.code))) {
4030
- return res.status(409).json({ code: String(err.code), error: err.message });
3820
+ if (['QUOTE_LOCK_EXPIRED'].includes(String((err as any).code))) {
3821
+ return c.json({ code: String((err as any).code), error: (err as any).message }, 409);
4031
3822
  }
4032
3823
  }
4033
3824
  logger.error('Error confirming fast checkout', {
4034
- sessionId: req.params.id,
4035
- error: err.message,
4036
- stack: err.stack,
3825
+ sessionId: c.req.param('id'),
3826
+ error: (err as any).message,
3827
+ stack: (err as any).stack,
4037
3828
  });
4038
- res.status(500).json({ code: err.code, error: err.message });
3829
+ return c.json({ code: (err as any).code, error: (err as any).message }, 500);
4039
3830
  }
4040
3831
  });
4041
3832
 
4042
3833
  // upsell
4043
- router.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (req, res) => {
3834
+ app.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (c) => {
3835
+ const body = c.get('sanitizedBody') ?? {};
4044
3836
  try {
4045
- // validate session
4046
- const checkoutSession = req.doc as CheckoutSession;
3837
+ const checkoutSession = c.get('doc') as CheckoutSession;
4047
3838
 
4048
3839
  if (checkoutSession.line_items) {
4049
- // validate line items
4050
3840
  if (checkoutSession.line_items.length > 1) {
4051
- return res.status(400).json({ error: 'Upsell not supported for checkoutSession with multiple line items' });
3841
+ return c.json({ error: 'Upsell not supported for checkoutSession with multiple line items' }, 400);
4052
3842
  }
4053
3843
 
4054
- // validate from and to
4055
- const [from, to] = await Promise.all([Price.findByPk(req.body.from), Price.findByPk(req.body.to)]);
3844
+ const [from, to] = await Promise.all([Price.findByPk((body as any).from), Price.findByPk((body as any).to)]);
4056
3845
  if (!from) {
4057
- return res.status(400).json({ error: 'Upsell from price not found' });
3846
+ return c.json({ error: 'Upsell from price not found' }, 400);
4058
3847
  }
4059
3848
  if (!to) {
4060
- return res.status(400).json({ error: 'Upsell to price not found' });
3849
+ return c.json({ error: 'Upsell to price not found' }, 400);
4061
3850
  }
4062
3851
 
4063
3852
  if (canUpsell(from, to) === false) {
4064
- return res.status(400).json({ error: `Upsell not possible from ${from} to ${to}` });
3853
+ return c.json({ error: `Upsell not possible from ${from} to ${to}` }, 400);
4065
3854
  }
4066
3855
 
4067
3856
  const index = checkoutSession.line_items.findIndex((x) => x.price_id === from.id);
4068
3857
  if (index === -1) {
4069
- return res.status(400).json({ error: 'Upsell from not exist in checkoutSession line items' });
3858
+ return c.json({ error: 'Upsell from not exist in checkoutSession line items' }, 400);
4070
3859
  }
4071
3860
 
4072
3861
  const items = cloneDeep(checkoutSession.line_items);
4073
3862
  items[index] = merge(items[index], { upsell_price_id: to.id });
4074
3863
  await checkoutSession.update({ line_items: items });
4075
- logger.info('CheckoutSession updated on upsell', { id: req.params.id, from: from.id, to: to.id });
3864
+ logger.info('CheckoutSession updated on upsell', { id: c.req.param('id'), from: from.id, to: to.id });
4076
3865
 
4077
- // recalculate amount
4078
3866
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
4079
3867
  }
4080
3868
 
4081
3869
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
4082
- res.json({ ...checkoutSession.toJSON(), line_items: items });
3870
+ return c.json({ ...checkoutSession.toJSON(), line_items: items });
4083
3871
  } catch (err) {
4084
3872
  logger.error(err);
4085
- res.status(500).json({ error: err.message });
3873
+ return c.json({ error: (err as any).message }, 500);
4086
3874
  }
4087
3875
  });
4088
3876
 
4089
- router.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (req, res) => {
3877
+ app.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (c) => {
3878
+ const body = c.get('sanitizedBody') ?? {};
4090
3879
  try {
4091
- // validate session
4092
- const checkoutSession = req.doc as CheckoutSession;
3880
+ const checkoutSession = c.get('doc') as CheckoutSession;
4093
3881
 
4094
- // validate from
4095
- const from = await Price.findByPk(req.body.from);
3882
+ const from = await Price.findByPk((body as any).from);
4096
3883
  if (!from) {
4097
- return res.status(400).json({ error: 'Upsell from price not found' });
3884
+ return c.json({ error: 'Upsell from price not found' }, 400);
4098
3885
  }
4099
3886
 
4100
3887
  if (checkoutSession.line_items) {
4101
3888
  const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === from.id);
4102
3889
  if (index === -1) {
4103
- return res.status(400).json({ error: 'Upsell not configured for checkout session' });
3890
+ return c.json({ error: 'Upsell not configured for checkout session' }, 400);
4104
3891
  }
4105
3892
 
4106
3893
  const items = cloneDeep(checkoutSession.line_items);
4107
3894
  items[index] = merge(items[index], { upsell_price_id: '' });
4108
3895
  await checkoutSession.update({ line_items: items });
4109
- logger.info('CheckoutSession updated on downsell', { id: req.params.id, from: from.id });
3896
+ logger.info('CheckoutSession updated on downsell', { id: c.req.param('id'), from: from.id });
4110
3897
 
4111
- // recalculate amount
4112
3898
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
4113
3899
  }
4114
3900
 
4115
3901
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
4116
3902
  logger.info('Checkout session updated after downsell', {
4117
- sessionId: req.params.id,
3903
+ sessionId: c.req.param('id'),
4118
3904
  fromPriceId: from.id,
4119
3905
  newAmount: checkoutSession.amount_total,
4120
3906
  });
4121
- res.json({ ...checkoutSession.toJSON(), line_items: items });
3907
+ return c.json({ ...checkoutSession.toJSON(), line_items: items });
4122
3908
  } catch (err) {
4123
3909
  logger.error('Error processing downsell', {
4124
- sessionId: req.params.id,
4125
- error: err.message,
4126
- stack: err.stack,
3910
+ sessionId: c.req.param('id'),
3911
+ error: (err as any).message,
3912
+ stack: (err as any).stack,
4127
3913
  });
4128
- res.status(500).json({ error: err.message });
3914
+ return c.json({ error: (err as any).message }, 500);
4129
3915
  }
4130
3916
  });
4131
3917
 
4132
3918
  // adjust quantity
4133
- router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req, res) => {
3919
+ app.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (c) => {
3920
+ const body = c.get('sanitizedBody') ?? {};
4134
3921
  try {
4135
- const checkoutSession = req.doc as CheckoutSession;
4136
- const { itemId, quantity, currency_id: currencyIdInput } = req.body;
3922
+ const checkoutSession = c.get('doc') as CheckoutSession;
3923
+ const { itemId, quantity, currency_id: currencyIdInput } = body as any;
4137
3924
  if (!checkoutSession.line_items) {
4138
- return res.status(400).json({ error: 'Line items not found' });
3925
+ return c.json({ error: 'Line items not found' }, 400);
4139
3926
  }
4140
3927
 
4141
3928
  const item = checkoutSession.line_items.find((x) => x.price_id === itemId);
4142
3929
  if (!item) {
4143
- return res.status(400).json({ error: 'Item not found' });
3930
+ return c.json({ error: 'Item not found' }, 400);
4144
3931
  }
4145
3932
 
4146
- // Final Freeze: When adjusting quantity, clear all quote-related fields
4147
- // Quote will be created only at Submit time
4148
3933
  const items = cloneDeep(checkoutSession.line_items).map((lineItem: any) => {
4149
3934
  const cleaned = { ...lineItem };
4150
- // Clear quote-related fields - price will be calculated by frontend using live rate
4151
3935
  delete cleaned.quote_id;
4152
3936
  delete cleaned.quoted_amount;
4153
3937
  delete cleaned.custom_amount;
@@ -4166,18 +3950,15 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
4166
3950
  }
4167
3951
  await validateInventory(items, true);
4168
3952
 
4169
- // Build update payload - clear quote_locked_at since we're in preview mode
4170
3953
  const updatePayload: { line_items: any; currency_id?: string; metadata?: any } = { line_items: items };
4171
3954
  if (currencyIdInput && currencyIdInput !== checkoutSession.currency_id) {
4172
3955
  updatePayload.currency_id = currencyIdInput;
4173
3956
  }
4174
- // Clear quote lock since quantity changed
4175
3957
  if (checkoutSession.metadata?.quote_locked_at) {
4176
3958
  updatePayload.metadata = { ...checkoutSession.metadata, quote_locked_at: undefined };
4177
3959
  }
4178
3960
  await checkoutSession.update(updatePayload);
4179
3961
 
4180
- // Also clear quote_locked_at on payment intent if exists
4181
3962
  if (checkoutSession.payment_intent_id) {
4182
3963
  const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
4183
3964
  if (paymentIntent?.quote_locked_at) {
@@ -4185,7 +3966,6 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
4185
3966
  }
4186
3967
  }
4187
3968
 
4188
- // Expand line items for response (no quote enrichment needed - frontend uses live rate)
4189
3969
  const lineItems = await Price.expand(checkoutSession.line_items, { upsell: true });
4190
3970
 
4191
3971
  logger.info('Adjusted quantity and cleared quote data', {
@@ -4194,76 +3974,77 @@ router.put('/:id/adjust-quantity', user, ensureCheckoutSessionOpen, async (req,
4194
3974
  quantity,
4195
3975
  });
4196
3976
 
4197
- res.json({ ...checkoutSession.toJSON(), line_items: lineItems, quotes: {}, rateUnavailable: false });
3977
+ return c.json({ ...checkoutSession.toJSON(), line_items: lineItems, quotes: {}, rateUnavailable: false });
4198
3978
  } catch (err) {
4199
3979
  logger.error(err);
4200
- res.status(400).json({ error: err.message });
3980
+ return c.json({ error: (err as any).message }, 400);
4201
3981
  }
4202
3982
  });
4203
- // eslint-disable-next-line consistent-return
4204
- router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
4205
- const doc = req.doc as CheckoutSession;
3983
+
3984
+ app.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (c) => {
3985
+ const doc = c.get('doc') as CheckoutSession;
4206
3986
 
4207
3987
  if (doc.status === 'complete') {
4208
- return res.status(400).json({ error: 'Cannot expire checkout session that is already completed' });
3988
+ return c.json({ error: 'Cannot expire checkout session that is already completed' }, 400);
4209
3989
  }
4210
3990
  if (doc.status === 'expired') {
4211
- return res.status(400).json({ error: 'Cannot expire checkout session that is already expired' });
3991
+ return c.json({ error: 'Cannot expire checkout session that is already expired' }, 400);
4212
3992
  }
4213
3993
  if (doc.payment_status === 'paid') {
4214
- return res.status(400).json({ error: 'Cannot expire checkout session that is already paid' });
3994
+ return c.json({ error: 'Cannot expire checkout session that is already paid' }, 400);
4215
3995
  }
4216
3996
 
4217
3997
  await doc.update({ status: 'expired', expires_at: dayjs().unix() });
4218
3998
  logger.info('Checkout session expired', {
4219
- sessionId: req.params.id,
4220
- userId: req.user?.did,
3999
+ sessionId: c.req.param('id'),
4000
+ userId: c.get('user')?.did,
4221
4001
  expiresAt: doc.expires_at,
4222
4002
  });
4223
4003
 
4224
- res.json(doc);
4004
+ return c.json(doc);
4225
4005
  });
4226
4006
 
4227
4007
  // Return the expanded price to cross-sell-to for the checkout session
4228
- router.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
4008
+ app.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
4229
4009
  try {
4230
- const checkoutSession = req.doc as CheckoutSession;
4231
- const skipError = req.query.skipError === 'true';
4010
+ const checkoutSession = c.get('doc') as CheckoutSession;
4011
+ const skipError = c.req.query('skipError') === 'true';
4232
4012
  const result = await getCrossSellItem(checkoutSession);
4233
4013
 
4234
- if (skipError && result.error) {
4235
- return res.status(200).json(result);
4014
+ if (skipError && (result as any).error) {
4015
+ return c.json(result, 200);
4236
4016
  }
4237
- return res.status(result.error ? 400 : 200).json(result);
4017
+ return c.json(result, (result as any).error ? 400 : 200);
4238
4018
  } catch (err) {
4239
4019
  logger.error(err);
4240
- res.status(500).json({ error: err.message });
4020
+ return c.json({ error: (err as any).message }, 500);
4241
4021
  }
4242
4022
  });
4243
4023
 
4244
- router.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
4024
+ app.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
4025
+ const body = c.get('sanitizedBody') ?? {};
4245
4026
  try {
4246
- const checkoutSession = req.doc as CheckoutSession;
4247
- if (!req.body.to) {
4248
- return res.status(400).json({ error: 'Cross sell item is required' });
4027
+ const checkoutSession = c.get('doc') as CheckoutSession;
4028
+ if (!(body as any).to) {
4029
+ return c.json({ error: 'Cross sell item is required' }, 400);
4249
4030
  }
4250
4031
 
4251
4032
  const result = await getCrossSellItem(checkoutSession);
4252
4033
  // @ts-ignore
4253
4034
  if (result.error) {
4254
- return res.status(400).json(result);
4035
+ return c.json(result, 400);
4255
4036
  }
4256
4037
 
4257
4038
  // @ts-ignore
4258
4039
  const to = result as TPriceExpanded;
4259
- if (to.id !== req.body.to) {
4260
- return res.status(400).json({ error: 'Cross sell item does not match' });
4040
+ if (to.id !== (body as any).to) {
4041
+ return c.json({ error: 'Cross sell item does not match' }, 400);
4261
4042
  }
4262
4043
 
4263
4044
  if (checkoutSession.line_items) {
4264
4045
  const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === to.id);
4265
4046
  if (index > -1) {
4266
- return res.status(400).json({ error: 'Cross sell item already exist' });
4047
+ return c.json({ error: 'Cross sell item already exist' }, 400);
4267
4048
  }
4268
4049
 
4269
4050
  const items = cloneDeep(checkoutSession.line_items);
@@ -4273,74 +4054,70 @@ router.put('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res)
4273
4054
  cross_sell: true,
4274
4055
  });
4275
4056
  await checkoutSession.update({ line_items: items });
4276
- logger.info('CheckoutSession updated on add cross-sell', { id: req.params.id, crossSell: to.id });
4057
+ logger.info('CheckoutSession updated on add cross-sell', { id: c.req.param('id'), crossSell: to.id });
4277
4058
 
4278
- // recalculate amount
4279
4059
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
4280
4060
  }
4281
4061
 
4282
4062
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
4283
- res.json({ ...checkoutSession.toJSON(), line_items: items });
4063
+ return c.json({ ...checkoutSession.toJSON(), line_items: items });
4284
4064
  } catch (err) {
4285
4065
  logger.error(err);
4286
- res.status(500).json({ error: err.message });
4066
+ return c.json({ error: (err as any).message }, 500);
4287
4067
  }
4288
4068
  });
4289
4069
 
4290
- router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
4070
+ app.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (c) => {
4291
4071
  try {
4292
- const checkoutSession = req.doc as CheckoutSession;
4072
+ const checkoutSession = c.get('doc') as CheckoutSession;
4293
4073
  if (checkoutSession.line_items) {
4294
4074
  const index = checkoutSession.line_items.findIndex((x) => x.cross_sell);
4295
4075
  if (index === -1) {
4296
- return res.status(400).json({ error: 'Cross sell item not exist' });
4076
+ return c.json({ error: 'Cross sell item not exist' }, 400);
4297
4077
  }
4298
4078
 
4299
4079
  const items = cloneDeep(checkoutSession.line_items);
4300
4080
  items.splice(index, 1);
4301
4081
  await checkoutSession.update({ line_items: items });
4302
- logger.info('CheckoutSession updated on remove cross-sell', { id: req.params.id });
4082
+ logger.info('CheckoutSession updated on remove cross-sell', { id: c.req.param('id') });
4303
4083
 
4304
- // recalculate amount
4305
4084
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
4306
4085
  }
4307
4086
 
4308
4087
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
4309
- res.json({ ...checkoutSession.toJSON(), line_items: items });
4088
+ return c.json({ ...checkoutSession.toJSON(), line_items: items });
4310
4089
  } catch (err) {
4311
4090
  logger.error(err);
4312
- res.status(500).json({ error: err.message });
4091
+ return c.json({ error: (err as any).message }, 500);
4313
4092
  }
4314
4093
  });
4315
4094
 
4316
4095
  // Apply promotion code discount (preview only - doesn't create actual discount record)
4317
- router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req, res) => {
4096
+ app.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (c) => {
4097
+ const body = c.get('sanitizedBody') ?? {};
4318
4098
  try {
4319
- const checkoutSession = req.doc as CheckoutSession;
4320
- const { promotion_code: promotionCode, currency_id: currencyId } = req.body;
4099
+ const checkoutSession = c.get('doc') as CheckoutSession;
4100
+ const { promotion_code: promotionCode, currency_id: currencyId } = body as any;
4321
4101
 
4322
4102
  if (!promotionCode) {
4323
- return res.status(400).json({ error: 'Promotion code is required' });
4103
+ return c.json({ error: 'Promotion code is required' }, 400);
4324
4104
  }
4325
4105
 
4326
- if (!req.user) {
4327
- return res.status(403).json({ error: 'Authentication required' });
4106
+ if (!c.get('user')) {
4107
+ return c.json({ error: 'Authentication required' }, 403);
4328
4108
  }
4329
4109
 
4330
- // Find customer by DID (may not exist yet for new users who haven't submitted)
4331
- const customer = await Customer.findOne({ where: { did: req.user.did } });
4332
- // Use customer.id if exists, otherwise fall back to DID for promo validation
4333
- const customerId = customer?.id || req.user.did;
4110
+ const customer = await Customer.findOne({ where: { did: c.get('user').did } });
4111
+ const customerId = customer?.id || c.get('user').did;
4334
4112
 
4335
- // Get currency
4336
4113
  const curCurrencyId = currencyId || checkoutSession.currency_id;
4337
4114
  if (!curCurrencyId) {
4338
- return res.status(400).json({ error: 'Currency not found in checkout session' });
4115
+ return c.json({ error: 'Currency not found in checkout session' }, 400);
4339
4116
  }
4340
4117
 
4341
4118
  const currency = await PaymentCurrency.findByPk(curCurrencyId);
4342
4119
  if (!currency) {
4343
- return res.status(400).json({ error: 'Currency not found' });
4120
+ return c.json({ error: 'Currency not found' }, 400);
4344
4121
  }
4345
4122
 
4346
4123
  const foundPromotionCode = (await PromotionCode.findOne({
@@ -4355,57 +4132,46 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4355
4132
  })) as PromotionCode & { coupon: Coupon };
4356
4133
 
4357
4134
  if (!foundPromotionCode || !foundPromotionCode.active) {
4358
- return res.status(400).json({ error: 'Promotion code not found or inactive' });
4135
+ return c.json({ error: 'Promotion code not found or inactive' }, 400);
4359
4136
  }
4360
4137
 
4361
- // Validate subscription grouping + repeating coupon combination
4362
4138
  if (checkoutSession.enable_subscription_grouping) {
4363
4139
  if (foundPromotionCode?.coupon?.duration === 'repeating') {
4364
- return res.status(400).json({
4365
- error:
4366
- 'Repeating coupons cannot be used with subscription grouping. This limitation ensures proper discount management across multiple subscriptions in a single purchase.',
4367
- });
4140
+ return c.json(
4141
+ {
4142
+ error:
4143
+ 'Repeating coupons cannot be used with subscription grouping. This limitation ensures proper discount management across multiple subscriptions in a single purchase.',
4144
+ },
4145
+ 400
4146
+ );
4368
4147
  }
4369
4148
  }
4370
4149
 
4371
4150
  const checkoutItems = checkoutSession.line_items || [];
4372
- // Get line items with quote data for accurate discount calculation
4373
4151
  const expandedItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
4374
4152
 
4375
- // Enrich line items with quote data for dynamic pricing
4376
- // This ensures discount calculation uses the same amounts as the actual payment
4377
- const quoteResult = await enrichCheckoutSessionWithQuotes(
4378
- checkoutSession,
4379
- expandedItems,
4380
- curCurrencyId,
4381
- { skipGeneration: true } // Don't generate new quotes, just use existing ones
4382
- );
4153
+ const quoteResult = await enrichCheckoutSessionWithQuotes(checkoutSession, expandedItems, curCurrencyId, {
4154
+ skipGeneration: true,
4155
+ });
4383
4156
  const itemsWithQuotes = quoteResult.lineItems;
4384
4157
 
4385
- // Calculate trial status for discount application (same logic as in calculateAndUpdateAmount)
4386
4158
  const now = dayjs().unix();
4387
4159
  const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
4388
4160
  const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
4389
4161
 
4390
- // Apply discount using our new function
4391
4162
  const discountResult = await applyDiscountsToLineItems({
4392
4163
  lineItems: itemsWithQuotes,
4393
4164
  promotionCodeId: foundPromotionCode.id,
4394
4165
  couponId: foundPromotionCode.coupon_id,
4395
4166
  customerId,
4396
4167
  currency,
4397
- billingContext: {
4398
- trialing: isTrialing,
4399
- },
4168
+ billingContext: { trialing: isTrialing },
4400
4169
  });
4401
4170
 
4402
4171
  if (!discountResult.discountSummary.appliedCoupon) {
4403
- return res
4404
- .status(400)
4405
- .json({ error: discountResult.notValidReason || 'Promotion code cannot be applied to this order' });
4172
+ return c.json({ error: discountResult.notValidReason || 'Promotion code cannot be applied to this order' }, 400);
4406
4173
  }
4407
4174
 
4408
- // Create discount configuration for checkout session
4409
4175
  const coupon = await Coupon.findByPk(foundPromotionCode.coupon_id);
4410
4176
  const discountConfig = {
4411
4177
  promotion_code: foundPromotionCode.id,
@@ -4415,7 +4181,6 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4415
4181
  verification_data: { code: promotionCode },
4416
4182
  };
4417
4183
 
4418
- // Update checkout session with discount preview (without detailed coupon info in line_items)
4419
4184
  await checkoutSession.update({
4420
4185
  discounts: [discountConfig],
4421
4186
  currency_id: curCurrencyId,
@@ -4427,8 +4192,8 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4427
4192
  discount_amounts: item.discount_amounts,
4428
4193
  };
4429
4194
  }),
4430
- amount_subtotal: checkoutSession.amount_total, // Original amount
4431
- amount_total: discountResult.discountSummary.finalTotal, // Discounted amount
4195
+ amount_subtotal: checkoutSession.amount_total,
4196
+ amount_total: discountResult.discountSummary.finalTotal,
4432
4197
  total_details: {
4433
4198
  ...checkoutSession.total_details,
4434
4199
  amount_discount: discountResult.discountSummary.totalDiscountAmount,
@@ -4439,7 +4204,6 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4439
4204
  },
4440
4205
  });
4441
4206
 
4442
- // Create enhanced line items with complete coupon information for response
4443
4207
  const enhancedLineItemsWithCoupon = await expandLineItemsWithCouponInfo(
4444
4208
  discountResult.enhancedLineItems,
4445
4209
  [discountConfig],
@@ -4454,10 +4218,9 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4454
4218
  finalAmount: discountResult.discountSummary.finalTotal,
4455
4219
  });
4456
4220
 
4457
- // Expand discounts with complete details for response
4458
4221
  const enhancedDiscounts = await expandDiscountsWithDetails([discountConfig]);
4459
4222
 
4460
- res.json({
4223
+ return c.json({
4461
4224
  ...checkoutSession.toJSON(),
4462
4225
  line_items: enhancedLineItemsWithCoupon,
4463
4226
  discounts: enhancedDiscounts,
@@ -4465,36 +4228,35 @@ router.post('/:id/apply-promotion', user, ensureCheckoutSessionOpen, async (req,
4465
4228
  });
4466
4229
  } catch (err) {
4467
4230
  logger.error('Error applying promotion code', {
4468
- sessionId: req.params.id,
4469
- error: err.message,
4470
- stack: err.stack,
4231
+ sessionId: c.req.param('id'),
4232
+ error: (err as any).message,
4233
+ stack: (err as any).stack,
4471
4234
  });
4472
- res.status(400).json({ error: err.message });
4235
+ return c.json({ error: (err as any).message }, 400);
4473
4236
  }
4474
4237
  });
4475
4238
 
4476
4239
  // Recalculate promotion code discount when currency changes
4477
- router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async (req, res) => {
4240
+ app.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async (c) => {
4241
+ const body = c.get('sanitizedBody') ?? {};
4478
4242
  try {
4479
- const checkoutSession = req.doc as CheckoutSession;
4480
- const { currency_id: newCurrencyId } = req.body;
4243
+ const checkoutSession = c.get('doc') as CheckoutSession;
4244
+ const { currency_id: newCurrencyId } = body as any;
4481
4245
 
4482
4246
  if (!newCurrencyId) {
4483
- return res.status(400).json({ error: 'Currency ID is required' });
4247
+ return c.json({ error: 'Currency ID is required' }, 400);
4484
4248
  }
4485
4249
 
4486
- if (!req.user) {
4487
- return res.status(403).json({ error: 'Authentication required' });
4250
+ if (!c.get('user')) {
4251
+ return c.json({ error: 'Authentication required' }, 403);
4488
4252
  }
4489
4253
 
4490
- // Try recovering discounts from confirmed records before deciding there is nothing to recalculate.
4491
4254
  if (!checkoutSession.discounts?.length) {
4492
4255
  await recoverDiscountConfigFromRecords(checkoutSession);
4493
4256
  }
4494
4257
 
4495
- // Check if there are existing discounts to recalculate
4496
4258
  if (!checkoutSession.discounts?.length) {
4497
- return res.json({
4259
+ return c.json({
4498
4260
  ...checkoutSession.toJSON(),
4499
4261
  discounts: [],
4500
4262
  discount_applied: false,
@@ -4502,49 +4264,42 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4502
4264
  });
4503
4265
  }
4504
4266
 
4505
- // Get new currency
4506
4267
  const currency = await PaymentCurrency.findByPk(newCurrencyId);
4507
4268
  if (!currency) {
4508
- return res.status(400).json({ error: 'Currency not found' });
4269
+ return c.json({ error: 'Currency not found' }, 400);
4509
4270
  }
4510
4271
 
4511
4272
  const checkoutItems = checkoutSession.line_items || [];
4512
- // Get line items - no need for quote data since frontend calculates discount amounts
4513
4273
  const expandedItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
4514
4274
  const supportedCurrencyIds = getSupportedPaymentCurrencies(expandedItems as any[]);
4515
4275
  if (!supportedCurrencyIds.includes(newCurrencyId)) {
4516
- return res.status(400).json({ error: 'Currency not supported for this checkout session' });
4276
+ return c.json({ error: 'Currency not supported for this checkout session' }, 400);
4517
4277
  }
4518
4278
 
4519
- // Get the first discount (assuming only one promotion code at a time)
4520
4279
  const existingDiscount = checkoutSession.discounts[0];
4521
4280
  if (!existingDiscount) {
4522
- return res.status(400).json({ error: 'No discount found' });
4281
+ return c.json({ error: 'No discount found' }, 400);
4523
4282
  }
4524
4283
 
4525
4284
  const promotionCodeId = existingDiscount.promotion_code;
4526
4285
  const couponId = existingDiscount.coupon;
4527
4286
 
4528
4287
  if (!promotionCodeId || !couponId) {
4529
- return res.status(400).json({ error: 'Invalid discount configuration' });
4288
+ return c.json({ error: 'Invalid discount configuration' }, 400);
4530
4289
  }
4531
4290
 
4532
- // Validate promotion code and coupon still exist and are active
4533
4291
  const [foundPromotionCode, coupon] = await Promise.all([
4534
4292
  PromotionCode.findByPk(promotionCodeId),
4535
4293
  Coupon.findByPk(couponId),
4536
4294
  ]);
4537
4295
 
4538
4296
  if (!foundPromotionCode) {
4539
- return res.status(400).json({ error: 'Promotion code not found' });
4297
+ return c.json({ error: 'Promotion code not found' }, 400);
4540
4298
  }
4541
4299
  if (!coupon) {
4542
- return res.status(400).json({ error: 'Coupon not found' });
4300
+ return c.json({ error: 'Coupon not found' }, 400);
4543
4301
  }
4544
4302
 
4545
- // Re-submit scenario: discount-status queue may have deactivated the promo/coupon
4546
- // because this session's own usage pushed it past max_redemptions.
4547
- // Exclude current session's confirmed discount records before checking active/valid.
4548
4303
  const currentSessionUsage = await Discount.count({
4549
4304
  where: {
4550
4305
  checkout_session_id: checkoutSession.id,
@@ -4554,25 +4309,21 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4554
4309
  });
4555
4310
 
4556
4311
  if (!foundPromotionCode.active && currentSessionUsage === 0) {
4557
- return res.status(400).json({ error: 'Promotion code no longer active' });
4312
+ return c.json({ error: 'Promotion code no longer active' }, 400);
4558
4313
  }
4559
4314
 
4560
4315
  if (!coupon.valid && currentSessionUsage === 0) {
4561
- return res.status(400).json({ error: 'Coupon no longer valid' });
4316
+ return c.json({ error: 'Coupon no longer valid' }, 400);
4562
4317
  }
4563
4318
 
4564
- // Check if coupon can be applied with the new currency (for fixed amount coupons)
4565
4319
  const canApplyWithCurrency =
4566
- coupon.percent_off > 0 || // Percentage discounts always work
4567
- coupon.currency_id === currency.id || // Same currency
4568
- coupon.currency_options?.[currency.id]?.amount_off; // Has currency option
4320
+ coupon.percent_off > 0 ||
4321
+ coupon.currency_id === currency.id ||
4322
+ coupon.currency_options?.[currency.id]?.amount_off;
4569
4323
 
4570
4324
  if (!canApplyWithCurrency) {
4571
- // Rollback previously confirmed discount usage/records for this open checkout session.
4572
- // This resets coupon/promotion counters when discount becomes inapplicable.
4573
4325
  await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
4574
4326
 
4575
- // Remove discount if it can't be applied with new currency
4576
4327
  await checkoutSession.update({
4577
4328
  discounts: [],
4578
4329
  line_items: expandedItems.map((item) => {
@@ -4596,7 +4347,7 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4596
4347
  promotionCode: foundPromotionCode.code,
4597
4348
  });
4598
4349
 
4599
- return res.json({
4350
+ return c.json({
4600
4351
  ...checkoutSession.toJSON(),
4601
4352
  line_items: expandedItems,
4602
4353
  discounts: [],
@@ -4605,7 +4356,6 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4605
4356
  });
4606
4357
  }
4607
4358
 
4608
- // Mark which items are discountable based on coupon product restrictions
4609
4359
  const enhancedLineItems = expandedItems.map((item) => {
4610
4360
  const isDiscountable =
4611
4361
  !coupon.applies_to?.products?.length ||
@@ -4615,20 +4365,17 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4615
4365
  return {
4616
4366
  ...cur,
4617
4367
  discountable: isDiscountable,
4618
- discount_amounts: [], // Frontend will calculate actual amounts
4368
+ discount_amounts: [],
4619
4369
  };
4620
4370
  });
4621
4371
 
4622
- // Create updated discount configuration - NO discount_amount, frontend calculates it
4623
4372
  const discountConfig = {
4624
4373
  promotion_code: promotionCodeId,
4625
4374
  coupon: couponId,
4626
- // discount_amount removed - frontend calculates based on live exchange rate
4627
4375
  verification_method: existingDiscount.verification_method,
4628
4376
  verification_data: existingDiscount.verification_data,
4629
4377
  };
4630
4378
 
4631
- // Update checkout session with discountable flags only
4632
4379
  await checkoutSession.update({
4633
4380
  discounts: [discountConfig],
4634
4381
  line_items: enhancedLineItems,
@@ -4642,13 +4389,10 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4642
4389
  promotionCode: foundPromotionCode.code,
4643
4390
  });
4644
4391
 
4645
- // Expand discounts with complete details for response (coupon_details for frontend calculation)
4646
4392
  const enhancedDiscounts = await expandDiscountsWithDetails([discountConfig]);
4647
-
4648
- // Expand line items for response
4649
4393
  const responseItems = await Price.expand(enhancedLineItems, { product: true, upsell: true });
4650
4394
 
4651
- res.json({
4395
+ return c.json({
4652
4396
  ...checkoutSession.toJSON(),
4653
4397
  line_items: responseItems,
4654
4398
  discounts: enhancedDiscounts,
@@ -4657,50 +4401,43 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
4657
4401
  });
4658
4402
  } catch (err) {
4659
4403
  logger.error('Error recalculating promotion code', {
4660
- sessionId: req.params.id,
4661
- error: err.message,
4662
- stack: err.stack,
4404
+ sessionId: c.req.param('id'),
4405
+ error: (err as any).message,
4406
+ stack: (err as any).stack,
4663
4407
  });
4664
- res.status(400).json({ error: err.message });
4408
+ return c.json({ error: (err as any).message }, 400);
4665
4409
  }
4666
4410
  });
4667
4411
 
4668
4412
  // Remove promotion code discount (remove preview)
4669
- router.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (req, res) => {
4413
+ app.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (c) => {
4670
4414
  try {
4671
- const checkoutSession = req.doc as CheckoutSession;
4415
+ const checkoutSession = c.get('doc') as CheckoutSession;
4672
4416
 
4673
4417
  if (checkoutSession.status !== 'open') {
4674
- return res.status(400).json({ error: 'Checkout session is not open' });
4418
+ return c.json({ error: 'Checkout session is not open' }, 400);
4675
4419
  }
4676
4420
 
4677
- // Get currency
4678
4421
  const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
4679
4422
  if (!currency) {
4680
- return res.status(400).json({ error: 'Currency not found' });
4423
+ return c.json({ error: 'Currency not found' }, 400);
4681
4424
  }
4682
4425
 
4683
- // Get original line items without discount information
4684
4426
  const originalItems = await Price.expand(checkoutSession.line_items || [], { product: true, upsell: true });
4685
4427
 
4686
- // Calculate trial status for accurate original amount calculation
4687
4428
  const now = dayjs().unix();
4688
4429
  const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
4689
4430
  const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
4690
4431
 
4691
4432
  await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
4692
- // Calculate original amounts without any discounts
4693
4433
  const originalResult = await applyDiscountsToLineItems({
4694
4434
  lineItems: originalItems,
4695
- couponId: 'dummy', // Won't be found, so no discount applied
4435
+ couponId: 'dummy',
4696
4436
  customerId: 'dummy',
4697
4437
  currency,
4698
- billingContext: {
4699
- trialing: isTrialing,
4700
- },
4438
+ billingContext: { trialing: isTrialing },
4701
4439
  });
4702
4440
 
4703
- // Update checkout session to remove discount
4704
4441
  await checkoutSession.update({
4705
4442
  discounts: [],
4706
4443
  line_items: originalResult.enhancedLineItems.map((item: TLineItemExpanded) => ({
@@ -4722,22 +4459,20 @@ router.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (r
4722
4459
  },
4723
4460
  });
4724
4461
 
4725
- logger.info('Promotion code removed from checkout session', {
4726
- sessionId: checkoutSession.id,
4727
- });
4462
+ logger.info('Promotion code removed from checkout session', { sessionId: checkoutSession.id });
4728
4463
 
4729
- res.json({
4464
+ return c.json({
4730
4465
  ...checkoutSession.toJSON(),
4731
4466
  line_items: originalResult.enhancedLineItems,
4732
4467
  discount_applied: false,
4733
4468
  });
4734
4469
  } catch (err) {
4735
4470
  logger.error('Error removing promotion code', {
4736
- sessionId: req.params.id,
4737
- error: err.message,
4738
- stack: err.stack,
4471
+ sessionId: c.req.param('id'),
4472
+ error: (err as any).message,
4473
+ stack: (err as any).stack,
4739
4474
  });
4740
- res.status(500).json({ error: err.message });
4475
+ return c.json({ error: (err as any).message }, 500);
4741
4476
  }
4742
4477
  });
4743
4478
 
@@ -4751,46 +4486,45 @@ const amountSchema = Joi.object({
4751
4486
  }),
4752
4487
  priceId: Joi.string().required(),
4753
4488
  });
4489
+
4754
4490
  // change payment amount
4755
- router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
4491
+ app.put('/:id/amount', ensureCheckoutSessionOpen, async (c) => {
4492
+ const body = c.get('sanitizedBody') ?? {};
4756
4493
  try {
4757
- const { error, value } = amountSchema.validate(req.body, {
4758
- stripUnknown: true,
4759
- });
4494
+ const { error, value } = amountSchema.validate(body, { stripUnknown: true });
4760
4495
  if (error) {
4761
- return res.status(400).json({ error: error.message });
4496
+ return c.json({ error: error.message }, 400);
4762
4497
  }
4763
4498
 
4764
4499
  const { amount, priceId } = value;
4765
- const checkoutSession = req.doc as CheckoutSession;
4500
+ const checkoutSession = c.get('doc') as CheckoutSession;
4766
4501
  const items = await Price.expand(checkoutSession.line_items);
4767
4502
  const item = items.find((x) => x.price_id === priceId);
4768
4503
  if (!item) {
4769
- return res.status(400).json({ error: 'LineItem not in checkout session' });
4504
+ return c.json({ error: 'LineItem not in checkout session' }, 400);
4770
4505
  }
4771
4506
  if (!item.price.custom_unit_amount) {
4772
- return res.status(400).json({ error: 'PriceItem not customizable for checkout session' });
4507
+ return c.json({ error: 'PriceItem not customizable for checkout session' }, 400);
4773
4508
  }
4774
4509
 
4775
- // validate amount on donation settings
4776
4510
  if (checkoutSession.payment_link_id) {
4777
4511
  const link = await PaymentLink.findByPk(checkoutSession.payment_link_id);
4778
4512
  if (!checkoutSession.currency_id) {
4779
- return res.status(400).json({ error: 'Currency not found in checkout session' });
4513
+ return c.json({ error: 'Currency not found in checkout session' }, 400);
4780
4514
  }
4781
4515
  const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
4782
4516
  if (!currency) {
4783
- return res.status(404).json({ error: 'Currency not found' });
4517
+ return c.json({ error: 'Currency not found' }, 404);
4784
4518
  }
4785
4519
  if (link?.donation_settings?.amount && currency) {
4786
4520
  const input = Number(fromUnitToToken(amount, currency.decimal));
4787
4521
  const { minimum, maximum, presets, custom } = link.donation_settings.amount;
4788
4522
  if (custom) {
4789
4523
  if (input < Number(minimum)) {
4790
- return res.status(400).json({ error: 'Custom amount should not be smaller than minimum' });
4524
+ return c.json({ error: 'Custom amount should not be smaller than minimum' }, 400);
4791
4525
  }
4792
4526
  if (input > Number(maximum)) {
4793
- return res.status(400).json({ error: 'Custom amount should not be smaller than maximum' });
4527
+ return c.json({ error: 'Custom amount should not be smaller than maximum' }, 400);
4794
4528
  }
4795
4529
  const precisionError = formatAmountPrecisionLimit(
4796
4530
  input.toString(),
@@ -4798,45 +4532,43 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
4798
4532
  'Custom amount'
4799
4533
  );
4800
4534
  if (precisionError) {
4801
- return res.status(400).json({ error: precisionError });
4535
+ return c.json({ error: precisionError }, 400);
4802
4536
  }
4803
- } else if (presets?.some((x) => Number(x) === input) === false) {
4804
- return res.status(400).json({ error: 'Custom amount must be one of the presets' });
4537
+ } else if (presets?.some((x: any) => Number(x) === input) === false) {
4538
+ return c.json({ error: 'Custom amount must be one of the presets' }, 400);
4805
4539
  }
4806
4540
  }
4807
4541
  } else {
4808
4542
  const { minimum, maximum } = item.price.custom_unit_amount;
4809
4543
  if (new BN(amount).lt(new BN(minimum))) {
4810
- return res.status(400).json({ error: 'Custom amount should not be smaller than minimum' });
4544
+ return c.json({ error: 'Custom amount should not be smaller than minimum' }, 400);
4811
4545
  }
4812
4546
  if (new BN(amount).gt(new BN(maximum))) {
4813
- return res.status(400).json({ error: 'Custom amount should not be smaller than maximum' });
4547
+ return c.json({ error: 'Custom amount should not be smaller than maximum' }, 400);
4814
4548
  }
4815
4549
  }
4816
4550
 
4817
- // update line items
4818
4551
  const newItems = cloneDeep(checkoutSession.line_items);
4819
4552
  const newItem = newItems.find((x) => x.price_id === priceId);
4820
4553
  if (newItem) {
4821
4554
  newItem.custom_amount = amount;
4822
4555
  }
4823
4556
  await checkoutSession.update({ line_items: newItems.map((x) => omit(x, ['price'])) as LineItem[] });
4824
- logger.info('CheckoutSession updated on amount', { id: req.params.id, amount, priceId });
4557
+ logger.info('CheckoutSession updated on amount', { id: c.req.param('id'), amount, priceId });
4825
4558
 
4826
- // recalculate amount
4827
4559
  await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
4828
4560
 
4829
- return res.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
4561
+ return c.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
4830
4562
  } catch (err) {
4831
4563
  logger.error(err);
4832
4564
  if (err instanceof CustomError) {
4833
- return res.status(getStatusFromError(err)).json({ error: formatError(err) });
4565
+ return c.json({ error: formatError(err) }, getStatusFromError(err) as any);
4834
4566
  }
4835
- return res.status(500).json({ error: err.message });
4567
+ return c.json({ error: (err as any).message }, 500);
4836
4568
  }
4837
4569
  });
4838
4570
 
4839
- const schema = Joi.object<{
4571
+ const listSchema = Joi.object<{
4840
4572
  page: number;
4841
4573
  pageSize: number;
4842
4574
  status?: string;
@@ -4861,8 +4593,10 @@ const schema = Joi.object<{
4861
4593
  subscription_id: Joi.string().empty(''),
4862
4594
  livemode: Joi.boolean().empty(''),
4863
4595
  });
4864
- router.get('/', auth, async (req, res) => {
4865
- const { page, pageSize, livemode, ...query } = await schema.validateAsync(req.query, {
4596
+
4597
+ app.get('/', auth, async (c) => {
4598
+ const query = c.req.query();
4599
+ const { page, pageSize, livemode, ...rest } = await listSchema.validateAsync(query, {
4866
4600
  stripUnknown: false,
4867
4601
  allowUnknown: true,
4868
4602
  });
@@ -4870,41 +4604,40 @@ router.get('/', auth, async (req, res) => {
4870
4604
 
4871
4605
  ['status', 'payment_status', 'nft_mint_status'].forEach((key) => {
4872
4606
  // @ts-ignore
4873
- if (query[key]) {
4607
+ if (rest[key]) {
4874
4608
  // @ts-ignore
4875
- where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
4609
+ where[key] = rest[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
4876
4610
  }
4877
4611
  });
4878
- if (query.customer_id) {
4879
- where.customer_id = query.customer_id;
4612
+ if (rest.customer_id) {
4613
+ where.customer_id = rest.customer_id;
4880
4614
  }
4881
- if (query.payment_intent_id) {
4882
- where.payment_intent_id = query.payment_intent_id;
4615
+ if (rest.payment_intent_id) {
4616
+ where.payment_intent_id = rest.payment_intent_id;
4883
4617
  }
4884
- if (query.payment_link_id) {
4885
- where.payment_link_id = query.payment_link_id;
4618
+ if (rest.payment_link_id) {
4619
+ where.payment_link_id = rest.payment_link_id;
4886
4620
  }
4887
- if (query.subscription_id) {
4888
- where.subscription_id = query.subscription_id;
4621
+ if (rest.subscription_id) {
4622
+ where.subscription_id = rest.subscription_id;
4889
4623
  }
4890
- if (query.customer_did && isValid(query.customer_did)) {
4891
- const customer = await Customer.findOne({ where: { did: query.customer_did } });
4624
+ if (rest.customer_did && isValid(rest.customer_did)) {
4625
+ const customer = await Customer.findOne({ where: { did: rest.customer_did } });
4892
4626
  if (customer) {
4893
4627
  where.customer_id = customer.id;
4894
4628
  } else {
4895
- res.json({ count: 0, list: [] });
4896
- return;
4629
+ return c.json({ count: 0, list: [] });
4897
4630
  }
4898
4631
  }
4899
4632
  if (typeof livemode === 'boolean') {
4900
4633
  where.livemode = livemode;
4901
4634
  }
4902
4635
 
4903
- Object.keys(query)
4636
+ Object.keys(rest)
4904
4637
  .filter((x) => x.startsWith('metadata.'))
4905
4638
  .forEach((key: string) => {
4906
4639
  // @ts-ignore
4907
- where[key] = query[key];
4640
+ where[key] = rest[key];
4908
4641
  });
4909
4642
 
4910
4643
  try {
@@ -4931,7 +4664,7 @@ router.get('/', auth, async (req, res) => {
4931
4664
  });
4932
4665
  });
4933
4666
 
4934
- const condition = { where: { livemode: !!req.livemode } };
4667
+ const condition = { where: { livemode: !!c.get('livemode') } };
4935
4668
  const products =
4936
4669
  productIds.size > 0
4937
4670
  ? (await Product.findAll({ ...condition, where: { ...condition.where, id: Array.from(productIds) } })).map(
@@ -4951,25 +4684,26 @@ router.get('/', auth, async (req, res) => {
4951
4684
  x.url = getUrl(`/checkout/${x.submit_type}/${x.id}`);
4952
4685
  });
4953
4686
 
4954
- res.json({ count, list: docs });
4687
+ return c.json({ count, list: docs });
4955
4688
  } catch (err) {
4956
4689
  logger.error(err);
4957
- res.json({ count: 0, list: [] });
4690
+ return c.json({ count: 0, list: [] });
4958
4691
  }
4959
4692
  });
4960
4693
 
4961
- router.put('/:id', auth, async (req, res) => {
4962
- const doc = await CheckoutSession.findByPk(req.params.id);
4694
+ app.put('/:id', auth, async (c) => {
4695
+ const body = c.get('sanitizedBody') ?? {};
4696
+ const doc = await CheckoutSession.findByPk(c.req.param('id'));
4963
4697
 
4964
4698
  if (!doc) {
4965
- return res.status(404).json({ error: 'CheckoutSession not found' });
4699
+ return c.json({ error: 'CheckoutSession not found' }, 404);
4966
4700
  }
4967
4701
 
4968
- const raw = pick(req.body, ['metadata']);
4702
+ const raw = pick(body as any, ['metadata']);
4969
4703
  if (raw.metadata) {
4970
4704
  const { error: metadataError } = MetadataSchema.validate(raw.metadata);
4971
4705
  if (metadataError) {
4972
- return res.status(400).json({ error: metadataError });
4706
+ return c.json({ error: metadataError }, 400);
4973
4707
  }
4974
4708
  raw.metadata = formatMetadata(raw.metadata);
4975
4709
  }
@@ -4979,18 +4713,18 @@ router.put('/:id', auth, async (req, res) => {
4979
4713
  sessionId: doc.id,
4980
4714
  updatedFields: Object.keys(raw),
4981
4715
  });
4982
- res.json(doc);
4716
+ return c.json(doc);
4983
4717
  });
4984
4718
 
4985
- router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) => {
4719
+ app.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (c) => {
4720
+ const body = c.get('sanitizedBody') ?? {};
4986
4721
  try {
4987
- const checkoutSession = req.doc as CheckoutSession;
4988
- const { slippage_percent: slippagePercent } = req.body;
4989
- const rawConfig = req.body?.slippage_config || req.body?.slippage || null;
4722
+ const checkoutSession = c.get('doc') as CheckoutSession;
4723
+ const { slippage_percent: slippagePercent } = body as any;
4724
+ const rawConfig = (body as any)?.slippage_config || (body as any)?.slippage || null;
4990
4725
 
4991
4726
  const normalizePercent = (value: any) => {
4992
4727
  const normalized = typeof value === 'string' ? Number(value) : value;
4993
- // Only validate that it's a non-negative finite number, no upper limit
4994
4728
  if (!Number.isFinite(normalized) || normalized < 0) {
4995
4729
  return null;
4996
4730
  }
@@ -5002,12 +4736,10 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5002
4736
  const mode = rawConfig.mode === 'rate' ? 'rate' : 'percent';
5003
4737
  const minRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
5004
4738
 
5005
- // For rate mode, min_acceptable_rate is required; percent is derived
5006
4739
  if (mode === 'rate') {
5007
4740
  if (minRate === undefined || minRate === null || minRate === '') {
5008
- return res.status(400).json({ error: 'min_acceptable_rate is required for rate mode' });
4741
+ return c.json({ error: 'min_acceptable_rate is required for rate mode' }, 400);
5009
4742
  }
5010
- // Accept any non-negative percent value for rate mode (calculated from rate)
5011
4743
  const percent = normalizePercent(rawConfig.percent);
5012
4744
  const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency;
5013
4745
  config = {
@@ -5018,16 +4750,14 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5018
4750
  updated_at_ms: Date.now(),
5019
4751
  };
5020
4752
  } else {
5021
- // Percent mode: validate percent
5022
4753
  const percent = normalizePercent(rawConfig.percent);
5023
4754
  if (percent === null) {
5024
- return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
4755
+ return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
5025
4756
  }
5026
4757
  const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency;
5027
4758
  config = {
5028
4759
  mode,
5029
4760
  percent,
5030
- // Also save min_acceptable_rate if provided (calculated by frontend from current rate)
5031
4761
  ...(minRate ? { min_acceptable_rate: String(minRate) } : {}),
5032
4762
  ...(baseCurrency ? { base_currency: String(baseCurrency) } : {}),
5033
4763
  updated_at_ms: Date.now(),
@@ -5036,7 +4766,7 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5036
4766
  } else if (slippagePercent !== undefined && slippagePercent !== null) {
5037
4767
  const slippageValue = normalizePercent(slippagePercent);
5038
4768
  if (slippageValue === null) {
5039
- return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
4769
+ return c.json({ error: 'slippage_percent must be a non-negative number' }, 400);
5040
4770
  }
5041
4771
  config = {
5042
4772
  mode: 'percent',
@@ -5044,7 +4774,7 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5044
4774
  updated_at_ms: Date.now(),
5045
4775
  };
5046
4776
  } else {
5047
- return res.status(400).json({ error: 'slippage config is required' });
4777
+ return c.json({ error: 'slippage config is required' }, 400);
5048
4778
  }
5049
4779
 
5050
4780
  const nextMetadata = {
@@ -5057,14 +4787,12 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5057
4787
  slippageConfig: config,
5058
4788
  });
5059
4789
 
5060
- // Final Freeze: Don't create Quotes during Preview (slippage change)
5061
- // Slippage is applied at Submit time when Quote is created
5062
4790
  const items = await Price.expand(checkoutSession.line_items, { upsell: true });
5063
4791
  const enriched = await enrichCheckoutSessionWithQuotes(checkoutSession, items, checkoutSession.currency_id, {
5064
- skipGeneration: true, // Final Freeze: Don't create quotes during Preview
4792
+ skipGeneration: true,
5065
4793
  });
5066
4794
 
5067
- res.json({
4795
+ return c.json({
5068
4796
  ...checkoutSession.toJSON(),
5069
4797
  line_items: enriched.lineItems,
5070
4798
  quotes: enriched.quotes,
@@ -5073,13 +4801,13 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5073
4801
  });
5074
4802
  } catch (err) {
5075
4803
  logger.error('Error updating checkout session slippage', {
5076
- sessionId: req.params.id,
4804
+ sessionId: c.req.param('id'),
5077
4805
  error: err,
5078
4806
  });
5079
4807
  if (err instanceof CustomError) {
5080
- return res.status(getStatusFromError(err)).json({ code: err.code, error: err.message });
4808
+ return c.json({ code: (err as any).code, error: (err as any).message }, getStatusFromError(err) as any);
5081
4809
  }
5082
- res.status(500).json({ error: err.message });
4810
+ return c.json({ error: (err as any).message }, 500);
5083
4811
  }
5084
4812
  });
5085
4813
 
@@ -5089,19 +4817,19 @@ router.put('/:id/slippage', user, ensureCheckoutSessionOpen, async (req, res) =>
5089
4817
  * because quotes are currency-specific (e.g., TBA quote amount with 18 decimals
5090
4818
  * cannot be used for USD with 2 decimals)
5091
4819
  */
5092
- router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req, res) => {
4820
+ app.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (c) => {
4821
+ const body = c.get('sanitizedBody') ?? {};
5093
4822
  try {
5094
- const checkoutSession = req.doc as CheckoutSession;
5095
- const { currency_id: newCurrencyId } = req.body;
4823
+ const checkoutSession = c.get('doc') as CheckoutSession;
4824
+ const { currency_id: newCurrencyId } = body as any;
5096
4825
 
5097
4826
  if (!newCurrencyId) {
5098
- return res.status(400).json({ error: 'currency_id is required' });
4827
+ return c.json({ error: 'currency_id is required' }, 400);
5099
4828
  }
5100
4829
 
5101
- // Validate the new currency exists
5102
4830
  const newCurrency = await PaymentCurrency.findByPk(newCurrencyId);
5103
4831
  if (!newCurrency) {
5104
- return res.status(400).json({ error: 'Currency not found' });
4832
+ return c.json({ error: 'Currency not found' }, 400);
5105
4833
  }
5106
4834
 
5107
4835
  const expandedItemsForSupportCheck = await Price.expand(checkoutSession.line_items || [], {
@@ -5110,14 +4838,13 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
5110
4838
  });
5111
4839
  const supportedCurrencyIds = getSupportedPaymentCurrencies(expandedItemsForSupportCheck as any[]);
5112
4840
  if (!supportedCurrencyIds.includes(newCurrencyId)) {
5113
- return res.status(400).json({ error: 'Currency not supported for this checkout session' });
4841
+ return c.json({ error: 'Currency not supported for this checkout session' }, 400);
5114
4842
  }
5115
4843
 
5116
4844
  const oldCurrencyId = checkoutSession.currency_id;
5117
4845
  const currencyChanged = oldCurrencyId && oldCurrencyId !== newCurrencyId;
5118
4846
 
5119
4847
  if (currencyChanged) {
5120
- // Clear quote-related fields from line_items
5121
4848
  const cleanedLineItems = checkoutSession.line_items.map((item: any) => {
5122
4849
  const {
5123
4850
  quote_id: quoteId,
@@ -5129,11 +4856,9 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
5129
4856
  return rest;
5130
4857
  });
5131
4858
 
5132
- // Clear discount amounts - they will be recalculated by recalculate-promotion
5133
- // This prevents showing stale values (e.g., ABT units when switching to USD)
5134
4859
  const cleanedDiscounts = checkoutSession.discounts?.map((discount: any) => ({
5135
4860
  ...discount,
5136
- discount_amount: null, // Will be recalculated
4861
+ discount_amount: null,
5137
4862
  }));
5138
4863
 
5139
4864
  await checkoutSession.update({
@@ -5142,7 +4867,7 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
5142
4867
  discounts: cleanedDiscounts,
5143
4868
  total_details: {
5144
4869
  ...checkoutSession.total_details,
5145
- amount_discount: '0', // Clear until recalculated
4870
+ amount_discount: '0',
5146
4871
  },
5147
4872
  });
5148
4873
 
@@ -5152,31 +4877,173 @@ router.put('/:id/switch-currency', user, ensureCheckoutSessionOpen, async (req,
5152
4877
  newCurrencyId,
5153
4878
  });
5154
4879
  } else {
5155
- // Just update the currency_id if it's the same (first time setting)
5156
- await checkoutSession.update({
5157
- currency_id: newCurrencyId,
5158
- });
4880
+ await checkoutSession.update({ currency_id: newCurrencyId });
5159
4881
  }
5160
4882
 
5161
- // Reload and expand line items
5162
4883
  await checkoutSession.reload();
5163
4884
  const expandedLineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
5164
4885
 
5165
- res.json({
4886
+ return c.json({
5166
4887
  ...checkoutSession.toJSON(),
5167
4888
  line_items: expandedLineItems,
5168
4889
  currency_changed: currencyChanged,
5169
4890
  });
5170
4891
  } catch (err) {
5171
4892
  logger.error('Error switching checkout session currency', {
5172
- sessionId: req.params.id,
4893
+ sessionId: c.req.param('id'),
5173
4894
  error: err,
5174
4895
  });
5175
4896
  if (err instanceof CustomError) {
5176
- return res.status(getStatusFromError(err)).json({ code: err.code, error: err.message });
4897
+ return c.json({ code: (err as any).code, error: (err as any).message }, getStatusFromError(err) as any);
4898
+ }
4899
+ return c.json({ error: (err as any).message }, 500);
4900
+ }
4901
+ });
4902
+
4903
+ // [restored — missed in the bulk conversion, caught by the route-count check]
4904
+ app.post('/:id/abort-stripe', user, ensureCheckoutSessionOpen, async (c) => {
4905
+ try {
4906
+ const checkoutSession = c.get('doc') as CheckoutSession;
4907
+
4908
+ if (checkoutSession.status === 'complete') {
4909
+ return c.json({ error: 'Checkout session already completed' }, 400);
4910
+ }
4911
+
4912
+ // cancel stripe subscriptions if any
4913
+ const canceledSubscriptions: string[] = [];
4914
+ if (['subscription', 'setup'].includes(checkoutSession.mode)) {
4915
+ const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
4916
+ const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
4917
+
4918
+ const cancelOps = subscriptions.map(async (sub) => {
4919
+ const stripeSubId = sub.payment_details?.stripe?.subscription_id;
4920
+ if (!stripeSubId) {
4921
+ return null;
4922
+ }
4923
+ const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
4924
+ if (!method || method.type !== 'stripe') {
4925
+ return null;
4926
+ }
4927
+ const client = method.getStripeClient();
4928
+ try {
4929
+ await client.subscriptions.cancel(stripeSubId);
4930
+ await sub.update({
4931
+ payment_details: omit(sub.payment_details || {}, 'stripe'),
4932
+ payment_settings: {
4933
+ payment_method_options: omit(sub.payment_settings?.payment_method_options || {}, 'stripe'),
4934
+ payment_method_types: sub.payment_settings?.payment_method_types || [],
4935
+ },
4936
+ });
4937
+ canceledSubscriptions.push(sub.id);
4938
+ } catch (err: any) {
4939
+ logger.error('Failed to cancel stripe subscription for checkout abort', {
4940
+ checkoutSessionId: checkoutSession.id,
4941
+ subscriptionId: sub.id,
4942
+ error: err.message,
4943
+ });
4944
+ }
4945
+ return sub.id;
4946
+ });
4947
+ await Promise.all(cancelOps);
4948
+ }
4949
+
4950
+ // remove related invoice if created
4951
+ try {
4952
+ const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
4953
+ if (existInvoice) {
4954
+ await destroyExistingInvoice(existInvoice);
4955
+ }
4956
+ } catch (error: any) {
4957
+ logger.error('Failed to destroy invoice on checkout abort', {
4958
+ checkoutSessionId: checkoutSession.id,
4959
+ error: error.message,
4960
+ });
5177
4961
  }
5178
- res.status(500).json({ error: err.message });
4962
+ return c.json({ checkoutSessionId: checkoutSession.id, canceledSubscriptions });
4963
+ } catch (err: any) {
4964
+ logger.error('Error aborting stripe for checkout session', {
4965
+ sessionId: c.req.param('id'),
4966
+ error: err.message,
4967
+ stack: err.stack,
4968
+ });
4969
+ return c.json({ error: err.message }, 500);
4970
+ }
4971
+ });
4972
+
4973
+ // Skip payment method for $0 subscription — user chose "Skip, bind later"
4974
+ app.post('/:id/skip-payment-method', user, ensureCheckoutSessionOpen, async (c) => {
4975
+ try {
4976
+ if (!c.get('user')) {
4977
+ return c.json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' }, 403);
4978
+ }
4979
+
4980
+ const checkoutSession = c.get('doc') as CheckoutSession;
4981
+
4982
+ if (!['subscription', 'setup'].includes(checkoutSession.mode)) {
4983
+ return c.json({ error: 'Skip payment method is only supported for subscriptions' }, 400);
4984
+ }
4985
+
4986
+ const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
4987
+ const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
4988
+
4989
+ if (!subscriptions.length) {
4990
+ return c.json({ error: 'No subscriptions found for this checkout session' }, 400);
4991
+ }
4992
+
4993
+ // Cancel Stripe setup intents and activate subscriptions concurrently
4994
+ await Promise.all(
4995
+ subscriptions.map(async (sub) => {
4996
+ const stripeSubId = sub.payment_details?.stripe?.subscription_id;
4997
+ if (stripeSubId) {
4998
+ const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
4999
+ if (method?.type === 'stripe') {
5000
+ const client = method.getStripeClient();
5001
+ try {
5002
+ const stripeSub = await client.subscriptions.retrieve(stripeSubId, {
5003
+ expand: ['pending_setup_intent'],
5004
+ });
5005
+ if (stripeSub.pending_setup_intent && typeof stripeSub.pending_setup_intent !== 'string') {
5006
+ await client.setupIntents.cancel(stripeSub.pending_setup_intent.id);
5007
+ }
5008
+ await client.subscriptions.update(stripeSubId, { cancel_at_period_end: true });
5009
+ } catch (err: any) {
5010
+ logger.error('Failed to update Stripe subscription for skip-payment-method', {
5011
+ checkoutSessionId: checkoutSession.id,
5012
+ subscriptionId: sub.id,
5013
+ stripeSubId,
5014
+ error: err.message,
5015
+ });
5016
+ }
5017
+ }
5018
+ }
5019
+
5020
+ // Activate the local subscription with cancel_at_period_end
5021
+ await sub.update({
5022
+ status: sub.trial_end && sub.trial_end > Date.now() / 1000 ? 'trialing' : 'active',
5023
+ cancel_at_period_end: true,
5024
+ });
5025
+ await addSubscriptionJob(sub, 'cycle', false, sub.trial_end);
5026
+ })
5027
+ );
5028
+
5029
+ // Complete the checkout session
5030
+ await checkoutSession.update({
5031
+ status: 'complete',
5032
+ payment_status: 'no_payment_required',
5033
+ });
5034
+
5035
+ return c.json({
5036
+ checkoutSession: { id: checkoutSession.id, status: 'complete' },
5037
+ skipped: true,
5038
+ });
5039
+ } catch (err: any) {
5040
+ logger.error('Error in skip-payment-method', {
5041
+ sessionId: c.req.param('id'),
5042
+ error: err.message,
5043
+ stack: err.stack,
5044
+ });
5045
+ return c.json({ error: err.message }, 500);
5179
5046
  }
5180
5047
  });
5181
5048
 
5182
- export default router;
5049
+ export default app;