payment-kit 1.29.1 → 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 (310) 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/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,29 +1,33 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention */
2
+ // Phase 3 (express→hono) — hono fork of routes/invoices.ts. Sub-app with
3
+ // routes relative to /api/invoices (mounted via mountResourceGroup). The
4
+ // business logic is unchanged; only the express plumbing becomes hono:
5
+ // req.body → c.get('sanitizedBody') ?? {}; res.status(n).json(x) → c.json(x, n).
2
6
  import { isValid } from '@arcblock/did';
3
- import { Router } from 'express';
7
+ import { Hono } from 'hono';
4
8
  import Joi from 'joi';
5
9
  import pick from 'lodash/pick';
6
10
  import { Op } from 'sequelize';
7
11
 
8
12
  import { BN, fromUnitToToken } from '@ocap/util';
9
- import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
10
- import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
11
- import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../integrations/stripe/resource';
12
- import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
13
- import { authenticate } from '../libs/security';
14
- import { expandLineItems } from '../libs/session';
15
- import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../libs/util';
16
- import dayjs from '../libs/dayjs';
17
- import { Customer } from '../store/models/customer';
18
- import { Invoice } from '../store/models/invoice';
19
- import { InvoiceItem } from '../store/models/invoice-item';
20
- import { PaymentCurrency } from '../store/models/payment-currency';
21
- import { PaymentIntent } from '../store/models/payment-intent';
22
- import { PaymentMethod } from '../store/models/payment-method';
23
- import { Price } from '../store/models/price';
24
- import { Product } from '../store/models/product';
25
- import { Subscription } from '../store/models/subscription';
26
- import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
13
+ import { syncStripeInvoice } from '../../integrations/stripe/handlers/invoice';
14
+ import { syncStripePayment } from '../../integrations/stripe/handlers/payment-intent';
15
+ import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../../integrations/stripe/resource';
16
+ import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../../libs/api';
17
+ import { authenticate } from '../../middlewares/hono/security';
18
+ import { expandLineItems } from '../../libs/session';
19
+ import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../../libs/util';
20
+ import dayjs from '../../libs/dayjs';
21
+ import { Customer } from '../../store/models/customer';
22
+ import { Invoice } from '../../store/models/invoice';
23
+ import { InvoiceItem } from '../../store/models/invoice-item';
24
+ import { PaymentCurrency } from '../../store/models/payment-currency';
25
+ import { PaymentIntent } from '../../store/models/payment-intent';
26
+ import { PaymentMethod } from '../../store/models/payment-method';
27
+ import { Price } from '../../store/models/price';
28
+ import { Product } from '../../store/models/product';
29
+ import { Subscription } from '../../store/models/subscription';
30
+ import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../../libs/invoice';
27
31
  import {
28
32
  CheckoutSession,
29
33
  PaymentLink,
@@ -34,12 +38,12 @@ import {
34
38
  CreditGrant,
35
39
  TaxRate,
36
40
  PriceQuote,
37
- } from '../store/models';
38
- import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../libs/pagination';
39
- import logger from '../libs/logger';
40
- import { returnOverdraftProtectionQueue, returnStakeQueue } from '../queues/subscription';
41
- import { checkRemainingStake } from '../libs/subscription';
42
- import { getExchangeRateService } from '../libs/exchange-rate';
41
+ } from '../../store/models';
42
+ import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../../libs/pagination';
43
+ import logger from '../../libs/logger';
44
+ import { returnOverdraftProtectionQueue, returnStakeQueue } from '../../queues/subscription';
45
+ import { checkRemainingStake } from '../../libs/subscription';
46
+ import { getExchangeRateService } from '../../libs/exchange-rate';
43
47
 
44
48
  // Simple format amount helper for backend
45
49
  function formatAmountForDisplay(amount: string, decimal: number, symbol: string): string {
@@ -47,7 +51,7 @@ function formatAmountForDisplay(amount: string, decimal: number, symbol: string)
47
51
  return `${parseFloat(tokenValue).toFixed(Math.min(decimal, 6))} ${symbol}`;
48
52
  }
49
53
 
50
- const router = Router();
54
+ const app = new Hono();
51
55
  const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
52
56
  const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true, embed: true });
53
57
  const authPortal = authenticate<Invoice>({
@@ -235,7 +239,7 @@ const attachQuoteMetadataToLines = (lines: any[] | undefined, quotesById: Map<st
235
239
  });
236
240
  };
237
241
 
238
- router.get('/', authMine, async (req, res) => {
242
+ app.get('/', authMine, async (c) => {
239
243
  try {
240
244
  // eslint-disable-next-line @typescript-eslint/naming-convention
241
245
  const {
@@ -249,7 +253,7 @@ router.get('/', authMine, async (req, res) => {
249
253
  include_overdraft_protection = true,
250
254
  include_quote = false,
251
255
  ...query
252
- } = await schema.validateAsync(req.query, {
256
+ } = await schema.validateAsync(c.req.query(), {
253
257
  stripUnknown: false,
254
258
  allowUnknown: true,
255
259
  });
@@ -270,8 +274,8 @@ router.get('/', authMine, async (req, res) => {
270
274
  if (ignore_zero) {
271
275
  where.subtotal = { [Op.ne]: '0' };
272
276
  }
273
- if (query.customer_id) {
274
- where.customer_id = query.customer_id;
277
+ if (c.get('customer_id') ?? query.customer_id) {
278
+ where.customer_id = c.get('customer_id') ?? query.customer_id;
275
279
  }
276
280
  if (query.currency_id) {
277
281
  where.currency_id = query.currency_id;
@@ -281,8 +285,7 @@ router.get('/', authMine, async (req, res) => {
281
285
  if (customer) {
282
286
  where.customer_id = customer.id;
283
287
  } else {
284
- res.json({ count: 0, list: [], paging: { page, pageSize } });
285
- return;
288
+ return c.json({ count: 0, list: [], paging: { page, pageSize } });
286
289
  }
287
290
  }
288
291
  if (query.subscription_id) {
@@ -333,7 +336,7 @@ router.get('/', authMine, async (req, res) => {
333
336
  }),
334
337
  Invoice.findAll({
335
338
  where,
336
- order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
339
+ order: getOrder(c.req.query(), [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
337
340
  limit: pageSize,
338
341
  offset: (page - 1) * pageSize,
339
342
  subQuery: false,
@@ -375,12 +378,11 @@ router.get('/', authMine, async (req, res) => {
375
378
  }),
376
379
  ]);
377
380
 
378
- res.json({
381
+ return c.json({
379
382
  count,
380
383
  list,
381
384
  paging: { page, pageSize },
382
385
  });
383
- return;
384
386
  }
385
387
 
386
388
  const sources: DataSource<Invoice>[] = [];
@@ -393,7 +395,7 @@ router.get('/', authMine, async (req, res) => {
393
395
  fetch: async (limit: number, offset: number = 0) => {
394
396
  const result = await Invoice.findAll({
395
397
  where,
396
- order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
398
+ order: getOrder(c.req.query(), [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
397
399
  limit,
398
400
  offset,
399
401
  include: [
@@ -541,7 +543,7 @@ router.get('/', authMine, async (req, res) => {
541
543
  defaultTimeOrderBy(query.o === 'asc' ? 'asc' : 'desc')
542
544
  );
543
545
 
544
- res.json({
546
+ return c.json({
545
547
  count: result.total,
546
548
  list: result.data,
547
549
  paging: result.paging,
@@ -549,15 +551,15 @@ router.get('/', authMine, async (req, res) => {
549
551
  } catch (err) {
550
552
  logger.error('Failed to fetch invoices', {
551
553
  error: err,
552
- page: req.query.page,
553
- pageSize: req.query.pageSize,
554
- include_staking: req.query.include_staking,
555
- subscription_id: req.query.subscription_id,
554
+ page: c.req.query('page'),
555
+ pageSize: c.req.query('pageSize'),
556
+ include_staking: c.req.query('include_staking'),
557
+ subscription_id: c.req.query('subscription_id'),
556
558
  });
557
- res.json({
559
+ return c.json({
558
560
  count: 0,
559
561
  list: [],
560
- paging: { page: Number(req.query.page || 1), pageSize: Number(req.query.pageSize || 10) },
562
+ paging: { page: Number(c.req.query('page') || 1), pageSize: Number(c.req.query('pageSize') || 10) },
561
563
  });
562
564
  }
563
565
  });
@@ -574,16 +576,17 @@ const rechargeSchema = createListParamSchema<{
574
576
  recharge_address: Joi.string().empty(''),
575
577
  });
576
578
 
577
- router.get('/recharge', authMine, async (req, res) => {
578
- const { page, pageSize, ...query } = await rechargeSchema.validateAsync(req.query, {
579
+ // Static path registered before /:id so hono matches it first.
580
+ app.get('/recharge', authMine, async (c) => {
581
+ const { page, pageSize, ...query } = await rechargeSchema.validateAsync(c.req.query(), {
579
582
  stripUnknown: false,
580
583
  allowUnknown: true,
581
584
  });
582
585
  const where = getWhereFromKvQuery(query.q);
583
- if (query.customer_id) {
584
- const customer = await Customer.findByPkOrDid(query.customer_id);
586
+ if (c.get('customer_id') ?? query.customer_id) {
587
+ const customer = await Customer.findByPkOrDid(c.get('customer_id') ?? query.customer_id);
585
588
  if (!customer) {
586
- return res.status(404).json({ error: 'Customer not found' });
589
+ return c.json({ error: 'Customer not found' }, 404);
587
590
  }
588
591
  where.customer_id = customer.id;
589
592
  }
@@ -603,23 +606,24 @@ router.get('/recharge', authMine, async (req, res) => {
603
606
  },
604
607
  offset: (page - 1) * pageSize,
605
608
  limit: pageSize,
606
- order: getOrder(req.query, [['created_at', 'DESC']]),
609
+ order: getOrder(c.req.query(), [['created_at', 'DESC']]),
607
610
  include: [
608
611
  { model: PaymentCurrency, as: 'paymentCurrency' },
609
612
  { model: PaymentMethod, as: 'paymentMethod' },
610
613
  ],
611
614
  });
612
615
 
613
- return res.json({ count, list: invoices, paging: { page, pageSize } });
616
+ return c.json({ count, list: invoices, paging: { page, pageSize } });
614
617
  } catch (err) {
615
618
  logger.error(err);
616
- return res.status(400).json({ error: err.message });
619
+ return c.json({ error: err.message }, 400);
617
620
  }
618
621
  });
619
622
 
620
623
  const searchSchema = createListParamSchema<{}>({});
621
- router.get('/search', authMine, async (req, res) => {
622
- const { page, pageSize, livemode, q, o } = await searchSchema.validateAsync(req.query, {
624
+ // Static path registered before /:id so hono matches it first.
625
+ app.get('/search', authMine, async (c) => {
626
+ const { page, pageSize, livemode, q, o } = await searchSchema.validateAsync(c.req.query(), {
623
627
  stripUnknown: false,
624
628
  allowUnknown: true,
625
629
  });
@@ -631,7 +635,7 @@ router.get('/search', authMine, async (req, res) => {
631
635
 
632
636
  const { rows: list, count } = await Invoice.findAndCountAll({
633
637
  where,
634
- order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
638
+ order: getOrder(c.req.query(), [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
635
639
  offset: (page - 1) * pageSize,
636
640
  limit: pageSize,
637
641
  distinct: true,
@@ -641,7 +645,7 @@ router.get('/search', authMine, async (req, res) => {
641
645
  ],
642
646
  });
643
647
 
644
- res.json({ count, list, paging: { page, pageSize } });
648
+ return c.json({ count, list, paging: { page, pageSize } });
645
649
  });
646
650
 
647
651
  const retryUncollectibleSchema = Joi.object({
@@ -664,14 +668,15 @@ const retryUncollectibleSchema = Joi.object({
664
668
  .optional(),
665
669
  currencyId: Joi.string().trim().allow('').optional(),
666
670
  });
667
- router.get('/retry-uncollectible', authAdmin, async (req, res) => {
671
+ // Static path registered before /:id so hono matches it first.
672
+ app.get('/retry-uncollectible', authAdmin, async (c) => {
668
673
  try {
669
- const { error, value } = retryUncollectibleSchema.validate(req.query, {
674
+ const { error, value } = retryUncollectibleSchema.validate(c.req.query(), {
670
675
  stripUnknown: true,
671
676
  });
672
677
 
673
678
  if (error) {
674
- return res.status(400).json({ error: error.message });
679
+ return c.json({ error: error.message }, 400);
675
680
  }
676
681
 
677
682
  const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = value;
@@ -684,60 +689,64 @@ router.get('/retry-uncollectible', authAdmin, async (req, res) => {
684
689
  currencyId,
685
690
  });
686
691
 
687
- return res.json(result);
692
+ return c.json(result);
688
693
  } catch (error) {
689
694
  logger.error('Failed to retry uncollectible invoices', { error });
690
- return res.status(500).json({
691
- error: 'Failed to retry uncollectible invoices',
692
- message: error.message,
693
- });
695
+ return c.json(
696
+ {
697
+ error: 'Failed to retry uncollectible invoices',
698
+ message: error.message,
699
+ },
700
+ 500
701
+ );
694
702
  }
695
703
  });
696
704
 
697
- router.get('/:id/return-stake', authAdmin, async (req, res) => {
698
- const doc = await Invoice.findByPk(req.params.id as string);
705
+ // Static path /:id/return-stake registered before plain /:id so hono matches it first.
706
+ app.get('/:id/return-stake', authAdmin, async (c) => {
707
+ const doc = await Invoice.findByPk(c.req.param('id') as string);
699
708
  if (!doc) {
700
- return res.status(404).json({ error: 'Invoice not found' });
709
+ return c.json({ error: 'Invoice not found' }, 404);
701
710
  }
702
711
  if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
703
- return res.status(400).json({ error: 'Invoice is not a stake invoice' });
712
+ return c.json({ error: 'Invoice is not a stake invoice' }, 400);
704
713
  }
705
714
  const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
706
715
  if (!paymentCurrency) {
707
- return res.status(400).json({ error: 'Payment currency not found' });
716
+ return c.json({ error: 'Payment currency not found' }, 400);
708
717
  }
709
718
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
710
719
  if (!paymentMethod) {
711
- return res.status(400).json({ error: 'Payment method not found' });
720
+ return c.json({ error: 'Payment method not found' }, 400);
712
721
  }
713
722
  const stakingAddress = doc.metadata?.payment_details?.arcblock?.address;
714
723
  if (!stakingAddress) {
715
- return res.status(400).json({ error: 'Staking address not found' });
724
+ return c.json({ error: 'Staking address not found' }, 400);
716
725
  }
717
726
  const { staked } = await checkRemainingStake(paymentMethod, paymentCurrency, stakingAddress, '0');
718
- return res.json(staked);
727
+ return c.json(staked);
719
728
  });
720
- router.post('/:id/return-stake', authAdmin, async (req, res) => {
721
- const doc = await Invoice.findByPk(req.params.id as string);
729
+ app.post('/:id/return-stake', authAdmin, async (c) => {
730
+ const doc = await Invoice.findByPk(c.req.param('id') as string);
722
731
  if (!doc) {
723
- return res.status(404).json({ error: 'Invoice not found' });
732
+ return c.json({ error: 'Invoice not found' }, 404);
724
733
  }
725
734
  if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
726
- return res.status(400).json({ error: 'Invoice is not a stake invoice' });
735
+ return c.json({ error: 'Invoice is not a stake invoice' }, 400);
727
736
  }
728
737
  if (doc.status !== 'paid') {
729
- return res.status(400).json({ error: 'Invoice is not paid' });
738
+ return c.json({ error: 'Invoice is not paid' }, 400);
730
739
  }
731
740
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
732
741
  if (!paymentMethod) {
733
- return res.status(400).json({ error: 'Payment method not found' });
742
+ return c.json({ error: 'Payment method not found' }, 400);
734
743
  }
735
744
  if (paymentMethod.type !== 'arcblock') {
736
- return res.status(400).json({ error: 'Can only return stake for arcblock payment method' });
745
+ return c.json({ error: 'Can only return stake for arcblock payment method' }, 400);
737
746
  }
738
747
  const subscription = await Subscription.findByPk(doc.subscription_id);
739
748
  if (!subscription) {
740
- return res.status(400).json({ error: 'Subscription not found' });
749
+ return c.json({ error: 'Subscription not found' }, 400);
741
750
  }
742
751
  try {
743
752
  if (doc.billing_reason === 'stake') {
@@ -749,26 +758,146 @@ router.post('/:id/return-stake', authAdmin, async (req, res) => {
749
758
  paymentCurrencyId: doc.currency_id,
750
759
  },
751
760
  });
752
- return res.json({ success: true, subscriptionId: subscription.id });
761
+ return c.json({ success: true, subscriptionId: subscription.id });
753
762
  }
754
763
  if (doc.billing_reason === 'stake_overdraft_protection' && subscription.status === 'canceled') {
755
764
  await returnOverdraftProtectionQueue.pushAndWait({
756
765
  id: `return-overdraft-protection-${subscription.id}`,
757
766
  job: { subscriptionId: subscription.id },
758
767
  });
759
- return res.json({ success: true, subscriptionId: subscription.id });
768
+ return c.json({ success: true, subscriptionId: subscription.id });
760
769
  }
761
- return res.json({ success: false, error: 'Subscription is not canceled' });
770
+ return c.json({ success: false, error: 'Subscription is not canceled' });
762
771
  } catch (error) {
763
772
  logger.error('Failed to return stake', { error, subscriptionId: subscription.id, invoiceId: doc.id });
764
- return res.status(400).json({ error: 'Failed to return stake' });
773
+ return c.json({ error: 'Failed to return stake' }, 400);
774
+ }
775
+ });
776
+
777
+ // Static path /:id/payment-options — registered before plain /:id so hono matches it first.
778
+ app.get('/:id/payment-options', authPortal, async (c) => {
779
+ try {
780
+ const invoice = await Invoice.findByPk(c.req.param('id'), {
781
+ include: [
782
+ { model: InvoiceItem, as: 'lines' },
783
+ { model: PaymentCurrency, as: 'paymentCurrency' },
784
+ ],
785
+ });
786
+
787
+ if (!invoice) {
788
+ return c.json({ error: 'Invoice not found' }, 404);
789
+ }
790
+
791
+ // Get all available currencies for this invoice's products
792
+ const invoiceItems = (invoice as any).lines || [];
793
+ const priceIds = invoiceItems.map((item: any) => item.price_id).filter(Boolean);
794
+
795
+ // Get all unique product IDs from prices
796
+ const prices = await Price.findAll({
797
+ where: { id: priceIds },
798
+ include: [{ model: PaymentCurrency, as: 'currencies' }],
799
+ });
800
+
801
+ // Collect all unique currencies
802
+ const currencyMap = new Map<string, any>();
803
+ const currentCurrencyId = invoice.currency_id;
804
+
805
+ // Add current currency first
806
+ const { paymentCurrency } = invoice as any;
807
+ if (paymentCurrency) {
808
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
809
+ currencyMap.set(paymentCurrency.id, {
810
+ ...paymentCurrency.toJSON(),
811
+ method: paymentMethod?.toJSON(),
812
+ });
813
+ }
814
+
815
+ // Add other available currencies from prices
816
+ for (const price of prices) {
817
+ const priceCurrencies = (price as any).currencies || [];
818
+ for (const currency of priceCurrencies) {
819
+ if (!currencyMap.has(currency.id)) {
820
+ // eslint-disable-next-line no-await-in-loop
821
+ const paymentMethod = await PaymentMethod.findByPk(currency.payment_method_id);
822
+ currencyMap.set(currency.id, {
823
+ ...currency.toJSON(),
824
+ method: paymentMethod?.toJSON(),
825
+ });
826
+ }
827
+ }
828
+ }
829
+
830
+ // Calculate estimated amounts for each currency
831
+ const exchangeRateService = getExchangeRateService();
832
+ const options = [];
833
+
834
+ for (const [currencyId, currency] of currencyMap) {
835
+ let estimatedAmount = '';
836
+ const isCurrentMethod = currencyId === currentCurrencyId;
837
+
838
+ // For current method, use the invoice amount
839
+ if (isCurrentMethod) {
840
+ estimatedAmount = formatAmountForDisplay(invoice.amount_due, currency.decimal, currency.symbol);
841
+ } else {
842
+ // For other methods, calculate based on USD base amount and current exchange rate
843
+ try {
844
+ // Get USD total from line items
845
+ let usdTotal = 0;
846
+ for (const item of invoiceItems) {
847
+ const itemPrice = prices.find((p) => p.id === item.price_id);
848
+ if (itemPrice?.base_amount) {
849
+ usdTotal += parseFloat(itemPrice.base_amount) * item.quantity;
850
+ }
851
+ }
852
+
853
+ if (usdTotal > 0) {
854
+ // Get exchange rate for this currency
855
+ const rateSymbol =
856
+ currency.method?.type === 'arcblock' ? 'ABT' : `${currency.symbol}@${currency.method?.type}`;
857
+ // eslint-disable-next-line no-await-in-loop
858
+ const rateResult = await exchangeRateService.getRate(rateSymbol);
859
+ const rate = parseFloat(rateResult.rate);
860
+
861
+ if (rate > 0) {
862
+ const tokenAmount = usdTotal / rate;
863
+ const tokenAmountInUnits = Math.floor(tokenAmount * 10 ** currency.decimal);
864
+ estimatedAmount = `≈ ${formatAmountForDisplay(tokenAmountInUnits.toString(), currency.decimal, currency.symbol)}`;
865
+ }
866
+ }
867
+ } catch (error) {
868
+ logger.warn('Failed to calculate estimated amount for currency', {
869
+ currencyId,
870
+ error: (error as Error).message,
871
+ });
872
+ estimatedAmount = '—';
873
+ }
874
+ }
875
+
876
+ options.push({
877
+ currency,
878
+ estimatedAmount,
879
+ isCurrentMethod,
880
+ });
881
+ }
882
+
883
+ // Sort: current method first, then by symbol
884
+ options.sort((a, b) => {
885
+ if (a.isCurrentMethod) return -1;
886
+ if (b.isCurrentMethod) return 1;
887
+ return a.currency.symbol.localeCompare(b.currency.symbol);
888
+ });
889
+
890
+ return c.json({ options });
891
+ } catch (err: any) {
892
+ logger.error('Failed to get payment options for invoice', { error: err, invoiceId: c.req.param('id') });
893
+ return c.json({ error: `Failed to get payment options: ${err.message}` }, 500);
765
894
  }
766
895
  });
767
896
 
768
- router.get('/:id', authPortal, async (req, res) => {
897
+ app.get('/:id', authPortal, async (c) => {
769
898
  try {
770
899
  const doc = (await Invoice.findOne({
771
- where: { id: req.params.id },
900
+ where: { id: c.req.param('id') },
772
901
  include: [
773
902
  { model: PaymentCurrency, as: 'paymentCurrency' },
774
903
  { model: PaymentMethod, as: 'paymentMethod' },
@@ -791,7 +920,7 @@ router.get('/:id', authPortal, async (req, res) => {
791
920
  })) as TInvoiceExpanded | null;
792
921
 
793
922
  if (doc) {
794
- const shouldSync = req.query.sync === 'true' || !!req.query.forceSync;
923
+ const shouldSync = c.req.query('sync') === 'true' || !!c.req.query('forceSync');
795
924
  // Sync Stripe invoice when sync=true query parameter is present
796
925
  if (doc.metadata?.stripe_id && doc.status !== 'paid') {
797
926
  // @ts-ignore
@@ -953,7 +1082,7 @@ router.get('/:id', authPortal, async (req, res) => {
953
1082
  const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id || doc.metadata.prev_invoice_id, {
954
1083
  attributes: ['id', 'number', 'status', 'billing_reason'],
955
1084
  });
956
- return res.json({
1085
+ return c.json({
957
1086
  ...json,
958
1087
  discountDetails,
959
1088
  relatedInvoice,
@@ -963,7 +1092,7 @@ router.get('/:id', authPortal, async (req, res) => {
963
1092
  quotes,
964
1093
  });
965
1094
  }
966
- return res.json({
1095
+ return c.json({
967
1096
  ...json,
968
1097
  discountDetails,
969
1098
  relatedCreditGrants,
@@ -972,146 +1101,25 @@ router.get('/:id', authPortal, async (req, res) => {
972
1101
  quotes,
973
1102
  });
974
1103
  }
975
- return res.status(404).json(null);
1104
+ return c.json(null, 404);
976
1105
  } catch (err) {
977
1106
  logger.error(err);
978
- return res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
979
- }
980
- });
981
-
982
- /**
983
- * Get available payment options for an invoice with estimated prices
984
- * Used when user wants to switch payment method after price change
985
- */
986
- router.get('/:id/payment-options', authPortal, async (req, res) => {
987
- try {
988
- const invoice = await Invoice.findByPk(req.params.id, {
989
- include: [
990
- { model: InvoiceItem, as: 'lines' },
991
- { model: PaymentCurrency, as: 'paymentCurrency' },
992
- ],
993
- });
994
-
995
- if (!invoice) {
996
- return res.status(404).json({ error: 'Invoice not found' });
997
- }
998
-
999
- // Get all available currencies for this invoice's products
1000
- const invoiceItems = (invoice as any).lines || [];
1001
- const priceIds = invoiceItems.map((item: any) => item.price_id).filter(Boolean);
1002
-
1003
- // Get all unique product IDs from prices
1004
- const prices = await Price.findAll({
1005
- where: { id: priceIds },
1006
- include: [{ model: PaymentCurrency, as: 'currencies' }],
1007
- });
1008
-
1009
- // Collect all unique currencies
1010
- const currencyMap = new Map<string, any>();
1011
- const currentCurrencyId = invoice.currency_id;
1012
-
1013
- // Add current currency first
1014
- const { paymentCurrency } = invoice as any;
1015
- if (paymentCurrency) {
1016
- const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
1017
- currencyMap.set(paymentCurrency.id, {
1018
- ...paymentCurrency.toJSON(),
1019
- method: paymentMethod?.toJSON(),
1020
- });
1021
- }
1022
-
1023
- // Add other available currencies from prices
1024
- for (const price of prices) {
1025
- const priceCurrencies = (price as any).currencies || [];
1026
- for (const currency of priceCurrencies) {
1027
- if (!currencyMap.has(currency.id)) {
1028
- // eslint-disable-next-line no-await-in-loop
1029
- const paymentMethod = await PaymentMethod.findByPk(currency.payment_method_id);
1030
- currencyMap.set(currency.id, {
1031
- ...currency.toJSON(),
1032
- method: paymentMethod?.toJSON(),
1033
- });
1034
- }
1035
- }
1036
- }
1037
-
1038
- // Calculate estimated amounts for each currency
1039
- const exchangeRateService = getExchangeRateService();
1040
- const options = [];
1041
-
1042
- for (const [currencyId, currency] of currencyMap) {
1043
- let estimatedAmount = '';
1044
- const isCurrentMethod = currencyId === currentCurrencyId;
1045
-
1046
- // For current method, use the invoice amount
1047
- if (isCurrentMethod) {
1048
- estimatedAmount = formatAmountForDisplay(invoice.amount_due, currency.decimal, currency.symbol);
1049
- } else {
1050
- // For other methods, calculate based on USD base amount and current exchange rate
1051
- try {
1052
- // Get USD total from line items
1053
- let usdTotal = 0;
1054
- for (const item of invoiceItems) {
1055
- const itemPrice = prices.find((p) => p.id === item.price_id);
1056
- if (itemPrice?.base_amount) {
1057
- usdTotal += parseFloat(itemPrice.base_amount) * item.quantity;
1058
- }
1059
- }
1060
-
1061
- if (usdTotal > 0) {
1062
- // Get exchange rate for this currency
1063
- const rateSymbol =
1064
- currency.method?.type === 'arcblock' ? 'ABT' : `${currency.symbol}@${currency.method?.type}`;
1065
- // eslint-disable-next-line no-await-in-loop
1066
- const rateResult = await exchangeRateService.getRate(rateSymbol);
1067
- const rate = parseFloat(rateResult.rate);
1068
-
1069
- if (rate > 0) {
1070
- const tokenAmount = usdTotal / rate;
1071
- const tokenAmountInUnits = Math.floor(tokenAmount * 10 ** currency.decimal);
1072
- estimatedAmount = `≈ ${formatAmountForDisplay(tokenAmountInUnits.toString(), currency.decimal, currency.symbol)}`;
1073
- }
1074
- }
1075
- } catch (error) {
1076
- logger.warn('Failed to calculate estimated amount for currency', {
1077
- currencyId,
1078
- error: (error as Error).message,
1079
- });
1080
- estimatedAmount = '—';
1081
- }
1082
- }
1083
-
1084
- options.push({
1085
- currency,
1086
- estimatedAmount,
1087
- isCurrentMethod,
1088
- });
1089
- }
1090
-
1091
- // Sort: current method first, then by symbol
1092
- options.sort((a, b) => {
1093
- if (a.isCurrentMethod) return -1;
1094
- if (b.isCurrentMethod) return 1;
1095
- return a.currency.symbol.localeCompare(b.currency.symbol);
1096
- });
1097
-
1098
- return res.json({ options });
1099
- } catch (err: any) {
1100
- logger.error('Failed to get payment options for invoice', { error: err, invoiceId: req.params.id });
1101
- return res.status(500).json({ error: `Failed to get payment options: ${err.message}` });
1107
+ return c.json({ error: `Failed to get invoice: ${err.message}` }, 500);
1102
1108
  }
1103
1109
  });
1104
1110
 
1105
- router.post('/pay-stripe', authPortal, async (req, res) => {
1111
+ // Static path registered before PUT /:id so hono matches it first.
1112
+ app.post('/pay-stripe', authPortal, async (c) => {
1106
1113
  try {
1107
- const { invoice_ids, subscription_id, customer_id, currency_id } = req.body;
1114
+ const body = c.get('sanitizedBody') ?? {};
1115
+ const { invoice_ids, subscription_id, customer_id, currency_id } = body as any;
1108
1116
 
1109
1117
  if (!currency_id) {
1110
- return res.status(400).json({ error: 'currency_id is required' });
1118
+ return c.json({ error: 'currency_id is required' }, 400);
1111
1119
  }
1112
1120
 
1113
1121
  if (!invoice_ids && !subscription_id && !customer_id) {
1114
- return res.status(400).json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' });
1122
+ return c.json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' }, 400);
1115
1123
  }
1116
1124
 
1117
1125
  let invoices: Invoice[];
@@ -1132,7 +1140,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1132
1140
  });
1133
1141
 
1134
1142
  if (invoices.length === 0) {
1135
- return res.status(404).json({ error: 'No payable invoices found' });
1143
+ return c.json({ error: 'No payable invoices found' }, 404);
1136
1144
  }
1137
1145
 
1138
1146
  // @ts-ignore
@@ -1144,7 +1152,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1144
1152
  });
1145
1153
 
1146
1154
  if (!subscription) {
1147
- return res.status(404).json({ error: 'Subscription not found' });
1155
+ return c.json({ error: 'Subscription not found' }, 404);
1148
1156
  }
1149
1157
 
1150
1158
  // @ts-ignore
@@ -1165,7 +1173,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1165
1173
  } else {
1166
1174
  customer = await Customer.findByPkOrDid(customer_id!);
1167
1175
  if (!customer) {
1168
- return res.status(404).json({ error: 'Customer not found' });
1176
+ return c.json({ error: 'Customer not found' }, 404);
1169
1177
  }
1170
1178
 
1171
1179
  invoices = await Invoice.findAll({
@@ -1181,22 +1189,22 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1181
1189
  });
1182
1190
 
1183
1191
  if (invoices.length === 0) {
1184
- return res.status(404).json({ error: 'No payable invoices found' });
1192
+ return c.json({ error: 'No payable invoices found' }, 404);
1185
1193
  }
1186
1194
 
1187
1195
  paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
1188
1196
  }
1189
1197
 
1190
1198
  if (!customer) {
1191
- return res.status(404).json({ error: 'Customer not found' });
1199
+ return c.json({ error: 'Customer not found' }, 404);
1192
1200
  }
1193
1201
 
1194
1202
  if (!paymentMethod || paymentMethod.type !== 'stripe') {
1195
- return res.status(400).json({ error: 'Not using Stripe payment method' });
1203
+ return c.json({ error: 'Not using Stripe payment method' }, 400);
1196
1204
  }
1197
1205
 
1198
1206
  if (invoices.length === 0) {
1199
- return res.status(400).json({ error: 'No payable invoices found' });
1207
+ return c.json({ error: 'No payable invoices found' }, 400);
1200
1208
  }
1201
1209
 
1202
1210
  await ensureStripeCustomer(customer, paymentMethod);
@@ -1205,7 +1213,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1205
1213
 
1206
1214
  const paymentCurrency = await PaymentCurrency.findByPk(currency_id);
1207
1215
  if (!paymentCurrency) {
1208
- return res.status(404).json({ error: `Payment currency ${currency_id} not found` });
1216
+ return c.json({ error: `Payment currency ${currency_id} not found` }, 404);
1209
1217
  }
1210
1218
  const totalAmount = invoices.reduce((sum, invoice) => {
1211
1219
  const amount = invoice.amount_remaining || '0';
@@ -1220,7 +1228,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1220
1228
 
1221
1229
  const setupIntent = await ensureStripeSetupIntentForInvoicePayment(customer, paymentMethod, metadata);
1222
1230
 
1223
- return res.json({
1231
+ return c.json({
1224
1232
  client_secret: setupIntent.client_secret,
1225
1233
  publishable_key: settings.stripe?.publishable_key,
1226
1234
  setup_intent_id: setupIntent.id,
@@ -1232,53 +1240,54 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
1232
1240
  } catch (err) {
1233
1241
  logger.error('Failed to create setup intent for stripe payment', {
1234
1242
  error: err,
1235
- body: req.body,
1243
+ body: c.get('sanitizedBody'),
1236
1244
  });
1237
- return res.status(400).json({ error: err.message });
1245
+ return c.json({ error: err.message }, 400);
1238
1246
  }
1239
1247
  });
1240
1248
 
1241
1249
  // eslint-disable-next-line consistent-return
1242
- router.put('/:id', authAdmin, async (req, res) => {
1250
+ app.put('/:id', authAdmin, async (c) => {
1243
1251
  try {
1244
- const doc = await Invoice.findByPk(req.params.id as string);
1252
+ const doc = await Invoice.findByPk(c.req.param('id') as string);
1245
1253
  if (!doc) {
1246
- return res.status(404).json({ error: 'Invoice not found' });
1254
+ return c.json({ error: 'Invoice not found' }, 404);
1247
1255
  }
1248
1256
 
1249
- const raw = pick(req.body, ['metadata']);
1257
+ const body = c.get('sanitizedBody') ?? {};
1258
+ const raw = pick(body, ['metadata']);
1250
1259
  if (raw.metadata) {
1251
1260
  const { error } = MetadataSchema.validate(raw.metadata);
1252
1261
  if (error) {
1253
- return res.status(400).json({ error: error.message });
1262
+ return c.json({ error: error.message }, 400);
1254
1263
  }
1255
1264
  raw.metadata = formatMetadata(raw.metadata);
1256
1265
  }
1257
1266
 
1258
1267
  await doc.update(raw);
1259
- res.json(doc);
1268
+ return c.json(doc);
1260
1269
  } catch (err) {
1261
1270
  logger.error(err);
1262
- res.json(null);
1271
+ return c.json(null);
1263
1272
  }
1264
1273
  });
1265
1274
 
1266
- router.post('/:id/void', authAdmin, async (req, res) => {
1267
- const invoice = await Invoice.findByPk(req.params.id as string);
1275
+ app.post('/:id/void', authAdmin, async (c) => {
1276
+ const invoice = await Invoice.findByPk(c.req.param('id') as string);
1268
1277
  if (!invoice) {
1269
- return res.status(404).json({ error: 'Invoice not found' });
1278
+ return c.json({ error: 'Invoice not found' }, 404);
1270
1279
  }
1271
1280
  if (['paid', 'void', 'draft'].includes(invoice.status)) {
1272
- return res.status(400).json({ error: 'Can not void this invoice' });
1281
+ return c.json({ error: 'Can not void this invoice' }, 400);
1273
1282
  }
1274
1283
  const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
1275
1284
  if (!paymentMethod) {
1276
- return res.status(400).json({ error: 'Payment method not found' });
1285
+ return c.json({ error: 'Payment method not found' }, 400);
1277
1286
  }
1278
1287
  if (invoice.subscription_id) {
1279
1288
  const subscription = await Subscription.findByPk(invoice.subscription_id);
1280
1289
  if (subscription && !subscription.isImmutable()) {
1281
- return res.status(400).json({ error: 'Subscription is not immutable, can not void invoice' });
1290
+ return c.json({ error: 'Subscription is not immutable, can not void invoice' }, 400);
1282
1291
  }
1283
1292
  }
1284
1293
  try {
@@ -1303,11 +1312,11 @@ router.post('/:id/void', authAdmin, async (req, res) => {
1303
1312
  voided_at: dayjs().unix(),
1304
1313
  },
1305
1314
  });
1306
- return res.json(invoice);
1315
+ return c.json(invoice);
1307
1316
  } catch (error) {
1308
1317
  logger.error('Failed to void invoice', { error, invoiceId: invoice.id });
1309
- return res.status(400).json({ error: 'Failed to void invoice' });
1318
+ return c.json({ error: 'Failed to void invoice' }, 400);
1310
1319
  }
1311
1320
  });
1312
1321
 
1313
- export default router;
1322
+ export default app;