@unifiedcommerce/core 0.3.4 → 0.4.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 (33) hide show
  1. package/dist/interfaces/mcp/tools/webhooks.d.ts +1 -1
  2. package/dist/interfaces/rest/index.d.ts.map +1 -1
  3. package/dist/interfaces/rest/index.js +14 -3
  4. package/dist/interfaces/rest/routes/customers.d.ts +5 -0
  5. package/dist/interfaces/rest/routes/customers.d.ts.map +1 -0
  6. package/dist/interfaces/rest/routes/customers.js +74 -0
  7. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  8. package/dist/interfaces/rest/routes/inventory.js +14 -1
  9. package/dist/interfaces/rest/schemas/customers.d.ts +422 -0
  10. package/dist/interfaces/rest/schemas/customers.d.ts.map +1 -0
  11. package/dist/interfaces/rest/schemas/customers.js +150 -0
  12. package/dist/interfaces/rest/schemas/inventory.d.ts +92 -0
  13. package/dist/interfaces/rest/schemas/inventory.d.ts.map +1 -1
  14. package/dist/interfaces/rest/schemas/inventory.js +20 -0
  15. package/dist/modules/customers/service.d.ts +2 -0
  16. package/dist/modules/customers/service.d.ts.map +1 -1
  17. package/dist/modules/customers/service.js +15 -0
  18. package/dist/modules/inventory/service.d.ts +4 -0
  19. package/dist/modules/inventory/service.d.ts.map +1 -1
  20. package/dist/modules/inventory/service.js +12 -0
  21. package/dist/runtime/server.d.ts.map +1 -1
  22. package/dist/runtime/server.js +29 -0
  23. package/package.json +2 -2
  24. package/src/config/types.ts +2 -0
  25. package/src/interfaces/rest/index.ts +50 -3
  26. package/src/interfaces/rest/routes/customers.ts +94 -0
  27. package/src/interfaces/rest/routes/entity-aliases.ts +346 -0
  28. package/src/interfaces/rest/routes/inventory.ts +17 -0
  29. package/src/interfaces/rest/schemas/customers.ts +155 -0
  30. package/src/interfaces/rest/schemas/inventory.ts +21 -0
  31. package/src/modules/customers/service.ts +25 -0
  32. package/src/modules/inventory/service.ts +17 -0
  33. package/src/runtime/server.ts +41 -0
@@ -0,0 +1,346 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { z, createRoute } from "@hono/zod-openapi";
3
+ import type { Kernel } from "../../../runtime/kernel.js";
4
+ import type { CreateEntityInput, UpdateEntityInput } from "../../../modules/catalog/schemas.js";
5
+ import {
6
+ type AppEnv,
7
+ mapErrorToResponse,
8
+ mapErrorToStatus,
9
+ parseInclude,
10
+ parsePagination,
11
+ parseSort,
12
+ } from "../utils.js";
13
+ import { errorResponses } from "../schemas/shared.js";
14
+
15
+ /**
16
+ * Generate ergonomic alias routes for an entity type.
17
+ *
18
+ * E.g., if config has `entities.product.alias = "products"`, this creates:
19
+ * GET /api/products
20
+ * GET /api/products/:idOrSlug
21
+ * POST /api/products
22
+ * PATCH /api/products/:id
23
+ * DELETE /api/products/:id
24
+ * POST /api/products/:id/publish
25
+ * POST /api/products/:id/archive
26
+ * GET /api/products/:id/variants
27
+ * POST /api/products/:id/variants
28
+ * GET /api/products/:id/options
29
+ * GET /api/products/:id/attributes/:locale
30
+ * PUT /api/products/:id/attributes/:locale
31
+ *
32
+ * All delegate to the same catalog service with `type` pre-injected.
33
+ */
34
+ export function entityAliasRoutes(kernel: Kernel): OpenAPIHono<AppEnv>[] {
35
+ const routers: OpenAPIHono<AppEnv>[] = [];
36
+ const entities = kernel.config.entities ?? {};
37
+
38
+ for (const [entityType, entityConfig] of Object.entries(entities)) {
39
+ if (!entityConfig.alias) continue;
40
+
41
+ const alias = entityConfig.alias;
42
+ const tag = alias.charAt(0).toUpperCase() + alias.slice(1);
43
+ const router = new OpenAPIHono<AppEnv>();
44
+
45
+ // ─── LIST ─────────────────────────────────────────────────
46
+ const listRoute = createRoute({
47
+ method: "get",
48
+ path: "/",
49
+ tags: [tag],
50
+ summary: `List ${alias}`,
51
+ description: `Alias for GET /api/catalog/entities?type=${entityType}`,
52
+ request: {
53
+ query: z.object({
54
+ status: z.string().optional(),
55
+ category: z.string().optional(),
56
+ brand: z.string().optional(),
57
+ include: z.string().optional(),
58
+ sort: z.string().optional(),
59
+ page: z.string().optional(),
60
+ limit: z.string().optional(),
61
+ }),
62
+ },
63
+ responses: {
64
+ 200: {
65
+ content: { "application/json": { schema: z.object({ data: z.array(z.record(z.string(), z.unknown())), meta: z.record(z.string(), z.unknown()) }) } },
66
+ description: `${tag} list`,
67
+ },
68
+ },
69
+ });
70
+
71
+ // @ts-expect-error -- openapi handler union return type
72
+ router.openapi(listRoute, async (c) => {
73
+ const include = parseInclude(c.req.query("include"));
74
+ const pagination = parsePagination(c.req.query());
75
+ const filter: Record<string, string> = { type: entityType };
76
+ const status = c.req.query("status");
77
+ const category = c.req.query("category");
78
+ const brand = c.req.query("brand");
79
+ if (status) filter.status = status;
80
+ if (category) filter.category = category;
81
+ if (brand) filter.brand = brand;
82
+
83
+ const payload: Record<string, unknown> = { filter, pagination };
84
+ const sort = parseSort(c.req.query("sort"));
85
+ if (sort) payload.sort = sort;
86
+
87
+ const result = await kernel.services.catalog.list(payload, c.get("actor"));
88
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
89
+
90
+ let items = result.value.items;
91
+ if (include.size > 0) {
92
+ const opts = {
93
+ includeAttributes: include.has("attributes"),
94
+ includeVariants: include.has("variants"),
95
+ includeOptionTypes: include.has("optionTypes"),
96
+ includeCategories: include.has("categories"),
97
+ includeBrands: include.has("brands"),
98
+ includeMedia: include.has("media"),
99
+ includeInventory: include.has("inventory"),
100
+ includePricing: include.has("pricing"),
101
+ };
102
+ items = await Promise.all(
103
+ items.map(async (item) => {
104
+ const full = await kernel.services.catalog.getById(item.id, opts);
105
+ return full.ok ? full.value : item;
106
+ }),
107
+ );
108
+ }
109
+
110
+ return c.json({ data: items, meta: { pagination: result.value.pagination } });
111
+ });
112
+
113
+ // ─── GET BY ID/SLUG ───────────────────────────────────────
114
+ const getRoute = createRoute({
115
+ method: "get",
116
+ path: "/{idOrSlug}",
117
+ tags: [tag],
118
+ summary: `Get ${alias.slice(0, -1)} by ID or slug`,
119
+ request: {
120
+ params: z.object({ idOrSlug: z.string() }),
121
+ query: z.object({ include: z.string().optional() }),
122
+ },
123
+ responses: {
124
+ 200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} detail` },
125
+ ...errorResponses,
126
+ },
127
+ });
128
+
129
+ // @ts-expect-error -- openapi handler union return type
130
+ router.openapi(getRoute, async (c) => {
131
+ const idOrSlug = c.req.valid("param").idOrSlug;
132
+ const include = parseInclude(c.req.query("include"));
133
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrSlug);
134
+ const opts = {
135
+ includeAttributes: include.has("attributes"),
136
+ includeVariants: include.has("variants"),
137
+ includeOptionTypes: include.has("optionTypes"),
138
+ includeCategories: include.has("categories"),
139
+ includeBrands: include.has("brands"),
140
+ includeMedia: include.has("media"),
141
+ includeInventory: include.has("inventory"),
142
+ includePricing: include.has("pricing"),
143
+ };
144
+
145
+ const result = isUUID
146
+ ? await kernel.services.catalog.getById(idOrSlug, opts)
147
+ : await kernel.services.catalog.getBySlug(idOrSlug, opts);
148
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
149
+ return c.json({ data: result.value });
150
+ });
151
+
152
+ // ─── CREATE ───────────────────────────────────────────────
153
+ const createEntityRoute = createRoute({
154
+ method: "post",
155
+ path: "/",
156
+ tags: [tag],
157
+ summary: `Create ${alias.slice(0, -1)}`,
158
+ request: {
159
+ body: { content: { "application/json": { schema: z.object({ slug: z.string().optional(), title: z.string().optional(), description: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Create${tag}Request`) } } },
160
+ },
161
+ responses: {
162
+ 201: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} created` },
163
+ ...errorResponses,
164
+ },
165
+ });
166
+
167
+ // @ts-expect-error -- openapi handler union return type
168
+ router.openapi(createEntityRoute, async (c) => {
169
+ const body = c.req.valid("json");
170
+ const input: CreateEntityInput = { type: entityType, ...body };
171
+ const result = await kernel.services.catalog.create(input, c.get("actor"));
172
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
173
+ return c.json({ data: result.value }, 201);
174
+ });
175
+
176
+ // ─── UPDATE ───────────────────────────────────────────────
177
+ const updateRoute = createRoute({
178
+ method: "patch",
179
+ path: "/{id}",
180
+ tags: [tag],
181
+ summary: `Update ${alias.slice(0, -1)}`,
182
+ request: {
183
+ params: z.object({ id: z.string().uuid() }),
184
+ body: { content: { "application/json": { schema: z.object({ slug: z.string().optional(), title: z.string().optional(), description: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Update${tag}Request`) } } },
185
+ },
186
+ responses: {
187
+ 200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} updated` },
188
+ ...errorResponses,
189
+ },
190
+ });
191
+
192
+ // @ts-expect-error -- openapi handler union return type
193
+ router.openapi(updateRoute, async (c) => {
194
+ const { id } = c.req.valid("param");
195
+ const body = c.req.valid("json");
196
+ const input: UpdateEntityInput = { ...body };
197
+ const result = await kernel.services.catalog.update(id, input, c.get("actor"));
198
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
199
+ return c.json({ data: result.value });
200
+ });
201
+
202
+ // ─── DELETE ────────────────────────────────────────────────
203
+ const deleteRoute = createRoute({
204
+ method: "delete",
205
+ path: "/{id}",
206
+ tags: [tag],
207
+ summary: `Delete ${alias.slice(0, -1)}`,
208
+ request: { params: z.object({ id: z.string().uuid() }) },
209
+ responses: {
210
+ 200: { content: { "application/json": { schema: z.object({ data: z.object({ deleted: z.literal(true) }) }) } }, description: "Deleted" },
211
+ ...errorResponses,
212
+ },
213
+ });
214
+
215
+ // @ts-expect-error -- openapi handler union return type
216
+ router.openapi(deleteRoute, async (c) => {
217
+ const { id } = c.req.valid("param");
218
+ const result = await kernel.services.catalog.delete(id, c.get("actor"));
219
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
220
+ return c.json({ data: { deleted: true as const } });
221
+ });
222
+
223
+ // ─── PUBLISH / ARCHIVE ────────────────────────────────────
224
+ for (const action of ["publish", "archive"] as const) {
225
+ const actionRoute = createRoute({
226
+ method: "post",
227
+ path: `/{id}/${action}`,
228
+ tags: [tag],
229
+ summary: `${action.charAt(0).toUpperCase() + action.slice(1)} ${alias.slice(0, -1)}`,
230
+ request: { params: z.object({ id: z.string().uuid() }) },
231
+ responses: {
232
+ 200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} ${action}ed` },
233
+ ...errorResponses,
234
+ },
235
+ });
236
+
237
+ // @ts-expect-error -- openapi handler union return type
238
+ router.openapi(actionRoute, async (c) => {
239
+ const { id } = c.req.valid("param");
240
+ const result = await kernel.services.catalog[action](id, c.get("actor"));
241
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
242
+ return c.json({ data: result.value });
243
+ });
244
+ }
245
+
246
+ // ─── ATTRIBUTES ───────────────────────────────────────────
247
+ const getAttrsRoute = createRoute({
248
+ method: "get",
249
+ path: "/{id}/attributes/{locale}",
250
+ tags: [tag],
251
+ summary: `Get ${alias.slice(0, -1)} attributes`,
252
+ request: { params: z.object({ id: z.string().uuid(), locale: z.string() }) },
253
+ responses: {
254
+ 200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: "Attributes" },
255
+ ...errorResponses,
256
+ },
257
+ });
258
+
259
+ // @ts-expect-error -- openapi handler union return type
260
+ router.openapi(getAttrsRoute, async (c) => {
261
+ const { id, locale } = c.req.valid("param");
262
+ const result = await kernel.services.catalog.getAttributes(id, locale);
263
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
264
+ return c.json({ data: result.value });
265
+ });
266
+
267
+ const setAttrsRoute = createRoute({
268
+ method: "put",
269
+ path: "/{id}/attributes/{locale}",
270
+ tags: [tag],
271
+ summary: `Set ${alias.slice(0, -1)} attributes`,
272
+ request: {
273
+ params: z.object({ id: z.string().uuid(), locale: z.string() }),
274
+ body: { content: { "application/json": { schema: z.record(z.string(), z.unknown()).openapi(`Set${tag}AttributesRequest`) } } },
275
+ },
276
+ responses: {
277
+ 200: { content: { "application/json": { schema: z.object({ data: z.object({ updated: z.literal(true) }) }) } }, description: "Attributes set" },
278
+ ...errorResponses,
279
+ },
280
+ });
281
+
282
+ // @ts-expect-error -- openapi handler union return type
283
+ router.openapi(setAttrsRoute, async (c) => {
284
+ const { id, locale } = c.req.valid("param");
285
+ const body = c.req.valid("json");
286
+ const result = await kernel.services.catalog.setAttributes(id, locale, body);
287
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
288
+ return c.json({ data: { updated: true as const } });
289
+ });
290
+
291
+ // ─── VARIANTS ─────────────────────────────────────────────
292
+ if (entityConfig.variants.enabled) {
293
+ const createVariantRoute = createRoute({
294
+ method: "post",
295
+ path: "/{id}/variants",
296
+ tags: [tag],
297
+ summary: `Create variant for ${alias.slice(0, -1)}`,
298
+ request: {
299
+ params: z.object({ id: z.string().uuid() }),
300
+ body: { content: { "application/json": { schema: z.object({ sku: z.string().optional(), options: z.record(z.string(), z.string()).optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Create${tag}VariantRequest`) } } },
301
+ },
302
+ responses: {
303
+ 201: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: "Variant created" },
304
+ ...errorResponses,
305
+ },
306
+ });
307
+
308
+ // @ts-expect-error -- openapi handler union return type
309
+ router.openapi(createVariantRoute, async (c) => {
310
+ const { id } = c.req.valid("param");
311
+ const body = c.req.valid("json");
312
+ const result = await kernel.services.catalog.createVariant({ entityId: id, ...body }, c.get("actor"));
313
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
314
+ return c.json({ data: result.value }, 201);
315
+ });
316
+
317
+ const listOptionsRoute = createRoute({
318
+ method: "get",
319
+ path: "/{id}/options",
320
+ tags: [tag],
321
+ summary: `List option types for ${alias.slice(0, -1)}`,
322
+ request: { params: z.object({ id: z.string().uuid() }) },
323
+ responses: {
324
+ 200: { content: { "application/json": { schema: z.object({ data: z.array(z.record(z.string(), z.unknown())) }) } }, description: "Option types" },
325
+ ...errorResponses,
326
+ },
327
+ });
328
+
329
+ // @ts-expect-error -- openapi handler union return type
330
+ router.openapi(listOptionsRoute, async (c) => {
331
+ const { id } = c.req.valid("param");
332
+ const result = await kernel.services.catalog.getById(id, { includeOptionTypes: true });
333
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
334
+ return c.json({ data: result.value.optionTypes ?? [] });
335
+ });
336
+ }
337
+
338
+ // Store alias → entityType for the tag group
339
+ (router as unknown as Record<string, string>).__aliasTag = tag;
340
+ (router as unknown as Record<string, string>).__aliasPath = alias;
341
+
342
+ routers.push(router);
343
+ }
344
+
345
+ return routers;
346
+ }
@@ -7,12 +7,29 @@ import {
7
7
  createWarehouseRoute,
8
8
  inventoryCheckRoute,
9
9
  listWarehousesRoute,
10
+ listInventoryLevelsRoute,
10
11
  } from "../schemas/inventory.js";
11
12
  import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
12
13
 
13
14
  export function inventoryRoutes(kernel: Kernel) {
14
15
  const router = new OpenAPIHono<AppEnv>();
15
16
 
17
+ // @ts-expect-error -- openapi handler union return type
18
+ router.openapi(listInventoryLevelsRoute, async (c) => {
19
+ const actor = c.get("actor");
20
+ const warehouseId = c.req.query("warehouseId");
21
+ const entityId = c.req.query("entityId");
22
+ const result = await kernel.services.inventory.listLevels(
23
+ {
24
+ ...(warehouseId ? { warehouseId } : {}),
25
+ ...(entityId ? { entityId } : {}),
26
+ },
27
+ actor,
28
+ );
29
+ if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
30
+ return c.json({ data: result.value });
31
+ });
32
+
16
33
  // @ts-expect-error -- openapi handler union return type
17
34
  router.openapi(inventoryCheckRoute, async (c) => {
18
35
  const entityIds = (c.req.query("entityIds") ?? "")
@@ -0,0 +1,155 @@
1
+ import { z, createRoute } from "@hono/zod-openapi";
2
+ import { errorResponses } from "./shared.js";
3
+
4
+ export const listCustomersRoute = createRoute({
5
+ method: "get",
6
+ path: "/",
7
+ tags: ["Customers"],
8
+ summary: "List customers",
9
+ request: {
10
+ query: z.object({
11
+ page: z.string().optional().openapi({ example: "1" }),
12
+ limit: z.string().optional().openapi({ example: "20" }),
13
+ }),
14
+ },
15
+ responses: {
16
+ 200: {
17
+ content: {
18
+ "application/json": {
19
+ schema: z.object({
20
+ data: z.array(z.record(z.string(), z.unknown())),
21
+ meta: z.object({
22
+ pagination: z.object({
23
+ page: z.number(),
24
+ limit: z.number(),
25
+ total: z.number(),
26
+ totalPages: z.number(),
27
+ }),
28
+ }),
29
+ }),
30
+ },
31
+ },
32
+ description: "Customer list",
33
+ },
34
+ },
35
+ });
36
+
37
+ export const getCustomerRoute = createRoute({
38
+ method: "get",
39
+ path: "/{id}",
40
+ tags: ["Customers"],
41
+ summary: "Get customer by ID",
42
+ request: {
43
+ params: z.object({
44
+ id: z.string().uuid().openapi({ example: "b482a588-..." }),
45
+ }),
46
+ },
47
+ responses: {
48
+ 200: {
49
+ content: {
50
+ "application/json": {
51
+ schema: z.object({ data: z.record(z.string(), z.unknown()) }),
52
+ },
53
+ },
54
+ description: "Customer detail",
55
+ },
56
+ ...errorResponses,
57
+ },
58
+ });
59
+
60
+ export const updateCustomerRoute = createRoute({
61
+ method: "patch",
62
+ path: "/{id}",
63
+ tags: ["Customers"],
64
+ summary: "Update a customer",
65
+ request: {
66
+ params: z.object({
67
+ id: z.string().uuid(),
68
+ }),
69
+ body: {
70
+ content: {
71
+ "application/json": {
72
+ schema: z.object({
73
+ firstName: z.string().optional(),
74
+ lastName: z.string().optional(),
75
+ email: z.string().email().optional(),
76
+ phone: z.string().optional(),
77
+ metadata: z.record(z.string(), z.unknown()).optional(),
78
+ }).openapi("UpdateCustomerRequest"),
79
+ },
80
+ },
81
+ },
82
+ },
83
+ responses: {
84
+ 200: {
85
+ content: {
86
+ "application/json": {
87
+ schema: z.object({ data: z.record(z.string(), z.unknown()) }),
88
+ },
89
+ },
90
+ description: "Customer updated",
91
+ },
92
+ ...errorResponses,
93
+ },
94
+ });
95
+
96
+ export const getCustomerOrdersRoute = createRoute({
97
+ method: "get",
98
+ path: "/{id}/orders",
99
+ tags: ["Customers"],
100
+ summary: "List orders for a customer",
101
+ request: {
102
+ params: z.object({
103
+ id: z.string().uuid(),
104
+ }),
105
+ query: z.object({
106
+ status: z.string().optional(),
107
+ page: z.string().optional().openapi({ example: "1" }),
108
+ limit: z.string().optional().openapi({ example: "20" }),
109
+ }),
110
+ },
111
+ responses: {
112
+ 200: {
113
+ content: {
114
+ "application/json": {
115
+ schema: z.object({
116
+ data: z.array(z.record(z.string(), z.unknown())),
117
+ meta: z.object({
118
+ pagination: z.object({
119
+ page: z.number(),
120
+ limit: z.number(),
121
+ total: z.number(),
122
+ totalPages: z.number(),
123
+ }),
124
+ }),
125
+ }),
126
+ },
127
+ },
128
+ description: "Customer orders",
129
+ },
130
+ ...errorResponses,
131
+ },
132
+ });
133
+
134
+ export const getCustomerAddressesRoute = createRoute({
135
+ method: "get",
136
+ path: "/{id}/addresses",
137
+ tags: ["Customers"],
138
+ summary: "List addresses for a customer",
139
+ request: {
140
+ params: z.object({
141
+ id: z.string().uuid(),
142
+ }),
143
+ },
144
+ responses: {
145
+ 200: {
146
+ content: {
147
+ "application/json": {
148
+ schema: z.object({ data: z.array(z.record(z.string(), z.unknown())) }),
149
+ },
150
+ },
151
+ description: "Customer addresses",
152
+ },
153
+ ...errorResponses,
154
+ },
155
+ });
@@ -14,6 +14,27 @@ export const CreateWarehouseBodySchema = z.object({
14
14
 
15
15
  // ─── Route Definitions ──────────────────────────────────────────────────────
16
16
 
17
+ export const listInventoryLevelsRoute = createRoute({
18
+ method: "get",
19
+ path: "/levels",
20
+ tags: ["Inventory"],
21
+ summary: "List inventory levels",
22
+ description: "Lists all inventory levels, optionally filtered by warehouse or entity.",
23
+ request: {
24
+ query: z.object({
25
+ warehouseId: z.string().uuid().optional().openapi({ example: "uuid" }),
26
+ entityId: z.string().uuid().optional().openapi({ example: "uuid" }),
27
+ }),
28
+ },
29
+ responses: {
30
+ 200: {
31
+ content: { "application/json": { schema: z.object({ data: z.array(z.record(z.string(), z.unknown())) }) } },
32
+ description: "Inventory levels",
33
+ },
34
+ ...errorResponses,
35
+ },
36
+ });
37
+
17
38
  export const inventoryCheckRoute = createRoute({
18
39
  method: "get",
19
40
  path: "/check",
@@ -43,6 +43,15 @@ export class CustomerService {
43
43
  return customer;
44
44
  }
45
45
 
46
+ async list(
47
+ actor?: Actor | null,
48
+ ctx?: TxContext,
49
+ ): Promise<Result<Customer[]>> {
50
+ const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
51
+ const customers = await this.repo.findAll(orgId, ctx);
52
+ return Ok(customers);
53
+ }
54
+
46
55
  async getById(
47
56
  id: string,
48
57
  actor?: Actor | null,
@@ -64,6 +73,22 @@ export class CustomerService {
64
73
  return Ok(customer);
65
74
  }
66
75
 
76
+ async update(
77
+ id: string,
78
+ updates: Partial<
79
+ Omit<Customer, "id" | "userId" | "createdAt" | "updatedAt">
80
+ >,
81
+ actor?: Actor | null,
82
+ ctx?: TxContext,
83
+ ): Promise<Result<Customer>> {
84
+ const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
85
+ const existing = await this.repo.findById(orgId, id, ctx);
86
+ if (!existing) return Err(new CommerceNotFoundError("Customer not found."));
87
+ const updated = await this.repo.update(id, updates, ctx);
88
+ if (!updated) return Err(new CommerceNotFoundError("Customer not found."));
89
+ return Ok(updated);
90
+ }
91
+
67
92
  async updateByUserId(
68
93
  userId: string,
69
94
  updates: Partial<
@@ -110,6 +110,23 @@ export class InventoryService {
110
110
  return Ok(available);
111
111
  }
112
112
 
113
+ async listLevels(
114
+ params?: { warehouseId?: string; entityId?: string },
115
+ actor?: Actor | null,
116
+ ctx?: TxContext,
117
+ ): Promise<Result<InventoryLevel[]>> {
118
+ if (params?.entityId) {
119
+ const levels = await this.repo.findLevelsByEntityId(params.entityId, ctx);
120
+ return Ok(levels);
121
+ }
122
+ if (params?.warehouseId) {
123
+ const levels = await this.repo.findLevelsByWarehouseId(params.warehouseId, ctx);
124
+ return Ok(levels);
125
+ }
126
+ const levels = await this.repo.findAllLevels(ctx);
127
+ return Ok(levels);
128
+ }
129
+
113
130
  async checkMultiple(
114
131
  entityIds: string[],
115
132
  ctx?: TxContext,
@@ -267,6 +267,47 @@ export async function createServer(config: CommerceConfig) {
267
267
  version: config.version ?? "0.0.1",
268
268
  description: "Headless commerce engine REST API. Includes core and plugin endpoints.",
269
269
  },
270
+ tags: [
271
+ // ── Entity aliases (generated from config.entities[type].alias) ──
272
+ ...Object.entries(config.entities ?? {})
273
+ .filter(([, cfg]) => cfg.alias)
274
+ .map(([type, cfg]) => {
275
+ const name = cfg.alias!.charAt(0).toUpperCase() + cfg.alias!.slice(1);
276
+ return { name, description: `Ergonomic alias for ${type} entities — CRUD, variants, attributes` };
277
+ }),
278
+ // ── Storefront ──
279
+ { name: "Catalog", description: "Products, categories, brands, variants, and option types (unified entity API)" },
280
+ { name: "Search", description: "Full-text search and typeahead suggestions" },
281
+ { name: "Pricing", description: "Base prices, price modifiers, and customer group pricing" },
282
+ { name: "Carts", description: "Shopping cart lifecycle — create, add items, update quantities" },
283
+ { name: "Checkout", description: "Convert a cart into a paid order" },
284
+ { name: "Promotions", description: "Discount codes, validation, and usage tracking" },
285
+ // ── Admin ──
286
+ { name: "Orders", description: "Order management — list, detail, status transitions, fulfillments" },
287
+ { name: "Customers", description: "Customer profiles, addresses, groups, and order history" },
288
+ { name: "Inventory", description: "Stock levels, warehouse management, adjustments, and reservations" },
289
+ { name: "Media", description: "File uploads, entity attachments, and signed URLs" },
290
+ { name: "Payments", description: "Payment provider webhooks and event processing" },
291
+ // ── Operations ──
292
+ { name: "Webhooks", description: "Outbound webhook endpoint registration and management" },
293
+ { name: "Audit", description: "Immutable audit log — who changed what and when" },
294
+ { name: "Admin Jobs", description: "Background job queue — view failed jobs and retry" },
295
+ ],
296
+ });
297
+
298
+ // Serve enriched spec with x-tagGroups vendor extension (Scalar/Redocly sidebar grouping)
299
+ const aliasTags = Object.values(config.entities ?? {})
300
+ .filter((cfg) => cfg.alias)
301
+ .map((cfg) => cfg.alias!.charAt(0).toUpperCase() + cfg.alias!.slice(1));
302
+ const tagGroups = [
303
+ ...(aliasTags.length > 0 ? [{ name: "Quick Access", tags: aliasTags }] : []),
304
+ { name: "Storefront", tags: ["Catalog", "Search", "Pricing", "Carts", "Checkout", "Promotions"] },
305
+ { name: "Admin", tags: ["Orders", "Customers", "Inventory", "Media", "Payments"] },
306
+ { name: "Operations", tags: ["Webhooks", "Audit", "Admin Jobs"] },
307
+ ];
308
+ app.get("/api/doc-ext", (c) => {
309
+ const spec = app.getOpenAPIDocument({ openapi: "3.0.0", info: { title: "UnifiedCommerce API", version: config.version ?? "0.0.1" } });
310
+ return c.json({ ...spec, "x-tagGroups": tagGroups });
270
311
  });
271
312
  } else {
272
313
  app.get("/api/doc", (c) => c.json({ error: { code: "NOT_FOUND", message: "Not found." } }, 404));