@unifiedcommerce/core 0.1.1 → 0.2.1

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 (257) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +8 -3
  3. package/dist/config/types.d.ts +3 -1
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/interfaces/mcp/server.d.ts +3 -5
  9. package/dist/interfaces/mcp/server.d.ts.map +1 -1
  10. package/dist/interfaces/mcp/server.js +25 -510
  11. package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
  12. package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
  13. package/dist/interfaces/mcp/tool-builder.js +224 -0
  14. package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
  15. package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
  16. package/dist/interfaces/mcp/tools/analytics.js +70 -0
  17. package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
  18. package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
  19. package/dist/interfaces/mcp/tools/cart.js +47 -0
  20. package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
  21. package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
  22. package/dist/interfaces/mcp/tools/catalog.js +284 -0
  23. package/dist/interfaces/mcp/tools/index.d.ts +3 -0
  24. package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
  25. package/dist/interfaces/mcp/tools/index.js +20 -0
  26. package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
  27. package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
  28. package/dist/interfaces/mcp/tools/inventory.js +143 -0
  29. package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
  30. package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
  31. package/dist/interfaces/mcp/tools/orders.js +82 -0
  32. package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
  33. package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
  34. package/dist/interfaces/mcp/tools/pricing.js +90 -0
  35. package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
  36. package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
  37. package/dist/interfaces/mcp/tools/promotions.js +109 -0
  38. package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
  39. package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
  40. package/dist/interfaces/mcp/tools/registry.js +55 -0
  41. package/dist/interfaces/mcp/tools/search.d.ts +14 -0
  42. package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
  43. package/dist/interfaces/mcp/tools/search.js +39 -0
  44. package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
  45. package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
  46. package/dist/interfaces/mcp/tools/webhooks.js +48 -0
  47. package/dist/interfaces/mcp/transport.d.ts +17 -2
  48. package/dist/interfaces/mcp/transport.d.ts.map +1 -1
  49. package/dist/interfaces/mcp/transport.js +91 -44
  50. package/dist/interfaces/rest/router.d.ts.map +1 -1
  51. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  52. package/dist/interfaces/rest/routes/checkout.js +1 -1
  53. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  54. package/dist/interfaces/rest/routes/promotions.js +3 -2
  55. package/dist/kernel/database/adapter.d.ts +8 -0
  56. package/dist/kernel/database/adapter.d.ts.map +1 -1
  57. package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
  58. package/dist/kernel/factory/repository-factory.js +3 -1
  59. package/dist/kernel/local-api.d.ts.map +1 -1
  60. package/dist/kernel/local-api.js +2 -0
  61. package/dist/kernel/plugin/manifest.d.ts +3 -3
  62. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  63. package/dist/kernel/plugin/manifest.js +36 -7
  64. package/dist/runtime/kernel.d.ts +1 -2
  65. package/dist/runtime/kernel.d.ts.map +1 -1
  66. package/dist/runtime/kernel.js +16 -8
  67. package/dist/runtime/server.d.ts.map +1 -1
  68. package/dist/runtime/server.js +8 -3
  69. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  70. package/dist/test-utils/create-pglite-adapter.js +7 -6
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +3 -1
  73. package/src/adapters/console-email.ts +43 -0
  74. package/src/auth/access.ts +187 -0
  75. package/src/auth/auth-schema.ts +139 -0
  76. package/src/auth/middleware.ts +161 -0
  77. package/src/auth/org.ts +41 -0
  78. package/src/auth/permissions.ts +28 -0
  79. package/src/auth/setup.ts +171 -0
  80. package/src/auth/system-actor.ts +19 -0
  81. package/src/auth/types.ts +10 -0
  82. package/src/config/defaults.ts +82 -0
  83. package/src/config/define-config.ts +53 -0
  84. package/src/config/types.ts +301 -0
  85. package/src/generated/plugin-capabilities.d.ts +20 -0
  86. package/src/generated/plugin-manifest.ts +23 -0
  87. package/src/generated/plugin-repositories.d.ts +20 -0
  88. package/src/hooks/checkout-completion.ts +262 -0
  89. package/src/hooks/checkout.ts +677 -0
  90. package/src/hooks/order-emails.ts +62 -0
  91. package/src/index.ts +215 -0
  92. package/src/interfaces/mcp/agent-prompt.ts +174 -0
  93. package/src/interfaces/mcp/context-enrichment.ts +177 -0
  94. package/src/interfaces/mcp/server.ts +47 -0
  95. package/src/interfaces/mcp/tool-builder.ts +261 -0
  96. package/src/interfaces/mcp/tools/analytics.ts +76 -0
  97. package/src/interfaces/mcp/tools/cart.ts +57 -0
  98. package/src/interfaces/mcp/tools/catalog.ts +299 -0
  99. package/src/interfaces/mcp/tools/index.ts +22 -0
  100. package/src/interfaces/mcp/tools/inventory.ts +161 -0
  101. package/src/interfaces/mcp/tools/orders.ts +104 -0
  102. package/src/interfaces/mcp/tools/pricing.ts +94 -0
  103. package/src/interfaces/mcp/tools/promotions.ts +106 -0
  104. package/src/interfaces/mcp/tools/registry.ts +101 -0
  105. package/src/interfaces/mcp/tools/search.ts +42 -0
  106. package/src/interfaces/mcp/tools/webhooks.ts +48 -0
  107. package/src/interfaces/mcp/transport.ts +128 -0
  108. package/src/interfaces/rest/customer-portal.ts +299 -0
  109. package/src/interfaces/rest/index.ts +74 -0
  110. package/src/interfaces/rest/router.ts +333 -0
  111. package/src/interfaces/rest/routes/admin-jobs.ts +58 -0
  112. package/src/interfaces/rest/routes/audit.ts +50 -0
  113. package/src/interfaces/rest/routes/carts.ts +89 -0
  114. package/src/interfaces/rest/routes/catalog.ts +493 -0
  115. package/src/interfaces/rest/routes/checkout.ts +284 -0
  116. package/src/interfaces/rest/routes/inventory.ts +70 -0
  117. package/src/interfaces/rest/routes/media.ts +86 -0
  118. package/src/interfaces/rest/routes/orders.ts +78 -0
  119. package/src/interfaces/rest/routes/payments.ts +60 -0
  120. package/src/interfaces/rest/routes/pricing.ts +57 -0
  121. package/src/interfaces/rest/routes/promotions.ts +93 -0
  122. package/src/interfaces/rest/routes/search.ts +71 -0
  123. package/src/interfaces/rest/routes/webhooks.ts +46 -0
  124. package/src/interfaces/rest/schemas/admin-jobs.ts +40 -0
  125. package/src/interfaces/rest/schemas/audit.ts +46 -0
  126. package/src/interfaces/rest/schemas/carts.ts +125 -0
  127. package/src/interfaces/rest/schemas/catalog.ts +450 -0
  128. package/src/interfaces/rest/schemas/checkout.ts +66 -0
  129. package/src/interfaces/rest/schemas/customer-portal.ts +195 -0
  130. package/src/interfaces/rest/schemas/inventory.ts +138 -0
  131. package/src/interfaces/rest/schemas/media.ts +75 -0
  132. package/src/interfaces/rest/schemas/orders.ts +104 -0
  133. package/src/interfaces/rest/schemas/pricing.ts +80 -0
  134. package/src/interfaces/rest/schemas/promotions.ts +110 -0
  135. package/src/interfaces/rest/schemas/responses.ts +85 -0
  136. package/src/interfaces/rest/schemas/search.ts +58 -0
  137. package/src/interfaces/rest/schemas/shared.ts +62 -0
  138. package/src/interfaces/rest/schemas/webhooks.ts +68 -0
  139. package/src/interfaces/rest/utils.ts +104 -0
  140. package/src/interfaces/rest/webhook-router.ts +50 -0
  141. package/src/kernel/compensation/executor.ts +61 -0
  142. package/src/kernel/compensation/types.ts +26 -0
  143. package/src/kernel/database/adapter.ts +21 -0
  144. package/src/kernel/database/drizzle-db.ts +56 -0
  145. package/src/kernel/database/migrate.ts +76 -0
  146. package/src/kernel/database/plugin-types.ts +34 -0
  147. package/src/kernel/database/schema.ts +49 -0
  148. package/src/kernel/database/scoped-db.ts +68 -0
  149. package/src/kernel/database/tx-context.ts +46 -0
  150. package/src/kernel/error-mapper.ts +15 -0
  151. package/src/kernel/errors.ts +89 -0
  152. package/src/kernel/factory/repository-factory.ts +244 -0
  153. package/src/kernel/hooks/create-context.ts +43 -0
  154. package/src/kernel/hooks/executor.ts +88 -0
  155. package/src/kernel/hooks/registry.ts +74 -0
  156. package/src/kernel/hooks/types.ts +52 -0
  157. package/src/kernel/http-error.ts +44 -0
  158. package/src/kernel/jobs/adapter.ts +36 -0
  159. package/src/kernel/jobs/drizzle-adapter.ts +58 -0
  160. package/src/kernel/jobs/runner.ts +153 -0
  161. package/src/kernel/jobs/schema.ts +46 -0
  162. package/src/kernel/jobs/types.ts +30 -0
  163. package/src/kernel/local-api.ts +187 -0
  164. package/src/kernel/plugin/manifest.ts +271 -0
  165. package/src/kernel/query/executor.ts +184 -0
  166. package/src/kernel/query/registry.ts +46 -0
  167. package/src/kernel/result.ts +33 -0
  168. package/src/kernel/schema/extra-columns.ts +37 -0
  169. package/src/kernel/service-registry.ts +76 -0
  170. package/src/kernel/service-timing.ts +89 -0
  171. package/src/kernel/state-machine/machine.ts +101 -0
  172. package/src/modules/analytics/drizzle-adapter.ts +426 -0
  173. package/src/modules/analytics/hooks.ts +11 -0
  174. package/src/modules/analytics/models.ts +125 -0
  175. package/src/modules/analytics/repository/index.ts +6 -0
  176. package/src/modules/analytics/service.ts +245 -0
  177. package/src/modules/analytics/types.ts +180 -0
  178. package/src/modules/audit/hooks.ts +78 -0
  179. package/src/modules/audit/schema.ts +33 -0
  180. package/src/modules/audit/service.ts +151 -0
  181. package/src/modules/cart/access.ts +27 -0
  182. package/src/modules/cart/matcher.ts +26 -0
  183. package/src/modules/cart/repository/index.ts +234 -0
  184. package/src/modules/cart/schema.ts +42 -0
  185. package/src/modules/cart/schemas.ts +38 -0
  186. package/src/modules/cart/service.ts +541 -0
  187. package/src/modules/catalog/repository/index.ts +772 -0
  188. package/src/modules/catalog/schema.ts +203 -0
  189. package/src/modules/catalog/schemas.ts +104 -0
  190. package/src/modules/catalog/service.ts +1544 -0
  191. package/src/modules/customers/repository/index.ts +327 -0
  192. package/src/modules/customers/schema.ts +64 -0
  193. package/src/modules/customers/service.ts +171 -0
  194. package/src/modules/fulfillment/repository/index.ts +426 -0
  195. package/src/modules/fulfillment/schema.ts +101 -0
  196. package/src/modules/fulfillment/service.ts +555 -0
  197. package/src/modules/fulfillment/types.ts +59 -0
  198. package/src/modules/inventory/repository/index.ts +509 -0
  199. package/src/modules/inventory/schema.ts +94 -0
  200. package/src/modules/inventory/schemas.ts +38 -0
  201. package/src/modules/inventory/service.ts +490 -0
  202. package/src/modules/media/adapter.ts +17 -0
  203. package/src/modules/media/repository/index.ts +274 -0
  204. package/src/modules/media/schema.ts +41 -0
  205. package/src/modules/media/service.ts +151 -0
  206. package/src/modules/orders/repository/index.ts +287 -0
  207. package/src/modules/orders/schema.ts +66 -0
  208. package/src/modules/orders/service.ts +619 -0
  209. package/src/modules/orders/stale-order-cleanup.ts +76 -0
  210. package/src/modules/organization/service.ts +191 -0
  211. package/src/modules/payments/adapter.ts +47 -0
  212. package/src/modules/payments/repository/index.ts +6 -0
  213. package/src/modules/payments/service.ts +107 -0
  214. package/src/modules/pricing/repository/index.ts +291 -0
  215. package/src/modules/pricing/schema.ts +71 -0
  216. package/src/modules/pricing/schemas.ts +38 -0
  217. package/src/modules/pricing/service.ts +494 -0
  218. package/src/modules/promotions/repository/index.ts +325 -0
  219. package/src/modules/promotions/schema.ts +62 -0
  220. package/src/modules/promotions/schemas.ts +38 -0
  221. package/src/modules/promotions/service.ts +598 -0
  222. package/src/modules/search/adapter.ts +57 -0
  223. package/src/modules/search/hooks.ts +12 -0
  224. package/src/modules/search/repository/index.ts +6 -0
  225. package/src/modules/search/service.ts +315 -0
  226. package/src/modules/shipping/calculator.ts +188 -0
  227. package/src/modules/shipping/repository/index.ts +6 -0
  228. package/src/modules/shipping/service.ts +51 -0
  229. package/src/modules/tax/adapter.ts +60 -0
  230. package/src/modules/tax/repository/index.ts +6 -0
  231. package/src/modules/tax/service.ts +53 -0
  232. package/src/modules/webhooks/hook.ts +34 -0
  233. package/src/modules/webhooks/repository/index.ts +278 -0
  234. package/src/modules/webhooks/schema.ts +56 -0
  235. package/src/modules/webhooks/service.ts +117 -0
  236. package/src/modules/webhooks/signing.ts +6 -0
  237. package/src/modules/webhooks/ssrf-guard.ts +71 -0
  238. package/src/modules/webhooks/tasks.ts +52 -0
  239. package/src/modules/webhooks/worker.ts +134 -0
  240. package/src/runtime/commerce.ts +145 -0
  241. package/src/runtime/kernel.ts +426 -0
  242. package/src/runtime/logger.ts +36 -0
  243. package/src/runtime/server.ts +355 -0
  244. package/src/runtime/shutdown.ts +43 -0
  245. package/src/test-utils/create-pglite-adapter.ts +129 -0
  246. package/src/test-utils/create-plugin-test-app.ts +128 -0
  247. package/src/test-utils/create-repository-test-harness.ts +16 -0
  248. package/src/test-utils/create-test-config.ts +190 -0
  249. package/src/test-utils/create-test-kernel.ts +7 -0
  250. package/src/test-utils/create-test-plugin-context.ts +75 -0
  251. package/src/test-utils/rest-api-test-utils.ts +265 -0
  252. package/src/test-utils/test-actors.ts +62 -0
  253. package/src/test-utils/typed-hooks.ts +54 -0
  254. package/src/types/commerce-types.ts +34 -0
  255. package/src/utils/id.ts +3 -0
  256. package/src/utils/logger.ts +18 -0
  257. package/src/utils/pagination.ts +22 -0
@@ -0,0 +1,284 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { checkoutRoute } from "../schemas/checkout.js";
4
+ import {
5
+ applyPromotionCodes,
6
+ authorizePayment,
7
+ calculateShipping,
8
+ calculateTax,
9
+ checkInventoryAvailability,
10
+ completeCheckout,
11
+ recordAnalyticsEvent,
12
+ resolveCurrentPrices,
13
+ validateCartNotEmpty,
14
+ validatePaymentMethod,
15
+ type CheckoutData,
16
+ type OrderResult,
17
+ } from "../../../hooks/checkout.js";
18
+ import { runAfterHooks, runBeforeHooks } from "../../../kernel/hooks/executor.js";
19
+ import { createHookContext } from "../../../kernel/hooks/create-context.js";
20
+ import type { AfterHook, BeforeHook, ServiceContainer } from "../../../kernel/hooks/types.js";
21
+ import type { PluginDb } from "../../../kernel/database/plugin-types.js";
22
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
23
+ import { isCommerceError } from "../../../kernel/errors.js";
24
+ import { makeId } from "../../../utils/id.js";
25
+ import type { ShippingAddress } from "../../../modules/shipping/calculator.js";
26
+
27
+ export function checkoutRoutes(kernel: Kernel) {
28
+ const router = new OpenAPIHono<AppEnv>();
29
+
30
+ // @ts-expect-error -- openapi() enforces strict response typing but our handler
31
+ // returns union responses (201 | 400 | 422). The route definition documents the
32
+ // contract; the defaultHook handles Zod validation; the handler returns dynamic status.
33
+ router.openapi(checkoutRoute, async (c) => {
34
+ const body = c.req.valid("json");
35
+
36
+ const actor = c.get("actor");
37
+ const checkoutData: CheckoutData = {
38
+ checkoutId: makeId(),
39
+ cartId: body.cartId,
40
+ currency: body.currency ?? "USD",
41
+ paymentMethodId: body.paymentMethodId,
42
+ lineItems: [],
43
+ subtotal: 0,
44
+ discountTotal: 0,
45
+ taxTotal: 0,
46
+ shippingTotal: 0,
47
+ total: 0,
48
+ ...(body.customerId !== undefined ? { customerId: body.customerId } : {}),
49
+ ...(body.customerGroupIds !== undefined
50
+ ? { customerGroupIds: body.customerGroupIds }
51
+ : {}),
52
+ ...(body.promotionCodes !== undefined
53
+ ? { promotionCodes: body.promotionCodes }
54
+ : {}),
55
+ ...(body.shippingAddress != null
56
+ ? {
57
+ shippingAddress: {
58
+ line1: body.shippingAddress.line1,
59
+ city: body.shippingAddress.city,
60
+ postalCode: body.shippingAddress.postalCode,
61
+ country: body.shippingAddress.country,
62
+ ...(body.shippingAddress.line2 != null ? { line2: body.shippingAddress.line2 } : {}),
63
+ ...(body.shippingAddress.state != null ? { state: body.shippingAddress.state } : {}),
64
+ },
65
+ }
66
+ : {}),
67
+ };
68
+
69
+ // ── Phase 1: Validate & Calculate (inside DB transaction — fast SQL only) ──
70
+ const validationHooks: BeforeHook<CheckoutData>[] = [
71
+ validateCartNotEmpty,
72
+ resolveCurrentPrices,
73
+ checkInventoryAvailability,
74
+ applyPromotionCodes,
75
+ calculateTax,
76
+ calculateShipping,
77
+ ...(kernel.hooks.resolve("checkout.beforePayment") as BeforeHook<CheckoutData>[]),
78
+ validatePaymentMethod,
79
+ ];
80
+
81
+ // ── Phase 2: Payment Authorization (outside transaction — external API call) ──
82
+ const paymentHooks: BeforeHook<CheckoutData>[] = [
83
+ authorizePayment,
84
+ ...(kernel.hooks.resolve("checkout.beforeCreate") as BeforeHook<CheckoutData>[]),
85
+ ];
86
+
87
+ const afterHooks: AfterHook<OrderResult>[] = [
88
+ completeCheckout,
89
+ recordAnalyticsEvent,
90
+ ...(kernel.hooks.resolve("checkout.afterCreate") as AfterHook<OrderResult>[]),
91
+ ];
92
+
93
+ const context = createHookContext({
94
+ actor,
95
+ logger: kernel.logger,
96
+ services: kernel.services as ServiceContainer,
97
+ context: { moduleName: "checkout" },
98
+ origin: "rest",
99
+ kernel: { database: { db: kernel.database.db as PluginDb } },
100
+ });
101
+
102
+ try {
103
+ // Phase 1: DB transaction for validation — releases connection immediately after
104
+ const validated = await kernel.database.transaction(async (_tx) => {
105
+ context.tx = _tx;
106
+ return runBeforeHooks(
107
+ validationHooks,
108
+ checkoutData,
109
+ "create",
110
+ context,
111
+ );
112
+ });
113
+
114
+ // Phase 2: Payment authorization — NO DB connection held while calling Stripe/etc.
115
+ // If Stripe takes 5s, the DB connection pool is not affected.
116
+ context.tx = null;
117
+ const processed = await runBeforeHooks(
118
+ paymentHooks,
119
+ validated,
120
+ "create",
121
+ context,
122
+ );
123
+
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
+ }
150
+
151
+ const orderPayload = {
152
+ currency: processed.currency,
153
+ subtotal: processed.subtotal,
154
+ taxTotal: processed.taxTotal,
155
+ shippingTotal: processed.shippingTotal,
156
+ discountTotal: processed.discountTotal,
157
+ grandTotal: processed.total,
158
+ paymentIntentId: processed.paymentIntentId,
159
+ paymentMethodId: processed.paymentMethodId,
160
+ metadata: {
161
+ // H2 fix: Merge hook-injected metadata (e.g., BNPL fee) before core fields
162
+ ...(typeof processed.metadata === "object" && processed.metadata !== null
163
+ ? processed.metadata
164
+ : {}),
165
+ cartId: processed.cartId,
166
+ paymentIntentId: processed.paymentIntentId,
167
+ checkoutId: processed.checkoutId,
168
+ promotionCodes: processed.promotionCodes,
169
+ appliedPromotions: processed.appliedPromotions,
170
+ shippingAddress: processed.shippingAddress,
171
+ },
172
+ lineItems: processed.lineItems.map((lineItem) => {
173
+ const payload = {
174
+ entityId: lineItem.entityId,
175
+ entityType: lineItem.entityType ?? "product",
176
+ title: lineItem.title ?? lineItem.entityId,
177
+ quantity: lineItem.quantity,
178
+ unitPrice: lineItem.resolvedUnitPrice ?? 0,
179
+ totalPrice: lineItem.resolvedTotal ?? 0,
180
+ };
181
+ return lineItem.variantId !== undefined
182
+ ? { ...payload, variantId: lineItem.variantId }
183
+ : payload;
184
+ }),
185
+ ...(customerUuid !== undefined
186
+ ? { customerId: customerUuid }
187
+ : {}),
188
+ };
189
+
190
+ const order = await kernel.services.orders.create(orderPayload, actor);
191
+
192
+ if (!order.ok) {
193
+ return c.json(
194
+ mapErrorToResponse(order.error),
195
+ mapErrorToStatus(order.error),
196
+ );
197
+ }
198
+
199
+ if (order.ok && (processed.appliedPromotions?.length ?? 0) > 0) {
200
+ await kernel.services.promotions.recordUsage({
201
+ promotions: processed.appliedPromotions ?? [],
202
+ orderId: order.value.id,
203
+ ...(customerUuid !== undefined
204
+ ? { customerId: customerUuid }
205
+ : {}),
206
+ });
207
+ }
208
+
209
+ if (order.ok) {
210
+ await kernel.services.tax.reportTransaction({
211
+ transactionId: order.value.id,
212
+ transactionDate: new Date(),
213
+ currency: processed.currency,
214
+ amount:
215
+ processed.subtotal -
216
+ processed.discountTotal +
217
+ processed.shippingTotal,
218
+ shipping: processed.shippingTotal,
219
+ salesTax: processed.taxTotal,
220
+ lineItems: processed.lineItems.map((lineItem, index) => ({
221
+ id: lineItem.id ?? `${order.value.id}-${index + 1}`,
222
+ entityId: lineItem.entityId,
223
+ description: lineItem.title ?? lineItem.entityId,
224
+ quantity: lineItem.quantity,
225
+ unitPrice: lineItem.resolvedUnitPrice ?? 0,
226
+ ...(lineItem.discountAmount !== undefined
227
+ ? { discount: lineItem.discountAmount }
228
+ : {}),
229
+ })),
230
+ ...(customerUuid !== undefined
231
+ ? { customerId: customerUuid }
232
+ : {}),
233
+ ...(processed.shippingAddress !== undefined
234
+ ? { toAddress: processed.shippingAddress }
235
+ : {}),
236
+ });
237
+ }
238
+
239
+ // Stash paymentMethodId for completeCheckout compensation chain
240
+ context.context.paymentMethodId = processed.paymentMethodId;
241
+
242
+ const afterReport = await runAfterHooks(
243
+ afterHooks,
244
+ null,
245
+ order.value,
246
+ "create",
247
+ context,
248
+ );
249
+
250
+ await kernel.services.cart.markAsCheckedOut(body.cartId, actor);
251
+
252
+ return c.json(
253
+ {
254
+ data: {
255
+ ...order.value,
256
+ // Stripe Elements requires clientSecret to collect card details on the frontend
257
+ ...(processed.paymentClientSecret
258
+ ? { paymentClientSecret: processed.paymentClientSecret }
259
+ : {}),
260
+ },
261
+ meta: afterReport.hasErrors
262
+ ? { hookErrors: afterReport.errors }
263
+ : undefined,
264
+ },
265
+ 201,
266
+ );
267
+ } catch (error) {
268
+ const message = isCommerceError(error)
269
+ ? error.message
270
+ : "Checkout failed.";
271
+ return c.json(
272
+ {
273
+ error: {
274
+ code: "CHECKOUT_FAILED",
275
+ message,
276
+ },
277
+ },
278
+ 422,
279
+ );
280
+ }
281
+ });
282
+
283
+ return router;
284
+ }
@@ -0,0 +1,70 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import {
4
+ inventoryAdjustRoute,
5
+ inventoryReserveRoute,
6
+ inventoryReleaseRoute,
7
+ createWarehouseRoute,
8
+ inventoryCheckRoute,
9
+ listWarehousesRoute,
10
+ } from "../schemas/inventory.js";
11
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
12
+
13
+ export function inventoryRoutes(kernel: Kernel) {
14
+ const router = new OpenAPIHono<AppEnv>();
15
+
16
+ // @ts-expect-error -- openapi handler union return type
17
+ router.openapi(inventoryCheckRoute, async (c) => {
18
+ const entityIds = (c.req.query("entityIds") ?? "")
19
+ .split(",")
20
+ .map((item) => item.trim())
21
+ .filter(Boolean);
22
+
23
+ const result = await kernel.services.inventory.checkMultiple(entityIds);
24
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
25
+ return c.json({ data: result.value });
26
+ });
27
+
28
+ // @ts-expect-error -- openapi handler union return type
29
+ router.openapi(inventoryAdjustRoute, async (c) => {
30
+ const body = c.req.valid("json");
31
+ const result = await kernel.services.inventory.adjust(body, c.get("actor"));
32
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
33
+ return c.json({ data: result.value });
34
+ });
35
+
36
+ // @ts-expect-error -- openapi handler union return type
37
+ router.openapi(inventoryReserveRoute, async (c) => {
38
+ const body = c.req.valid("json");
39
+ const result = await kernel.services.inventory.reserve(body, c.get("actor"));
40
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
41
+ return c.json({ data: { reserved: true } });
42
+ });
43
+
44
+ // @ts-expect-error -- openapi handler union return type
45
+ router.openapi(inventoryReleaseRoute, async (c) => {
46
+ const body = c.req.valid("json");
47
+ const result = await kernel.services.inventory.release(body, c.get("actor"));
48
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
49
+ return c.json({ data: { released: true } });
50
+ });
51
+
52
+ // @ts-expect-error -- openapi handler union return type
53
+ router.openapi(createWarehouseRoute, async (c) => {
54
+ const body = c.req.valid("json") as Parameters<typeof kernel.services.inventory.createWarehouse>[0];
55
+ const actor = c.get("actor");
56
+ const result = await kernel.services.inventory.createWarehouse(body, actor);
57
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
58
+ return c.json({ data: result.value }, 201);
59
+ });
60
+
61
+ // @ts-expect-error -- openapi handler union return type
62
+ router.openapi(listWarehousesRoute, async (c) => {
63
+ const actor = c.get("actor");
64
+ const result = await kernel.services.inventory.listWarehouses(actor);
65
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
66
+ return c.json({ data: result.value });
67
+ });
68
+
69
+ return router;
70
+ }
@@ -0,0 +1,86 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import type { AttachMediaInput } from "../../../modules/media/service.js";
4
+ import { attachMediaRoute, getMediaRoute, deleteMediaRoute } from "../schemas/media.js";
5
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
6
+
7
+ export function mediaRoutes(kernel: Kernel) {
8
+ const router = new OpenAPIHono<AppEnv>();
9
+
10
+ // Upload requires authentication + media:write permission
11
+ router.use("/upload", requirePerm("media:write"));
12
+
13
+ router.post("/upload", async (c) => {
14
+ const body = await c.req.parseBody();
15
+ const file = body.file as File;
16
+
17
+ if (!file) {
18
+ return c.json({ error: { code: "VALIDATION_FAILED", message: "file is required" } }, 422);
19
+ }
20
+
21
+ const buffer = await file.arrayBuffer();
22
+ const actor = c.get("actor");
23
+ const result = await kernel.services.media.upload({
24
+ filename: file.name,
25
+ contentType: file.type,
26
+ data: buffer,
27
+ alt: String(body.alt ?? ""),
28
+ }, actor);
29
+
30
+ if (!result.ok) {
31
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
32
+ }
33
+
34
+ return c.json({ data: result.value }, 201);
35
+ });
36
+
37
+ router.openapi(getMediaRoute, async (c) => {
38
+ const signed = c.req.query("signed") === "true";
39
+
40
+ // Signed URLs require authentication
41
+ if (signed && !c.get("actor")) {
42
+ return c.json(
43
+ { error: { code: "UNAUTHORIZED", message: "Authentication required for signed URLs." } },
44
+ 401,
45
+ );
46
+ }
47
+
48
+ const result = signed
49
+ ? await kernel.services.media.getSignedUrl(c.req.param("id"))
50
+ : await kernel.services.media.getUrl(c.req.param("id"));
51
+
52
+ if (!result.ok) {
53
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
54
+ }
55
+
56
+ return c.redirect(result.value, 302);
57
+ });
58
+
59
+ // @ts-expect-error -- openapi handler union return type
60
+ router.openapi(deleteMediaRoute, async (c) => {
61
+ const actor = c.get("actor");
62
+ if (!actor || (!actor.permissions.includes("media:write") && !actor.permissions.includes("*:*"))) {
63
+ return c.json({ error: { code: "FORBIDDEN", message: "media:write permission required." } }, 403);
64
+ }
65
+ const result = await kernel.services.media.delete(c.req.param("id"));
66
+ if (!result.ok) {
67
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
68
+ }
69
+ return c.json({ data: { deleted: true } });
70
+ });
71
+
72
+ // Attach requires media:write
73
+ router.use("/attach", requirePerm("media:write"));
74
+
75
+ // @ts-expect-error -- openapi handler union return type
76
+ router.openapi(attachMediaRoute, async (c) => {
77
+ const body = c.req.valid("json") as AttachMediaInput;
78
+ const result = await kernel.services.media.attachToEntity(body);
79
+ if (!result.ok) {
80
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
81
+ }
82
+ return c.json({ data: { attached: true } }, 201);
83
+ });
84
+
85
+ return router;
86
+ }
@@ -0,0 +1,78 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { changeOrderStatusRoute, listOrdersRoute, getOrderRoute, getOrderFulfillmentsRoute } from "../schemas/orders.js";
4
+ import { type AppEnv, isUUID, mapErrorToResponse, mapErrorToStatus, parsePagination } from "../utils.js";
5
+
6
+ export function orderRoutes(kernel: Kernel) {
7
+ const router = new OpenAPIHono<AppEnv>();
8
+
9
+ // @ts-expect-error -- openapi handler union return type
10
+ router.openapi(listOrdersRoute, async (c) => {
11
+ const pagination = parsePagination(c.req.query());
12
+ const status = c.req.query("status");
13
+ const result = await kernel.services.orders.list(
14
+ {
15
+ page: pagination.page,
16
+ limit: pagination.limit,
17
+ ...(status !== undefined ? { status } : {}),
18
+ },
19
+ c.get("actor"),
20
+ );
21
+
22
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
23
+ return c.json({
24
+ data: result.value.items,
25
+ meta: {
26
+ pagination: result.value.pagination,
27
+ },
28
+ });
29
+ });
30
+
31
+ // @ts-expect-error -- openapi handler union return type
32
+ router.openapi(getOrderRoute, async (c) => {
33
+ const idOrNumber = c.req.param("idOrNumber");
34
+ const result = isUUID(idOrNumber)
35
+ ? await kernel.services.orders.getById(idOrNumber, c.get("actor"))
36
+ : await kernel.services.orders.getByNumber(idOrNumber, c.get("actor"));
37
+
38
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
39
+ return c.json({ data: result.value });
40
+ });
41
+
42
+ // @ts-expect-error -- openapi() enforces strict response typing but our handler
43
+ // returns union responses (200 | 400 | 404). The route definition documents the
44
+ // contract; the handler returns dynamic status.
45
+ router.openapi(changeOrderStatusRoute, async (c) => {
46
+ const body = c.req.valid("json");
47
+ const result = await kernel.services.orders.changeStatus(
48
+ {
49
+ orderId: c.req.param("id"),
50
+ newStatus: body.status,
51
+ ...(body.reason !== undefined ? { reason: body.reason } : {}),
52
+ },
53
+ c.get("actor"),
54
+ );
55
+
56
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
57
+ return c.json({ data: result.value });
58
+ });
59
+
60
+ // @ts-expect-error -- openapi handler union return type
61
+ router.openapi(getOrderFulfillmentsRoute, async (c) => {
62
+ const orderId = c.req.param("id");
63
+ const actor = c.get("actor");
64
+
65
+ // Verify the order exists and the actor has access before returning fulfillments
66
+ const orderResult = isUUID(orderId)
67
+ ? await kernel.services.orders.getById(orderId, actor)
68
+ : await kernel.services.orders.getByNumber(orderId, actor);
69
+
70
+ if (!orderResult.ok) return c.json(mapErrorToResponse(orderResult.error), mapErrorToStatus(orderResult.error));
71
+
72
+ const result = await kernel.services.fulfillment.getByOrderId(orderResult.value.id);
73
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
74
+ return c.json({ data: result.value });
75
+ });
76
+
77
+ return router;
78
+ }
@@ -0,0 +1,60 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
3
+ import type { Kernel } from "../../../runtime/kernel.js";
4
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
5
+ import { processedWebhookEvents } from "../../../modules/webhooks/schema.js";
6
+
7
+ type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
8
+
9
+ export function paymentRoutes(kernel: Kernel) {
10
+ const router = new OpenAPIHono<AppEnv>();
11
+
12
+ router.post("/webhook", async (c) => {
13
+ const result = await kernel.services.payments.verifyWebhook(c.req.raw);
14
+ if (!result.ok) {
15
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
16
+ }
17
+
18
+ const event = result.value;
19
+ const db = kernel.database.db as Db;
20
+
21
+ // Atomic idempotency: INSERT ... ON CONFLICT DO NOTHING ... RETURNING
22
+ // If the row already exists, RETURNING yields zero rows → duplicate.
23
+ // If the row is new, RETURNING yields one row → process the event.
24
+ // No TOCTOU race: the UNIQUE constraint on event_id is the single source of truth.
25
+ const [inserted] = await db
26
+ .insert(processedWebhookEvents)
27
+ .values({
28
+ eventId: event.id,
29
+ provider: "stripe",
30
+ eventType: event.type,
31
+ })
32
+ .onConflictDoNothing()
33
+ .returning({ id: processedWebhookEvents.id });
34
+
35
+ if (!inserted) {
36
+ // Row already existed — this is a duplicate delivery
37
+ return c.json({ data: { received: true, duplicate: true } });
38
+ }
39
+
40
+ // Process the event (first time only)
41
+ if (event.type === "payment_intent.succeeded") {
42
+ const data = event.data as Record<string, unknown> | undefined;
43
+ const metadata = data?.metadata as Record<string, unknown> | undefined;
44
+ if (typeof metadata?.orderId === "string") {
45
+ await kernel.services.orders.changeStatus(
46
+ {
47
+ orderId: metadata.orderId,
48
+ newStatus: "confirmed",
49
+ reason: "stripe_webhook_payment_intent_succeeded",
50
+ },
51
+ null,
52
+ );
53
+ }
54
+ }
55
+
56
+ return c.json({ data: { received: true } });
57
+ });
58
+
59
+ return router;
60
+ }
@@ -0,0 +1,57 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import type { Kernel } from "../../../runtime/kernel.js";
3
+ import { setBasePriceRoute, createModifierRoute, listPricesRoute } from "../schemas/pricing.js";
4
+ import { type AppEnv, mapErrorToResponse, mapErrorToStatus, requirePerm } from "../utils.js";
5
+
6
+ export function pricingRoutes(kernel: Kernel) {
7
+ const router = new OpenAPIHono<AppEnv>();
8
+
9
+ router.use("/prices", requirePerm("pricing:manage"));
10
+
11
+ // @ts-expect-error -- openapi() enforces strict response typing but our handler
12
+ // returns union responses (201 | 400 | 422). The route definition documents the
13
+ // contract; the handler returns dynamic status.
14
+ router.openapi(setBasePriceRoute, async (c) => {
15
+ const actor = c.get("actor");
16
+ const result = await kernel.services.pricing.setBasePrice(c.req.valid("json"), actor);
17
+ if (!result.ok) {
18
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
19
+ }
20
+ return c.json({ data: result.value }, 201);
21
+ });
22
+
23
+ // @ts-expect-error -- openapi handler union return type
24
+ router.openapi(listPricesRoute, async (c) => {
25
+ const entityId = c.req.query("entityId");
26
+ const variantId = c.req.query("variantId");
27
+ const currency = c.req.query("currency");
28
+ const customerGroupId = c.req.query("customerGroupId");
29
+
30
+ const result = await kernel.services.pricing.listPrices({
31
+ ...(entityId !== undefined ? { entityId } : {}),
32
+ ...(variantId !== undefined ? { variantId } : {}),
33
+ ...(currency !== undefined ? { currency } : {}),
34
+ ...(customerGroupId !== undefined ? { customerGroupId } : {}),
35
+ });
36
+ if (!result.ok) {
37
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
38
+ }
39
+ return c.json({ data: result.value });
40
+ });
41
+
42
+ router.use("/modifiers", requirePerm("pricing:manage"));
43
+
44
+ // @ts-expect-error -- openapi() enforces strict response typing but our handler
45
+ // returns union responses (201 | 400 | 422). The route definition documents the
46
+ // contract; the handler returns dynamic status.
47
+ router.openapi(createModifierRoute, async (c) => {
48
+ const actor = c.get("actor");
49
+ const result = await kernel.services.pricing.createModifier(c.req.valid("json"), actor);
50
+ if (!result.ok) {
51
+ return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
52
+ }
53
+ return c.json({ data: result.value }, 201);
54
+ });
55
+
56
+ return router;
57
+ }