@porulle/core 0.7.0 → 0.9.0

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 (325) hide show
  1. package/dist/auth/middleware.d.ts.map +1 -1
  2. package/dist/auth/middleware.js +26 -7
  3. package/dist/auth/setup.d.ts.map +1 -1
  4. package/dist/auth/setup.js +8 -5
  5. package/dist/config/defaults.d.ts.map +1 -1
  6. package/dist/config/defaults.js +7 -0
  7. package/dist/config/types.d.ts +26 -1
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.d.ts +3 -0
  10. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  11. package/dist/generated/plugin-manifest.js +1 -0
  12. package/dist/hooks/checkout-completion.d.ts.map +1 -1
  13. package/dist/hooks/checkout-completion.js +2 -2
  14. package/dist/hooks/checkout.d.ts.map +1 -1
  15. package/dist/hooks/checkout.js +17 -2
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/interfaces/rest/index.d.ts.map +1 -1
  20. package/dist/interfaces/rest/index.js +12 -0
  21. package/dist/interfaces/rest/routes/admin/staff.d.ts +12 -0
  22. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -0
  23. package/dist/interfaces/rest/routes/admin/staff.js +206 -0
  24. package/dist/interfaces/rest/routes/analytics.d.ts +5 -0
  25. package/dist/interfaces/rest/routes/analytics.d.ts.map +1 -0
  26. package/dist/interfaces/rest/routes/analytics.js +24 -0
  27. package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/carts.js +39 -2
  29. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  30. package/dist/interfaces/rest/routes/catalog.js +17 -1
  31. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  32. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  33. package/dist/interfaces/rest/routes/checkout.js +97 -21
  34. package/dist/interfaces/rest/routes/documents.d.ts +10 -0
  35. package/dist/interfaces/rest/routes/documents.d.ts.map +1 -0
  36. package/dist/interfaces/rest/routes/documents.js +40 -0
  37. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  38. package/dist/interfaces/rest/routes/inventory.js +13 -0
  39. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  40. package/dist/interfaces/rest/routes/orders.js +111 -1
  41. package/dist/interfaces/rest/routes/pricing.d.ts.map +1 -1
  42. package/dist/interfaces/rest/routes/pricing.js +36 -1
  43. package/dist/interfaces/rest/routes/settings.d.ts +5 -0
  44. package/dist/interfaces/rest/routes/settings.d.ts.map +1 -0
  45. package/dist/interfaces/rest/routes/settings.js +50 -0
  46. package/dist/interfaces/rest/routes/shipping.d.ts +5 -0
  47. package/dist/interfaces/rest/routes/shipping.d.ts.map +1 -0
  48. package/dist/interfaces/rest/routes/shipping.js +68 -0
  49. package/dist/interfaces/rest/routes/tax.d.ts +5 -0
  50. package/dist/interfaces/rest/routes/tax.d.ts.map +1 -0
  51. package/dist/interfaces/rest/routes/tax.js +68 -0
  52. package/dist/interfaces/rest/schemas/admin-staff.d.ts +899 -0
  53. package/dist/interfaces/rest/schemas/admin-staff.d.ts.map +1 -0
  54. package/dist/interfaces/rest/schemas/admin-staff.js +98 -0
  55. package/dist/interfaces/rest/schemas/analytics.d.ts +252 -0
  56. package/dist/interfaces/rest/schemas/analytics.d.ts.map +1 -0
  57. package/dist/interfaces/rest/schemas/analytics.js +33 -0
  58. package/dist/interfaces/rest/schemas/carts.d.ts +656 -0
  59. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  60. package/dist/interfaces/rest/schemas/carts.js +38 -0
  61. package/dist/interfaces/rest/schemas/catalog.d.ts +1218 -1
  62. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  63. package/dist/interfaces/rest/schemas/catalog.js +48 -0
  64. package/dist/interfaces/rest/schemas/checkout.d.ts +2 -0
  65. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  66. package/dist/interfaces/rest/schemas/checkout.js +4 -0
  67. package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
  68. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  69. package/dist/interfaces/rest/schemas/documents.d.ts +509 -0
  70. package/dist/interfaces/rest/schemas/documents.d.ts.map +1 -0
  71. package/dist/interfaces/rest/schemas/documents.js +65 -0
  72. package/dist/interfaces/rest/schemas/orders.d.ts +3110 -43
  73. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  74. package/dist/interfaces/rest/schemas/orders.js +234 -1
  75. package/dist/interfaces/rest/schemas/pricing.d.ts +405 -0
  76. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  77. package/dist/interfaces/rest/schemas/pricing.js +70 -0
  78. package/dist/interfaces/rest/schemas/responses.d.ts +190 -0
  79. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  80. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  81. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  82. package/dist/interfaces/rest/schemas/settings.d.ts +397 -0
  83. package/dist/interfaces/rest/schemas/settings.d.ts.map +1 -0
  84. package/dist/interfaces/rest/schemas/settings.js +79 -0
  85. package/dist/interfaces/rest/schemas/shipping.d.ts +1093 -0
  86. package/dist/interfaces/rest/schemas/shipping.d.ts.map +1 -0
  87. package/dist/interfaces/rest/schemas/shipping.js +146 -0
  88. package/dist/interfaces/rest/schemas/tax.d.ts +1074 -0
  89. package/dist/interfaces/rest/schemas/tax.d.ts.map +1 -0
  90. package/dist/interfaces/rest/schemas/tax.js +132 -0
  91. package/dist/kernel/database/schema.d.ts +4 -0
  92. package/dist/kernel/database/schema.d.ts.map +1 -1
  93. package/dist/kernel/database/schema.js +8 -0
  94. package/dist/kernel/database/scoped-db.d.ts +2 -1
  95. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  96. package/dist/kernel/database/scoped-db.js +62 -1
  97. package/dist/kernel/hooks/types.d.ts +1 -1
  98. package/dist/kernel/hooks/types.d.ts.map +1 -1
  99. package/dist/kernel/plugin/manifest.d.ts +26 -0
  100. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  101. package/dist/kernel/plugin/manifest.js +17 -2
  102. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  103. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  104. package/dist/modules/analytics/module.d.ts.map +1 -1
  105. package/dist/modules/analytics/module.js +6 -0
  106. package/dist/modules/analytics/reports.d.ts +58 -0
  107. package/dist/modules/analytics/reports.d.ts.map +1 -0
  108. package/dist/modules/analytics/reports.js +249 -0
  109. package/dist/modules/analytics/service.d.ts +10 -0
  110. package/dist/modules/analytics/service.d.ts.map +1 -1
  111. package/dist/modules/analytics/service.js +14 -0
  112. package/dist/modules/cart/repository/index.d.ts +7 -0
  113. package/dist/modules/cart/repository/index.d.ts.map +1 -1
  114. package/dist/modules/cart/repository/index.js +25 -1
  115. package/dist/modules/cart/schema.d.ts +17 -0
  116. package/dist/modules/cart/schema.d.ts.map +1 -1
  117. package/dist/modules/cart/schema.js +2 -0
  118. package/dist/modules/cart/schemas.d.ts +1 -0
  119. package/dist/modules/cart/schemas.d.ts.map +1 -1
  120. package/dist/modules/cart/schemas.js +1 -0
  121. package/dist/modules/cart/service.d.ts +32 -0
  122. package/dist/modules/cart/service.d.ts.map +1 -1
  123. package/dist/modules/cart/service.js +61 -2
  124. package/dist/modules/catalog/entity-service.d.ts +21 -0
  125. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  126. package/dist/modules/catalog/entity-service.js +166 -5
  127. package/dist/modules/catalog/schema.d.ts +34 -0
  128. package/dist/modules/catalog/schema.d.ts.map +1 -1
  129. package/dist/modules/catalog/schema.js +5 -0
  130. package/dist/modules/catalog/schemas.d.ts +3 -0
  131. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  132. package/dist/modules/catalog/schemas.js +3 -0
  133. package/dist/modules/catalog/service.d.ts +36 -0
  134. package/dist/modules/catalog/service.d.ts.map +1 -1
  135. package/dist/modules/catalog/service.js +6 -0
  136. package/dist/modules/customers/service.d.ts.map +1 -1
  137. package/dist/modules/customers/service.js +6 -0
  138. package/dist/modules/documents/module.d.ts +7 -0
  139. package/dist/modules/documents/module.d.ts.map +1 -0
  140. package/dist/modules/documents/module.js +12 -0
  141. package/dist/modules/documents/pdf.d.ts +25 -0
  142. package/dist/modules/documents/pdf.d.ts.map +1 -0
  143. package/dist/modules/documents/pdf.js +80 -0
  144. package/dist/modules/documents/render.d.ts +32 -0
  145. package/dist/modules/documents/render.d.ts.map +1 -0
  146. package/dist/modules/documents/render.js +190 -0
  147. package/dist/modules/documents/repository/index.d.ts +25 -0
  148. package/dist/modules/documents/repository/index.d.ts.map +1 -0
  149. package/dist/modules/documents/repository/index.js +47 -0
  150. package/dist/modules/documents/schema.d.ts +214 -0
  151. package/dist/modules/documents/schema.d.ts.map +1 -0
  152. package/dist/modules/documents/schema.js +42 -0
  153. package/dist/modules/documents/service.d.ts +51 -0
  154. package/dist/modules/documents/service.d.ts.map +1 -0
  155. package/dist/modules/documents/service.js +119 -0
  156. package/dist/modules/fulfillment/service.d.ts +18 -0
  157. package/dist/modules/fulfillment/service.d.ts.map +1 -1
  158. package/dist/modules/fulfillment/service.js +85 -1
  159. package/dist/modules/orders/repository/index.d.ts +18 -1
  160. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  161. package/dist/modules/orders/repository/index.js +86 -1
  162. package/dist/modules/orders/schema.d.ts +391 -0
  163. package/dist/modules/orders/schema.d.ts.map +1 -1
  164. package/dist/modules/orders/schema.js +56 -1
  165. package/dist/modules/orders/service.d.ts +102 -3
  166. package/dist/modules/orders/service.d.ts.map +1 -1
  167. package/dist/modules/orders/service.js +653 -29
  168. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  169. package/dist/modules/orders/stale-order-cleanup.js +13 -1
  170. package/dist/modules/pricing/repository/index.d.ts +5 -0
  171. package/dist/modules/pricing/repository/index.d.ts.map +1 -1
  172. package/dist/modules/pricing/repository/index.js +19 -0
  173. package/dist/modules/pricing/service.d.ts +18 -0
  174. package/dist/modules/pricing/service.d.ts.map +1 -1
  175. package/dist/modules/pricing/service.js +43 -5
  176. package/dist/modules/settings/module.d.ts +6 -0
  177. package/dist/modules/settings/module.d.ts.map +1 -0
  178. package/dist/modules/settings/module.js +11 -0
  179. package/dist/modules/settings/repository/index.d.ts +16 -0
  180. package/dist/modules/settings/repository/index.d.ts.map +1 -0
  181. package/dist/modules/settings/repository/index.js +41 -0
  182. package/dist/modules/settings/schema.d.ts +121 -0
  183. package/dist/modules/settings/schema.d.ts.map +1 -0
  184. package/dist/modules/settings/schema.js +24 -0
  185. package/dist/modules/settings/service.d.ts +26 -0
  186. package/dist/modules/settings/service.d.ts.map +1 -0
  187. package/dist/modules/settings/service.js +51 -0
  188. package/dist/modules/shipping/calculator.d.ts +5 -0
  189. package/dist/modules/shipping/calculator.d.ts.map +1 -1
  190. package/dist/modules/shipping/calculator.js +10 -0
  191. package/dist/modules/shipping/module.d.ts +5 -1
  192. package/dist/modules/shipping/module.d.ts.map +1 -1
  193. package/dist/modules/shipping/module.js +4 -1
  194. package/dist/modules/shipping/repository/index.d.ts +27 -2
  195. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  196. package/dist/modules/shipping/repository/index.js +105 -1
  197. package/dist/modules/shipping/schema.d.ts +419 -0
  198. package/dist/modules/shipping/schema.d.ts.map +1 -0
  199. package/dist/modules/shipping/schema.js +56 -0
  200. package/dist/modules/shipping/service.d.ts +54 -0
  201. package/dist/modules/shipping/service.d.ts.map +1 -1
  202. package/dist/modules/shipping/service.js +161 -2
  203. package/dist/modules/tax/adapter.d.ts +13 -0
  204. package/dist/modules/tax/adapter.d.ts.map +1 -1
  205. package/dist/modules/tax/module.d.ts +5 -1
  206. package/dist/modules/tax/module.d.ts.map +1 -1
  207. package/dist/modules/tax/module.js +4 -1
  208. package/dist/modules/tax/repository/index.d.ts +38 -2
  209. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  210. package/dist/modules/tax/repository/index.js +138 -1
  211. package/dist/modules/tax/schema.d.ts +355 -0
  212. package/dist/modules/tax/schema.d.ts.map +1 -0
  213. package/dist/modules/tax/schema.js +55 -0
  214. package/dist/modules/tax/service.d.ts +60 -1
  215. package/dist/modules/tax/service.d.ts.map +1 -1
  216. package/dist/modules/tax/service.js +227 -2
  217. package/dist/runtime/kernel-modules.d.ts +15 -2
  218. package/dist/runtime/kernel-modules.d.ts.map +1 -1
  219. package/dist/runtime/kernel-modules.js +4 -0
  220. package/dist/runtime/kernel-types.d.ts +5 -1
  221. package/dist/runtime/kernel-types.d.ts.map +1 -1
  222. package/dist/runtime/kernel-types.js +2 -0
  223. package/dist/runtime/server.js +1 -1
  224. package/dist/test-utils/create-plugin-test-app.d.ts +3 -0
  225. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  226. package/dist/test-utils/create-plugin-test-app.js +15 -3
  227. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  228. package/dist/test-utils/create-test-config.js +2 -0
  229. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  230. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  231. package/dist/test-utils/order-test-helpers.js +18 -0
  232. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  233. package/dist/test-utils/rest-api-test-utils.js +7 -0
  234. package/dist/test-utils/test-actors.js +1 -1
  235. package/dist/testing.d.ts +1 -0
  236. package/dist/testing.d.ts.map +1 -1
  237. package/dist/testing.js +1 -0
  238. package/package.json +3 -3
  239. package/src/auth/middleware.ts +26 -7
  240. package/src/auth/setup.ts +11 -4
  241. package/src/config/defaults.ts +7 -0
  242. package/src/config/types.ts +26 -1
  243. package/src/generated/plugin-capabilities.d.ts +1 -0
  244. package/src/generated/plugin-manifest.ts +1 -0
  245. package/src/generated/plugin-repositories.d.ts +1 -0
  246. package/src/hooks/checkout-completion.ts +22 -16
  247. package/src/hooks/checkout.ts +36 -11
  248. package/src/index.ts +1 -0
  249. package/src/interfaces/rest/index.ts +12 -0
  250. package/src/interfaces/rest/routes/admin/staff.ts +260 -0
  251. package/src/interfaces/rest/routes/analytics.ts +33 -0
  252. package/src/interfaces/rest/routes/carts.ts +52 -2
  253. package/src/interfaces/rest/routes/catalog.ts +34 -0
  254. package/src/interfaces/rest/routes/checkout.ts +114 -27
  255. package/src/interfaces/rest/routes/documents.ts +61 -0
  256. package/src/interfaces/rest/routes/inventory.ts +14 -0
  257. package/src/interfaces/rest/routes/orders.ts +148 -1
  258. package/src/interfaces/rest/routes/pricing.ts +44 -1
  259. package/src/interfaces/rest/routes/settings.ts +61 -0
  260. package/src/interfaces/rest/routes/shipping.ts +92 -0
  261. package/src/interfaces/rest/routes/tax.ts +90 -0
  262. package/src/interfaces/rest/schemas/admin-staff.ts +110 -0
  263. package/src/interfaces/rest/schemas/analytics.ts +39 -0
  264. package/src/interfaces/rest/schemas/carts.ts +40 -0
  265. package/src/interfaces/rest/schemas/catalog.ts +53 -0
  266. package/src/interfaces/rest/schemas/checkout.ts +4 -0
  267. package/src/interfaces/rest/schemas/documents.ts +72 -0
  268. package/src/interfaces/rest/schemas/orders.ts +258 -1
  269. package/src/interfaces/rest/schemas/pricing.ts +75 -0
  270. package/src/interfaces/rest/schemas/settings.ts +93 -0
  271. package/src/interfaces/rest/schemas/shipping.ts +164 -0
  272. package/src/interfaces/rest/schemas/tax.ts +148 -0
  273. package/src/kernel/database/schema.ts +12 -0
  274. package/src/kernel/database/scoped-db.ts +70 -1
  275. package/src/kernel/hooks/types.ts +1 -0
  276. package/src/kernel/plugin/manifest.ts +41 -2
  277. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  278. package/src/modules/analytics/module.ts +10 -0
  279. package/src/modules/analytics/reports.ts +311 -0
  280. package/src/modules/analytics/service.ts +23 -0
  281. package/src/modules/cart/repository/index.ts +30 -1
  282. package/src/modules/cart/schema.ts +2 -0
  283. package/src/modules/cart/schemas.ts +1 -0
  284. package/src/modules/cart/service.ts +104 -2
  285. package/src/modules/catalog/entity-service.ts +196 -4
  286. package/src/modules/catalog/schema.ts +5 -0
  287. package/src/modules/catalog/schemas.ts +3 -0
  288. package/src/modules/catalog/service.ts +30 -0
  289. package/src/modules/customers/service.ts +6 -0
  290. package/src/modules/documents/module.ts +19 -0
  291. package/src/modules/documents/pdf.ts +101 -0
  292. package/src/modules/documents/render.ts +231 -0
  293. package/src/modules/documents/repository/index.ts +68 -0
  294. package/src/modules/documents/schema.ts +44 -0
  295. package/src/modules/documents/service.ts +176 -0
  296. package/src/modules/fulfillment/service.ts +139 -1
  297. package/src/modules/orders/repository/index.ts +127 -1
  298. package/src/modules/orders/schema.ts +58 -1
  299. package/src/modules/orders/service.ts +1041 -65
  300. package/src/modules/orders/stale-order-cleanup.ts +18 -4
  301. package/src/modules/pricing/repository/index.ts +34 -0
  302. package/src/modules/pricing/service.ts +76 -6
  303. package/src/modules/settings/module.ts +18 -0
  304. package/src/modules/settings/repository/index.ts +59 -0
  305. package/src/modules/settings/schema.ts +25 -0
  306. package/src/modules/settings/service.ts +82 -0
  307. package/src/modules/shipping/calculator.ts +27 -0
  308. package/src/modules/shipping/module.ts +6 -2
  309. package/src/modules/shipping/repository/index.ts +136 -2
  310. package/src/modules/shipping/schema.ts +58 -0
  311. package/src/modules/shipping/service.ts +260 -1
  312. package/src/modules/tax/adapter.ts +9 -0
  313. package/src/modules/tax/module.ts +6 -2
  314. package/src/modules/tax/repository/index.ts +169 -2
  315. package/src/modules/tax/schema.ts +57 -0
  316. package/src/modules/tax/service.ts +313 -2
  317. package/src/runtime/kernel-modules.ts +4 -0
  318. package/src/runtime/kernel-types.ts +6 -0
  319. package/src/runtime/server.ts +1 -1
  320. package/src/test-utils/create-plugin-test-app.ts +17 -4
  321. package/src/test-utils/create-test-config.ts +2 -0
  322. package/src/test-utils/order-test-helpers.ts +26 -0
  323. package/src/test-utils/rest-api-test-utils.ts +7 -0
  324. package/src/test-utils/test-actors.ts +1 -1
  325. package/src/testing.ts +1 -0
@@ -0,0 +1,33 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { getReportRoute, listReportsRoute } from "../schemas/analytics.js";
4
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
5
+
6
+ export function analyticsRoutes(kernel: Kernel) {
7
+ const router = new OpenAPIHono<AppEnv>();
8
+
9
+ router.use("/reports", requirePerm("analytics:read"));
10
+ router.use("/reports/:name", requirePerm("analytics:read"));
11
+
12
+ router.openapi(listReportsRoute, (c) => {
13
+ return c.json({ data: kernel.services.analytics.listReports() });
14
+ });
15
+
16
+ // @ts-expect-error -- openapi handler union return type
17
+ router.openapi(getReportRoute, async (c) => {
18
+ const { date, from, to } = c.req.valid("query");
19
+ const result = await kernel.services.analytics.getReport(
20
+ c.req.param("name"),
21
+ {
22
+ ...(date !== undefined ? { date } : {}),
23
+ ...(from !== undefined ? { from } : {}),
24
+ ...(to !== undefined ? { to } : {}),
25
+ },
26
+ c.get("actor"),
27
+ );
28
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
29
+ return c.json({ data: result.value });
30
+ });
31
+
32
+ return router;
33
+ }
@@ -1,8 +1,8 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { Kernel } from "../../../runtime/kernel.js";
3
3
  import type { CreateCartInput, AddCartItemInput } from "../../../modules/cart/schemas.js";
4
- import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute } from "../schemas/carts.js";
5
- import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
4
+ import { createCartRoute, addCartItemRoute, updateCartItemQuantityRoute, getCartRoute, removeCartItemRoute, listCartsRoute, recoverCartRoute } from "../schemas/carts.js";
5
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
6
6
 
7
7
  export function cartRoutes(kernel: Kernel) {
8
8
  const router = new OpenAPIHono<AppEnv>();
@@ -21,6 +21,56 @@ export function cartRoutes(kernel: Kernel) {
21
21
  return c.json({ data: result.value }, 201);
22
22
  });
23
23
 
24
+ // @ts-expect-error -- openapi handler union return type
25
+ router.openapi(listCartsRoute, async (c) => {
26
+ const actor = c.get("actor");
27
+ const pagination = parsePagination(c.req.query());
28
+ const status = c.req.query("status");
29
+ const olderThanRaw = c.req.query("olderThan");
30
+ const hasCustomerRaw = c.req.query("hasCustomer");
31
+
32
+ let olderThan: Date | undefined;
33
+ if (olderThanRaw) {
34
+ const d = new Date(olderThanRaw);
35
+ if (Number.isNaN(d.getTime())) {
36
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "olderThan must be an ISO timestamp." } }, 422);
37
+ }
38
+ olderThan = d;
39
+ }
40
+
41
+ const result = await kernel.services.cart.list(
42
+ {
43
+ page: pagination.page,
44
+ limit: pagination.limit,
45
+ ...(status !== undefined ? { status } : {}),
46
+ ...(olderThan !== undefined ? { olderThan } : {}),
47
+ ...(hasCustomerRaw === "true" ? { hasCustomer: true } : {}),
48
+ ...(hasCustomerRaw === "false" ? { hasCustomer: false } : {}),
49
+ },
50
+ actor,
51
+ );
52
+ if (!result.ok)
53
+ return c.json(
54
+ mapErrorToResponse(result.error),
55
+ mapErrorToStatus(result.error),
56
+ );
57
+ return c.json({
58
+ data: result.value.items,
59
+ meta: { pagination: result.value.pagination },
60
+ });
61
+ });
62
+
63
+ // @ts-expect-error -- openapi handler union return type
64
+ router.openapi(recoverCartRoute, async (c) => {
65
+ const result = await kernel.services.cart.recover(c.req.param("id"), c.get("actor"));
66
+ if (!result.ok)
67
+ return c.json(
68
+ mapErrorToResponse(result.error),
69
+ mapErrorToStatus(result.error),
70
+ );
71
+ return c.json({ data: result.value });
72
+ });
73
+
24
74
  // @ts-expect-error -- openapi handler union return type
25
75
  router.openapi(getCartRoute, async (c) => {
26
76
  const actor = c.get("actor");
@@ -44,6 +44,8 @@ import {
44
44
  createOptionValueRoute,
45
45
  createVariantRoute,
46
46
  generateVariantsRoute,
47
+ quickVariantRoute,
48
+ bulkVariantsRoute,
47
49
  } from "../schemas/catalog.js";
48
50
  import {
49
51
  type AppEnv,
@@ -508,6 +510,38 @@ export function catalogRoutes(kernel: Kernel) {
508
510
  return c.json({ data: result.value }, 201);
509
511
  });
510
512
 
513
+ // @ts-expect-error -- openapi handler union return type
514
+ router.openapi(quickVariantRoute, async (c) => {
515
+ const body = c.req.valid("json") as { options: Record<string, string>; sku?: string; barcode?: string };
516
+ const result = await kernel.services.catalog.quickCreateVariant(
517
+ c.req.param("id"),
518
+ body,
519
+ c.get("actor"),
520
+ );
521
+ if (!result.ok)
522
+ return c.json(
523
+ mapErrorToResponse(result.error),
524
+ mapErrorToStatus(result.error),
525
+ );
526
+ return c.json({ data: result.value }, result.value.created ? 201 : 200);
527
+ });
528
+
529
+ // @ts-expect-error -- openapi handler union return type
530
+ router.openapi(bulkVariantsRoute, async (c) => {
531
+ const body = c.req.valid("json") as { axes: Array<{ name: string; values: string[] }>; skuPrefix?: string };
532
+ const result = await kernel.services.catalog.bulkCreateVariants(
533
+ c.req.param("id"),
534
+ body,
535
+ c.get("actor"),
536
+ );
537
+ if (!result.ok)
538
+ return c.json(
539
+ mapErrorToResponse(result.error),
540
+ mapErrorToStatus(result.error),
541
+ );
542
+ return c.json({ data: result.value }, 201);
543
+ });
544
+
511
545
  // @ts-expect-error -- openapi handler union return type
512
546
  router.openapi(generateVariantsRoute, async (c) => {
513
547
  const body = c.req.valid("json") as VariantGenerationStrategy;
@@ -21,8 +21,58 @@ import type { AfterHook, BeforeHook, ServiceContainer } from "../../../kernel/ho
21
21
  import type { PluginDb } from "../../../kernel/database/plugin-types.js";
22
22
  import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
23
23
  import { isCommerceError } from "../../../kernel/errors.js";
24
+ import { assertPermission } from "../../../auth/permissions.js";
24
25
  import { makeId } from "../../../utils/id.js";
25
26
  import type { ShippingAddress } from "../../../modules/shipping/calculator.js";
27
+ import type { Actor } from "../../../auth/types.js";
28
+
29
+ /**
30
+ * SEC-07 — resolve the customer profile a checkout order is attributed to.
31
+ * A self-service actor may ONLY attribute the order to its own customer
32
+ * profile; only actors with org-level `customers:read` (staff/clienteling) may
33
+ * name an arbitrary `customerId`. Returns undefined for guests / unresolved
34
+ * (guest checkout). Exported for unit testing.
35
+ */
36
+ export async function resolveCheckoutCustomerUuid(
37
+ customers: Kernel["services"]["customers"],
38
+ actor: Actor | null,
39
+ customerId: string | undefined,
40
+ ): Promise<string | undefined> {
41
+ const actorUserId = actor?.userId;
42
+ let canActForOthers = false;
43
+ if (actorUserId) {
44
+ try {
45
+ assertPermission(actor, "customers:read");
46
+ canActForOthers = true;
47
+ } catch {
48
+ canActForOthers = false;
49
+ }
50
+ }
51
+
52
+ if (customerId) {
53
+ const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
54
+ if (uuidRe.test(customerId)) {
55
+ const byId = await customers.getById(customerId, actor);
56
+ // Honor a profile UUID only if it is the actor's own, or the actor may
57
+ // act for other customers.
58
+ if (byId.ok && (canActForOthers || byId.value.userId === actorUserId)) {
59
+ return byId.value.id;
60
+ }
61
+ }
62
+ if (canActForOthers) {
63
+ // Staff may resolve/create a customer by a supplied user_id.
64
+ const byUser = await customers.getByUserId(customerId, actor);
65
+ if (byUser.ok) return byUser.value.id;
66
+ }
67
+ }
68
+
69
+ // Self-service default: the authenticated actor's own customer profile.
70
+ if (actorUserId && !canActForOthers) {
71
+ const own = await customers.getByUserId(actorUserId, actor);
72
+ if (own.ok) return own.value.id;
73
+ }
74
+ return undefined;
75
+ }
26
76
 
27
77
  export function checkoutRoutes(kernel: Kernel) {
28
78
  const router = new OpenAPIHono<AppEnv>();
@@ -34,6 +84,48 @@ export function checkoutRoutes(kernel: Kernel) {
34
84
  const body = c.req.valid("json");
35
85
 
36
86
  const actor = c.get("actor");
87
+
88
+ // Idempotent replay: a re-submitted checkout (offline POS queue, network
89
+ // retry) returns the already-created order BEFORE the pipeline runs — no
90
+ // double payment authorization, no duplicate order.
91
+ if (body.idempotencyKey) {
92
+ const replay = await kernel.services.orders.getByIdempotencyKey(
93
+ body.idempotencyKey,
94
+ actor,
95
+ );
96
+ if (replay.ok && replay.value) {
97
+ // IDOR guard: an idempotency key must only replay the requester's OWN
98
+ // order. Without this, a same-org customer who supplies/guesses another
99
+ // customer's key would receive that customer's order (PII, totals, lines).
100
+ let canActForOthers = false;
101
+ try {
102
+ assertPermission(actor, "customers:read");
103
+ canActForOthers = true;
104
+ } catch {
105
+ canActForOthers = false;
106
+ }
107
+ const ownCustomer = actor
108
+ ? await resolveCheckoutCustomerUuid(kernel.services.customers, actor, undefined)
109
+ : undefined;
110
+ const replayCustomer = replay.value.customerId ?? null;
111
+ const ownsOrder =
112
+ (replayCustomer !== null && replayCustomer === ownCustomer) ||
113
+ (replayCustomer === null && !ownCustomer);
114
+ if (canActForOthers || ownsOrder) {
115
+ return c.json({ data: replay.value }, 201);
116
+ }
117
+ return c.json(
118
+ {
119
+ error: {
120
+ code: "IDEMPOTENCY_CONFLICT",
121
+ message: "Idempotency key does not belong to this account.",
122
+ },
123
+ },
124
+ 409,
125
+ );
126
+ }
127
+ }
128
+
37
129
  const checkoutData: CheckoutData = {
38
130
  checkoutId: makeId(),
39
131
  cartId: body.cartId,
@@ -121,34 +213,19 @@ export function checkoutRoutes(kernel: Kernel) {
121
213
  context,
122
214
  );
123
215
 
124
- // Resolve customer profile UUID from customerId (may be a profile UUID or a Better Auth user_id)
125
- let customerUuid: string | undefined = undefined;
126
- if (processed.customerId) {
127
- const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
128
- if (uuidRe.test(processed.customerId)) {
129
- // Looks like a profile UUID — try direct lookup (no auto-create)
130
- const byIdResult = await kernel.services.customers.getById(
131
- processed.customerId,
132
- actor,
133
- );
134
- if (byIdResult.ok) {
135
- customerUuid = byIdResult.value.id;
136
- }
137
- }
138
- if (!customerUuid) {
139
- // Fall back to user_id lookup (auto-creates customer profile if needed)
140
- const byUserIdResult = await kernel.services.customers.getByUserId(
141
- processed.customerId,
142
- actor,
143
- );
144
- if (byUserIdResult.ok) {
145
- customerUuid = byUserIdResult.value.id;
146
- }
147
- }
148
- // If both lookups fail, we still allow guest checkout (customerUuid remains undefined)
149
- }
216
+ // SEC-07: resolve the order's customer server-side. A self-service actor
217
+ // can only attribute the order to its own profile; a client-supplied
218
+ // foreign customerId is ignored unless the actor is staff.
219
+ const customerUuid = await resolveCheckoutCustomerUuid(
220
+ kernel.services.customers,
221
+ actor,
222
+ processed.customerId,
223
+ );
150
224
 
151
225
  const orderPayload = {
226
+ ...(body.idempotencyKey !== undefined
227
+ ? { idempotencyKey: body.idempotencyKey }
228
+ : {}),
152
229
  currency: processed.currency,
153
230
  subtotal: processed.subtotal,
154
231
  taxTotal: processed.taxTotal,
@@ -177,6 +254,11 @@ export function checkoutRoutes(kernel: Kernel) {
177
254
  quantity: lineItem.quantity,
178
255
  unitPrice: lineItem.resolvedUnitPrice ?? 0,
179
256
  totalPrice: lineItem.resolvedTotal ?? 0,
257
+ // Per-line tax from product tax classes (issue #57)
258
+ taxAmount: lineItem.taxAmount ?? 0,
259
+ ...(lineItem.discountAmount !== undefined
260
+ ? { discountAmount: lineItem.discountAmount }
261
+ : {}),
180
262
  };
181
263
  return lineItem.variantId !== undefined
182
264
  ? { ...payload, variantId: lineItem.variantId }
@@ -187,7 +269,12 @@ export function checkoutRoutes(kernel: Kernel) {
187
269
  : {}),
188
270
  };
189
271
 
190
- const order = await kernel.services.orders.create(orderPayload, actor);
272
+ // Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
273
+ // + promotions/tax) and reserves stock in its own after-hooks — so it hands
274
+ // the order primitive precomputed totals rather than re-deriving them.
275
+ const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
276
+ trustedPricing: true,
277
+ });
191
278
 
192
279
  if (!order.ok) {
193
280
  return c.json(
@@ -0,0 +1,61 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import {
4
+ emailInvoiceRoute,
5
+ invoiceHtmlRoute,
6
+ invoicePdfRoute,
7
+ receiptHtmlRoute,
8
+ } from "../schemas/documents.js";
9
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
10
+
11
+ /**
12
+ * Order document rendering (issue #47). Mounted under /orders. Access
13
+ * control rides on OrderService.getById (orders:read, or orders:read:own
14
+ * for the order's customer), so no extra scope is needed here.
15
+ */
16
+ export function documentRoutes(kernel: Kernel) {
17
+ const router = new OpenAPIHono<AppEnv>();
18
+
19
+ router.openapi(invoicePdfRoute, async (c) => {
20
+ const result = await kernel.services.documents.renderInvoicePdf(
21
+ c.req.param("id"),
22
+ c.get("actor"),
23
+ );
24
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
25
+ return c.body(result.value.pdf.slice().buffer as ArrayBuffer, 200, {
26
+ "content-type": "application/pdf",
27
+ "content-disposition": `inline; filename="invoice-${result.value.invoiceNumber}.pdf"`,
28
+ });
29
+ });
30
+
31
+ router.openapi(invoiceHtmlRoute, async (c) => {
32
+ const result = await kernel.services.documents.renderInvoiceHtml(
33
+ c.req.param("id"),
34
+ c.get("actor"),
35
+ );
36
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
37
+ return c.html(result.value.html);
38
+ });
39
+
40
+ router.openapi(receiptHtmlRoute, async (c) => {
41
+ const result = await kernel.services.documents.renderReceiptHtml(
42
+ c.req.param("id"),
43
+ c.get("actor"),
44
+ );
45
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
46
+ return c.html(result.value.html);
47
+ });
48
+
49
+ // @ts-expect-error -- openapi handler union return type
50
+ router.openapi(emailInvoiceRoute, async (c) => {
51
+ const result = await kernel.services.documents.emailInvoice(
52
+ c.req.param("id"),
53
+ c.req.valid("json").to,
54
+ c.get("actor"),
55
+ );
56
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
57
+ return c.json({ data: result.value });
58
+ });
59
+
60
+ return router;
61
+ }
@@ -17,6 +17,14 @@ export function inventoryRoutes(kernel: Kernel) {
17
17
  // Inventory levels listing requires inventory:read
18
18
  router.use("/levels", requirePerm("inventory:read"));
19
19
 
20
+ // Warehouse config + reservation are staff operations, never public. Gate them
21
+ // at the route (reserve/release are also called internally by checkout with the
22
+ // customer's actor, so the service itself must stay callable — the boundary is
23
+ // the HTTP route). Blocks anonymous (401) and customers lacking the perm (403).
24
+ router.use("/warehouses", requirePerm("inventory:read"));
25
+ router.use("/reserve", requirePerm("inventory:adjust"));
26
+ router.use("/release", requirePerm("inventory:adjust"));
27
+
20
28
  // @ts-expect-error -- openapi handler union return type
21
29
  router.openapi(listInventoryLevelsRoute, async (c) => {
22
30
  const actor = c.get("actor");
@@ -78,6 +86,12 @@ export function inventoryRoutes(kernel: Kernel) {
78
86
  router.openapi(createWarehouseRoute, async (c) => {
79
87
  const body = c.req.valid("json") as Parameters<typeof kernel.services.inventory.createWarehouse>[0];
80
88
  const actor = c.get("actor");
89
+ // Creating a warehouse (write) needs the higher inventory:adjust perm; the
90
+ // router.use above already requires inventory:read for the /warehouses path.
91
+ const perms = (actor as { permissions?: string[] } | null)?.permissions ?? [];
92
+ if (!(perms.includes("inventory:adjust") || perms.includes("inventory:*") || perms.includes("*:*"))) {
93
+ return c.json({ error: { code: "FORBIDDEN", message: "Permission 'inventory:adjust' is required." } }, 403);
94
+ }
81
95
  const result = await kernel.services.inventory.createWarehouse(body, actor);
82
96
  if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
83
97
  return c.json({ data: result.value }, 201);
@@ -1,8 +1,9 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { Kernel } from "../../../runtime/kernel.js";
3
- import { changeOrderStatusRoute, listOrdersRoute, orderLookupRoute, getOrderRoute, getOrderFulfillmentsRoute, createOrderRoute, refundOrderRoute, captureOrderRoute } from "../schemas/orders.js";
3
+ import { changeOrderStatusRoute, listOrdersRoute, orderLookupRoute, getOrderRoute, getOrderFulfillmentsRoute, createOrderRoute, refundOrderRoute, captureOrderRoute, createOrderFulfillmentRoute, addOrderLineItemRoute, updateOrderLineItemRoute, removeOrderLineItemRoute, refundOrderLinesRoute, undoOrderRefundRoute, listOrderRefundsRoute, refundCapStatusRoute, createOrderNoteRoute, listOrderNotesRoute, deleteOrderNoteRoute, orderTimelineRoute } from "../schemas/orders.js";
4
4
  import { type AppEnv, isUUID, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
5
5
  import type { CreateOrderInput } from "../../../modules/orders/service.js";
6
+ import { assertPermission } from "../../../auth/permissions.js";
6
7
 
7
8
  export function orderRoutes(kernel: Kernel) {
8
9
  const router = new OpenAPIHono<AppEnv>();
@@ -62,6 +63,11 @@ export function orderRoutes(kernel: Kernel) {
62
63
 
63
64
  // @ts-expect-error -- openapi handler union return type
64
65
  router.openapi(createOrderRoute, async (c) => {
66
+ try {
67
+ assertPermission(c.get("actor"), "orders:manage");
68
+ } catch (error) {
69
+ return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
70
+ }
65
71
  const body = c.req.valid("json") as CreateOrderInput;
66
72
  const result = await kernel.services.orders.create(body, c.get("actor"));
67
73
  if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
@@ -82,6 +88,80 @@ export function orderRoutes(kernel: Kernel) {
82
88
  return c.json({ data: result.value });
83
89
  });
84
90
 
91
+ // ── Notes + activity timeline (issue #56) ──────────────────────────
92
+
93
+ // @ts-expect-error -- openapi handler union return type
94
+ router.openapi(createOrderNoteRoute, async (c) => {
95
+ const body = c.req.valid("json") as { body: string; pinned?: boolean };
96
+ const result = await kernel.services.orders.addNote(c.req.param("id"), body, c.get("actor"));
97
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
98
+ return c.json({ data: result.value }, 201);
99
+ });
100
+
101
+ // @ts-expect-error -- openapi handler union return type
102
+ router.openapi(listOrderNotesRoute, async (c) => {
103
+ const result = await kernel.services.orders.listNotes(c.req.param("id"), c.get("actor"));
104
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
105
+ return c.json({ data: result.value });
106
+ });
107
+
108
+ // @ts-expect-error -- openapi handler union return type
109
+ router.openapi(deleteOrderNoteRoute, async (c) => {
110
+ const result = await kernel.services.orders.deleteNote(
111
+ c.req.param("id"),
112
+ c.req.param("noteId"),
113
+ c.get("actor"),
114
+ );
115
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
116
+ return c.json({ data: result.value });
117
+ });
118
+
119
+ // @ts-expect-error -- openapi handler union return type
120
+ router.openapi(orderTimelineRoute, async (c) => {
121
+ const result = await kernel.services.orders.timeline(c.req.param("id"), c.get("actor"));
122
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
123
+ return c.json({ data: result.value });
124
+ });
125
+
126
+ // ── Line-level refunds (issue #52) ─────────────────────────────────
127
+
128
+ // @ts-expect-error -- openapi handler union return type
129
+ router.openapi(refundCapStatusRoute, async (c) => {
130
+ const result = await kernel.services.orders.refundCapStatus(c.get("actor"));
131
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
132
+ return c.json({ data: result.value });
133
+ });
134
+
135
+ // @ts-expect-error -- openapi handler union return type
136
+ router.openapi(refundOrderLinesRoute, async (c) => {
137
+ const body = c.req.valid("json") as { lines: Array<{ lineItemId: string; quantity: number }>; reason?: string };
138
+ const result = await kernel.services.orders.refundLines(
139
+ c.req.param("id"),
140
+ body,
141
+ c.get("actor"),
142
+ );
143
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
144
+ return c.json({ data: result.value }, 201);
145
+ });
146
+
147
+ // @ts-expect-error -- openapi handler union return type
148
+ router.openapi(undoOrderRefundRoute, async (c) => {
149
+ const result = await kernel.services.orders.undoRefund(
150
+ c.req.param("id"),
151
+ c.req.param("refundId"),
152
+ c.get("actor"),
153
+ );
154
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
155
+ return c.json({ data: result.value });
156
+ });
157
+
158
+ // @ts-expect-error -- openapi handler union return type
159
+ router.openapi(listOrderRefundsRoute, async (c) => {
160
+ const result = await kernel.services.orders.listRefunds(c.req.param("id"), c.get("actor"));
161
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
162
+ return c.json({ data: result.value });
163
+ });
164
+
85
165
  // @ts-expect-error -- openapi handler union return type
86
166
  router.openapi(captureOrderRoute, async (c) => {
87
167
  const body = c.req.valid("json") as { amount?: number } | undefined;
@@ -94,6 +174,47 @@ export function orderRoutes(kernel: Kernel) {
94
174
  return c.json({ data: result.value });
95
175
  });
96
176
 
177
+ // @ts-expect-error -- openapi handler union return type
178
+ router.openapi(addOrderLineItemRoute, async (c) => {
179
+ try {
180
+ assertPermission(c.get("actor"), "orders:manage");
181
+ } catch (error) {
182
+ return c.json(mapErrorToResponse(error), mapErrorToStatus(error));
183
+ }
184
+ const body = c.req.valid("json");
185
+ const result = await kernel.services.orders.addLineItem(
186
+ c.req.param("id"),
187
+ body,
188
+ c.get("actor"),
189
+ );
190
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
191
+ return c.json({ data: result.value }, 201);
192
+ });
193
+
194
+ // @ts-expect-error -- openapi handler union return type
195
+ router.openapi(updateOrderLineItemRoute, async (c) => {
196
+ const body = c.req.valid("json");
197
+ const result = await kernel.services.orders.updateOrderLineItem(
198
+ c.req.param("id"),
199
+ c.req.param("lineItemId"),
200
+ body,
201
+ c.get("actor"),
202
+ );
203
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
204
+ return c.json({ data: result.value });
205
+ });
206
+
207
+ // @ts-expect-error -- openapi handler union return type
208
+ router.openapi(removeOrderLineItemRoute, async (c) => {
209
+ const result = await kernel.services.orders.removeLineItem(
210
+ c.req.param("id"),
211
+ c.req.param("lineItemId"),
212
+ c.get("actor"),
213
+ );
214
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
215
+ return c.json({ data: result.value });
216
+ });
217
+
97
218
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
98
219
  // returns union responses (200 | 400 | 404). The route definition documents the
99
220
  // contract; the handler returns dynamic status.
@@ -112,6 +233,32 @@ export function orderRoutes(kernel: Kernel) {
112
233
  return c.json({ data: result.value });
113
234
  });
114
235
 
236
+ // @ts-expect-error -- openapi handler union return type
237
+ router.openapi(createOrderFulfillmentRoute, async (c) => {
238
+ const actor = c.get("actor");
239
+ const orderId = c.req.param("id");
240
+
241
+ // Verify the order exists and the actor has access before recording
242
+ const orderResult = await kernel.services.orders.getById(orderId, actor);
243
+ if (!orderResult.ok) return c.json(mapErrorToResponse(orderResult.error), mapErrorToStatus(orderResult.error));
244
+
245
+ const body = c.req.valid("json") as {
246
+ lineItems: Array<{ orderLineItemId: string; quantity: number }>;
247
+ carrier?: string;
248
+ trackingNumber?: string;
249
+ trackingUrl?: string;
250
+ type?: string;
251
+ status?: string;
252
+ metadata?: Record<string, unknown>;
253
+ };
254
+ const result = await kernel.services.fulfillment.createFulfillment(
255
+ { ...body, orderId: orderResult.value.id },
256
+ actor,
257
+ );
258
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
259
+ return c.json({ data: result.value }, 201);
260
+ });
261
+
115
262
  // @ts-expect-error -- openapi handler union return type
116
263
  router.openapi(getOrderFulfillmentsRoute, async (c) => {
117
264
  const orderId = c.req.param("id");
@@ -4,7 +4,7 @@ import { CommerceValidationError } from "../../../kernel/errors.js";
4
4
  import type { DrizzleDatabase } from "../../../kernel/database/drizzle-db.js";
5
5
  import { CatalogRepository } from "../../../modules/catalog/repository/index.js";
6
6
  import type { Kernel } from "../../../runtime/kernel.js";
7
- import { setBasePriceRoute, createModifierRoute, listPricesRoute } from "../schemas/pricing.js";
7
+ import { setBasePriceRoute, createModifierRoute, listPricesRoute, listModifiersRoute, updateModifierRoute, deleteModifierRoute } from "../schemas/pricing.js";
8
8
  import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
9
9
 
10
10
  export function pricingRoutes(kernel: Kernel) {
@@ -45,6 +45,49 @@ export function pricingRoutes(kernel: Kernel) {
45
45
  });
46
46
 
47
47
  router.use("/modifiers", requirePerm("pricing:manage"));
48
+ router.use("/modifiers/:id", requirePerm("pricing:manage"));
49
+
50
+ // @ts-expect-error -- openapi handler union return type
51
+ router.openapi(listModifiersRoute, async (c) => {
52
+ const actor = c.get("actor");
53
+ const entityId = c.req.query("entityId");
54
+ const currency = c.req.query("currency");
55
+ const active = c.req.query("active");
56
+
57
+ const result = await kernel.services.pricing.listModifiers({
58
+ ...(entityId !== undefined ? { entityId } : {}),
59
+ ...(currency !== undefined ? { currency } : {}),
60
+ ...(active === "true" ? { active: true } : {}),
61
+ }, actor);
62
+ if (!result.ok) {
63
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
64
+ }
65
+ return c.json({ data: result.value });
66
+ });
67
+
68
+ // @ts-expect-error -- openapi handler union return type
69
+ router.openapi(updateModifierRoute, async (c) => {
70
+ const actor = c.get("actor");
71
+ const result = await kernel.services.pricing.updateModifier(
72
+ c.req.param("id"),
73
+ c.req.valid("json"),
74
+ actor,
75
+ );
76
+ if (!result.ok) {
77
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
78
+ }
79
+ return c.json({ data: result.value });
80
+ });
81
+
82
+ // @ts-expect-error -- openapi handler union return type
83
+ router.openapi(deleteModifierRoute, async (c) => {
84
+ const actor = c.get("actor");
85
+ const result = await kernel.services.pricing.deleteModifier(c.req.param("id"), actor);
86
+ if (!result.ok) {
87
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
88
+ }
89
+ return c.json({ data: result.value });
90
+ });
48
91
 
49
92
  // @ts-expect-error -- openapi() enforces strict response typing but our handler
50
93
  // returns union responses (201 | 400 | 422). The route definition documents the