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,17 +1,21 @@
1
1
  /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/require-await */
2
- import { Router } from 'express';
2
+ // Phase 3 (express→hono) hono fork of routes/promotion-codes.ts. Sub-app with
3
+ // routes relative to /api/promotion-codes (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).
6
+ import { Hono } from 'hono';
3
7
  import Joi from 'joi';
4
8
  import pick from 'lodash/pick';
5
9
 
6
- import { createIdGenerator, formatMetadata } from '../libs/util';
10
+ import { createIdGenerator, formatMetadata } from '../../libs/util';
7
11
 
8
- import { authenticate } from '../libs/security';
9
- import { PromotionCode, Coupon, PaymentCurrency } from '../store/models';
10
- import { getRedemptionData } from '../libs/discount/redemption';
11
- import { createListParamSchema, MetadataSchema } from '../libs/api';
12
- import logger from '../libs/logger';
12
+ import { authenticate } from '../../middlewares/hono/security';
13
+ import { PromotionCode, Coupon, PaymentCurrency } from '../../store/models';
14
+ import { getRedemptionData } from '../../libs/discount/redemption';
15
+ import { createListParamSchema, MetadataSchema } from '../../libs/api';
16
+ import logger from '../../libs/logger';
13
17
 
14
- const router = Router();
18
+ const app = new Hono();
15
19
  const authAdmin = authenticate({ component: true, roles: ['owner', 'admin'] });
16
20
 
17
21
  // Get expanded promotion code with all related information
@@ -90,7 +94,7 @@ export async function createPromotionCode(payload: any) {
90
94
  * POST /api/promotion-codes
91
95
  * Create a new promotion code
92
96
  */
93
- router.post('/', authAdmin, async (req, res) => {
97
+ app.post('/', authAdmin, async (c) => {
94
98
  try {
95
99
  const schema = Joi.object({
96
100
  coupon_id: Joi.string().required(),
@@ -131,34 +135,38 @@ router.post('/', authAdmin, async (req, res) => {
131
135
  metadata: Joi.object().optional(),
132
136
  });
133
137
 
134
- const { error, value } = schema.validate(req.body);
138
+ const body = c.get('sanitizedBody') ?? {};
139
+ const { error, value } = schema.validate(body);
135
140
  if (error) {
136
- return res.status(400).json({
137
- error: 'Invalid request',
138
- details: error.details?.[0]?.message || 'Validation error',
139
- });
141
+ return c.json(
142
+ {
143
+ error: 'Invalid request',
144
+ details: error.details?.[0]?.message || 'Validation error',
145
+ },
146
+ 400
147
+ );
140
148
  }
141
149
 
142
150
  const promotionCode = await createPromotionCode({
143
151
  ...value,
144
- livemode: req.livemode,
145
- created_via: req.user?.via || 'api',
152
+ livemode: c.get('livemode'),
153
+ created_via: c.get('user')?.via || 'api',
146
154
  });
147
155
 
148
156
  logger.info('Promotion code created', {
149
157
  promotionCodeId: promotionCode.id,
150
158
  code: promotionCode.code,
151
- requestedBy: req.user?.did,
159
+ requestedBy: c.get('user')?.did,
152
160
  });
153
161
 
154
162
  const doc = await getExpandedPromotionCode(promotionCode.id as string);
155
- return res.json(doc);
163
+ return c.json(doc);
156
164
  } catch (error) {
157
165
  logger.error('Error creating promotion code', {
158
- error: error.message,
159
- body: req.body,
166
+ error: (error as any).message,
167
+ body: c.get('sanitizedBody') ?? {},
160
168
  });
161
- return res.status(400).json({ error: error.message });
169
+ return c.json({ error: (error as any).message }, 400);
162
170
  }
163
171
  });
164
172
 
@@ -166,7 +174,7 @@ router.post('/', authAdmin, async (req, res) => {
166
174
  * GET /api/promotion-codes
167
175
  * List all promotion codes with pagination
168
176
  */
169
- router.get('/', authAdmin, async (req, res) => {
177
+ app.get('/', authAdmin, async (c) => {
170
178
  const schema = Joi.object({
171
179
  page: Joi.number().integer().positive().default(1),
172
180
  pageSize: Joi.number().integer().positive().max(100).default(20),
@@ -174,19 +182,22 @@ router.get('/', authAdmin, async (req, res) => {
174
182
  active: Joi.boolean().optional(),
175
183
  });
176
184
 
177
- const { error, value } = schema.validate(req.query, {
185
+ const { error, value } = schema.validate(c.req.query(), {
178
186
  stripUnknown: false,
179
187
  allowUnknown: true,
180
188
  });
181
189
  if (error) {
182
- return res.status(400).json({
183
- error: 'Invalid request',
184
- details: error.details?.[0]?.message || 'Validation error',
185
- });
190
+ return c.json(
191
+ {
192
+ error: 'Invalid request',
193
+ details: error.details?.[0]?.message || 'Validation error',
194
+ },
195
+ 400
196
+ );
186
197
  }
187
198
 
188
199
  const { page, pageSize, coupon_id, active } = value;
189
- const where: any = { livemode: req.livemode };
200
+ const where: any = { livemode: c.get('livemode') };
190
201
 
191
202
  if (coupon_id) {
192
203
  where.coupon_id = coupon_id;
@@ -204,32 +215,155 @@ router.get('/', authAdmin, async (req, res) => {
204
215
  order: [['created_at', 'DESC']],
205
216
  });
206
217
 
207
- return res.json({
218
+ return c.json({
208
219
  count,
209
220
  list: rows.map((promotionCode) => promotionCode.toJSON()),
210
221
  paging: { page, pageSize },
211
222
  });
212
223
  });
213
224
 
225
+ /**
226
+ * GET /api/promotion-codes/by-code/:code
227
+ * Get promotion code details by code (admin only)
228
+ * Registered before /:id so the static segment wins.
229
+ */
230
+ app.get('/by-code/:code', authAdmin, async (c) => {
231
+ const code = c.req.param('code');
232
+ if (!code) {
233
+ return c.json({ error: 'Code parameter is required' }, 400);
234
+ }
235
+
236
+ const promotionCode = await PromotionCode.findByCode(code.toLowerCase());
237
+
238
+ if (!promotionCode) {
239
+ return c.json({ error: 'Promotion code not found' }, 404);
240
+ }
241
+ const doc = await getExpandedPromotionCode(promotionCode.id as string);
242
+
243
+ return c.json(doc);
244
+ });
245
+
246
+ /**
247
+ * GET /api/promotion-codes/:id/used
248
+ * Check if a promotion code is being used
249
+ * Registered before /:id to avoid shadowing by the generic handler.
250
+ */
251
+ app.get('/:id/used', authAdmin, async (c) => {
252
+ const promotionCode = await PromotionCode.findOne({
253
+ where: { id: c.req.param('id'), livemode: c.get('livemode') },
254
+ });
255
+
256
+ if (!promotionCode) {
257
+ return c.json({ error: 'Promotion code not found' }, 404);
258
+ }
259
+
260
+ const used = await promotionCode.isUsed();
261
+ if (!used) {
262
+ await promotionCode.update({ locked: false });
263
+ }
264
+
265
+ return c.json({ used });
266
+ });
267
+
268
+ // Create redemptions pagination schema for promotion codes
269
+ const promotionCodeRedemptionsSchema = createListParamSchema<{
270
+ type?: string;
271
+ }>({
272
+ type: Joi.string().valid('customer', 'subscription').optional(),
273
+ });
274
+
275
+ /**
276
+ * GET /api/promotion-codes/:id/redemptions
277
+ * Get active redemptions for a promotion code with detailed admin analytics
278
+ * Registered before /:id to avoid shadowing by the generic handler.
279
+ */
280
+ app.get('/:id/redemptions', authAdmin, async (c) => {
281
+ try {
282
+ const promotionCodeId = c.req.param('id') as string;
283
+
284
+ const { page, pageSize, type } = await promotionCodeRedemptionsSchema.validateAsync(c.req.query(), {
285
+ stripUnknown: false,
286
+ allowUnknown: true,
287
+ });
288
+
289
+ const promotionCode = await PromotionCode.findByPk(promotionCodeId);
290
+ if (!promotionCode) {
291
+ return c.json({ error: 'Promotion code not found' }, 404);
292
+ }
293
+
294
+ const result = await getRedemptionData(
295
+ { promotion_code_id: promotionCodeId },
296
+ { page, pageSize, type: type as 'customer' | 'subscription' | undefined },
297
+ promotionCode,
298
+ 'promotion_code'
299
+ );
300
+
301
+ return c.json(result);
302
+ } catch (error: any) {
303
+ logger.error('Error getting promotion code redemptions', {
304
+ error: error.message,
305
+ stack: error.stack,
306
+ promotionCodeId: c.req.param('id'),
307
+ });
308
+ return c.json({ error: error.message }, 500);
309
+ }
310
+ });
311
+
214
312
  /**
215
313
  * GET /api/promotion-codes/:id
216
314
  * Get promotion code details by ID (admin only)
217
315
  */
218
- router.get('/:id', authAdmin, async (req, res) => {
219
- const promotionCode = await getExpandedPromotionCode(req.params.id as string);
316
+ app.get('/:id', authAdmin, async (c) => {
317
+ const promotionCode = await getExpandedPromotionCode(c.req.param('id') as string);
220
318
 
221
319
  if (!promotionCode) {
222
- return res.status(404).json({ error: 'Promotion code not found' });
320
+ return c.json({ error: 'Promotion code not found' }, 404);
223
321
  }
224
322
 
225
- return res.json(promotionCode);
323
+ return c.json(promotionCode);
324
+ });
325
+
326
+ /**
327
+ * PUT /api/promotion-codes/:id/archive
328
+ * Archive a promotion code (set active to false, making it unusable)
329
+ * Registered before PUT /:id so the static sub-path wins.
330
+ */
331
+ app.put('/:id/archive', authAdmin, async (c) => {
332
+ try {
333
+ const promotionCode = await PromotionCode.findOne({
334
+ where: { id: c.req.param('id'), livemode: c.get('livemode') },
335
+ });
336
+
337
+ if (!promotionCode) {
338
+ return c.json({ error: 'Promotion code not found' }, 404);
339
+ }
340
+
341
+ if (!promotionCode.active) {
342
+ return c.json({ error: 'Promotion code is already archived' }, 400);
343
+ }
344
+
345
+ // Archive the promotion code by setting active to false
346
+ await promotionCode.update({ active: false });
347
+
348
+ logger.info('Promotion code archived', {
349
+ promotionCodeId: c.req.param('id'),
350
+ requestedBy: c.get('user')?.did,
351
+ });
352
+ return c.json(promotionCode);
353
+ } catch (error) {
354
+ logger.error('Error archiving promotion code', {
355
+ error: (error as any).message,
356
+ id: c.req.param('id'),
357
+ });
358
+ return c.json({ error: (error as any).message }, 400);
359
+ }
226
360
  });
227
361
 
228
362
  /**
229
363
  * PUT /api/promotion-codes/:id
230
364
  * Update a promotion code (limited fields can be updated)
231
365
  */
232
- router.put('/:id', authAdmin, async (req, res) => {
366
+ app.put('/:id', authAdmin, async (c) => {
233
367
  try {
234
368
  const schema = Joi.object({
235
369
  description: Joi.string().empty('').max(250).optional(),
@@ -252,30 +386,34 @@ router.put('/:id', authAdmin, async (req, res) => {
252
386
  metadata: MetadataSchema,
253
387
  });
254
388
 
255
- const { error, value } = schema.validate(req.body, {
389
+ const body = c.get('sanitizedBody') ?? {};
390
+ const { error, value } = schema.validate(body, {
256
391
  stripUnknown: true,
257
392
  });
258
393
  if (error) {
259
- return res.status(400).json({
260
- error: 'Invalid request',
261
- details: error.details?.[0]?.message || 'Validation error',
262
- });
394
+ return c.json(
395
+ {
396
+ error: 'Invalid request',
397
+ details: error.details?.[0]?.message || 'Validation error',
398
+ },
399
+ 400
400
+ );
263
401
  }
264
402
 
265
403
  const promotionCode = await PromotionCode.findOne({
266
- where: { id: req.params.id },
404
+ where: { id: c.req.param('id') },
267
405
  });
268
406
 
269
407
  if (!promotionCode) {
270
- return res.status(404).json({ error: 'Promotion code not found' });
408
+ return c.json({ error: 'Promotion code not found' }, 404);
271
409
  }
272
410
 
273
411
  if (promotionCode.locked) {
274
- return res.status(403).json({ error: 'Promotion code is locked and cannot be modified' });
412
+ return c.json({ error: 'Promotion code is locked and cannot be modified' }, 403);
275
413
  }
276
414
 
277
415
  if (!promotionCode.active) {
278
- return res.status(403).json({ error: 'Promotion code is invalid and cannot be modified' });
416
+ return c.json({ error: 'Promotion code is invalid and cannot be modified' }, 403);
279
417
  }
280
418
 
281
419
  // Format restrictions currency_options if provided
@@ -290,9 +428,12 @@ router.put('/:id', authAdmin, async (req, res) => {
290
428
  await promotionCode.update({ locked: true });
291
429
  const allowedUpdates = pick(value, ['metadata']);
292
430
  if (Object.keys(allowedUpdates).length === 0) {
293
- return res.status(403).json({
294
- error: 'Promotion code is being used. Only metadata can be updated.',
295
- });
431
+ return c.json(
432
+ {
433
+ error: 'Promotion code is being used. Only metadata can be updated.',
434
+ },
435
+ 403
436
+ );
296
437
  }
297
438
  await promotionCode.update(PromotionCode.formatBeforeSave(allowedUpdates));
298
439
  } else {
@@ -300,55 +441,20 @@ router.put('/:id', authAdmin, async (req, res) => {
300
441
  }
301
442
 
302
443
  logger.info('Promotion code updated', {
303
- promotionCodeId: req.params.id,
444
+ promotionCodeId: c.req.param('id'),
304
445
  updatedFields: Object.keys(value),
305
- requestedBy: req.user?.did,
446
+ requestedBy: c.get('user')?.did,
306
447
  });
307
448
 
308
- const doc = await getExpandedPromotionCode(req.params.id as string);
309
- return res.json(doc);
449
+ const doc = await getExpandedPromotionCode(c.req.param('id') as string);
450
+ return c.json(doc);
310
451
  } catch (error) {
311
452
  logger.error('Error updating promotion code', {
312
- error: error.message,
313
- id: req.params.id,
314
- body: req.body,
315
- });
316
- return res.status(400).json({ error: error.message });
317
- }
318
- });
319
-
320
- /**
321
- * PUT /api/promotion-codes/:id/archive
322
- * Archive a promotion code (set active to false, making it unusable)
323
- */
324
- router.put('/:id/archive', authAdmin, async (req, res) => {
325
- try {
326
- const promotionCode = await PromotionCode.findOne({
327
- where: { id: req.params.id, livemode: req.livemode },
328
- });
329
-
330
- if (!promotionCode) {
331
- return res.status(404).json({ error: 'Promotion code not found' });
332
- }
333
-
334
- if (!promotionCode.active) {
335
- return res.status(400).json({ error: 'Promotion code is already archived' });
336
- }
337
-
338
- // Archive the promotion code by setting active to false
339
- await promotionCode.update({ active: false });
340
-
341
- logger.info('Promotion code archived', {
342
- promotionCodeId: req.params.id,
343
- requestedBy: req.user?.did,
453
+ error: (error as any).message,
454
+ id: c.req.param('id'),
455
+ body: c.get('sanitizedBody') ?? {},
344
456
  });
345
- return res.json(promotionCode);
346
- } catch (error) {
347
- logger.error('Error archiving promotion code', {
348
- error: error.message,
349
- id: req.params.id,
350
- });
351
- return res.status(400).json({ error: error.message });
457
+ return c.json({ error: (error as any).message }, 400);
352
458
  }
353
459
  });
354
460
 
@@ -356,127 +462,43 @@ router.put('/:id/archive', authAdmin, async (req, res) => {
356
462
  * DELETE /api/promotion-codes/:id
357
463
  * Delete a promotion code (mark as inactive if used, otherwise hard delete)
358
464
  */
359
- router.delete('/:id', authAdmin, async (req, res) => {
465
+ app.delete('/:id', authAdmin, async (c) => {
360
466
  try {
361
467
  const promotionCode = await PromotionCode.findOne({
362
- where: { id: req.params.id, livemode: req.livemode },
468
+ where: { id: c.req.param('id'), livemode: c.get('livemode') },
363
469
  });
364
470
 
365
471
  if (!promotionCode) {
366
- return res.status(404).json({ error: 'Promotion code not found' });
472
+ return c.json({ error: 'Promotion code not found' }, 404);
367
473
  }
368
474
 
369
475
  if (promotionCode.locked) {
370
- return res.status(403).json({ error: 'Promotion code is locked and cannot be deleted' });
476
+ return c.json({ error: 'Promotion code is locked and cannot be deleted' }, 403);
371
477
  }
372
478
 
373
479
  // Check if promotion code is being used
374
480
  if (await promotionCode.isUsed()) {
375
481
  // Mark as inactive instead of deleting
376
482
  await promotionCode.update({ locked: true });
377
- return res.status(403).json({ error: 'Promotion code is locked and cannot be deleted' });
483
+ return c.json({ error: 'Promotion code is locked and cannot be deleted' }, 403);
378
484
  }
379
485
 
380
486
  // Hard delete if not used
381
487
  await promotionCode.destroy();
382
488
 
383
489
  logger.info('Promotion code deleted', {
384
- promotionCodeId: req.params.id,
385
- requestedBy: req.user?.did,
490
+ promotionCodeId: c.req.param('id'),
491
+ requestedBy: c.get('user')?.did,
386
492
  });
387
493
 
388
- return res.json(promotionCode);
494
+ return c.json(promotionCode);
389
495
  } catch (error) {
390
496
  logger.error('Error deleting promotion code', {
391
- error: error.message,
392
- id: req.params.id,
393
- });
394
- return res.status(400).json({ error: error.message });
395
- }
396
- });
397
-
398
- /**
399
- * GET /api/promotion-codes/:id/used
400
- * Check if a promotion code is being used
401
- */
402
- router.get('/:id/used', authAdmin, async (req, res) => {
403
- const promotionCode = await PromotionCode.findOne({
404
- where: { id: req.params.id, livemode: req.livemode },
405
- });
406
-
407
- if (!promotionCode) {
408
- return res.status(404).json({ error: 'Promotion code not found' });
409
- }
410
-
411
- const used = await promotionCode.isUsed();
412
- if (!used) {
413
- await promotionCode.update({ locked: false });
414
- }
415
-
416
- return res.json({ used });
417
- });
418
-
419
- /**
420
- * GET /api/promotion-codes/by-code/:code
421
- * Get promotion code details by code (admin only)
422
- */
423
- router.get('/by-code/:code', authAdmin, async (req, res) => {
424
- const { code } = req.params;
425
- if (!code) {
426
- return res.status(400).json({ error: 'Code parameter is required' });
427
- }
428
-
429
- const promotionCode = await PromotionCode.findByCode(code.toLowerCase());
430
-
431
- if (!promotionCode) {
432
- return res.status(404).json({ error: 'Promotion code not found' });
433
- }
434
- const doc = await getExpandedPromotionCode(promotionCode.id as string);
435
-
436
- return res.json(doc);
437
- });
438
-
439
- // Create redemptions pagination schema for promotion codes
440
- const promotionCodeRedemptionsSchema = createListParamSchema<{
441
- type?: string;
442
- }>({
443
- type: Joi.string().valid('customer', 'subscription').optional(),
444
- });
445
-
446
- /**
447
- * GET /api/promotion-codes/:id/redemptions
448
- * Get active redemptions for a promotion code with detailed admin analytics
449
- */
450
- router.get('/:id/redemptions', authAdmin, async (req, res) => {
451
- try {
452
- const promotionCodeId = req.params.id as string;
453
-
454
- const { page, pageSize, type } = await promotionCodeRedemptionsSchema.validateAsync(req.query, {
455
- stripUnknown: false,
456
- allowUnknown: true,
457
- });
458
-
459
- const promotionCode = await PromotionCode.findByPk(promotionCodeId);
460
- if (!promotionCode) {
461
- return res.status(404).json({ error: 'Promotion code not found' });
462
- }
463
-
464
- const result = await getRedemptionData(
465
- { promotion_code_id: promotionCodeId },
466
- { page, pageSize, type: type as 'customer' | 'subscription' | undefined },
467
- promotionCode,
468
- 'promotion_code'
469
- );
470
-
471
- return res.json(result);
472
- } catch (error: any) {
473
- logger.error('Error getting promotion code redemptions', {
474
- error: error.message,
475
- stack: error.stack,
476
- promotionCodeId: req.params.id,
497
+ error: (error as any).message,
498
+ id: c.req.param('id'),
477
499
  });
478
- return res.status(500).json({ error: error.message });
500
+ return c.json({ error: (error as any).message }, 400);
479
501
  }
480
502
  });
481
503
 
482
- export default router;
504
+ export default app;
@@ -0,0 +1,24 @@
1
+ // Phase 3 (express→hono) — hono fork of routes/redirect.ts. Sub-app with
2
+ // routes relative to /api/redirect (mounted via mountResourceGroup). The
3
+ // business logic is unchanged; only the express plumbing becomes hono:
4
+ // res.redirect(u) → return c.redirect(u).
5
+ import qs from 'querystring';
6
+
7
+ import { getUrl } from '@blocklet/sdk/lib/component';
8
+ import { Hono } from 'hono';
9
+
10
+ const app = new Hono();
11
+
12
+ app.get('/checkout/:entryId', (c) => {
13
+ const entryId = c.req.param('entryId');
14
+ if (entryId.startsWith('plink_')) {
15
+ return c.redirect(getUrl(`/checkout/pay/${entryId}?${qs.stringify(c.req.query() as any)}`));
16
+ }
17
+ if (entryId.startsWith('prctbl_')) {
18
+ return c.redirect(getUrl(`/checkout/pricing-table/${entryId}?${qs.stringify(c.req.query() as any)}`));
19
+ }
20
+
21
+ return c.redirect(getUrl('/'));
22
+ });
23
+
24
+ export default app;