@voyantjs/products 0.52.1 → 0.52.3

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 (73) hide show
  1. package/dist/action-ledger-drift.d.ts +29 -0
  2. package/dist/action-ledger-drift.d.ts.map +1 -0
  3. package/dist/action-ledger-drift.js +335 -0
  4. package/dist/action-ledger.d.ts +104 -0
  5. package/dist/action-ledger.d.ts.map +1 -0
  6. package/dist/action-ledger.js +100 -0
  7. package/dist/booking-extension.d.ts +3 -3
  8. package/dist/catalog-policy.d.ts.map +1 -1
  9. package/dist/catalog-policy.js +18 -0
  10. package/dist/content-shape.d.ts +2 -0
  11. package/dist/content-shape.d.ts.map +1 -1
  12. package/dist/content-shape.js +2 -0
  13. package/dist/events.d.ts +1 -1
  14. package/dist/events.d.ts.map +1 -1
  15. package/dist/route-env.d.ts +22 -0
  16. package/dist/route-env.d.ts.map +1 -0
  17. package/dist/route-env.js +1 -0
  18. package/dist/routes-associations.d.ts +164 -0
  19. package/dist/routes-associations.d.ts.map +1 -0
  20. package/dist/routes-associations.js +100 -0
  21. package/dist/routes-catalog.d.ts +436 -0
  22. package/dist/routes-catalog.d.ts.map +1 -0
  23. package/dist/routes-catalog.js +104 -0
  24. package/dist/routes-configuration.d.ts +773 -0
  25. package/dist/routes-configuration.d.ts.map +1 -0
  26. package/dist/routes-configuration.js +364 -0
  27. package/dist/routes-core.d.ts +302 -0
  28. package/dist/routes-core.d.ts.map +1 -0
  29. package/dist/routes-core.js +79 -0
  30. package/dist/routes-itinerary.d.ts +614 -0
  31. package/dist/routes-itinerary.d.ts.map +1 -0
  32. package/dist/routes-itinerary.js +309 -0
  33. package/dist/routes-maintenance.d.ts +32 -0
  34. package/dist/routes-maintenance.d.ts.map +1 -0
  35. package/dist/routes-maintenance.js +14 -0
  36. package/dist/routes-media.d.ts +634 -0
  37. package/dist/routes-media.d.ts.map +1 -0
  38. package/dist/routes-media.js +245 -0
  39. package/dist/routes-merchandising.d.ts +1108 -0
  40. package/dist/routes-merchandising.d.ts.map +1 -0
  41. package/dist/routes-merchandising.js +376 -0
  42. package/dist/routes-options.d.ts +363 -0
  43. package/dist/routes-options.d.ts.map +1 -0
  44. package/dist/routes-options.js +173 -0
  45. package/dist/routes-public.d.ts +4 -4
  46. package/dist/routes-translations.d.ts +477 -0
  47. package/dist/routes-translations.d.ts.map +1 -0
  48. package/dist/routes-translations.js +258 -0
  49. package/dist/routes.d.ts +417 -355
  50. package/dist/routes.d.ts.map +1 -1
  51. package/dist/routes.js +21 -1133
  52. package/dist/schema-core.d.ts +3 -3
  53. package/dist/schema-itinerary.d.ts +1 -1
  54. package/dist/schema-settings.d.ts +4 -4
  55. package/dist/service-catalog-plane.d.ts.map +1 -1
  56. package/dist/service-catalog-plane.js +48 -1
  57. package/dist/service-catalog.d.ts +2 -2
  58. package/dist/service-content-owned.d.ts.map +1 -1
  59. package/dist/service-content-owned.js +98 -4
  60. package/dist/service-public.d.ts +4 -4
  61. package/dist/service.d.ts +225 -97
  62. package/dist/service.d.ts.map +1 -1
  63. package/dist/service.js +91 -0
  64. package/dist/tasks/brochures.d.ts +1 -1
  65. package/dist/validation-catalog.d.ts +10 -10
  66. package/dist/validation-config.d.ts +17 -17
  67. package/dist/validation-content.d.ts +26 -26
  68. package/dist/validation-core.d.ts +46 -46
  69. package/dist/validation-core.d.ts.map +1 -1
  70. package/dist/validation-core.js +17 -1
  71. package/dist/validation-public.d.ts +25 -25
  72. package/dist/validation-shared.d.ts +11 -11
  73. package/package.json +13 -7
package/dist/routes.js CHANGED
@@ -1,1135 +1,23 @@
1
- import { parseJsonBody, parseQuery, RequestValidationError, requireUserId } from "@voyantjs/hono";
2
1
  import { Hono } from "hono";
3
- import { z } from "zod";
4
- import { emitProductContentChanged } from "./events.js";
5
- import { productsService } from "./service.js";
6
- import { destinationListQuerySchema, destinationTranslationListQuerySchema, duplicateItinerarySchema, insertDaySchema, insertDayServiceSchema, insertDestinationSchema, insertDestinationTranslationSchema, insertItinerarySchema, insertOptionUnitSchema, insertOptionUnitTranslationSchema, insertProductActivationSettingSchema, insertProductCapabilitySchema, insertProductCategorySchema, insertProductCategoryTranslationSchema, insertProductDeliveryFormatSchema, insertProductDestinationSchema, insertProductFaqSchema, insertProductFeatureSchema, insertProductLocationSchema, insertProductMediaSchema, insertProductNoteSchema, insertProductOptionSchema, insertProductOptionTranslationSchema, insertProductSchema, insertProductTagSchema, insertProductTagTranslationSchema, insertProductTicketSettingSchema, insertProductTranslationSchema, insertProductTypeSchema, insertProductVisibilitySettingSchema, insertVersionSchema, optionUnitListQuerySchema, optionUnitTranslationListQuerySchema, productActivationSettingListQuerySchema, productAggregatesQuerySchema, productCapabilityListQuerySchema, productCategoryListQuerySchema, productCategoryTranslationListQuerySchema, productDeliveryFormatListQuerySchema, productDestinationListQuerySchema, productFaqListQuerySchema, productFeatureListQuerySchema, productListQuerySchema, productLocationListQuerySchema, productMediaListQuerySchema, productOptionListQuerySchema, productOptionTranslationListQuerySchema, productTagListQuerySchema, productTagTranslationListQuerySchema, productTicketSettingListQuerySchema, productTranslationListQuerySchema, productTypeListQuerySchema, productVisibilitySettingListQuerySchema, reorderProductMediaSchema, updateDaySchema, updateDayServiceSchema, updateDestinationSchema, updateDestinationTranslationSchema, updateItinerarySchema, updateOptionUnitSchema, updateOptionUnitTranslationSchema, updateProductActivationSettingSchema, updateProductCapabilitySchema, updateProductCategorySchema, updateProductCategoryTranslationSchema, updateProductDeliveryFormatSchema, updateProductFaqSchema, updateProductFeatureSchema, updateProductLocationSchema, updateProductMediaSchema, updateProductOptionSchema, updateProductOptionTranslationSchema, updateProductSchema, updateProductTagSchema, updateProductTagTranslationSchema, updateProductTicketSettingSchema, updateProductTranslationSchema, updateProductTypeSchema, updateProductVisibilitySettingSchema, upsertProductBrochureSchema, } from "./validation.js";
7
- // ==========================================================================
8
- // Products method-chained routes for Hono RPC type inference
9
- // ==========================================================================
2
+ import { productAssociationRoutes } from "./routes-associations.js";
3
+ import { productCatalogRoutes } from "./routes-catalog.js";
4
+ import { productConfigurationRoutes } from "./routes-configuration.js";
5
+ import { productCoreRoutes } from "./routes-core.js";
6
+ import { productItineraryRoutes } from "./routes-itinerary.js";
7
+ import { productMaintenanceRoutes } from "./routes-maintenance.js";
8
+ import { productMediaRoutes } from "./routes-media.js";
9
+ import { productMerchandisingRoutes } from "./routes-merchandising.js";
10
+ import { productOptionRoutes } from "./routes-options.js";
11
+ import { productTranslationRoutes } from "./routes-translations.js";
12
+ // Product route groups stay split by domain area; mount at root to preserve public paths.
10
13
  export const productRoutes = new Hono()
11
- // GET /aggregates — dashboard KPIs (before /:id so the matcher doesn't swallow it)
12
- .get("/aggregates", async (c) => {
13
- const query = parseQuery(c, productAggregatesQuerySchema);
14
- return c.json({ data: await productsService.getProductAggregates(c.get("db"), query) });
15
- })
16
- // GET / — List products
17
- .get("/", async (c) => {
18
- const query = parseQuery(c, productListQuerySchema);
19
- return c.json(await productsService.listProducts(c.get("db"), query));
20
- })
21
- // POST / — Create product
22
- .post("/", async (c) => {
23
- const row = await productsService.createProduct(c.get("db"), await parseJsonBody(c, insertProductSchema));
24
- await c.get("eventBus")?.emit("product.created", { id: row.id });
25
- return c.json({ data: row }, 201);
26
- })
27
- // ==========================================================================
28
- // Product operating configuration
29
- // ==========================================================================
30
- .get("/activation-settings", async (c) => {
31
- const query = parseQuery(c, productActivationSettingListQuerySchema);
32
- return c.json(await productsService.listActivationSettings(c.get("db"), query));
33
- })
34
- .get("/activation-settings/:id", async (c) => {
35
- const row = await productsService.getActivationSettingById(c.get("db"), c.req.param("id"));
36
- if (!row) {
37
- return c.json({ error: "Product activation setting not found" }, 404);
38
- }
39
- return c.json({ data: row });
40
- })
41
- .post("/:id/activation-settings", async (c) => {
42
- const row = await productsService.upsertActivationSetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductActivationSettingSchema));
43
- if (!row) {
44
- return c.json({ error: "Product not found" }, 404);
45
- }
46
- return c.json({ data: row }, 201);
47
- })
48
- .patch("/activation-settings/:id", async (c) => {
49
- const row = await productsService.updateActivationSetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductActivationSettingSchema));
50
- if (!row) {
51
- return c.json({ error: "Product activation setting not found" }, 404);
52
- }
53
- return c.json({ data: row });
54
- })
55
- .delete("/activation-settings/:id", async (c) => {
56
- const row = await productsService.deleteActivationSetting(c.get("db"), c.req.param("id"));
57
- if (!row) {
58
- return c.json({ error: "Product activation setting not found" }, 404);
59
- }
60
- return c.json({ success: true }, 200);
61
- })
62
- .get("/ticket-settings", async (c) => {
63
- const query = parseQuery(c, productTicketSettingListQuerySchema);
64
- return c.json(await productsService.listTicketSettings(c.get("db"), query));
65
- })
66
- .get("/ticket-settings/:id", async (c) => {
67
- const row = await productsService.getTicketSettingById(c.get("db"), c.req.param("id"));
68
- if (!row) {
69
- return c.json({ error: "Product ticket setting not found" }, 404);
70
- }
71
- return c.json({ data: row });
72
- })
73
- .post("/:id/ticket-settings", async (c) => {
74
- const row = await productsService.upsertTicketSetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductTicketSettingSchema));
75
- if (!row) {
76
- return c.json({ error: "Product not found" }, 404);
77
- }
78
- return c.json({ data: row }, 201);
79
- })
80
- .patch("/ticket-settings/:id", async (c) => {
81
- const row = await productsService.updateTicketSetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductTicketSettingSchema));
82
- if (!row) {
83
- return c.json({ error: "Product ticket setting not found" }, 404);
84
- }
85
- return c.json({ data: row });
86
- })
87
- .delete("/ticket-settings/:id", async (c) => {
88
- const row = await productsService.deleteTicketSetting(c.get("db"), c.req.param("id"));
89
- if (!row) {
90
- return c.json({ error: "Product ticket setting not found" }, 404);
91
- }
92
- return c.json({ success: true }, 200);
93
- })
94
- .get("/visibility-settings", async (c) => {
95
- const query = parseQuery(c, productVisibilitySettingListQuerySchema);
96
- return c.json(await productsService.listVisibilitySettings(c.get("db"), query));
97
- })
98
- .get("/visibility-settings/:id", async (c) => {
99
- const row = await productsService.getVisibilitySettingById(c.get("db"), c.req.param("id"));
100
- if (!row) {
101
- return c.json({ error: "Product visibility setting not found" }, 404);
102
- }
103
- return c.json({ data: row });
104
- })
105
- .post("/:id/visibility-settings", async (c) => {
106
- const row = await productsService.upsertVisibilitySetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductVisibilitySettingSchema));
107
- if (!row) {
108
- return c.json({ error: "Product not found" }, 404);
109
- }
110
- return c.json({ data: row }, 201);
111
- })
112
- .patch("/visibility-settings/:id", async (c) => {
113
- const row = await productsService.updateVisibilitySetting(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductVisibilitySettingSchema));
114
- if (!row) {
115
- return c.json({ error: "Product visibility setting not found" }, 404);
116
- }
117
- return c.json({ data: row });
118
- })
119
- .delete("/visibility-settings/:id", async (c) => {
120
- const row = await productsService.deleteVisibilitySetting(c.get("db"), c.req.param("id"));
121
- if (!row) {
122
- return c.json({ error: "Product visibility setting not found" }, 404);
123
- }
124
- return c.json({ success: true }, 200);
125
- })
126
- .get("/capabilities", async (c) => {
127
- const query = parseQuery(c, productCapabilityListQuerySchema);
128
- return c.json(await productsService.listCapabilities(c.get("db"), query));
129
- })
130
- .get("/capabilities/:id", async (c) => {
131
- const row = await productsService.getCapabilityById(c.get("db"), c.req.param("id"));
132
- if (!row) {
133
- return c.json({ error: "Product capability not found" }, 404);
134
- }
135
- return c.json({ data: row });
136
- })
137
- .post("/:id/capabilities", async (c) => {
138
- const row = await productsService.createCapability(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductCapabilitySchema));
139
- if (!row) {
140
- return c.json({ error: "Product not found" }, 404);
141
- }
142
- return c.json({ data: row }, 201);
143
- })
144
- .patch("/capabilities/:id", async (c) => {
145
- const row = await productsService.updateCapability(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductCapabilitySchema));
146
- if (!row) {
147
- return c.json({ error: "Product capability not found" }, 404);
148
- }
149
- return c.json({ data: row });
150
- })
151
- .delete("/capabilities/:id", async (c) => {
152
- const row = await productsService.deleteCapability(c.get("db"), c.req.param("id"));
153
- if (!row) {
154
- return c.json({ error: "Product capability not found" }, 404);
155
- }
156
- return c.json({ success: true }, 200);
157
- })
158
- .get("/delivery-formats", async (c) => {
159
- const query = parseQuery(c, productDeliveryFormatListQuerySchema);
160
- return c.json(await productsService.listDeliveryFormats(c.get("db"), query));
161
- })
162
- .get("/delivery-formats/:id", async (c) => {
163
- const row = await productsService.getDeliveryFormatById(c.get("db"), c.req.param("id"));
164
- if (!row) {
165
- return c.json({ error: "Product delivery format not found" }, 404);
166
- }
167
- return c.json({ data: row });
168
- })
169
- .post("/:id/delivery-formats", async (c) => {
170
- const row = await productsService.createDeliveryFormat(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductDeliveryFormatSchema));
171
- if (!row) {
172
- return c.json({ error: "Product not found" }, 404);
173
- }
174
- return c.json({ data: row }, 201);
175
- })
176
- .patch("/delivery-formats/:id", async (c) => {
177
- const row = await productsService.updateDeliveryFormat(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductDeliveryFormatSchema));
178
- if (!row) {
179
- return c.json({ error: "Product delivery format not found" }, 404);
180
- }
181
- return c.json({ data: row });
182
- })
183
- .delete("/delivery-formats/:id", async (c) => {
184
- const row = await productsService.deleteDeliveryFormat(c.get("db"), c.req.param("id"));
185
- if (!row) {
186
- return c.json({ error: "Product delivery format not found" }, 404);
187
- }
188
- return c.json({ success: true }, 200);
189
- })
190
- .get("/features", async (c) => {
191
- const query = parseQuery(c, productFeatureListQuerySchema);
192
- return c.json(await productsService.listFeatures(c.get("db"), query));
193
- })
194
- .get("/features/:id", async (c) => {
195
- const row = await productsService.getFeatureById(c.get("db"), c.req.param("id"));
196
- if (!row) {
197
- return c.json({ error: "Product feature not found" }, 404);
198
- }
199
- return c.json({ data: row });
200
- })
201
- .post("/:id/features", async (c) => {
202
- const productId = c.req.param("id");
203
- const row = await productsService.createFeature(c.get("db"), productId, await parseJsonBody(c, insertProductFeatureSchema));
204
- if (!row) {
205
- return c.json({ error: "Product not found" }, 404);
206
- }
207
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "feature" });
208
- return c.json({ data: row }, 201);
209
- })
210
- .patch("/features/:id", async (c) => {
211
- const row = await productsService.updateFeature(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductFeatureSchema));
212
- if (!row) {
213
- return c.json({ error: "Product feature not found" }, 404);
214
- }
215
- if (row.productId) {
216
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "feature" });
217
- }
218
- return c.json({ data: row });
219
- })
220
- .delete("/features/:id", async (c) => {
221
- const row = await productsService.deleteFeature(c.get("db"), c.req.param("id"));
222
- if (!row) {
223
- return c.json({ error: "Product feature not found" }, 404);
224
- }
225
- if ("productId" in row && typeof row.productId === "string") {
226
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "feature" });
227
- }
228
- return c.json({ success: true }, 200);
229
- })
230
- .get("/faqs", async (c) => {
231
- const query = parseQuery(c, productFaqListQuerySchema);
232
- return c.json(await productsService.listFaqs(c.get("db"), query));
233
- })
234
- .get("/faqs/:id", async (c) => {
235
- const row = await productsService.getFaqById(c.get("db"), c.req.param("id"));
236
- if (!row) {
237
- return c.json({ error: "Product FAQ not found" }, 404);
238
- }
239
- return c.json({ data: row });
240
- })
241
- .post("/:id/faqs", async (c) => {
242
- const productId = c.req.param("id");
243
- const row = await productsService.createFaq(c.get("db"), productId, await parseJsonBody(c, insertProductFaqSchema));
244
- if (!row) {
245
- return c.json({ error: "Product not found" }, 404);
246
- }
247
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "faq" });
248
- return c.json({ data: row }, 201);
249
- })
250
- .patch("/faqs/:id", async (c) => {
251
- const row = await productsService.updateFaq(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductFaqSchema));
252
- if (!row) {
253
- return c.json({ error: "Product FAQ not found" }, 404);
254
- }
255
- if (row.productId) {
256
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "faq" });
257
- }
258
- return c.json({ data: row });
259
- })
260
- .delete("/faqs/:id", async (c) => {
261
- const row = await productsService.deleteFaq(c.get("db"), c.req.param("id"));
262
- if (!row) {
263
- return c.json({ error: "Product FAQ not found" }, 404);
264
- }
265
- if ("productId" in row && typeof row.productId === "string") {
266
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "faq" });
267
- }
268
- return c.json({ success: true }, 200);
269
- })
270
- .get("/locations", async (c) => {
271
- const query = parseQuery(c, productLocationListQuerySchema);
272
- return c.json(await productsService.listLocations(c.get("db"), query));
273
- })
274
- .get("/locations/:id", async (c) => {
275
- const row = await productsService.getLocationById(c.get("db"), c.req.param("id"));
276
- if (!row) {
277
- return c.json({ error: "Product location not found" }, 404);
278
- }
279
- return c.json({ data: row });
280
- })
281
- .post("/:id/locations", async (c) => {
282
- const productId = c.req.param("id");
283
- const row = await productsService.createLocation(c.get("db"), productId, await parseJsonBody(c, insertProductLocationSchema));
284
- if (!row) {
285
- return c.json({ error: "Product not found" }, 404);
286
- }
287
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "location" });
288
- return c.json({ data: row }, 201);
289
- })
290
- .patch("/locations/:id", async (c) => {
291
- const row = await productsService.updateLocation(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductLocationSchema));
292
- if (!row) {
293
- return c.json({ error: "Product location not found" }, 404);
294
- }
295
- if (row.productId) {
296
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "location" });
297
- }
298
- return c.json({ data: row });
299
- })
300
- .delete("/locations/:id", async (c) => {
301
- const row = await productsService.deleteLocation(c.get("db"), c.req.param("id"));
302
- if (!row) {
303
- return c.json({ error: "Product location not found" }, 404);
304
- }
305
- if ("productId" in row && typeof row.productId === "string") {
306
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "location" });
307
- }
308
- return c.json({ success: true }, 200);
309
- })
310
- .get("/destinations", async (c) => {
311
- const query = parseQuery(c, destinationListQuerySchema);
312
- return c.json(await productsService.listDestinations(c.get("db"), query));
313
- })
314
- .get("/destinations/:id", async (c) => {
315
- const row = await productsService.getDestinationById(c.get("db"), c.req.param("id"));
316
- if (!row) {
317
- return c.json({ error: "Destination not found" }, 404);
318
- }
319
- return c.json({ data: row });
320
- })
321
- .post("/destinations", async (c) => {
322
- const row = await productsService.createDestination(c.get("db"), await parseJsonBody(c, insertDestinationSchema));
323
- return c.json({ data: row }, 201);
324
- })
325
- .patch("/destinations/:id", async (c) => {
326
- const row = await productsService.updateDestination(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateDestinationSchema));
327
- if (!row) {
328
- return c.json({ error: "Destination not found" }, 404);
329
- }
330
- return c.json({ data: row });
331
- })
332
- .delete("/destinations/:id", async (c) => {
333
- const row = await productsService.deleteDestination(c.get("db"), c.req.param("id"));
334
- if (!row) {
335
- return c.json({ error: "Destination not found" }, 404);
336
- }
337
- return c.json({ success: true }, 200);
338
- })
339
- .get("/destination-translations", async (c) => {
340
- const query = parseQuery(c, destinationTranslationListQuerySchema);
341
- return c.json(await productsService.listDestinationTranslations(c.get("db"), query));
342
- })
343
- .post("/destinations/:id/translations", async (c) => {
344
- const row = await productsService.upsertDestinationTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertDestinationTranslationSchema));
345
- if (!row) {
346
- return c.json({ error: "Destination not found" }, 404);
347
- }
348
- return c.json({ data: row }, 201);
349
- })
350
- .patch("/destination-translations/:id", async (c) => {
351
- const row = await productsService.updateDestinationTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateDestinationTranslationSchema));
352
- if (!row) {
353
- return c.json({ error: "Destination translation not found" }, 404);
354
- }
355
- return c.json({ data: row });
356
- })
357
- .delete("/destination-translations/:id", async (c) => {
358
- const row = await productsService.deleteDestinationTranslation(c.get("db"), c.req.param("id"));
359
- if (!row) {
360
- return c.json({ error: "Destination translation not found" }, 404);
361
- }
362
- return c.json({ success: true }, 200);
363
- })
364
- // ──────────────────────────────────────────────────────────────────────────
365
- // Product category translations (locale-aware names + descriptions).
366
- // Mirrors the destinations translation surface above. The catalog plane's
367
- // taxonomy projection reads these per-slice locale and falls back to
368
- // `productCategories.name` when no row exists for a given locale.
369
- // ──────────────────────────────────────────────────────────────────────────
370
- .get("/product-category-translations", async (c) => {
371
- const query = parseQuery(c, productCategoryTranslationListQuerySchema);
372
- return c.json(await productsService.listProductCategoryTranslations(c.get("db"), query));
373
- })
374
- .post("/product-categories/:id/translations", async (c) => {
375
- const row = await productsService.upsertProductCategoryTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductCategoryTranslationSchema));
376
- if (!row) {
377
- return c.json({ error: "Product category not found" }, 404);
378
- }
379
- return c.json({ data: row }, 201);
380
- })
381
- .patch("/product-category-translations/:id", async (c) => {
382
- const row = await productsService.updateProductCategoryTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductCategoryTranslationSchema));
383
- if (!row) {
384
- return c.json({ error: "Product category translation not found" }, 404);
385
- }
386
- return c.json({ data: row });
387
- })
388
- .delete("/product-category-translations/:id", async (c) => {
389
- const row = await productsService.deleteProductCategoryTranslation(c.get("db"), c.req.param("id"));
390
- if (!row) {
391
- return c.json({ error: "Product category translation not found" }, 404);
392
- }
393
- return c.json({ success: true }, 200);
394
- })
395
- // ──────────────────────────────────────────────────────────────────────────
396
- // Product tag translations. Slimmer shape — tags are short labels with no
397
- // description / SEO blurbs (per #502 non-goals).
398
- // ──────────────────────────────────────────────────────────────────────────
399
- .get("/product-tag-translations", async (c) => {
400
- const query = parseQuery(c, productTagTranslationListQuerySchema);
401
- return c.json(await productsService.listProductTagTranslations(c.get("db"), query));
402
- })
403
- .post("/product-tags/:id/translations", async (c) => {
404
- const row = await productsService.upsertProductTagTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertProductTagTranslationSchema));
405
- if (!row) {
406
- return c.json({ error: "Product tag not found" }, 404);
407
- }
408
- return c.json({ data: row }, 201);
409
- })
410
- .patch("/product-tag-translations/:id", async (c) => {
411
- const row = await productsService.updateProductTagTranslation(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductTagTranslationSchema));
412
- if (!row) {
413
- return c.json({ error: "Product tag translation not found" }, 404);
414
- }
415
- return c.json({ data: row });
416
- })
417
- .delete("/product-tag-translations/:id", async (c) => {
418
- const row = await productsService.deleteProductTagTranslation(c.get("db"), c.req.param("id"));
419
- if (!row) {
420
- return c.json({ error: "Product tag translation not found" }, 404);
421
- }
422
- return c.json({ success: true }, 200);
423
- })
424
- .get("/destination-links", async (c) => {
425
- const query = parseQuery(c, productDestinationListQuerySchema);
426
- return c.json(await productsService.listProductDestinations(c.get("db"), query));
427
- })
428
- .post("/:id/destinations", async (c) => {
429
- const productId = c.req.param("id");
430
- const row = await productsService.assignProductDestination(c.get("db"), productId, await parseJsonBody(c, insertProductDestinationSchema));
431
- if (!row) {
432
- return c.json({ error: "Product or destination not found" }, 404);
433
- }
434
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "destination" });
435
- return c.json({ data: row }, 201);
436
- })
437
- .delete("/:id/destinations/:destinationId", async (c) => {
438
- const productId = c.req.param("id");
439
- const row = await productsService.removeProductDestination(c.get("db"), productId, c.req.param("destinationId"));
440
- if (!row) {
441
- return c.json({ error: "Product destination link not found" }, 404);
442
- }
443
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "destination" });
444
- return c.json({ success: true }, 200);
445
- })
446
- // ==========================================================================
447
- // Options
448
- // ==========================================================================
449
- // GET /options — List options
450
- .get("/options", async (c) => {
451
- const query = parseQuery(c, productOptionListQuerySchema);
452
- return c.json(await productsService.listOptions(c.get("db"), query));
453
- })
454
- // GET /options/:optionId — Get single option
455
- .get("/options/:optionId", async (c) => {
456
- const row = await productsService.getOptionById(c.get("db"), c.req.param("optionId"));
457
- if (!row) {
458
- return c.json({ error: "Product option not found" }, 404);
459
- }
460
- return c.json({ data: row });
461
- })
462
- // POST /:id/options — Create option for product
463
- .post("/:id/options", async (c) => {
464
- const productId = c.req.param("id");
465
- const row = await productsService.createOption(c.get("db"), productId, await parseJsonBody(c, insertProductOptionSchema));
466
- if (!row) {
467
- return c.json({ error: "Product not found" }, 404);
468
- }
469
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "option" });
470
- return c.json({ data: row }, 201);
471
- })
472
- // PATCH /options/:optionId — Update option
473
- .patch("/options/:optionId", async (c) => {
474
- const row = await productsService.updateOption(c.get("db"), c.req.param("optionId"), await parseJsonBody(c, updateProductOptionSchema));
475
- if (!row) {
476
- return c.json({ error: "Product option not found" }, 404);
477
- }
478
- if (row.productId) {
479
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "option" });
480
- }
481
- return c.json({ data: row });
482
- })
483
- // DELETE /options/:optionId — Delete option
484
- .delete("/options/:optionId", async (c) => {
485
- const row = await productsService.deleteOption(c.get("db"), c.req.param("optionId"));
486
- if (!row) {
487
- return c.json({ error: "Product option not found" }, 404);
488
- }
489
- if ("productId" in row && typeof row.productId === "string") {
490
- await emitProductContentChanged(c.get("eventBus"), { id: row.productId, axis: "option" });
491
- }
492
- return c.json({ success: true }, 200);
493
- })
494
- // ==========================================================================
495
- // Option Units
496
- // ==========================================================================
497
- // GET /units — List units
498
- .get("/units", async (c) => {
499
- const query = parseQuery(c, optionUnitListQuerySchema);
500
- return c.json(await productsService.listUnits(c.get("db"), query));
501
- })
502
- // GET /units/:unitId — Get single unit
503
- .get("/units/:unitId", async (c) => {
504
- const row = await productsService.getUnitById(c.get("db"), c.req.param("unitId"));
505
- if (!row) {
506
- return c.json({ error: "Option unit not found" }, 404);
507
- }
508
- return c.json({ data: row });
509
- })
510
- // POST /options/:optionId/units — Create unit for option
511
- .post("/options/:optionId/units", async (c) => {
512
- const row = await productsService.createUnit(c.get("db"), c.req.param("optionId"), await parseJsonBody(c, insertOptionUnitSchema));
513
- if (!row) {
514
- return c.json({ error: "Product option not found" }, 404);
515
- }
516
- return c.json({ data: row }, 201);
517
- })
518
- // PATCH /units/:unitId — Update unit
519
- .patch("/units/:unitId", async (c) => {
520
- const row = await productsService.updateUnit(c.get("db"), c.req.param("unitId"), await parseJsonBody(c, updateOptionUnitSchema));
521
- if (!row) {
522
- return c.json({ error: "Option unit not found" }, 404);
523
- }
524
- return c.json({ data: row });
525
- })
526
- // DELETE /units/:unitId — Delete unit
527
- .delete("/units/:unitId", async (c) => {
528
- const row = await productsService.deleteUnit(c.get("db"), c.req.param("unitId"));
529
- if (!row) {
530
- return c.json({ error: "Option unit not found" }, 404);
531
- }
532
- return c.json({ success: true }, 200);
533
- })
534
- // ==========================================================================
535
- // Translations
536
- // ==========================================================================
537
- .get("/translations", async (c) => {
538
- const query = parseQuery(c, productTranslationListQuerySchema);
539
- return c.json(await productsService.listProductTranslations(c.get("db"), query));
540
- })
541
- .get("/translations/:translationId", async (c) => {
542
- const row = await productsService.getProductTranslationById(c.get("db"), c.req.param("translationId"));
543
- if (!row) {
544
- return c.json({ error: "Product translation not found" }, 404);
545
- }
546
- return c.json({ data: row });
547
- })
548
- .post("/:id/translations", async (c) => {
549
- const productId = c.req.param("id");
550
- const row = await productsService.createProductTranslation(c.get("db"), productId, await parseJsonBody(c, insertProductTranslationSchema));
551
- if (!row) {
552
- return c.json({ error: "Product not found" }, 404);
553
- }
554
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "translation" });
555
- return c.json({ data: row }, 201);
556
- })
557
- .patch("/translations/:translationId", async (c) => {
558
- const row = await productsService.updateProductTranslation(c.get("db"), c.req.param("translationId"), await parseJsonBody(c, updateProductTranslationSchema));
559
- if (!row) {
560
- return c.json({ error: "Product translation not found" }, 404);
561
- }
562
- if (row.productId) {
563
- await emitProductContentChanged(c.get("eventBus"), {
564
- id: row.productId,
565
- axis: "translation",
566
- });
567
- }
568
- return c.json({ data: row });
569
- })
570
- .delete("/translations/:translationId", async (c) => {
571
- const row = await productsService.deleteProductTranslation(c.get("db"), c.req.param("translationId"));
572
- if (!row) {
573
- return c.json({ error: "Product translation not found" }, 404);
574
- }
575
- if ("productId" in row && typeof row.productId === "string") {
576
- await emitProductContentChanged(c.get("eventBus"), {
577
- id: row.productId,
578
- axis: "translation",
579
- });
580
- }
581
- return c.json({ success: true }, 200);
582
- })
583
- .get("/option-translations", async (c) => {
584
- const query = parseQuery(c, productOptionTranslationListQuerySchema);
585
- return c.json(await productsService.listOptionTranslations(c.get("db"), query));
586
- })
587
- .get("/option-translations/:translationId", async (c) => {
588
- const row = await productsService.getOptionTranslationById(c.get("db"), c.req.param("translationId"));
589
- if (!row) {
590
- return c.json({ error: "Option translation not found" }, 404);
591
- }
592
- return c.json({ data: row });
593
- })
594
- .post("/options/:optionId/translations", async (c) => {
595
- const row = await productsService.createOptionTranslation(c.get("db"), c.req.param("optionId"), await parseJsonBody(c, insertProductOptionTranslationSchema));
596
- if (!row) {
597
- return c.json({ error: "Product option not found" }, 404);
598
- }
599
- return c.json({ data: row }, 201);
600
- })
601
- .patch("/option-translations/:translationId", async (c) => {
602
- const row = await productsService.updateOptionTranslation(c.get("db"), c.req.param("translationId"), await parseJsonBody(c, updateProductOptionTranslationSchema));
603
- if (!row) {
604
- return c.json({ error: "Option translation not found" }, 404);
605
- }
606
- return c.json({ data: row });
607
- })
608
- .delete("/option-translations/:translationId", async (c) => {
609
- const row = await productsService.deleteOptionTranslation(c.get("db"), c.req.param("translationId"));
610
- if (!row) {
611
- return c.json({ error: "Option translation not found" }, 404);
612
- }
613
- return c.json({ success: true }, 200);
614
- })
615
- .get("/unit-translations", async (c) => {
616
- const query = parseQuery(c, optionUnitTranslationListQuerySchema);
617
- return c.json(await productsService.listUnitTranslations(c.get("db"), query));
618
- })
619
- .get("/unit-translations/:translationId", async (c) => {
620
- const row = await productsService.getUnitTranslationById(c.get("db"), c.req.param("translationId"));
621
- if (!row) {
622
- return c.json({ error: "Unit translation not found" }, 404);
623
- }
624
- return c.json({ data: row });
625
- })
626
- .post("/units/:unitId/translations", async (c) => {
627
- const row = await productsService.createUnitTranslation(c.get("db"), c.req.param("unitId"), await parseJsonBody(c, insertOptionUnitTranslationSchema));
628
- if (!row) {
629
- return c.json({ error: "Option unit not found" }, 404);
630
- }
631
- return c.json({ data: row }, 201);
632
- })
633
- .patch("/unit-translations/:translationId", async (c) => {
634
- const row = await productsService.updateUnitTranslation(c.get("db"), c.req.param("translationId"), await parseJsonBody(c, updateOptionUnitTranslationSchema));
635
- if (!row) {
636
- return c.json({ error: "Unit translation not found" }, 404);
637
- }
638
- return c.json({ data: row });
639
- })
640
- .delete("/unit-translations/:translationId", async (c) => {
641
- const row = await productsService.deleteUnitTranslation(c.get("db"), c.req.param("translationId"));
642
- if (!row) {
643
- return c.json({ error: "Unit translation not found" }, 404);
644
- }
645
- return c.json({ success: true }, 200);
646
- })
647
- // ==========================================================================
648
- // Product Types
649
- // ==========================================================================
650
- .get("/product-types", async (c) => {
651
- const query = parseQuery(c, productTypeListQuerySchema);
652
- return c.json(await productsService.listProductTypes(c.get("db"), query));
653
- })
654
- .get("/product-types/:typeId", async (c) => {
655
- const row = await productsService.getProductTypeById(c.get("db"), c.req.param("typeId"));
656
- if (!row) {
657
- return c.json({ error: "Product type not found" }, 404);
658
- }
659
- return c.json({ data: row });
660
- })
661
- .post("/product-types", async (c) => {
662
- return c.json({
663
- data: await productsService.createProductType(c.get("db"), await parseJsonBody(c, insertProductTypeSchema)),
664
- }, 201);
665
- })
666
- .patch("/product-types/:typeId", async (c) => {
667
- const row = await productsService.updateProductType(c.get("db"), c.req.param("typeId"), await parseJsonBody(c, updateProductTypeSchema));
668
- if (!row) {
669
- return c.json({ error: "Product type not found" }, 404);
670
- }
671
- return c.json({ data: row });
672
- })
673
- .delete("/product-types/:typeId", async (c) => {
674
- const row = await productsService.deleteProductType(c.get("db"), c.req.param("typeId"));
675
- if (!row) {
676
- return c.json({ error: "Product type not found" }, 404);
677
- }
678
- return c.json({ success: true }, 200);
679
- })
680
- // ==========================================================================
681
- // Product Categories
682
- // ==========================================================================
683
- .get("/product-categories", async (c) => {
684
- const query = parseQuery(c, productCategoryListQuerySchema);
685
- return c.json(await productsService.listProductCategories(c.get("db"), query));
686
- })
687
- .get("/product-categories/:categoryId", async (c) => {
688
- const row = await productsService.getProductCategoryById(c.get("db"), c.req.param("categoryId"));
689
- if (!row) {
690
- return c.json({ error: "Product category not found" }, 404);
691
- }
692
- return c.json({ data: row });
693
- })
694
- .post("/product-categories", async (c) => {
695
- return c.json({
696
- data: await productsService.createProductCategory(c.get("db"), await parseJsonBody(c, insertProductCategorySchema)),
697
- }, 201);
698
- })
699
- .patch("/product-categories/:categoryId", async (c) => {
700
- const row = await productsService.updateProductCategory(c.get("db"), c.req.param("categoryId"), await parseJsonBody(c, updateProductCategorySchema));
701
- if (!row) {
702
- return c.json({ error: "Product category not found" }, 404);
703
- }
704
- return c.json({ data: row });
705
- })
706
- .delete("/product-categories/:categoryId", async (c) => {
707
- const row = await productsService.deleteProductCategory(c.get("db"), c.req.param("categoryId"));
708
- if (!row) {
709
- return c.json({ error: "Product category not found" }, 404);
710
- }
711
- return c.json({ success: true }, 200);
712
- })
713
- // ==========================================================================
714
- // Product Tags
715
- // ==========================================================================
716
- .get("/product-tags", async (c) => {
717
- const query = parseQuery(c, productTagListQuerySchema);
718
- return c.json(await productsService.listProductTags(c.get("db"), query));
719
- })
720
- .get("/product-tags/:tagId", async (c) => {
721
- const row = await productsService.getProductTagById(c.get("db"), c.req.param("tagId"));
722
- if (!row) {
723
- return c.json({ error: "Product tag not found" }, 404);
724
- }
725
- return c.json({ data: row });
726
- })
727
- .post("/product-tags", async (c) => {
728
- return c.json({
729
- data: await productsService.createProductTag(c.get("db"), await parseJsonBody(c, insertProductTagSchema)),
730
- }, 201);
731
- })
732
- .patch("/product-tags/:tagId", async (c) => {
733
- const row = await productsService.updateProductTag(c.get("db"), c.req.param("tagId"), await parseJsonBody(c, updateProductTagSchema));
734
- if (!row) {
735
- return c.json({ error: "Product tag not found" }, 404);
736
- }
737
- return c.json({ data: row });
738
- })
739
- .delete("/product-tags/:tagId", async (c) => {
740
- const row = await productsService.deleteProductTag(c.get("db"), c.req.param("tagId"));
741
- if (!row) {
742
- return c.json({ error: "Product tag not found" }, 404);
743
- }
744
- return c.json({ success: true }, 200);
745
- })
746
- // ==========================================================================
747
- // Media
748
- // ==========================================================================
749
- // GET /media/:mediaId — Get single media item
750
- .get("/media/:mediaId", async (c) => {
751
- const row = await productsService.getMediaById(c.get("db"), c.req.param("mediaId"));
752
- if (!row) {
753
- return c.json({ error: "Media not found" }, 404);
754
- }
755
- return c.json({ data: row });
756
- })
757
- // PATCH /media/:mediaId — Update media metadata
758
- .patch("/media/:mediaId", async (c) => {
759
- const row = await productsService.updateMedia(c.get("db"), c.req.param("mediaId"), await parseJsonBody(c, updateProductMediaSchema));
760
- if (!row) {
761
- return c.json({ error: "Media not found" }, 404);
762
- }
763
- return c.json({ data: row });
764
- })
765
- // PATCH /media/:mediaId/set-cover — Set as cover image
766
- .patch("/media/:mediaId/set-cover", async (c) => {
767
- const media = await productsService.getMediaById(c.get("db"), c.req.param("mediaId"));
768
- if (!media) {
769
- return c.json({ error: "Media not found" }, 404);
770
- }
771
- const row = await productsService.setCoverMedia(c.get("db"), media.productId, media.id, media.dayId);
772
- if (!row) {
773
- return c.json({ error: "Failed to set cover" }, 500);
774
- }
775
- return c.json({ data: row });
776
- })
777
- // DELETE /media/:mediaId — Delete media
778
- .delete("/media/:mediaId", async (c) => {
779
- const row = await productsService.deleteMedia(c.get("db"), c.req.param("mediaId"));
780
- if (!row) {
781
- return c.json({ error: "Media not found" }, 404);
782
- }
783
- return c.json({ data: row });
784
- })
785
- // GET /:id/brochure — Get canonical brochure for product
786
- .get("/:id/brochure", async (c) => {
787
- const row = await productsService.getBrochure(c.get("db"), c.req.param("id"));
788
- if (!row) {
789
- return c.json({ error: "Product brochure not found" }, 404);
790
- }
791
- return c.json({ data: row });
792
- })
793
- // GET /:id/brochure/versions — List brochure history for product
794
- .get("/:id/brochure/versions", async (c) => {
795
- return c.json({ data: await productsService.listBrochures(c.get("db"), c.req.param("id")) });
796
- })
797
- // PUT /:id/brochure — Upsert canonical brochure for product
798
- .put("/:id/brochure", async (c) => {
799
- const row = await productsService.upsertBrochure(c.get("db"), c.req.param("id"), await parseJsonBody(c, upsertProductBrochureSchema));
800
- if (!row) {
801
- return c.json({ error: "Product not found" }, 404);
802
- }
803
- return c.json({ data: row }, 201);
804
- })
805
- // DELETE /:id/brochure — Delete canonical brochure for product
806
- .delete("/:id/brochure", async (c) => {
807
- const row = await productsService.deleteBrochure(c.get("db"), c.req.param("id"));
808
- if (!row) {
809
- return c.json({ error: "Product brochure not found" }, 404);
810
- }
811
- return c.json({ data: row });
812
- })
813
- // POST /:id/brochure/versions/:brochureId/set-current — Promote brochure version
814
- .post("/:id/brochure/versions/:brochureId/set-current", async (c) => {
815
- const row = await productsService.setCurrentBrochure(c.get("db"), c.req.param("id"), c.req.param("brochureId"));
816
- if (!row) {
817
- return c.json({ error: "Product brochure version not found" }, 404);
818
- }
819
- return c.json({ data: row });
820
- })
821
- // DELETE /:id/brochure/versions/:brochureId — Delete brochure version
822
- .delete("/:id/brochure/versions/:brochureId", async (c) => {
823
- const row = await productsService.deleteBrochureVersion(c.get("db"), c.req.param("id"), c.req.param("brochureId"));
824
- if (!row) {
825
- return c.json({ error: "Product brochure version not found" }, 404);
826
- }
827
- return c.json({ data: row });
828
- })
829
- // GET /:id — Get single product
830
- .get("/:id", async (c) => {
831
- const row = await productsService.getProductById(c.get("db"), c.req.param("id"));
832
- if (!row) {
833
- return c.json({ error: "Product not found" }, 404);
834
- }
835
- return c.json({ data: row });
836
- })
837
- // PATCH /:id — Update product
838
- .patch("/:id", async (c) => {
839
- const row = await productsService.updateProduct(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateProductSchema));
840
- if (!row) {
841
- return c.json({ error: "Product not found" }, 404);
842
- }
843
- await c.get("eventBus")?.emit("product.updated", { id: row.id });
844
- await emitProductContentChanged(c.get("eventBus"), { id: row.id, axis: "product" });
845
- return c.json({ data: row });
846
- })
847
- // DELETE /:id — Delete product
848
- .delete("/:id", async (c) => {
849
- const row = await productsService.deleteProduct(c.get("db"), c.req.param("id"));
850
- if (!row) {
851
- return c.json({ error: "Product not found" }, 404);
852
- }
853
- await c.get("eventBus")?.emit("product.deleted", { id: row.id });
854
- return c.json({ success: true }, 200);
855
- })
856
- // ==========================================================================
857
- // Itineraries
858
- // ==========================================================================
859
- .get("/:id/itineraries", async (c) => {
860
- return c.json({ data: await productsService.listItineraries(c.get("db"), c.req.param("id")) });
861
- })
862
- .post("/:id/itineraries", async (c) => {
863
- const row = await productsService.createItinerary(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertItinerarySchema));
864
- if (!row) {
865
- return c.json({ error: "Product not found" }, 404);
866
- }
867
- return c.json({ data: row }, 201);
868
- })
869
- .patch("/itineraries/:itineraryId", async (c) => {
870
- const row = await productsService.updateItinerary(c.get("db"), c.req.param("itineraryId"), await parseJsonBody(c, updateItinerarySchema));
871
- if (!row) {
872
- return c.json({ error: "Itinerary not found" }, 404);
873
- }
874
- return c.json({ data: row });
875
- })
876
- .delete("/itineraries/:itineraryId", async (c) => {
877
- const row = await productsService.deleteItinerary(c.get("db"), c.req.param("itineraryId"));
878
- if (!row) {
879
- return c.json({ error: "Itinerary not found" }, 404);
880
- }
881
- return c.json({ success: true }, 200);
882
- })
883
- .post("/itineraries/:itineraryId/duplicate", async (c) => {
884
- const body = await parseJsonBody(c, duplicateItinerarySchema);
885
- const row = await productsService.duplicateItinerary(c.get("db"), c.req.param("itineraryId"), body);
886
- if (!row) {
887
- return c.json({ error: "Itinerary not found" }, 404);
888
- }
889
- return c.json({ data: row }, 201);
890
- })
891
- // ==========================================================================
892
- // Days
893
- // ==========================================================================
894
- .get("/:id/itineraries/:itineraryId/days", async (c) => {
895
- return c.json({
896
- data: await productsService.listItineraryDays(c.get("db"), c.req.param("itineraryId")),
897
- });
898
- })
899
- .post("/:id/itineraries/:itineraryId/days", async (c) => {
900
- const row = await productsService.createItineraryDay(c.get("db"), c.req.param("id"), c.req.param("itineraryId"), await parseJsonBody(c, insertDaySchema));
901
- if (!row) {
902
- return c.json({ error: "Itinerary not found" }, 404);
903
- }
904
- return c.json({ data: row }, 201);
905
- })
906
- // GET /:id/days — List days for product
907
- .get("/:id/days", async (c) => {
908
- return c.json({ data: await productsService.listDays(c.get("db"), c.req.param("id")) });
909
- })
910
- // POST /:id/days — Add day to product
911
- .post("/:id/days", async (c) => {
912
- const productId = c.req.param("id");
913
- const row = await productsService.createDay(c.get("db"), productId, await parseJsonBody(c, insertDaySchema));
914
- if (!row) {
915
- return c.json({ error: "Product not found" }, 404);
916
- }
917
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
918
- return c.json({ data: row }, 201);
919
- })
920
- // PATCH /:id/days/:dayId — Update day
921
- .patch("/:id/days/:dayId", async (c) => {
922
- const productId = c.req.param("id");
923
- const row = await productsService.updateDay(c.get("db"), c.req.param("dayId"), await parseJsonBody(c, updateDaySchema));
924
- if (!row) {
925
- return c.json({ error: "Day not found" }, 404);
926
- }
927
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
928
- return c.json({ data: row });
929
- })
930
- // DELETE /:id/days/:dayId — Delete day
931
- .delete("/:id/days/:dayId", async (c) => {
932
- const productId = c.req.param("id");
933
- const row = await productsService.deleteDay(c.get("db"), c.req.param("dayId"));
934
- if (!row) {
935
- return c.json({ error: "Day not found" }, 404);
936
- }
937
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
938
- return c.json({ success: true }, 200);
939
- })
940
- // ==========================================================================
941
- // Day Services
942
- // ==========================================================================
943
- // GET /:id/days/:dayId/services — List services for a day
944
- .get("/:id/days/:dayId/services", async (c) => {
945
- return c.json({
946
- data: await productsService.listDayServices(c.get("db"), c.req.param("dayId")),
947
- });
948
- })
949
- // POST /:id/days/:dayId/services — Add service to day
950
- .post("/:id/days/:dayId/services", async (c) => {
951
- const productId = c.req.param("id");
952
- const row = await productsService.createDayService(c.get("db"), productId, c.req.param("dayId"), await parseJsonBody(c, insertDayServiceSchema));
953
- if (!row) {
954
- return c.json({ error: "Day not found" }, 404);
955
- }
956
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
957
- return c.json({ data: row }, 201);
958
- })
959
- // PATCH /:id/days/:dayId/services/:serviceId — Update service
960
- .patch("/:id/days/:dayId/services/:serviceId", async (c) => {
961
- const productId = c.req.param("id");
962
- const row = await productsService.updateDayService(c.get("db"), productId, c.req.param("serviceId"), await parseJsonBody(c, updateDayServiceSchema));
963
- if (!row) {
964
- return c.json({ error: "Service not found" }, 404);
965
- }
966
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
967
- return c.json({ data: row });
968
- })
969
- // DELETE /:id/days/:dayId/services/:serviceId — Delete service
970
- .delete("/:id/days/:dayId/services/:serviceId", async (c) => {
971
- const productId = c.req.param("id");
972
- const row = await productsService.deleteDayService(c.get("db"), productId, c.req.param("serviceId"));
973
- if (!row) {
974
- return c.json({ error: "Service not found" }, 404);
975
- }
976
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "day" });
977
- return c.json({ success: true }, 200);
978
- })
979
- // ==========================================================================
980
- // Versions
981
- // ==========================================================================
982
- // GET /:id/versions — List versions for product
983
- .get("/:id/versions", async (c) => {
984
- return c.json({ data: await productsService.listVersions(c.get("db"), c.req.param("id")) });
985
- })
986
- // POST /:id/versions — Create version snapshot
987
- .post("/:id/versions", async (c) => {
988
- const userId = requireUserId(c);
989
- const row = await productsService.createVersion(c.get("db"), c.req.param("id"), userId, await parseJsonBody(c, insertVersionSchema, {
990
- invalidJsonMessage: "Invalid JSON body",
991
- }).catch((error) => {
992
- if (error instanceof RequestValidationError && error.message === "Invalid JSON body") {
993
- return {};
994
- }
995
- throw error;
996
- }));
997
- if (!row) {
998
- return c.json({ error: "Product not found" }, 404);
999
- }
1000
- return c.json({ data: row }, 201);
1001
- })
1002
- // ==========================================================================
1003
- // Notes
1004
- // ==========================================================================
1005
- // GET /:id/notes — List notes for product
1006
- .get("/:id/notes", async (c) => {
1007
- return c.json({ data: await productsService.listNotes(c.get("db"), c.req.param("id")) });
1008
- })
1009
- // POST /:id/notes — Add note to product
1010
- .post("/:id/notes", async (c) => {
1011
- const userId = requireUserId(c);
1012
- const row = await productsService.createNote(c.get("db"), c.req.param("id"), userId, await parseJsonBody(c, insertProductNoteSchema));
1013
- if (!row) {
1014
- return c.json({ error: "Product not found" }, 404);
1015
- }
1016
- return c.json({ data: row }, 201);
1017
- })
1018
- // ==========================================================================
1019
- // Product <-> Category associations
1020
- // ==========================================================================
1021
- .get("/:id/categories", async (c) => {
1022
- return c.json({
1023
- data: await productsService.listProductCategories_(c.get("db"), c.req.param("id")),
1024
- });
1025
- })
1026
- .post("/:id/categories", async (c) => {
1027
- const productId = c.req.param("id");
1028
- const { categoryId, sortOrder } = await parseJsonBody(c, z.object({
1029
- categoryId: z.string(),
1030
- sortOrder: z.number().optional(),
1031
- }));
1032
- const row = await productsService.addProductToCategory(c.get("db"), productId, categoryId, sortOrder);
1033
- if (!row) {
1034
- return c.json({ error: "Already assigned or not found" }, 409);
1035
- }
1036
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "category" });
1037
- return c.json({ success: true }, 201);
1038
- })
1039
- .delete("/:id/categories/:categoryId", async (c) => {
1040
- const productId = c.req.param("id");
1041
- const row = await productsService.removeProductFromCategory(c.get("db"), productId, c.req.param("categoryId"));
1042
- if (!row) {
1043
- return c.json({ error: "Association not found" }, 404);
1044
- }
1045
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "category" });
1046
- return c.json({ success: true }, 200);
1047
- })
1048
- // ==========================================================================
1049
- // Product <-> Tag associations
1050
- // ==========================================================================
1051
- .get("/:id/tags", async (c) => {
1052
- return c.json({
1053
- data: await productsService.listProductTags_(c.get("db"), c.req.param("id")),
1054
- });
1055
- })
1056
- .post("/:id/tags", async (c) => {
1057
- const productId = c.req.param("id");
1058
- const { tagId } = await parseJsonBody(c, z.object({
1059
- tagId: z.string(),
1060
- }));
1061
- const row = await productsService.addProductTag(c.get("db"), productId, tagId);
1062
- if (!row) {
1063
- return c.json({ error: "Already assigned or not found" }, 409);
1064
- }
1065
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "tag" });
1066
- return c.json({ success: true }, 201);
1067
- })
1068
- .delete("/:id/tags/:tagId", async (c) => {
1069
- const productId = c.req.param("id");
1070
- const row = await productsService.removeProductTag(c.get("db"), productId, c.req.param("tagId"));
1071
- if (!row) {
1072
- return c.json({ error: "Association not found" }, 404);
1073
- }
1074
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "tag" });
1075
- return c.json({ success: true }, 200);
1076
- })
1077
- // ==========================================================================
1078
- // Product Media (nested under product)
1079
- // ==========================================================================
1080
- // GET /:id/media — List product-level media
1081
- .get("/:id/media", async (c) => {
1082
- const query = parseQuery(c, productMediaListQuerySchema);
1083
- return c.json(await productsService.listProductLevelMedia(c.get("db"), c.req.param("id"), query));
1084
- })
1085
- // POST /:id/media — Create media for product
1086
- .post("/:id/media", async (c) => {
1087
- const productId = c.req.param("id");
1088
- const row = await productsService.createMedia(c.get("db"), productId, await parseJsonBody(c, insertProductMediaSchema));
1089
- if (!row) {
1090
- return c.json({ error: "Product not found or invalid dayId" }, 404);
1091
- }
1092
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "media" });
1093
- return c.json({ data: row }, 201);
1094
- })
1095
- // POST /:id/media/reorder — Batch reorder media
1096
- .post("/:id/media/reorder", async (c) => {
1097
- const productId = c.req.param("id");
1098
- const data = await parseJsonBody(c, reorderProductMediaSchema);
1099
- const results = await productsService.reorderMedia(c.get("db"), data);
1100
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "media" });
1101
- return c.json({ data: results });
1102
- })
1103
- // GET /:id/days/:dayId/media — List day media
1104
- .get("/:id/days/:dayId/media", async (c) => {
1105
- const query = parseQuery(c, productMediaListQuerySchema);
1106
- return c.json(await productsService.listMedia(c.get("db"), c.req.param("id"), {
1107
- ...query,
1108
- dayId: c.req.param("dayId"),
1109
- }));
1110
- })
1111
- // POST /:id/days/:dayId/media — Create day media
1112
- .post("/:id/days/:dayId/media", async (c) => {
1113
- const productId = c.req.param("id");
1114
- const body = await parseJsonBody(c, insertProductMediaSchema);
1115
- const row = await productsService.createMedia(c.get("db"), productId, {
1116
- ...body,
1117
- dayId: c.req.param("dayId"),
1118
- });
1119
- if (!row) {
1120
- return c.json({ error: "Product or day not found" }, 404);
1121
- }
1122
- await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "media" });
1123
- return c.json({ data: row }, 201);
1124
- })
1125
- // ==========================================================================
1126
- // Recalculate
1127
- // ==========================================================================
1128
- // POST /:id/recalculate — Recalculate product cost and margin
1129
- .post("/:id/recalculate", async (c) => {
1130
- const result = await productsService.recalculate(c.get("db"), c.req.param("id"));
1131
- if (!result) {
1132
- return c.json({ error: "Product not found" }, 404);
1133
- }
1134
- return c.json({ data: result });
1135
- });
14
+ .route("/", productConfigurationRoutes)
15
+ .route("/", productMerchandisingRoutes)
16
+ .route("/", productOptionRoutes)
17
+ .route("/", productTranslationRoutes)
18
+ .route("/", productCatalogRoutes)
19
+ .route("/", productMediaRoutes)
20
+ .route("/", productItineraryRoutes)
21
+ .route("/", productAssociationRoutes)
22
+ .route("/", productMaintenanceRoutes)
23
+ .route("/", productCoreRoutes);