payment-kit 1.29.0 → 1.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +36 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +27 -24
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  236. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  237. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  238. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  239. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  240. package/cloudflare/queue-runtime-mode.ts +13 -0
  241. package/cloudflare/run-build.js +31 -56
  242. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  243. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  244. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  245. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  246. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  247. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  248. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  249. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  250. package/cloudflare/shims/cron.ts +38 -158
  251. package/cloudflare/shims/events.ts +124 -0
  252. package/cloudflare/shims/fastq.ts +15 -1
  253. package/cloudflare/shims/nedb-storage.ts +16 -8
  254. package/cloudflare/shims/node-fetch.ts +35 -0
  255. package/cloudflare/shims/xss.ts +8 -0
  256. package/cloudflare/tenant-middleware.ts +36 -0
  257. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  258. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  259. package/cloudflare/worker.ts +204 -433
  260. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  261. package/jest.config.js +3 -1
  262. package/package.json +33 -38
  263. package/scripts/core-env-whitelist.json +1 -0
  264. package/scripts/e2e-12b-runtime.ts +149 -0
  265. package/scripts/e2e-core-config.ts +125 -0
  266. package/scripts/e2e-d1-tenancy.ts +116 -0
  267. package/scripts/e2e-d2-cron-queue.ts +139 -0
  268. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  269. package/scripts/e2e-hono-s2.ts +125 -0
  270. package/scripts/e2e-hono-s3e.ts +135 -0
  271. package/scripts/e2e-hono-s4.ts +114 -0
  272. package/scripts/e2e-migration-contract.ts +100 -0
  273. package/scripts/e2e-s0.ts +61 -0
  274. package/scripts/e2e-s1.ts +107 -0
  275. package/scripts/e2e-s2.ts +178 -0
  276. package/scripts/e2e-s3.ts +110 -0
  277. package/scripts/e2e-s4.ts +191 -0
  278. package/scripts/e2e-s5.ts +139 -0
  279. package/scripts/e2e-s6.ts +127 -0
  280. package/scripts/e2e-tenant-model.ts +119 -0
  281. package/scripts/e2e-tenant-worker.ts +199 -0
  282. package/scripts/gen-sql-migrations.js +46 -0
  283. package/scripts/phase8-codemod.js +219 -0
  284. package/scripts/phase9a-env-getters-codemod.js +82 -0
  285. package/scripts/scan-core-env.js +109 -0
  286. package/scripts/scan-tenant-queries.js +235 -0
  287. package/scripts/schema-drift-guard.ts +210 -0
  288. package/scripts/tenant-scan-whitelist.json +1 -0
  289. package/src/env.d.ts +13 -1
  290. package/tsconfig.json +1 -1
  291. package/api/src/libs/did-space.ts +0 -235
  292. package/api/src/libs/middleware.ts +0 -50
  293. package/api/src/libs/security.ts +0 -192
  294. package/api/src/queues/space.ts +0 -662
  295. package/api/src/routes/credit-tokens.ts +0 -38
  296. package/api/src/routes/exchange-rates.ts +0 -87
  297. package/api/src/routes/index.ts +0 -142
  298. package/api/src/routes/integrations/stripe.ts +0 -61
  299. package/api/src/routes/meters.ts +0 -274
  300. package/api/src/routes/passports.ts +0 -68
  301. package/api/src/routes/redirect.ts +0 -20
  302. package/api/src/routes/tool.ts +0 -65
  303. package/api/src/routes/webhook-endpoints.ts +0 -126
  304. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  305. package/cloudflare/shims/did-space-js.ts +0 -17
  306. package/cloudflare/shims/did-space.ts +0 -11
  307. package/cloudflare/shims/express-compat/index.ts +0 -80
  308. package/cloudflare/shims/express-compat/types.ts +0 -41
  309. package/cloudflare/shims/lock.ts +0 -115
  310. package/cloudflare/shims/queue.ts +0 -611
  311. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  312. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,22 +1,27 @@
1
+ // Phase 3 (express→hono) — hono fork of routes/prices.ts. Sub-app with
2
+ // routes relative to /api/prices (mounted via mountResourceGroup). The
3
+ // business logic is unchanged; only the express plumbing becomes hono:
4
+ // req.body → c.get('sanitizedBody'); res.status(n).json(x) → c.json(x, n).
1
5
  import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
- import { Router } from 'express';
6
+ import { Hono } from 'hono';
3
7
  import Joi from 'joi';
4
8
  import pick from 'lodash/pick';
5
9
  import type { WhereOptions } from 'sequelize';
6
10
 
7
- import { createListParamSchema, getOrder, getWhereFromQuery, MetadataSchema } from '../libs/api';
8
- import { getExchangeRateService } from '../libs/exchange-rate';
9
- import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
10
- import logger from '../libs/logger';
11
- import { authenticate } from '../libs/security';
12
- import { canUpsell } from '../libs/session';
13
- import { PaymentCurrency } from '../store/models/payment-currency';
14
- import { Price } from '../store/models/price';
15
- import { Product } from '../store/models/product';
16
- import { checkCurrencySupportRecurring } from '../libs/product';
17
- import { ChainType, Meter, PaymentMethod } from '../store/models';
18
-
19
- const router = Router();
11
+ import { createListParamSchema, getOrder, getWhereFromQuery, MetadataSchema } from '../../libs/api';
12
+ import { allowChangeLockedPrice } from '../../libs/env';
13
+ import { getExchangeRateService } from '../../libs/exchange-rate';
14
+ import { getExchangeRateSymbol, hasTokenAddress } from '../../libs/exchange-rate/token-address-mapping';
15
+ import logger from '../../libs/logger';
16
+ import { authenticate } from '../../middlewares/hono/security';
17
+ import { canUpsell } from '../../libs/session';
18
+ import { PaymentCurrency } from '../../store/models/payment-currency';
19
+ import { Price } from '../../store/models/price';
20
+ import { Product } from '../../store/models/product';
21
+ import { checkCurrencySupportRecurring } from '../../libs/product';
22
+ import { ChainType, Meter, PaymentMethod } from '../../store/models';
23
+
24
+ const app = new Hono();
20
25
 
21
26
  const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
22
27
 
@@ -147,139 +152,84 @@ const paginationSchema = createListParamSchema<{
147
152
  product_id: Joi.string().empty(''),
148
153
  lookup_key: Joi.string().empty(''),
149
154
  });
150
- router.get('/', auth, async (req, res) => {
151
- const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
155
+
156
+ // search prices static route, registered before /:id to avoid shadowing
157
+ const searchSchema = createListParamSchema<{
158
+ query: string;
159
+ }>({
160
+ query: Joi.string(),
161
+ });
162
+ app.get('/search', auth, async (c) => {
163
+ const query = c.req.query();
164
+ const {
165
+ page,
166
+ pageSize,
167
+ query: searchQuery,
168
+ livemode,
169
+ } = await searchSchema.validateAsync(query, {
152
170
  stripUnknown: false,
153
171
  allowUnknown: true,
154
172
  });
155
- const where: WhereOptions<Price> = {};
156
173
 
157
- if (typeof active === 'boolean') {
158
- where.active = active;
159
- }
174
+ const where = getWhereFromQuery(searchQuery);
160
175
  if (typeof livemode === 'boolean') {
161
176
  where.livemode = livemode;
162
177
  }
163
- ['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
164
- // @ts-ignore
165
- if (query[key]) {
166
- // @ts-ignore
167
- where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
168
- }
169
- });
170
-
171
- Object.keys(query)
172
- .filter((x) => x.startsWith('recurring.'))
173
- .forEach((key: string) => {
174
- // @ts-ignore
175
- where[key] = query[key];
176
- });
177
-
178
178
  const { rows, count } = await Price.findAndCountAll({
179
179
  where,
180
180
  attributes: ['id'],
181
- order: getOrder(req.query, [['created_at', 'DESC']]),
181
+ order: getOrder(query, [['created_at', 'DESC']]),
182
182
  offset: (page - 1) * pageSize,
183
183
  limit: pageSize,
184
184
  });
185
185
 
186
- res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))), paging: { page, pageSize } });
186
+ return c.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
187
187
  });
188
188
 
189
- // search prices
190
- const searchSchema = createListParamSchema<{
191
- query: string;
192
- }>({
193
- query: Joi.string(),
194
- });
195
- router.get('/search', auth, async (req, res) => {
196
- const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
189
+ app.get('/', auth, async (c) => {
190
+ const query = c.req.query();
191
+ const { page, pageSize, active, livemode, ...rest } = await paginationSchema.validateAsync(query, {
197
192
  stripUnknown: false,
198
193
  allowUnknown: true,
199
194
  });
195
+ const where: WhereOptions<Price> = {};
200
196
 
201
- const where = getWhereFromQuery(query);
197
+ if (typeof active === 'boolean') {
198
+ where.active = active;
199
+ }
202
200
  if (typeof livemode === 'boolean') {
203
201
  where.livemode = livemode;
204
202
  }
203
+ ['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
204
+ // @ts-ignore
205
+ if (rest[key]) {
206
+ // @ts-ignore
207
+ where[key] = rest[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
208
+ }
209
+ });
210
+
211
+ Object.keys(rest)
212
+ .filter((x) => x.startsWith('recurring.'))
213
+ .forEach((key: string) => {
214
+ // @ts-ignore
215
+ where[key] = rest[key];
216
+ });
217
+
205
218
  const { rows, count } = await Price.findAndCountAll({
206
219
  where,
207
220
  attributes: ['id'],
208
- order: getOrder(req.query, [['created_at', 'DESC']]),
221
+ order: getOrder(query, [['created_at', 'DESC']]),
209
222
  offset: (page - 1) * pageSize,
210
223
  limit: pageSize,
211
224
  });
212
225
 
213
- res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
226
+ return c.json({
227
+ count,
228
+ list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))),
229
+ paging: { page, pageSize },
230
+ });
214
231
  });
215
232
 
216
- export async function createPrice(payload: any) {
217
- const raw: Price & { model: 'string' } = payload;
218
- raw.active = true;
219
- raw.locked = false;
220
- raw.livemode = !!payload.livemode;
221
- raw.currency_id = payload.currency_id;
222
-
223
- if (!raw.product_id) {
224
- throw new Error('product_id is required to create a price');
225
- }
226
-
227
- if (!raw.unit_amount) {
228
- throw new Error('price unit_amount is required');
229
- }
230
-
231
- const product = await Product.findByPk(raw.product_id);
232
- if (!product) {
233
- throw new Error(`product ${raw.product_id} not found for price`);
234
- }
235
-
236
- if (product.type === 'credit') {
237
- const creditConfig = raw.metadata.credit_config;
238
- if (!creditConfig) {
239
- throw new Error('credit_config is required');
240
- }
241
- const { error, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
242
- if (error) {
243
- throw new Error(`credit_config is invalid: ${error.message}`);
244
- }
245
- raw.metadata.credit_config = creditConfigValue;
246
- }
247
-
248
- const currencies = await PaymentCurrency.findAll({ where: { active: true } });
249
- const currency = currencies.find((x) => x.id === raw.currency_id);
250
- if (!currency) {
251
- throw new Error(`currency used in price or not active: ${raw.currency_id}`);
252
- }
253
-
254
- if (Array.isArray(raw.currency_options) === false) {
255
- raw.currency_options = [];
256
- }
257
- if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
258
- raw.currency_options.unshift({
259
- currency_id: raw.currency_id,
260
- unit_amount: raw.unit_amount,
261
- tiers: null,
262
- custom_unit_amount: null,
263
- });
264
- }
265
-
266
- raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
267
- raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
268
- const isRecurring = payload.type === 'recurring';
269
- const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
270
- (raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
271
- isRecurring
272
- );
273
- if (!validate) {
274
- throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
275
- }
276
- if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
277
- raw.recurring.usage_type = 'licensed';
278
- }
279
- const price = await Price.insert(raw);
280
- return getExpandedPrice(price.id as string);
281
- }
282
-
283
233
  const priceQuantitySchema = Joi.object({
284
234
  quantity_available: Joi.number().integer().min(0).optional().default(0),
285
235
  quantity_limit_per_checkout: Joi.number().integer().min(0).optional().default(0),
@@ -302,69 +252,58 @@ const priceAmountSchema = Joi.object({
302
252
  // FIXME: @wangshijun use schema validation
303
253
  // create price
304
254
  // eslint-disable-next-line consistent-return
305
- router.post('/', auth, async (req, res) => {
255
+ app.post('/', auth, async (c) => {
256
+ const body = c.get('sanitizedBody') ?? {};
306
257
  try {
307
- const { error } = priceQuantitySchema.validate(
308
- pick(req.body, ['quantity_available', 'quantity_limit_per_checkout'])
309
- );
258
+ const { error } = priceQuantitySchema.validate(pick(body, ['quantity_available', 'quantity_limit_per_checkout']));
310
259
  if (error) {
311
- return res.status(400).json({ error: `Price create request invalid: ${error.message}` });
260
+ return c.json({ error: `Price create request invalid: ${error.message}` }, 400);
312
261
  }
313
262
  const { error: priceAmountError } = priceAmountSchema.validate(
314
- pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
263
+ pick(body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
315
264
  );
316
265
  if (priceAmountError) {
317
- return res.status(400).json({ error: `Price create request invalid: ${priceAmountError.message}` });
266
+ return c.json({ error: `Price create request invalid: ${priceAmountError.message}` }, 400);
318
267
  }
319
- if (req.body.metadata) {
320
- const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
268
+ if ((body as any).metadata) {
269
+ const { error: metadataError } = MetadataSchema.validate((body as any).metadata);
321
270
  if (metadataError) {
322
- return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
271
+ return c.json({ error: `metadata invalid: ${metadataError.message}` }, 400);
323
272
  }
324
273
  }
325
274
 
326
- if (req.body.pricing_type === 'dynamic') {
327
- const currencyIds = req.body.currency_options?.map((x: any) => x.currency_id).filter(Boolean) || [];
328
- if (req.body.currency_id) {
329
- currencyIds.push(req.body.currency_id);
275
+ if ((body as any).pricing_type === 'dynamic') {
276
+ const currencyIds = (body as any).currency_options?.map((x: any) => x.currency_id).filter(Boolean) || [];
277
+ if ((body as any).currency_id) {
278
+ currencyIds.push((body as any).currency_id);
330
279
  }
331
280
  const validationError = await validateDynamicPricingCurrencies(currencyIds);
332
281
  if (validationError) {
333
- return res.status(400).json({ error: validationError });
282
+ return c.json({ error: validationError }, 400);
334
283
  }
335
284
  }
336
285
 
337
286
  const result = await createPrice({
338
- ...req.body,
339
- livemode: !!req.livemode,
340
- currency_id: req.body.currency_id || req.baseCurrency.id,
341
- created_via: req.user?.via as string,
287
+ ...(body as any),
288
+ livemode: !!c.get('livemode'),
289
+ currency_id: (body as any).currency_id || c.get('baseCurrency').id,
290
+ created_via: c.get('user')?.via as string,
342
291
  quantity_sold: 0,
343
292
  });
344
293
 
345
- logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: req.user?.did });
346
- res.json(result);
294
+ logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: c.get('user')?.did });
295
+ return c.json(result);
347
296
  } catch (err) {
348
- logger.error('Error creating price', { error: err.message, request: req.body });
349
- res.status(400).json({ error: err.message });
297
+ logger.error('Error creating price', { error: (err as any).message, request: body });
298
+ return c.json({ error: (err as any).message }, 400);
350
299
  }
351
300
  });
352
301
 
353
- // get price detail
354
- router.get('/:id', auth, async (req, res) => {
355
- const doc = await getExpandedPrice(req.params.id as string);
356
- if (doc) {
357
- res.json(doc);
358
- } else {
359
- res.status(404).json(null);
360
- }
361
- });
362
-
363
- // get price used status
364
- router.get('/:id/used', auth, async (req, res) => {
365
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
302
+ // get price used status — static sub-path, registered before /:id
303
+ app.get('/:id/used', auth, async (c) => {
304
+ const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
366
305
  if (!price) {
367
- return res.status(404).json({ error: 'Price not found' });
306
+ return c.json({ error: 'Price not found' }, 404);
368
307
  }
369
308
 
370
309
  const used = await price.isUsed(true);
@@ -372,11 +311,11 @@ router.get('/:id/used', auth, async (req, res) => {
372
311
  await price.update({ locked: false });
373
312
  }
374
313
 
375
- return res.json({ used });
314
+ return c.json({ used });
376
315
  });
377
316
 
378
- router.get('/:id/upsell', auth, async (req, res) => {
379
- const price = await Price.findByPkOrLookupKey(req.params.id as string, {
317
+ app.get('/:id/upsell', auth, async (c) => {
318
+ const price = await Price.findByPkOrLookupKey(c.req.param('id') as string, {
380
319
  include: [{ model: PaymentCurrency, as: 'currency' }],
381
320
  });
382
321
 
@@ -386,55 +325,67 @@ router.get('/:id/upsell', auth, async (req, res) => {
386
325
  include: [{ model: PaymentCurrency, as: 'currency' }],
387
326
  });
388
327
  const upsells = prices.filter((x) => canUpsell(price, x));
389
- res.json(upsells);
390
- } else {
391
- res.json(null);
328
+ return c.json(upsells);
329
+ }
330
+ return c.json(null);
331
+ });
332
+
333
+ // get price detail
334
+ app.get('/:id', auth, async (c) => {
335
+ const doc = await getExpandedPrice(c.req.param('id') as string);
336
+ if (doc) {
337
+ return c.json(doc);
392
338
  }
339
+ return c.json(null, 404);
393
340
  });
394
341
 
395
342
  // update price
396
343
  // FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
397
- router.put('/:id', auth, async (req, res) => {
344
+ app.put('/:id', auth, async (c) => {
345
+ const body = c.get('sanitizedBody') ?? {};
398
346
  const quantityKeys = ['quantity_available', 'quantity_limit_per_checkout'];
399
- const { error } = priceQuantitySchema.validate(pick(req.body, quantityKeys));
347
+ const { error } = priceQuantitySchema.validate(pick(body, quantityKeys));
400
348
  if (error) {
401
- return res.status(400).json({ error: `Price update request invalid: ${error.message}` });
349
+ return c.json({ error: `Price update request invalid: ${error.message}` }, 400);
402
350
  }
403
351
 
404
352
  const { error: priceAmountError } = priceAmountSchema.validate(
405
- pick(req.body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
353
+ pick(body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
406
354
  );
407
355
  if (priceAmountError) {
408
- return res.status(400).json({ error: `Price update request invalid: ${priceAmountError.message}` });
356
+ return c.json({ error: `Price update request invalid: ${priceAmountError.message}` }, 400);
409
357
  }
410
- const doc = await Price.findByPkOrLookupKey(req.params.id as string);
358
+ const doc = await Price.findByPkOrLookupKey(c.req.param('id') as string);
411
359
 
412
360
  if (!doc) {
413
- return res.status(404).json({ error: 'price not found' });
361
+ return c.json({ error: 'price not found' }, 404);
414
362
  }
415
363
 
416
364
  const product = await Product.findByPk(doc.product_id);
417
365
  if (!product) {
418
- return res.status(404).json({ error: 'product not found' });
366
+ return c.json({ error: 'product not found' }, 404);
419
367
  }
420
368
 
421
369
  if (doc.active === false) {
422
- return res.status(403).json({ error: 'price archived' });
370
+ return c.json({ error: 'price archived' }, 403);
423
371
  }
424
372
 
425
- if (Number(req.body.quantity_available) > 0 && Number(req.body.quantity_available) < Number(doc.quantity_sold)) {
373
+ if (
374
+ Number((body as any).quantity_available) > 0 &&
375
+ Number((body as any).quantity_available) < Number(doc.quantity_sold)
376
+ ) {
426
377
  // 可售数量不得小于已售数量
427
- return res.status(400).json({ error: 'the available quantity cannot be less than the quantity sold' });
378
+ return c.json({ error: 'the available quantity cannot be less than the quantity sold' }, 400);
428
379
  }
429
380
 
430
- const locked = doc.locked && process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
431
- const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
381
+ const locked = doc.locked && !allowChangeLockedPrice();
382
+ const { error: metadataError } = MetadataSchema.validate((body as any).metadata);
432
383
  if (metadataError) {
433
- return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
384
+ return c.json({ error: `metadata invalid: ${metadataError.message}` }, 400);
434
385
  }
435
386
  const updates: Partial<Price> = Price.formatBeforeSave(
436
387
  pick(
437
- req.body,
388
+ body,
438
389
  locked
439
390
  ? ['nickname', 'description', 'metadata', 'upsell', 'lookup_key', ...quantityKeys]
440
391
  : [
@@ -465,7 +416,7 @@ router.put('/:id', auth, async (req, res) => {
465
416
  if (updates.lookup_key) {
466
417
  const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
467
418
  if (exist && exist.id !== doc.id) {
468
- return res.status(400).json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` });
419
+ return c.json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` }, 400);
469
420
  }
470
421
  }
471
422
 
@@ -473,11 +424,11 @@ router.put('/:id', auth, async (req, res) => {
473
424
  // Merge with existing credit_config if not provided
474
425
  const creditConfig = updates.metadata.credit_config || doc.metadata?.credit_config;
475
426
  if (!creditConfig) {
476
- return res.status(400).json({ error: 'credit_config is required' });
427
+ return c.json({ error: 'credit_config is required' }, 400);
477
428
  }
478
429
  const { error: creditConfigError, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
479
430
  if (creditConfigError) {
480
- return res.status(400).json({ error: `credit_config is invalid: ${creditConfigError.message}` });
431
+ return c.json({ error: `credit_config is invalid: ${creditConfigError.message}` }, 400);
481
432
  }
482
433
  updates.metadata.credit_config = creditConfigValue;
483
434
  }
@@ -486,9 +437,10 @@ router.put('/:id', auth, async (req, res) => {
486
437
  const currency =
487
438
  currencies.find((x) => x.id === updates?.currency_id || '') || currencies.find((x) => x.id === doc.currency_id);
488
439
  if (!currency) {
489
- return res
490
- .status(400)
491
- .json({ error: `currency used in price not found or not active: ${updates?.currency_id || doc.currency_id}` });
440
+ return c.json(
441
+ { error: `currency used in price not found or not active: ${updates?.currency_id || doc.currency_id}` },
442
+ 400
443
+ );
492
444
  }
493
445
  if (updates.unit_amount) {
494
446
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
@@ -519,9 +471,10 @@ router.put('/:id', auth, async (req, res) => {
519
471
  isRecurring
520
472
  );
521
473
  if (!validate) {
522
- return res
523
- .status(400)
524
- .json({ error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` });
474
+ return c.json(
475
+ { error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` },
476
+ 400
477
+ );
525
478
  }
526
479
 
527
480
  const pricingType = updates.pricing_type || doc.pricing_type;
@@ -532,112 +485,182 @@ router.put('/:id', auth, async (req, res) => {
532
485
  currencyIds.length ? currencyIds : [updates.currency_id || doc.currency_id]
533
486
  );
534
487
  if (validationError) {
535
- return res.status(400).json({ error: validationError });
488
+ return c.json({ error: validationError }, 400);
536
489
  }
537
490
  }
538
491
 
539
492
  try {
540
493
  await doc.update(Price.formatBeforeSave(updates));
541
- logger.info(`Price updated: ${req.params.id}`, { priceId: req.params.id, updates, requestedBy: req.user?.did });
542
- return res.json(await getExpandedPrice(req.params.id as string));
494
+ logger.info(`Price updated: ${c.req.param('id')}`, {
495
+ priceId: c.req.param('id'),
496
+ updates,
497
+ requestedBy: c.get('user')?.did,
498
+ });
499
+ return c.json(await getExpandedPrice(c.req.param('id') as string));
543
500
  } catch (err) {
544
- logger.error('Error updating price', { error: err.message, request: req.body });
545
- return res.status(400).json({ error: err.message });
501
+ logger.error('Error updating price', { error: (err as any).message, request: body });
502
+ return c.json({ error: (err as any).message }, 400);
546
503
  }
547
504
  });
548
505
 
549
506
  // archive
550
- router.put('/:id/archive', auth, async (req, res) => {
551
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
507
+ app.put('/:id/archive', auth, async (c) => {
508
+ const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
552
509
 
553
510
  if (!price) {
554
- return res.status(404).json({ error: 'price not found' });
511
+ return c.json({ error: 'price not found' }, 404);
555
512
  }
556
513
 
557
514
  if (price.active === false) {
558
- return res.status(403).json({ error: 'price already archived' });
515
+ return c.json({ error: 'price already archived' }, 403);
559
516
  }
560
517
 
561
518
  if (price.locked) {
562
- return res.status(403).json({ error: 'price locked' });
519
+ return c.json({ error: 'price locked' }, 403);
563
520
  }
564
521
 
565
522
  await price.update({ active: false });
566
523
 
567
- logger.info(`Price archived: ${req.params.id}`, { priceId: req.params.id, requestedBy: req.user?.did });
568
- return res.json(await getExpandedPrice(req.params.id as string));
569
- });
570
-
571
- // delete price
572
- router.delete('/:id', auth, async (req, res) => {
573
- try {
574
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
575
-
576
- if (!price) {
577
- return res.status(404).json({ error: 'Can not delete none existing price' });
578
- }
579
-
580
- if (price.locked) {
581
- return res.status(403).json({ error: 'Can not delete locked price' });
582
- }
583
-
584
- if (await price.isUsed(true)) {
585
- return res.status(403).json({ error: 'Can not delete price used by other resources' });
586
- }
587
-
588
- await price.destroy();
589
- return res.json(price);
590
- } catch (err) {
591
- logger.error('delete price error', err);
592
- return res.status(400).json({ error: err.message });
593
- }
524
+ logger.info(`Price archived: ${c.req.param('id')}`, { priceId: c.req.param('id'), requestedBy: c.get('user')?.did });
525
+ return c.json(await getExpandedPrice(c.req.param('id') as string));
594
526
  });
595
527
 
596
528
  const priceInventorySchema = Joi.object({
597
529
  quantity: Joi.number().integer().min(0).required(),
598
530
  action: Joi.string().valid('decrement', 'increment').required(),
599
531
  });
600
- router.put('/:id/inventory', auth, async (req, res) => {
532
+ app.put('/:id/inventory', auth, async (c) => {
533
+ const body = c.get('sanitizedBody') ?? {};
601
534
  try {
602
- const { error } = priceInventorySchema.validate(req.body);
535
+ const { error } = priceInventorySchema.validate(body);
603
536
  if (error) {
604
- return res.status(400).json({ error: `Price inventory update request invalid: ${error.message}` });
537
+ return c.json({ error: `Price inventory update request invalid: ${error.message}` }, 400);
605
538
  }
606
- const price = await Price.findByPkOrLookupKey(req.params.id as string);
539
+ const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
607
540
  if (!price) {
608
- return res.status(404).json({ error: 'price not found' });
541
+ return c.json({ error: 'price not found' }, 404);
609
542
  }
610
- const quantity = Number(req.body.quantity);
543
+ const quantity = Number((body as any).quantity);
611
544
  const limitPerCheckQuantity = price.quantity_limit_per_checkout;
612
545
  if (limitPerCheckQuantity > 0 && quantity > limitPerCheckQuantity) {
613
- return res
614
- .status(400)
615
- .json({ error: `quantity ${quantity} exceeds limit per checkout ${limitPerCheckQuantity}` });
546
+ return c.json({ error: `quantity ${quantity} exceeds limit per checkout ${limitPerCheckQuantity}` }, 400);
616
547
  }
617
548
 
618
- if (req.body.action === 'decrement') {
549
+ if ((body as any).action === 'decrement') {
619
550
  if (quantity > price.quantity_sold) {
620
- return res.status(400).json({ error: 'decrementing quantity exceeds sold quantity' });
551
+ return c.json({ error: 'decrementing quantity exceeds sold quantity' }, 400);
621
552
  }
622
- await price.decrement('quantity_sold', { by: req.body.quantity });
553
+ await price.decrement('quantity_sold', { by: (body as any).quantity });
623
554
  }
624
- if (req.body.action === 'increment') {
555
+ if ((body as any).action === 'increment') {
625
556
  if (price.quantity_available > 0 && price.quantity_sold + quantity > price.quantity_available) {
626
- return res.status(400).json({ error: 'incrementing sold quantity exceeds available quantity' });
557
+ return c.json({ error: 'incrementing sold quantity exceeds available quantity' }, 400);
627
558
  }
628
- await price.increment('quantity_sold', { by: req.body.quantity });
559
+ await price.increment('quantity_sold', { by: (body as any).quantity });
629
560
  }
630
- logger.info(`Price inventory updated: ${req.params.id}`, {
631
- priceId: req.params.id,
632
- action: req.body.action,
633
- quantity: req.body.quantity,
634
- requestedBy: req.user?.did,
561
+ logger.info(`Price inventory updated: ${c.req.param('id')}`, {
562
+ priceId: c.req.param('id'),
563
+ action: (body as any).action,
564
+ quantity: (body as any).quantity,
565
+ requestedBy: c.get('user')?.did,
635
566
  });
636
- return res.json(await getExpandedPrice(req.params.id as string));
567
+ return c.json(await getExpandedPrice(c.req.param('id') as string));
637
568
  } catch (err) {
638
569
  logger.error('update price inventory error', err);
639
- return res.status(400).json({ error: err.message });
570
+ return c.json({ error: (err as any).message }, 400);
640
571
  }
641
572
  });
642
573
 
643
- export default router;
574
+ // delete price
575
+ app.delete('/:id', auth, async (c) => {
576
+ try {
577
+ const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
578
+
579
+ if (!price) {
580
+ return c.json({ error: 'Can not delete none existing price' }, 404);
581
+ }
582
+
583
+ if (price.locked) {
584
+ return c.json({ error: 'Can not delete locked price' }, 403);
585
+ }
586
+
587
+ if (await price.isUsed(true)) {
588
+ return c.json({ error: 'Can not delete price used by other resources' }, 403);
589
+ }
590
+
591
+ await price.destroy();
592
+ return c.json(price);
593
+ } catch (err) {
594
+ logger.error('delete price error', err);
595
+ return c.json({ error: (err as any).message }, 400);
596
+ }
597
+ });
598
+
599
+ export async function createPrice(payload: any) {
600
+ const raw: Price & { model: 'string' } = payload;
601
+ raw.active = true;
602
+ raw.locked = false;
603
+ raw.livemode = !!payload.livemode;
604
+ raw.currency_id = payload.currency_id;
605
+
606
+ if (!raw.product_id) {
607
+ throw new Error('product_id is required to create a price');
608
+ }
609
+
610
+ if (!raw.unit_amount) {
611
+ throw new Error('price unit_amount is required');
612
+ }
613
+
614
+ const product = await Product.findByPk(raw.product_id);
615
+ if (!product) {
616
+ throw new Error(`product ${raw.product_id} not found for price`);
617
+ }
618
+
619
+ if (product.type === 'credit') {
620
+ const creditConfig = raw.metadata.credit_config;
621
+ if (!creditConfig) {
622
+ throw new Error('credit_config is required');
623
+ }
624
+ const { error, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
625
+ if (error) {
626
+ throw new Error(`credit_config is invalid: ${error.message}`);
627
+ }
628
+ raw.metadata.credit_config = creditConfigValue;
629
+ }
630
+
631
+ const currencies = await PaymentCurrency.findAll({ where: { active: true } });
632
+ const currency = currencies.find((x) => x.id === raw.currency_id);
633
+ if (!currency) {
634
+ throw new Error(`currency used in price or not active: ${raw.currency_id}`);
635
+ }
636
+
637
+ if (Array.isArray(raw.currency_options) === false) {
638
+ raw.currency_options = [];
639
+ }
640
+ if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
641
+ raw.currency_options.unshift({
642
+ currency_id: raw.currency_id,
643
+ unit_amount: raw.unit_amount,
644
+ tiers: null,
645
+ custom_unit_amount: null,
646
+ });
647
+ }
648
+
649
+ raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
650
+ raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
651
+ const isRecurring = payload.type === 'recurring';
652
+ const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
653
+ (raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
654
+ isRecurring
655
+ );
656
+ if (!validate) {
657
+ throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
658
+ }
659
+ if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
660
+ raw.recurring.usage_type = 'licensed';
661
+ }
662
+ const price = await Price.insert(raw);
663
+ return getExpandedPrice(price.id as string);
664
+ }
665
+
666
+ export default app;