@voyant-travel/inventory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/LICENSE +201 -0
  2. package/dist/action-ledger-drift.d.ts +29 -0
  3. package/dist/action-ledger-drift.d.ts.map +1 -0
  4. package/dist/action-ledger-drift.js +338 -0
  5. package/dist/action-ledger.d.ts +104 -0
  6. package/dist/action-ledger.d.ts.map +1 -0
  7. package/dist/action-ledger.js +100 -0
  8. package/dist/authoring/builder.d.ts +37 -0
  9. package/dist/authoring/builder.d.ts.map +1 -0
  10. package/dist/authoring/builder.js +248 -0
  11. package/dist/authoring/clone-content.d.ts +38 -0
  12. package/dist/authoring/clone-content.d.ts.map +1 -0
  13. package/dist/authoring/clone-content.js +367 -0
  14. package/dist/authoring/clone-pricing.d.ts +9 -0
  15. package/dist/authoring/clone-pricing.d.ts.map +1 -0
  16. package/dist/authoring/clone-pricing.js +242 -0
  17. package/dist/authoring/clone.d.ts +45 -0
  18. package/dist/authoring/clone.d.ts.map +1 -0
  19. package/dist/authoring/clone.js +142 -0
  20. package/dist/authoring/errors.d.ts +21 -0
  21. package/dist/authoring/errors.d.ts.map +1 -0
  22. package/dist/authoring/errors.js +13 -0
  23. package/dist/authoring/extension.d.ts +248 -0
  24. package/dist/authoring/extension.d.ts.map +1 -0
  25. package/dist/authoring/extension.js +116 -0
  26. package/dist/authoring/index.d.ts +12 -0
  27. package/dist/authoring/index.d.ts.map +1 -0
  28. package/dist/authoring/index.js +11 -0
  29. package/dist/authoring/schema.d.ts +85 -0
  30. package/dist/authoring/schema.d.ts.map +1 -0
  31. package/dist/authoring/schema.js +16 -0
  32. package/dist/authoring/service.d.ts +28 -0
  33. package/dist/authoring/service.d.ts.map +1 -0
  34. package/dist/authoring/service.js +66 -0
  35. package/dist/authoring/spec.d.ts +524 -0
  36. package/dist/authoring/spec.d.ts.map +1 -0
  37. package/dist/authoring/spec.js +167 -0
  38. package/dist/authoring/validate.d.ts +17 -0
  39. package/dist/authoring/validate.d.ts.map +1 -0
  40. package/dist/authoring/validate.js +83 -0
  41. package/dist/authoring.d.ts +2 -0
  42. package/dist/authoring.d.ts.map +1 -0
  43. package/dist/authoring.js +1 -0
  44. package/dist/booking-engine/handler-support.d.ts +91 -0
  45. package/dist/booking-engine/handler-support.d.ts.map +1 -0
  46. package/dist/booking-engine/handler-support.js +355 -0
  47. package/dist/booking-engine/handler.d.ts +404 -0
  48. package/dist/booking-engine/handler.d.ts.map +1 -0
  49. package/dist/booking-engine/handler.js +398 -0
  50. package/dist/booking-engine/index.d.ts +8 -0
  51. package/dist/booking-engine/index.d.ts.map +1 -0
  52. package/dist/booking-engine/index.js +7 -0
  53. package/dist/booking-engine.d.ts +2 -0
  54. package/dist/booking-engine.d.ts.map +1 -0
  55. package/dist/booking-engine.js +1 -0
  56. package/dist/booking-extension.d.ts +278 -0
  57. package/dist/booking-extension.d.ts.map +1 -0
  58. package/dist/booking-extension.js +161 -0
  59. package/dist/catalog-policy-departures.d.ts +52 -0
  60. package/dist/catalog-policy-departures.d.ts.map +1 -0
  61. package/dist/catalog-policy-departures.js +169 -0
  62. package/dist/catalog-policy-destinations.d.ts +43 -0
  63. package/dist/catalog-policy-destinations.d.ts.map +1 -0
  64. package/dist/catalog-policy-destinations.js +165 -0
  65. package/dist/catalog-policy-pricing.d.ts +55 -0
  66. package/dist/catalog-policy-pricing.d.ts.map +1 -0
  67. package/dist/catalog-policy-pricing.js +109 -0
  68. package/dist/catalog-policy-promotions.d.ts +52 -0
  69. package/dist/catalog-policy-promotions.d.ts.map +1 -0
  70. package/dist/catalog-policy-promotions.js +270 -0
  71. package/dist/catalog-policy-taxonomy.d.ts +51 -0
  72. package/dist/catalog-policy-taxonomy.d.ts.map +1 -0
  73. package/dist/catalog-policy-taxonomy.js +191 -0
  74. package/dist/catalog-policy.d.ts +33 -0
  75. package/dist/catalog-policy.d.ts.map +1 -0
  76. package/dist/catalog-policy.js +733 -0
  77. package/dist/content-shape.d.ts +15 -0
  78. package/dist/content-shape.d.ts.map +1 -0
  79. package/dist/content-shape.js +28 -0
  80. package/dist/draft-shape.d.ts +43 -0
  81. package/dist/draft-shape.d.ts.map +1 -0
  82. package/dist/draft-shape.js +48 -0
  83. package/dist/events.d.ts +37 -0
  84. package/dist/events.d.ts.map +1 -0
  85. package/dist/events.js +32 -0
  86. package/dist/extras/catalog-policy.d.ts +30 -0
  87. package/dist/extras/catalog-policy.d.ts.map +1 -0
  88. package/dist/extras/catalog-policy.js +319 -0
  89. package/dist/extras/content-shape.d.ts +5 -0
  90. package/dist/extras/content-shape.d.ts.map +1 -0
  91. package/dist/extras/content-shape.js +13 -0
  92. package/dist/extras/draft-shape.d.ts +34 -0
  93. package/dist/extras/draft-shape.d.ts.map +1 -0
  94. package/dist/extras/draft-shape.js +69 -0
  95. package/dist/extras/routes.d.ts +380 -0
  96. package/dist/extras/routes.d.ts.map +1 -0
  97. package/dist/extras/routes.js +59 -0
  98. package/dist/extras/schema-sourced-content.d.ts +254 -0
  99. package/dist/extras/schema-sourced-content.d.ts.map +1 -0
  100. package/dist/extras/schema-sourced-content.js +45 -0
  101. package/dist/extras/schema.d.ts +628 -0
  102. package/dist/extras/schema.d.ts.map +1 -0
  103. package/dist/extras/schema.js +87 -0
  104. package/dist/extras/service-catalog-plane.d.ts +77 -0
  105. package/dist/extras/service-catalog-plane.d.ts.map +1 -0
  106. package/dist/extras/service-catalog-plane.js +219 -0
  107. package/dist/extras/service-content-synthesizer.d.ts +41 -0
  108. package/dist/extras/service-content-synthesizer.d.ts.map +1 -0
  109. package/dist/extras/service-content-synthesizer.js +138 -0
  110. package/dist/extras/service-content.d.ts +48 -0
  111. package/dist/extras/service-content.d.ts.map +1 -0
  112. package/dist/extras/service-content.js +253 -0
  113. package/dist/extras/service.d.ts +185 -0
  114. package/dist/extras/service.d.ts.map +1 -0
  115. package/dist/extras/service.js +96 -0
  116. package/dist/extras/validation.d.ts +437 -0
  117. package/dist/extras/validation.d.ts.map +1 -0
  118. package/dist/extras/validation.js +149 -0
  119. package/dist/extras.d.ts +267 -0
  120. package/dist/extras.d.ts.map +1 -0
  121. package/dist/extras.js +19 -0
  122. package/dist/index.d.ts +22 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +32 -0
  125. package/dist/interface.d.ts +5869 -0
  126. package/dist/interface.d.ts.map +1 -0
  127. package/dist/interface.js +54 -0
  128. package/dist/public-routes.d.ts +2 -0
  129. package/dist/public-routes.d.ts.map +1 -0
  130. package/dist/public-routes.js +1 -0
  131. package/dist/public-validation.d.ts +2 -0
  132. package/dist/public-validation.d.ts.map +1 -0
  133. package/dist/public-validation.js +1 -0
  134. package/dist/read-model.d.ts +25 -0
  135. package/dist/read-model.d.ts.map +1 -0
  136. package/dist/read-model.js +99 -0
  137. package/dist/route-env.d.ts +22 -0
  138. package/dist/route-env.d.ts.map +1 -0
  139. package/dist/route-env.js +1 -0
  140. package/dist/routes-associations.d.ts +164 -0
  141. package/dist/routes-associations.d.ts.map +1 -0
  142. package/dist/routes-associations.js +100 -0
  143. package/dist/routes-catalog.d.ts +436 -0
  144. package/dist/routes-catalog.d.ts.map +1 -0
  145. package/dist/routes-catalog.js +104 -0
  146. package/dist/routes-configuration.d.ts +773 -0
  147. package/dist/routes-configuration.d.ts.map +1 -0
  148. package/dist/routes-configuration.js +364 -0
  149. package/dist/routes-content.d.ts +74 -0
  150. package/dist/routes-content.d.ts.map +1 -0
  151. package/dist/routes-content.js +117 -0
  152. package/dist/routes-core.d.ts +331 -0
  153. package/dist/routes-core.d.ts.map +1 -0
  154. package/dist/routes-core.js +95 -0
  155. package/dist/routes-itinerary.d.ts +759 -0
  156. package/dist/routes-itinerary.d.ts.map +1 -0
  157. package/dist/routes-itinerary.js +387 -0
  158. package/dist/routes-maintenance.d.ts +32 -0
  159. package/dist/routes-maintenance.d.ts.map +1 -0
  160. package/dist/routes-maintenance.js +14 -0
  161. package/dist/routes-media.d.ts +634 -0
  162. package/dist/routes-media.d.ts.map +1 -0
  163. package/dist/routes-media.js +245 -0
  164. package/dist/routes-merchandising.d.ts +1120 -0
  165. package/dist/routes-merchandising.d.ts.map +1 -0
  166. package/dist/routes-merchandising.js +377 -0
  167. package/dist/routes-options.d.ts +363 -0
  168. package/dist/routes-options.d.ts.map +1 -0
  169. package/dist/routes-options.js +173 -0
  170. package/dist/routes-public.d.ts +776 -0
  171. package/dist/routes-public.d.ts.map +1 -0
  172. package/dist/routes-public.js +119 -0
  173. package/dist/routes-translations.d.ts +489 -0
  174. package/dist/routes-translations.d.ts.map +1 -0
  175. package/dist/routes-translations.js +258 -0
  176. package/dist/routes.d.ts +5097 -0
  177. package/dist/routes.d.ts.map +1 -0
  178. package/dist/routes.js +64 -0
  179. package/dist/schema-core.d.ts +1238 -0
  180. package/dist/schema-core.d.ts.map +1 -0
  181. package/dist/schema-core.js +157 -0
  182. package/dist/schema-itinerary.d.ts +1169 -0
  183. package/dist/schema-itinerary.d.ts.map +1 -0
  184. package/dist/schema-itinerary.js +130 -0
  185. package/dist/schema-relations.d.ts +117 -0
  186. package/dist/schema-relations.d.ts.map +1 -0
  187. package/dist/schema-relations.js +192 -0
  188. package/dist/schema-settings.d.ts +1800 -0
  189. package/dist/schema-settings.d.ts.map +1 -0
  190. package/dist/schema-settings.js +220 -0
  191. package/dist/schema-shared.d.ts +15 -0
  192. package/dist/schema-shared.d.ts.map +1 -0
  193. package/dist/schema-shared.js +91 -0
  194. package/dist/schema-sourced-content.d.ts +262 -0
  195. package/dist/schema-sourced-content.d.ts.map +1 -0
  196. package/dist/schema-sourced-content.js +69 -0
  197. package/dist/schema-taxonomy.d.ts +1363 -0
  198. package/dist/schema-taxonomy.d.ts.map +1 -0
  199. package/dist/schema-taxonomy.js +203 -0
  200. package/dist/schema.d.ts +10 -0
  201. package/dist/schema.d.ts.map +1 -0
  202. package/dist/schema.js +9 -0
  203. package/dist/service-aggregates.d.ts +29 -0
  204. package/dist/service-aggregates.d.ts.map +1 -0
  205. package/dist/service-aggregates.js +56 -0
  206. package/dist/service-catalog-plane-destinations.d.ts +30 -0
  207. package/dist/service-catalog-plane-destinations.d.ts.map +1 -0
  208. package/dist/service-catalog-plane-destinations.js +143 -0
  209. package/dist/service-catalog-plane-taxonomy.d.ts +73 -0
  210. package/dist/service-catalog-plane-taxonomy.d.ts.map +1 -0
  211. package/dist/service-catalog-plane-taxonomy.js +242 -0
  212. package/dist/service-catalog-plane.d.ts +179 -0
  213. package/dist/service-catalog-plane.d.ts.map +1 -0
  214. package/dist/service-catalog-plane.js +431 -0
  215. package/dist/service-catalog.d.ts +251 -0
  216. package/dist/service-catalog.d.ts.map +1 -0
  217. package/dist/service-catalog.js +517 -0
  218. package/dist/service-configuration.d.ts +261 -0
  219. package/dist/service-configuration.d.ts.map +1 -0
  220. package/dist/service-configuration.js +343 -0
  221. package/dist/service-content-owned.d.ts +68 -0
  222. package/dist/service-content-owned.d.ts.map +1 -0
  223. package/dist/service-content-owned.js +329 -0
  224. package/dist/service-content-synthesizer.d.ts +90 -0
  225. package/dist/service-content-synthesizer.d.ts.map +1 -0
  226. package/dist/service-content-synthesizer.js +178 -0
  227. package/dist/service-content.d.ts +106 -0
  228. package/dist/service-content.d.ts.map +1 -0
  229. package/dist/service-content.js +388 -0
  230. package/dist/service-core.d.ts +194 -0
  231. package/dist/service-core.d.ts.map +1 -0
  232. package/dist/service-core.js +213 -0
  233. package/dist/service-delivery-formats.d.ts +58 -0
  234. package/dist/service-delivery-formats.d.ts.map +1 -0
  235. package/dist/service-delivery-formats.js +107 -0
  236. package/dist/service-destinations.d.ts +223 -0
  237. package/dist/service-destinations.d.ts.map +1 -0
  238. package/dist/service-destinations.js +310 -0
  239. package/dist/service-itinerary-history.d.ts +457 -0
  240. package/dist/service-itinerary-history.d.ts.map +1 -0
  241. package/dist/service-itinerary-history.js +135 -0
  242. package/dist/service-itinerary.d.ts +1149 -0
  243. package/dist/service-itinerary.d.ts.map +1 -0
  244. package/dist/service-itinerary.js +419 -0
  245. package/dist/service-media.d.ts +272 -0
  246. package/dist/service-media.d.ts.map +1 -0
  247. package/dist/service-media.js +320 -0
  248. package/dist/service-merchandising.d.ts +184 -0
  249. package/dist/service-merchandising.d.ts.map +1 -0
  250. package/dist/service-merchandising.js +181 -0
  251. package/dist/service-option-translations.d.ts +268 -0
  252. package/dist/service-option-translations.d.ts.map +1 -0
  253. package/dist/service-option-translations.js +300 -0
  254. package/dist/service-options.d.ts +181 -0
  255. package/dist/service-options.d.ts.map +1 -0
  256. package/dist/service-options.js +179 -0
  257. package/dist/service-product-destinations.d.ts +37 -0
  258. package/dist/service-product-destinations.d.ts.map +1 -0
  259. package/dist/service-product-destinations.js +94 -0
  260. package/dist/service-public.d.ts +664 -0
  261. package/dist/service-public.d.ts.map +1 -0
  262. package/dist/service-public.js +374 -0
  263. package/dist/service-taxonomy.d.ts +197 -0
  264. package/dist/service-taxonomy.d.ts.map +1 -0
  265. package/dist/service-taxonomy.js +221 -0
  266. package/dist/service.d.ts +3929 -0
  267. package/dist/service.d.ts.map +1 -0
  268. package/dist/service.js +28 -0
  269. package/dist/tasks/brochure-printers.d.ts +31 -0
  270. package/dist/tasks/brochure-printers.d.ts.map +1 -0
  271. package/dist/tasks/brochure-printers.js +149 -0
  272. package/dist/tasks/brochure-templates.d.ts +36 -0
  273. package/dist/tasks/brochure-templates.d.ts.map +1 -0
  274. package/dist/tasks/brochure-templates.js +110 -0
  275. package/dist/tasks/brochures.d.ts +43 -0
  276. package/dist/tasks/brochures.d.ts.map +1 -0
  277. package/dist/tasks/brochures.js +72 -0
  278. package/dist/tasks/generate-pdf.d.ts +8 -0
  279. package/dist/tasks/generate-pdf.d.ts.map +1 -0
  280. package/dist/tasks/generate-pdf.js +106 -0
  281. package/dist/tasks/index.d.ts +5 -0
  282. package/dist/tasks/index.d.ts.map +1 -0
  283. package/dist/tasks/index.js +4 -0
  284. package/dist/tasks/pdf-text.d.ts +2 -0
  285. package/dist/tasks/pdf-text.d.ts.map +1 -0
  286. package/dist/tasks/pdf-text.js +40 -0
  287. package/dist/tasks.d.ts +2 -0
  288. package/dist/tasks.d.ts.map +1 -0
  289. package/dist/tasks.js +1 -0
  290. package/dist/validation-catalog.d.ts +2 -0
  291. package/dist/validation-catalog.d.ts.map +1 -0
  292. package/dist/validation-catalog.js +3 -0
  293. package/dist/validation-config.d.ts +2 -0
  294. package/dist/validation-config.d.ts.map +1 -0
  295. package/dist/validation-config.js +3 -0
  296. package/dist/validation-content.d.ts +2 -0
  297. package/dist/validation-content.d.ts.map +1 -0
  298. package/dist/validation-content.js +3 -0
  299. package/dist/validation-core.d.ts +2 -0
  300. package/dist/validation-core.d.ts.map +1 -0
  301. package/dist/validation-core.js +3 -0
  302. package/dist/validation-public.d.ts +2 -0
  303. package/dist/validation-public.d.ts.map +1 -0
  304. package/dist/validation-public.js +3 -0
  305. package/dist/validation-shared.d.ts +2 -0
  306. package/dist/validation-shared.d.ts.map +1 -0
  307. package/dist/validation-shared.js +3 -0
  308. package/dist/validation.d.ts +2 -0
  309. package/dist/validation.d.ts.map +1 -0
  310. package/dist/validation.js +3 -0
  311. package/package.json +204 -0
@@ -0,0 +1,320 @@
1
+ import { and, asc, desc, eq, sql } from "drizzle-orm";
2
+ import { productDays, productItineraries, productMedia, products } from "./schema.js";
3
+ async function ensureProductExists(db, productId) {
4
+ const [product] = await db
5
+ .select({ id: products.id })
6
+ .from(products)
7
+ .where(eq(products.id, productId))
8
+ .limit(1);
9
+ return product ?? null;
10
+ }
11
+ async function getDayById(db, dayId) {
12
+ const [day] = await db
13
+ .select({
14
+ id: productDays.id,
15
+ itineraryId: productDays.itineraryId,
16
+ productId: productItineraries.productId,
17
+ })
18
+ .from(productDays)
19
+ .innerJoin(productItineraries, eq(productDays.itineraryId, productItineraries.id))
20
+ .where(eq(productDays.id, dayId))
21
+ .limit(1);
22
+ return day ?? null;
23
+ }
24
+ export const mediaProductsService = {
25
+ // ==========================================================================
26
+ // Product Media
27
+ // ==========================================================================
28
+ async listMedia(db, productId, query) {
29
+ const conditions = [eq(productMedia.productId, productId)];
30
+ if (query.dayId !== undefined) {
31
+ conditions.push(eq(productMedia.dayId, query.dayId));
32
+ }
33
+ if (query.mediaType) {
34
+ conditions.push(eq(productMedia.mediaType, query.mediaType));
35
+ }
36
+ if (query.isBrochure !== undefined) {
37
+ conditions.push(eq(productMedia.isBrochure, query.isBrochure));
38
+ }
39
+ if (query.isBrochureCurrent !== undefined) {
40
+ conditions.push(eq(productMedia.isBrochureCurrent, query.isBrochureCurrent));
41
+ }
42
+ const where = and(...conditions);
43
+ const [rows, countResult] = await Promise.all([
44
+ db
45
+ .select()
46
+ .from(productMedia)
47
+ .where(where)
48
+ .limit(query.limit)
49
+ .offset(query.offset)
50
+ .orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
51
+ db.select({ count: sql `count(*)::int` }).from(productMedia).where(where),
52
+ ]);
53
+ return {
54
+ data: rows,
55
+ total: countResult[0]?.count ?? 0,
56
+ limit: query.limit,
57
+ offset: query.offset,
58
+ };
59
+ },
60
+ async listProductLevelMedia(db, productId, query) {
61
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
62
+ const conditions = [eq(productMedia.productId, productId), sql `${productMedia.dayId} is null`];
63
+ if (query.mediaType) {
64
+ conditions.push(eq(productMedia.mediaType, query.mediaType));
65
+ }
66
+ if (query.isBrochure !== undefined) {
67
+ conditions.push(eq(productMedia.isBrochure, query.isBrochure));
68
+ }
69
+ if (query.isBrochureCurrent !== undefined) {
70
+ conditions.push(eq(productMedia.isBrochureCurrent, query.isBrochureCurrent));
71
+ }
72
+ const where = and(...conditions);
73
+ const [rows, countResult] = await Promise.all([
74
+ db
75
+ .select()
76
+ .from(productMedia)
77
+ .where(where)
78
+ .limit(query.limit)
79
+ .offset(query.offset)
80
+ .orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
81
+ db.select({ count: sql `count(*)::int` }).from(productMedia).where(where),
82
+ ]);
83
+ return {
84
+ data: rows,
85
+ total: countResult[0]?.count ?? 0,
86
+ limit: query.limit,
87
+ offset: query.offset,
88
+ };
89
+ },
90
+ async getMediaById(db, id) {
91
+ const [row] = await db.select().from(productMedia).where(eq(productMedia.id, id)).limit(1);
92
+ return row ?? null;
93
+ },
94
+ async createMedia(db, productId, data) {
95
+ const product = await ensureProductExists(db, productId);
96
+ if (!product) {
97
+ return null;
98
+ }
99
+ if (data.dayId) {
100
+ if (data.isBrochure) {
101
+ return null;
102
+ }
103
+ const day = await getDayById(db, data.dayId);
104
+ if (!day || day.productId !== productId) {
105
+ return null;
106
+ }
107
+ }
108
+ if (data.isBrochure) {
109
+ await db
110
+ .update(productMedia)
111
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
112
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
113
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
114
+ sql `${productMedia.dayId} is null`));
115
+ }
116
+ const [row] = await db
117
+ .insert(productMedia)
118
+ .values({
119
+ productId,
120
+ dayId: data.dayId ?? null,
121
+ mediaType: data.mediaType,
122
+ name: data.name,
123
+ url: data.url,
124
+ storageKey: data.storageKey ?? null,
125
+ mimeType: data.mimeType ?? null,
126
+ fileSize: data.fileSize ?? null,
127
+ altText: data.altText ?? null,
128
+ sortOrder: data.sortOrder,
129
+ isCover: data.isCover,
130
+ isBrochure: data.isBrochure,
131
+ isBrochureCurrent: data.isBrochureCurrent,
132
+ brochureVersion: data.brochureVersion ?? null,
133
+ })
134
+ .returning();
135
+ return row ?? null;
136
+ },
137
+ async updateMedia(db, id, data) {
138
+ const existing = await mediaProductsService.getMediaById(db, id);
139
+ if (!existing) {
140
+ return null;
141
+ }
142
+ if (data.isBrochure === true && existing.dayId) {
143
+ return null;
144
+ }
145
+ if (data.isBrochure === true) {
146
+ await db
147
+ .update(productMedia)
148
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
149
+ .where(and(eq(productMedia.productId, existing.productId), eq(productMedia.isBrochure, true),
150
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
151
+ sql `${productMedia.dayId} is null`,
152
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
153
+ sql `${productMedia.id} <> ${id}`));
154
+ }
155
+ const [row] = await db
156
+ .update(productMedia)
157
+ .set({ ...data, updatedAt: new Date() })
158
+ .where(eq(productMedia.id, id))
159
+ .returning();
160
+ return row ?? null;
161
+ },
162
+ async deleteMedia(db, id) {
163
+ const [row] = await db.delete(productMedia).where(eq(productMedia.id, id)).returning();
164
+ return row ?? null;
165
+ },
166
+ async setCoverMedia(db, productId, mediaId, dayId) {
167
+ // Unset existing cover in the same scope (product-level or day-level)
168
+ const scopeConditions = [eq(productMedia.productId, productId)];
169
+ if (dayId) {
170
+ scopeConditions.push(eq(productMedia.dayId, dayId));
171
+ }
172
+ else {
173
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
174
+ scopeConditions.push(sql `${productMedia.dayId} is null`);
175
+ }
176
+ await db
177
+ .update(productMedia)
178
+ .set({ isCover: false, updatedAt: new Date() })
179
+ .where(and(...scopeConditions));
180
+ const [row] = await db
181
+ .update(productMedia)
182
+ .set({ isCover: true, updatedAt: new Date() })
183
+ .where(eq(productMedia.id, mediaId))
184
+ .returning();
185
+ return row ?? null;
186
+ },
187
+ async reorderMedia(db, data) {
188
+ const results = await Promise.all(data.items.map(async (item) => {
189
+ const [row] = await db
190
+ .update(productMedia)
191
+ .set({ sortOrder: item.sortOrder, updatedAt: new Date() })
192
+ .where(eq(productMedia.id, item.id))
193
+ .returning({ id: productMedia.id });
194
+ return row;
195
+ }));
196
+ return results.filter((r) => r != null);
197
+ },
198
+ async getBrochure(db, productId) {
199
+ const [row] = await db
200
+ .select()
201
+ .from(productMedia)
202
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true), eq(productMedia.isBrochureCurrent, true),
203
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
204
+ sql `${productMedia.dayId} is null`))
205
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
206
+ .limit(1);
207
+ return row ?? null;
208
+ },
209
+ async listBrochures(db, productId) {
210
+ return db
211
+ .select()
212
+ .from(productMedia)
213
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
214
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
215
+ sql `${productMedia.dayId} is null`))
216
+ .orderBy(desc(productMedia.isBrochureCurrent), desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt));
217
+ },
218
+ async upsertBrochure(db, productId, data) {
219
+ const product = await ensureProductExists(db, productId);
220
+ if (!product) {
221
+ return null;
222
+ }
223
+ const brochures = await mediaProductsService.listBrochures(db, productId);
224
+ const nextVersion = brochures.reduce((maxVersion, brochure) => {
225
+ const version = brochure.brochureVersion ?? 0;
226
+ return version > maxVersion ? version : maxVersion;
227
+ }, 0) + 1;
228
+ return mediaProductsService.createMedia(db, productId, {
229
+ mediaType: "document",
230
+ dayId: null,
231
+ name: data.name,
232
+ url: data.url,
233
+ storageKey: data.storageKey ?? null,
234
+ mimeType: data.mimeType ?? "application/pdf",
235
+ fileSize: data.fileSize ?? null,
236
+ altText: data.altText ?? null,
237
+ sortOrder: data.sortOrder,
238
+ isCover: false,
239
+ isBrochure: true,
240
+ isBrochureCurrent: true,
241
+ brochureVersion: nextVersion,
242
+ });
243
+ },
244
+ async deleteBrochure(db, productId) {
245
+ const brochure = await mediaProductsService.getBrochure(db, productId);
246
+ if (!brochure) {
247
+ return null;
248
+ }
249
+ const [row] = await db.delete(productMedia).where(eq(productMedia.id, brochure.id)).returning();
250
+ const [previous] = await db
251
+ .select()
252
+ .from(productMedia)
253
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
254
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
255
+ sql `${productMedia.dayId} is null`))
256
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
257
+ .limit(1);
258
+ if (previous) {
259
+ await db
260
+ .update(productMedia)
261
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
262
+ .where(eq(productMedia.id, previous.id));
263
+ }
264
+ return row ?? null;
265
+ },
266
+ async setCurrentBrochure(db, productId, brochureId) {
267
+ const [brochure] = await db
268
+ .select()
269
+ .from(productMedia)
270
+ .where(and(eq(productMedia.id, brochureId), eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
271
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
272
+ sql `${productMedia.dayId} is null`))
273
+ .limit(1);
274
+ if (!brochure) {
275
+ return null;
276
+ }
277
+ await db
278
+ .update(productMedia)
279
+ .set({ isBrochureCurrent: false, updatedAt: new Date() })
280
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
281
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
282
+ sql `${productMedia.dayId} is null`));
283
+ const [row] = await db
284
+ .update(productMedia)
285
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
286
+ .where(eq(productMedia.id, brochureId))
287
+ .returning();
288
+ return row ?? null;
289
+ },
290
+ async deleteBrochureVersion(db, productId, brochureId) {
291
+ const [brochure] = await db
292
+ .select()
293
+ .from(productMedia)
294
+ .where(and(eq(productMedia.id, brochureId), eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
295
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
296
+ sql `${productMedia.dayId} is null`))
297
+ .limit(1);
298
+ if (!brochure) {
299
+ return null;
300
+ }
301
+ const [row] = await db.delete(productMedia).where(eq(productMedia.id, brochureId)).returning();
302
+ if (brochure.isBrochureCurrent) {
303
+ const [previous] = await db
304
+ .select()
305
+ .from(productMedia)
306
+ .where(and(eq(productMedia.productId, productId), eq(productMedia.isBrochure, true),
307
+ // agent-quality: raw-sql reviewed -- owner: inventory; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
308
+ sql `${productMedia.dayId} is null`))
309
+ .orderBy(desc(productMedia.brochureVersion), desc(productMedia.updatedAt), desc(productMedia.createdAt))
310
+ .limit(1);
311
+ if (previous) {
312
+ await db
313
+ .update(productMedia)
314
+ .set({ isBrochureCurrent: true, updatedAt: new Date() })
315
+ .where(eq(productMedia.id, previous.id));
316
+ }
317
+ }
318
+ return row ?? null;
319
+ },
320
+ };
@@ -0,0 +1,184 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { z } from "zod";
3
+ import type { insertProductFaqSchema, insertProductFeatureSchema, insertProductLocationSchema, productFaqListQuerySchema, productFeatureListQuerySchema, productLocationListQuerySchema, updateProductFaqSchema, updateProductFeatureSchema, updateProductLocationSchema } from "./validation.js";
4
+ type ProductFeatureListQuery = z.infer<typeof productFeatureListQuerySchema>;
5
+ type CreateProductFeatureInput = z.infer<typeof insertProductFeatureSchema>;
6
+ type UpdateProductFeatureInput = z.infer<typeof updateProductFeatureSchema>;
7
+ type ProductFaqListQuery = z.infer<typeof productFaqListQuerySchema>;
8
+ type CreateProductFaqInput = z.infer<typeof insertProductFaqSchema>;
9
+ type UpdateProductFaqInput = z.infer<typeof updateProductFaqSchema>;
10
+ type ProductLocationListQuery = z.infer<typeof productLocationListQuerySchema>;
11
+ type CreateProductLocationInput = z.infer<typeof insertProductLocationSchema>;
12
+ type UpdateProductLocationInput = z.infer<typeof updateProductLocationSchema>;
13
+ export declare const merchandisingProductsService: {
14
+ listFeatures(db: PostgresJsDatabase, query: ProductFeatureListQuery): Promise<{
15
+ data: {
16
+ id: string;
17
+ productId: string;
18
+ featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
19
+ title: string;
20
+ description: string | null;
21
+ sortOrder: number;
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ }[];
25
+ total: number;
26
+ limit: number;
27
+ offset: number;
28
+ }>;
29
+ getFeatureById(db: PostgresJsDatabase, id: string): Promise<{
30
+ id: string;
31
+ productId: string;
32
+ featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
33
+ title: string;
34
+ description: string | null;
35
+ sortOrder: number;
36
+ createdAt: Date;
37
+ updatedAt: Date;
38
+ } | null>;
39
+ createFeature(db: PostgresJsDatabase, productId: string, data: CreateProductFeatureInput): Promise<{
40
+ id: string;
41
+ description: string | null;
42
+ createdAt: Date;
43
+ updatedAt: Date;
44
+ title: string;
45
+ productId: string;
46
+ featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
47
+ sortOrder: number;
48
+ } | null>;
49
+ updateFeature(db: PostgresJsDatabase, id: string, data: UpdateProductFeatureInput): Promise<{
50
+ id: string;
51
+ productId: string;
52
+ featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
53
+ title: string;
54
+ description: string | null;
55
+ sortOrder: number;
56
+ createdAt: Date;
57
+ updatedAt: Date;
58
+ } | null>;
59
+ deleteFeature(db: PostgresJsDatabase, id: string): Promise<{
60
+ id: string;
61
+ } | null>;
62
+ listFaqs(db: PostgresJsDatabase, query: ProductFaqListQuery): Promise<{
63
+ data: {
64
+ id: string;
65
+ productId: string;
66
+ question: string;
67
+ answer: string;
68
+ sortOrder: number;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ }[];
72
+ total: number;
73
+ limit: number;
74
+ offset: number;
75
+ }>;
76
+ getFaqById(db: PostgresJsDatabase, id: string): Promise<{
77
+ id: string;
78
+ productId: string;
79
+ question: string;
80
+ answer: string;
81
+ sortOrder: number;
82
+ createdAt: Date;
83
+ updatedAt: Date;
84
+ } | null>;
85
+ createFaq(db: PostgresJsDatabase, productId: string, data: CreateProductFaqInput): Promise<{
86
+ id: string;
87
+ createdAt: Date;
88
+ updatedAt: Date;
89
+ productId: string;
90
+ sortOrder: number;
91
+ question: string;
92
+ answer: string;
93
+ } | null>;
94
+ updateFaq(db: PostgresJsDatabase, id: string, data: UpdateProductFaqInput): Promise<{
95
+ id: string;
96
+ productId: string;
97
+ question: string;
98
+ answer: string;
99
+ sortOrder: number;
100
+ createdAt: Date;
101
+ updatedAt: Date;
102
+ } | null>;
103
+ deleteFaq(db: PostgresJsDatabase, id: string): Promise<{
104
+ id: string;
105
+ } | null>;
106
+ listLocations(db: PostgresJsDatabase, query: ProductLocationListQuery): Promise<{
107
+ data: {
108
+ id: string;
109
+ productId: string;
110
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
111
+ title: string;
112
+ address: string | null;
113
+ city: string | null;
114
+ countryCode: string | null;
115
+ latitude: number | null;
116
+ longitude: number | null;
117
+ googlePlaceId: string | null;
118
+ applePlaceId: string | null;
119
+ tripadvisorLocationId: string | null;
120
+ sortOrder: number;
121
+ createdAt: Date;
122
+ updatedAt: Date;
123
+ }[];
124
+ total: number;
125
+ limit: number;
126
+ offset: number;
127
+ }>;
128
+ getLocationById(db: PostgresJsDatabase, id: string): Promise<{
129
+ id: string;
130
+ productId: string;
131
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
132
+ title: string;
133
+ address: string | null;
134
+ city: string | null;
135
+ countryCode: string | null;
136
+ latitude: number | null;
137
+ longitude: number | null;
138
+ googlePlaceId: string | null;
139
+ applePlaceId: string | null;
140
+ tripadvisorLocationId: string | null;
141
+ sortOrder: number;
142
+ createdAt: Date;
143
+ updatedAt: Date;
144
+ } | null>;
145
+ createLocation(db: PostgresJsDatabase, productId: string, data: CreateProductLocationInput): Promise<{
146
+ id: string;
147
+ createdAt: Date;
148
+ updatedAt: Date;
149
+ title: string;
150
+ productId: string;
151
+ sortOrder: number;
152
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
153
+ address: string | null;
154
+ city: string | null;
155
+ countryCode: string | null;
156
+ latitude: number | null;
157
+ longitude: number | null;
158
+ googlePlaceId: string | null;
159
+ applePlaceId: string | null;
160
+ tripadvisorLocationId: string | null;
161
+ } | null>;
162
+ updateLocation(db: PostgresJsDatabase, id: string, data: UpdateProductLocationInput): Promise<{
163
+ id: string;
164
+ productId: string;
165
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
166
+ title: string;
167
+ address: string | null;
168
+ city: string | null;
169
+ countryCode: string | null;
170
+ latitude: number | null;
171
+ longitude: number | null;
172
+ googlePlaceId: string | null;
173
+ applePlaceId: string | null;
174
+ tripadvisorLocationId: string | null;
175
+ sortOrder: number;
176
+ createdAt: Date;
177
+ updatedAt: Date;
178
+ } | null>;
179
+ deleteLocation(db: PostgresJsDatabase, id: string): Promise<{
180
+ id: string;
181
+ } | null>;
182
+ };
183
+ export {};
184
+ //# sourceMappingURL=service-merchandising.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-merchandising.d.ts","sourceRoot":"","sources":["../src/service-merchandising.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,KAAK,EACV,sBAAsB,EACtB,0BAA0B,EAC1B,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,8BAA8B,EAC9B,sBAAsB,EACtB,0BAA0B,EAC1B,2BAA2B,EAC5B,MAAM,iBAAiB,CAAA;AAExB,KAAK,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAA;AAC5E,KAAK,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAA;AAC3E,KAAK,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAA;AAC3E,KAAK,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AACpE,KAAK,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACnE,KAAK,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACnE,KAAK,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAA;AAC9E,KAAK,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAC7E,KAAK,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAY7E,eAAO,MAAM,4BAA4B;qBAChB,kBAAkB,SAAS,uBAAuB;;;;;;;;;;;;;;;uBAgChD,kBAAkB,MAAM,MAAM;;;;;;;;;;sBAK/B,kBAAkB,aAAa,MAAM,QAAQ,yBAAyB;;;;;;;;;;sBActE,kBAAkB,MAAM,MAAM,QAAQ,yBAAyB;;;;;;;;;;sBAU/D,kBAAkB,MAAM,MAAM;;;iBASnC,kBAAkB,SAAS,mBAAmB;;;;;;;;;;;;;;mBA4B5C,kBAAkB,MAAM,MAAM;;;;;;;;;kBAK/B,kBAAkB,aAAa,MAAM,QAAQ,qBAAqB;;;;;;;;;kBAclE,kBAAkB,MAAM,MAAM,QAAQ,qBAAqB;;;;;;;;;kBAU3D,kBAAkB,MAAM,MAAM;;;sBAS1B,kBAAkB,SAAS,wBAAwB;;;;;;;;;;;;;;;;;;;;;;wBAgCjD,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;;;uBAUlD,kBAAkB,aACX,MAAM,QACX,0BAA0B;;;;;;;;;;;;;;;;;uBAeT,kBAAkB,MAAM,MAAM,QAAQ,0BAA0B;;;;;;;;;;;;;;;;;uBAUhE,kBAAkB,MAAM,MAAM;;;CAQxD,CAAA"}
@@ -0,0 +1,181 @@
1
+ import { and, asc, eq, sql } from "drizzle-orm";
2
+ import { productFaqs, productFeatures, productLocations, products } from "./schema.js";
3
+ async function ensureProductExists(db, productId) {
4
+ const [product] = await db
5
+ .select({ id: products.id })
6
+ .from(products)
7
+ .where(eq(products.id, productId))
8
+ .limit(1);
9
+ return product ?? null;
10
+ }
11
+ export const merchandisingProductsService = {
12
+ async listFeatures(db, query) {
13
+ const conditions = [];
14
+ if (query.productId) {
15
+ conditions.push(eq(productFeatures.productId, query.productId));
16
+ }
17
+ if (query.featureType) {
18
+ conditions.push(eq(productFeatures.featureType, query.featureType));
19
+ }
20
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
21
+ const [rows, countResult] = await Promise.all([
22
+ db
23
+ .select()
24
+ .from(productFeatures)
25
+ .where(where)
26
+ .limit(query.limit)
27
+ .offset(query.offset)
28
+ .orderBy(asc(productFeatures.sortOrder), asc(productFeatures.createdAt)),
29
+ db.select({ count: sql `count(*)::int` }).from(productFeatures).where(where),
30
+ ]);
31
+ return {
32
+ data: rows,
33
+ total: countResult[0]?.count ?? 0,
34
+ limit: query.limit,
35
+ offset: query.offset,
36
+ };
37
+ },
38
+ async getFeatureById(db, id) {
39
+ const [row] = await db.select().from(productFeatures).where(eq(productFeatures.id, id)).limit(1);
40
+ return row ?? null;
41
+ },
42
+ async createFeature(db, productId, data) {
43
+ const product = await ensureProductExists(db, productId);
44
+ if (!product) {
45
+ return null;
46
+ }
47
+ const [row] = await db
48
+ .insert(productFeatures)
49
+ .values({ productId, ...data })
50
+ .returning();
51
+ return row ?? null;
52
+ },
53
+ async updateFeature(db, id, data) {
54
+ const [row] = await db
55
+ .update(productFeatures)
56
+ .set({ ...data, updatedAt: new Date() })
57
+ .where(eq(productFeatures.id, id))
58
+ .returning();
59
+ return row ?? null;
60
+ },
61
+ async deleteFeature(db, id) {
62
+ const [row] = await db
63
+ .delete(productFeatures)
64
+ .where(eq(productFeatures.id, id))
65
+ .returning({ id: productFeatures.id });
66
+ return row ?? null;
67
+ },
68
+ async listFaqs(db, query) {
69
+ const conditions = [];
70
+ if (query.productId) {
71
+ conditions.push(eq(productFaqs.productId, query.productId));
72
+ }
73
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
74
+ const [rows, countResult] = await Promise.all([
75
+ db
76
+ .select()
77
+ .from(productFaqs)
78
+ .where(where)
79
+ .limit(query.limit)
80
+ .offset(query.offset)
81
+ .orderBy(asc(productFaqs.sortOrder), asc(productFaqs.createdAt)),
82
+ db.select({ count: sql `count(*)::int` }).from(productFaqs).where(where),
83
+ ]);
84
+ return {
85
+ data: rows,
86
+ total: countResult[0]?.count ?? 0,
87
+ limit: query.limit,
88
+ offset: query.offset,
89
+ };
90
+ },
91
+ async getFaqById(db, id) {
92
+ const [row] = await db.select().from(productFaqs).where(eq(productFaqs.id, id)).limit(1);
93
+ return row ?? null;
94
+ },
95
+ async createFaq(db, productId, data) {
96
+ const product = await ensureProductExists(db, productId);
97
+ if (!product) {
98
+ return null;
99
+ }
100
+ const [row] = await db
101
+ .insert(productFaqs)
102
+ .values({ productId, ...data })
103
+ .returning();
104
+ return row ?? null;
105
+ },
106
+ async updateFaq(db, id, data) {
107
+ const [row] = await db
108
+ .update(productFaqs)
109
+ .set({ ...data, updatedAt: new Date() })
110
+ .where(eq(productFaqs.id, id))
111
+ .returning();
112
+ return row ?? null;
113
+ },
114
+ async deleteFaq(db, id) {
115
+ const [row] = await db
116
+ .delete(productFaqs)
117
+ .where(eq(productFaqs.id, id))
118
+ .returning({ id: productFaqs.id });
119
+ return row ?? null;
120
+ },
121
+ async listLocations(db, query) {
122
+ const conditions = [];
123
+ if (query.productId) {
124
+ conditions.push(eq(productLocations.productId, query.productId));
125
+ }
126
+ if (query.locationType) {
127
+ conditions.push(eq(productLocations.locationType, query.locationType));
128
+ }
129
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
130
+ const [rows, countResult] = await Promise.all([
131
+ db
132
+ .select()
133
+ .from(productLocations)
134
+ .where(where)
135
+ .limit(query.limit)
136
+ .offset(query.offset)
137
+ .orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
138
+ db.select({ count: sql `count(*)::int` }).from(productLocations).where(where),
139
+ ]);
140
+ return {
141
+ data: rows,
142
+ total: countResult[0]?.count ?? 0,
143
+ limit: query.limit,
144
+ offset: query.offset,
145
+ };
146
+ },
147
+ async getLocationById(db, id) {
148
+ const [row] = await db
149
+ .select()
150
+ .from(productLocations)
151
+ .where(eq(productLocations.id, id))
152
+ .limit(1);
153
+ return row ?? null;
154
+ },
155
+ async createLocation(db, productId, data) {
156
+ const product = await ensureProductExists(db, productId);
157
+ if (!product) {
158
+ return null;
159
+ }
160
+ const [row] = await db
161
+ .insert(productLocations)
162
+ .values({ productId, ...data })
163
+ .returning();
164
+ return row ?? null;
165
+ },
166
+ async updateLocation(db, id, data) {
167
+ const [row] = await db
168
+ .update(productLocations)
169
+ .set({ ...data, updatedAt: new Date() })
170
+ .where(eq(productLocations.id, id))
171
+ .returning();
172
+ return row ?? null;
173
+ },
174
+ async deleteLocation(db, id) {
175
+ const [row] = await db
176
+ .delete(productLocations)
177
+ .where(eq(productLocations.id, id))
178
+ .returning({ id: productLocations.id });
179
+ return row ?? null;
180
+ },
181
+ };