@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,517 @@
1
+ // agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
3
+ import { destinations, destinationTranslations, productCapabilities, productCategories, productCategoryProducts, productDestinations, productFaqs, productFeatures, productLocations, productMedia, products, productTagProducts, productTags, productTranslations, productTypes, productVisibilitySettings, } from "./schema.js";
4
+ function normalizeDate(value) {
5
+ if (!value) {
6
+ return null;
7
+ }
8
+ return value instanceof Date ? value.toISOString() : value;
9
+ }
10
+ function normalizeDateTime(value) {
11
+ if (!value) {
12
+ return null;
13
+ }
14
+ return value instanceof Date ? value.toISOString() : value;
15
+ }
16
+ function normalizeLanguageTag(value) {
17
+ const normalized = value?.trim().toLowerCase();
18
+ return normalized || null;
19
+ }
20
+ /**
21
+ * Cap applied to `description` on summary (list) payloads. Full richtext
22
+ * bodies stay on detail lookups (`includeContent: true`); see #1686 —
23
+ * unbounded per-row content drives Worker isolates toward the memory
24
+ * ceiling under storefront list load.
25
+ */
26
+ const SUMMARY_DESCRIPTION_MAX_LENGTH = 500;
27
+ function trimSummaryDescription(description) {
28
+ if (!description || description.length <= SUMMARY_DESCRIPTION_MAX_LENGTH) {
29
+ return description;
30
+ }
31
+ return description.slice(0, SUMMARY_DESCRIPTION_MAX_LENGTH);
32
+ }
33
+ function normalizeLanguageTagList(values) {
34
+ return Array.from(new Set(values
35
+ .map((value) => normalizeLanguageTag(value))
36
+ .filter((value) => Boolean(value))));
37
+ }
38
+ function resolveFallbackLanguageTags(languageTag, fallbackLanguageTags) {
39
+ const normalizedPrimary = normalizeLanguageTag(languageTag);
40
+ return normalizeLanguageTagList([normalizedPrimary, ...(fallbackLanguageTags ?? [])]);
41
+ }
42
+ async function loadCatalogHydrationData(db, productRows, options = {}) {
43
+ const productIds = productRows.map((product) => product.id);
44
+ const productTypeIds = Array.from(new Set(productRows
45
+ .map((product) => product.productTypeId)
46
+ .filter((value) => Boolean(value))));
47
+ const fallbackLanguageTags = resolveFallbackLanguageTags(options.languageTag, options.fallbackLanguageTags);
48
+ const [categoryRows, tagRows, translationRows, typeRows, capabilityRows, mediaRows, featuredRows, featureRows, faqRows, destinationRows, locationRows,] = await Promise.all([
49
+ db
50
+ .select({
51
+ productId: productCategoryProducts.productId,
52
+ id: productCategories.id,
53
+ parentId: productCategories.parentId,
54
+ name: productCategories.name,
55
+ slug: productCategories.slug,
56
+ description: productCategories.description,
57
+ sortOrder: productCategories.sortOrder,
58
+ })
59
+ .from(productCategoryProducts)
60
+ .innerJoin(productCategories, eq(productCategories.id, productCategoryProducts.categoryId))
61
+ .where(and(inArray(productCategoryProducts.productId, productIds), eq(productCategories.active, true)))
62
+ .orderBy(asc(productCategoryProducts.sortOrder), asc(productCategories.sortOrder), asc(productCategories.name)),
63
+ db
64
+ .select({
65
+ productId: productTagProducts.productId,
66
+ id: productTags.id,
67
+ name: productTags.name,
68
+ })
69
+ .from(productTagProducts)
70
+ .innerJoin(productTags, eq(productTags.id, productTagProducts.tagId))
71
+ .where(inArray(productTagProducts.productId, productIds))
72
+ .orderBy(asc(productTags.name)),
73
+ fallbackLanguageTags.length > 0
74
+ ? db
75
+ .select({
76
+ productId: productTranslations.productId,
77
+ languageTag: productTranslations.languageTag,
78
+ slug: productTranslations.slug,
79
+ name: productTranslations.name,
80
+ shortDescription: productTranslations.shortDescription,
81
+ description: productTranslations.description,
82
+ inclusionsHtml: productTranslations.inclusionsHtml,
83
+ exclusionsHtml: productTranslations.exclusionsHtml,
84
+ termsHtml: productTranslations.termsHtml,
85
+ seoTitle: productTranslations.seoTitle,
86
+ seoDescription: productTranslations.seoDescription,
87
+ updatedAt: productTranslations.updatedAt,
88
+ })
89
+ .from(productTranslations)
90
+ .where(and(inArray(productTranslations.productId, productIds), inArray(productTranslations.languageTag, fallbackLanguageTags)))
91
+ : Promise.resolve([]),
92
+ productTypeIds.length > 0
93
+ ? db
94
+ .select({
95
+ id: productTypes.id,
96
+ code: productTypes.code,
97
+ name: productTypes.name,
98
+ description: productTypes.description,
99
+ })
100
+ .from(productTypes)
101
+ .where(and(inArray(productTypes.id, productTypeIds), eq(productTypes.active, true)))
102
+ : Promise.resolve([]),
103
+ db
104
+ .select({
105
+ productId: productCapabilities.productId,
106
+ capability: productCapabilities.capability,
107
+ })
108
+ .from(productCapabilities)
109
+ .where(and(inArray(productCapabilities.productId, productIds), eq(productCapabilities.enabled, true)))
110
+ .orderBy(asc(productCapabilities.capability)),
111
+ db
112
+ .select({
113
+ productId: productMedia.productId,
114
+ id: productMedia.id,
115
+ mediaType: productMedia.mediaType,
116
+ name: productMedia.name,
117
+ url: productMedia.url,
118
+ mimeType: productMedia.mimeType,
119
+ altText: productMedia.altText,
120
+ sortOrder: productMedia.sortOrder,
121
+ isCover: productMedia.isCover,
122
+ isBrochure: productMedia.isBrochure,
123
+ isBrochureCurrent: productMedia.isBrochureCurrent,
124
+ brochureVersion: productMedia.brochureVersion,
125
+ })
126
+ .from(productMedia)
127
+ .where(inArray(productMedia.productId, productIds))
128
+ .orderBy(desc(productMedia.isCover), asc(productMedia.sortOrder), asc(productMedia.createdAt)),
129
+ db
130
+ .select({ productId: productVisibilitySettings.productId })
131
+ .from(productVisibilitySettings)
132
+ .where(and(inArray(productVisibilitySettings.productId, productIds), eq(productVisibilitySettings.isFeatured, true))),
133
+ options.includeContent
134
+ ? db
135
+ .select({
136
+ productId: productFeatures.productId,
137
+ id: productFeatures.id,
138
+ featureType: productFeatures.featureType,
139
+ title: productFeatures.title,
140
+ description: productFeatures.description,
141
+ sortOrder: productFeatures.sortOrder,
142
+ })
143
+ .from(productFeatures)
144
+ .where(inArray(productFeatures.productId, productIds))
145
+ .orderBy(asc(productFeatures.sortOrder), asc(productFeatures.createdAt))
146
+ : Promise.resolve([]),
147
+ options.includeContent
148
+ ? db
149
+ .select({
150
+ productId: productFaqs.productId,
151
+ id: productFaqs.id,
152
+ question: productFaqs.question,
153
+ answer: productFaqs.answer,
154
+ sortOrder: productFaqs.sortOrder,
155
+ })
156
+ .from(productFaqs)
157
+ .where(inArray(productFaqs.productId, productIds))
158
+ .orderBy(asc(productFaqs.sortOrder), asc(productFaqs.createdAt))
159
+ : Promise.resolve([]),
160
+ db
161
+ .select({
162
+ productId: productDestinations.productId,
163
+ destinationId: destinations.id,
164
+ parentId: destinations.parentId,
165
+ slug: destinations.slug,
166
+ canonicalPlaceId: destinations.canonicalPlaceId,
167
+ destinationType: destinations.destinationType,
168
+ latitude: destinations.latitude,
169
+ longitude: destinations.longitude,
170
+ sortOrder: productDestinations.sortOrder,
171
+ fallbackSortOrder: destinations.sortOrder,
172
+ translationLanguageTag: destinationTranslations.languageTag,
173
+ translationName: destinationTranslations.name,
174
+ translationDescription: destinationTranslations.description,
175
+ translationSeoTitle: destinationTranslations.seoTitle,
176
+ translationSeoDescription: destinationTranslations.seoDescription,
177
+ })
178
+ .from(productDestinations)
179
+ .innerJoin(destinations, eq(destinations.id, productDestinations.destinationId))
180
+ .leftJoin(destinationTranslations, and(eq(destinationTranslations.destinationId, destinations.id), fallbackLanguageTags.length > 0
181
+ ? inArray(destinationTranslations.languageTag, fallbackLanguageTags)
182
+ : sql `true`))
183
+ .where(inArray(productDestinations.productId, productIds))
184
+ .orderBy(asc(productDestinations.sortOrder), asc(destinations.sortOrder), asc(destinationTranslations.languageTag)),
185
+ db
186
+ .select({
187
+ productId: productLocations.productId,
188
+ id: productLocations.id,
189
+ locationType: productLocations.locationType,
190
+ title: productLocations.title,
191
+ address: productLocations.address,
192
+ city: productLocations.city,
193
+ countryCode: productLocations.countryCode,
194
+ latitude: productLocations.latitude,
195
+ longitude: productLocations.longitude,
196
+ sortOrder: productLocations.sortOrder,
197
+ })
198
+ .from(productLocations)
199
+ .where(inArray(productLocations.productId, productIds))
200
+ .orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
201
+ ]);
202
+ const categoriesByProduct = new Map();
203
+ for (const row of categoryRows) {
204
+ const existing = categoriesByProduct.get(row.productId) ?? [];
205
+ existing.push(row);
206
+ categoriesByProduct.set(row.productId, existing);
207
+ }
208
+ const tagsByProduct = new Map();
209
+ for (const row of tagRows) {
210
+ const existing = tagsByProduct.get(row.productId) ?? [];
211
+ existing.push(row);
212
+ tagsByProduct.set(row.productId, existing);
213
+ }
214
+ const translationsByProduct = new Map();
215
+ for (const row of translationRows) {
216
+ const existing = translationsByProduct.get(row.productId) ?? [];
217
+ existing.push(row);
218
+ translationsByProduct.set(row.productId, existing);
219
+ }
220
+ const capabilitiesByProduct = new Map();
221
+ for (const row of capabilityRows) {
222
+ const existing = capabilitiesByProduct.get(row.productId) ?? [];
223
+ existing.push(row.capability);
224
+ capabilitiesByProduct.set(row.productId, existing);
225
+ }
226
+ const mediaByProduct = new Map();
227
+ for (const row of mediaRows) {
228
+ const existing = mediaByProduct.get(row.productId) ?? [];
229
+ existing.push(row);
230
+ mediaByProduct.set(row.productId, existing);
231
+ }
232
+ const featuresByProduct = new Map();
233
+ for (const row of featureRows) {
234
+ const existing = featuresByProduct.get(row.productId) ?? [];
235
+ existing.push(row);
236
+ featuresByProduct.set(row.productId, existing);
237
+ }
238
+ const faqsByProduct = new Map();
239
+ for (const row of faqRows) {
240
+ const existing = faqsByProduct.get(row.productId) ?? [];
241
+ existing.push(row);
242
+ faqsByProduct.set(row.productId, existing);
243
+ }
244
+ const destinationsByProduct = new Map();
245
+ const destinationRowsByProductAndDestination = new Map();
246
+ for (const row of destinationRows) {
247
+ const key = `${row.productId}:${row.destinationId}`;
248
+ const existing = destinationRowsByProductAndDestination.get(key) ?? [];
249
+ existing.push(row);
250
+ destinationRowsByProductAndDestination.set(key, existing);
251
+ }
252
+ for (const rows of destinationRowsByProductAndDestination.values()) {
253
+ const first = rows[0];
254
+ if (!first) {
255
+ continue;
256
+ }
257
+ const translated = fallbackLanguageTags.length === 0
258
+ ? rows[0]
259
+ : (fallbackLanguageTags
260
+ .map((languageTag) => rows.find((row) => normalizeLanguageTag(row.translationLanguageTag) === languageTag))
261
+ .find(Boolean) ?? rows[0]);
262
+ const mapped = {
263
+ id: first.destinationId,
264
+ parentId: first.parentId ?? null,
265
+ slug: first.slug,
266
+ canonicalPlaceId: first.canonicalPlaceId ?? null,
267
+ destinationType: first.destinationType,
268
+ latitude: first.latitude ?? null,
269
+ longitude: first.longitude ?? null,
270
+ sortOrder: first.sortOrder ?? first.fallbackSortOrder ?? 0,
271
+ name: translated?.translationName ?? first.slug,
272
+ description: translated?.translationDescription ?? null,
273
+ seoTitle: translated?.translationSeoTitle ?? null,
274
+ seoDescription: translated?.translationSeoDescription ?? null,
275
+ };
276
+ const existing = destinationsByProduct.get(first.productId) ?? [];
277
+ existing.push(mapped);
278
+ destinationsByProduct.set(first.productId, existing);
279
+ }
280
+ const locationsByProduct = new Map();
281
+ for (const row of locationRows) {
282
+ const existing = locationsByProduct.get(row.productId) ?? [];
283
+ existing.push(row);
284
+ locationsByProduct.set(row.productId, existing);
285
+ }
286
+ const typeById = new Map(typeRows.map((row) => [row.id, row]));
287
+ const featuredIds = new Set(featuredRows.map((row) => row.productId));
288
+ const translationByProduct = new Map();
289
+ for (const productId of productIds) {
290
+ const rows = translationsByProduct.get(productId) ?? [];
291
+ const selected = fallbackLanguageTags.length === 0
292
+ ? null
293
+ : (fallbackLanguageTags
294
+ .map((languageTag) => rows.find((row) => normalizeLanguageTag(row.languageTag) === languageTag))
295
+ .find(Boolean) ?? null);
296
+ translationByProduct.set(productId, selected);
297
+ }
298
+ return {
299
+ categoriesByProduct,
300
+ tagsByProduct,
301
+ translationByProduct,
302
+ capabilitiesByProduct,
303
+ mediaByProduct,
304
+ featuresByProduct,
305
+ faqsByProduct,
306
+ destinationsByProduct,
307
+ locationsByProduct,
308
+ typeById,
309
+ featuredIds,
310
+ };
311
+ }
312
+ export const catalogProductsService = {
313
+ async hydrateProducts(db, productRows, options = {}) {
314
+ if (productRows.length === 0) {
315
+ return [];
316
+ }
317
+ const hydrationData = await loadCatalogHydrationData(db, productRows, options);
318
+ return productRows.map((product) => {
319
+ const translation = hydrationData.translationByProduct.get(product.id) ?? null;
320
+ const allMedia = (hydrationData.mediaByProduct.get(product.id) ?? []).map((row) => ({
321
+ id: row.id,
322
+ mediaType: row.mediaType,
323
+ name: row.name,
324
+ url: row.url,
325
+ mimeType: row.mimeType ?? null,
326
+ altText: row.altText ?? null,
327
+ sortOrder: row.sortOrder,
328
+ isCover: row.isCover,
329
+ isBrochure: row.isBrochure,
330
+ isBrochureCurrent: row.isBrochureCurrent,
331
+ brochureVersion: row.brochureVersion ?? null,
332
+ }));
333
+ const brochure = allMedia.find((item) => item.isBrochure && item.isBrochureCurrent) ??
334
+ allMedia.find((item) => item.isBrochure) ??
335
+ null;
336
+ const media = allMedia.filter((item) => !item.isBrochure);
337
+ const base = {
338
+ id: product.id,
339
+ name: translation?.name ?? product.name,
340
+ description: translation?.description ?? product.description ?? null,
341
+ inclusionsHtml: translation?.inclusionsHtml ?? product.inclusionsHtml ?? null,
342
+ exclusionsHtml: translation?.exclusionsHtml ?? product.exclusionsHtml ?? null,
343
+ termsHtml: translation?.termsHtml ?? product.termsHtml ?? null,
344
+ contentLanguageTag: translation?.languageTag ?? null,
345
+ slug: translation?.slug ?? null,
346
+ shortDescription: translation?.shortDescription ?? null,
347
+ seoTitle: translation?.seoTitle ?? null,
348
+ seoDescription: translation?.seoDescription ?? null,
349
+ bookingMode: product.bookingMode,
350
+ capacityMode: product.capacityMode,
351
+ visibility: product.visibility,
352
+ sellCurrency: product.sellCurrency,
353
+ sellAmountCents: product.sellAmountCents ?? null,
354
+ startDate: normalizeDate(product.startDate),
355
+ endDate: normalizeDate(product.endDate),
356
+ pax: product.pax ?? null,
357
+ contractTemplateId: product.contractTemplateId ?? null,
358
+ productType: product.productTypeId
359
+ ? (hydrationData.typeById.get(product.productTypeId) ?? null)
360
+ : null,
361
+ categories: (hydrationData.categoriesByProduct.get(product.id) ?? []).map((row) => ({
362
+ id: row.id,
363
+ parentId: row.parentId ?? null,
364
+ name: row.name,
365
+ slug: row.slug,
366
+ description: row.description ?? null,
367
+ sortOrder: row.sortOrder,
368
+ })),
369
+ tags: (hydrationData.tagsByProduct.get(product.id) ?? []).map((row) => ({
370
+ id: row.id,
371
+ name: row.name,
372
+ })),
373
+ capabilities: hydrationData.capabilitiesByProduct.get(product.id) ?? [],
374
+ destinations: (hydrationData.destinationsByProduct.get(product.id) ?? []).map((row) => ({
375
+ id: row.id,
376
+ parentId: row.parentId,
377
+ slug: row.slug,
378
+ canonicalPlaceId: row.canonicalPlaceId,
379
+ name: row.name,
380
+ description: row.description,
381
+ seoTitle: row.seoTitle,
382
+ seoDescription: row.seoDescription,
383
+ destinationType: row.destinationType,
384
+ latitude: row.latitude,
385
+ longitude: row.longitude,
386
+ sortOrder: row.sortOrder,
387
+ })),
388
+ locations: (hydrationData.locationsByProduct.get(product.id) ?? []).map((row) => ({
389
+ id: row.id,
390
+ locationType: row.locationType,
391
+ title: row.title,
392
+ address: row.address ?? null,
393
+ city: row.city ?? null,
394
+ countryCode: row.countryCode ?? null,
395
+ latitude: row.latitude ?? null,
396
+ longitude: row.longitude ?? null,
397
+ sortOrder: row.sortOrder,
398
+ })),
399
+ coverMedia: media.find((item) => item.isCover) ?? media[0] ?? null,
400
+ isFeatured: hydrationData.featuredIds.has(product.id),
401
+ };
402
+ if (!options.includeContent) {
403
+ // Summary (list) shape: heavy richtext fields are nulled and the
404
+ // long-form description is capped. Detail callers pass
405
+ // `includeContent: true` and keep the full content (#1686).
406
+ return {
407
+ ...base,
408
+ description: trimSummaryDescription(base.description),
409
+ inclusionsHtml: null,
410
+ exclusionsHtml: null,
411
+ termsHtml: null,
412
+ };
413
+ }
414
+ return {
415
+ ...base,
416
+ brochure,
417
+ media,
418
+ features: (hydrationData.featuresByProduct.get(product.id) ?? []).map((row) => ({
419
+ id: row.id,
420
+ featureType: row.featureType,
421
+ title: row.title,
422
+ description: row.description ?? null,
423
+ sortOrder: row.sortOrder,
424
+ })),
425
+ faqs: (hydrationData.faqsByProduct.get(product.id) ?? []).map((row) => ({
426
+ id: row.id,
427
+ question: row.question,
428
+ answer: row.answer,
429
+ sortOrder: row.sortOrder,
430
+ })),
431
+ };
432
+ });
433
+ },
434
+ async listSearchDocuments(db, query) {
435
+ const conditions = [];
436
+ if (query.status === "active") {
437
+ conditions.push(eq(products.status, "active"), eq(products.activated, true));
438
+ }
439
+ if (query.visibility === "public") {
440
+ conditions.push(eq(products.visibility, "public"));
441
+ }
442
+ if (query.productIds && query.productIds.length > 0) {
443
+ conditions.push(inArray(products.id, query.productIds));
444
+ }
445
+ const where = conditions.length > 0 ? and(...conditions) : undefined;
446
+ const [rows, countResult] = await Promise.all([
447
+ db
448
+ .select()
449
+ .from(products)
450
+ .where(where)
451
+ .orderBy(asc(products.createdAt), asc(products.id))
452
+ .limit(query.limit)
453
+ .offset(query.offset),
454
+ db.select({ count: sql `count(*)::int` }).from(products).where(where),
455
+ ]);
456
+ const localizedProducts = (await this.hydrateProducts(db, rows, {
457
+ includeContent: true,
458
+ languageTag: query.languageTag,
459
+ fallbackLanguageTags: query.fallbackLanguageTags ?? (query.languageTag ? ["en", "ro"] : []),
460
+ }));
461
+ const rowById = new Map(rows.map((row) => [row.id, row]));
462
+ return {
463
+ data: localizedProducts.map((product) => ({
464
+ id: `${product.id}:${product.contentLanguageTag ?? "default"}`,
465
+ productId: product.id,
466
+ languageTag: product.contentLanguageTag,
467
+ name: product.name,
468
+ slug: product.slug,
469
+ shortDescription: product.shortDescription,
470
+ description: product.description,
471
+ seoTitle: product.seoTitle,
472
+ seoDescription: product.seoDescription,
473
+ sellCurrency: product.sellCurrency,
474
+ sellAmountCents: product.sellAmountCents,
475
+ startDate: product.startDate,
476
+ endDate: product.endDate,
477
+ pax: product.pax,
478
+ productTypeCode: product.productType?.code ?? null,
479
+ productTypeName: product.productType?.name ?? null,
480
+ categoryIds: product.categories.map((category) => category.id),
481
+ categoryNames: product.categories.map((category) => category.name),
482
+ categorySlugs: product.categories.map((category) => category.slug),
483
+ tagIds: product.tags.map((tag) => tag.id),
484
+ tagNames: product.tags.map((tag) => tag.name),
485
+ capabilities: product.capabilities,
486
+ destinationIds: product.destinations.map((destination) => destination.id),
487
+ destinationNames: product.destinations.map((destination) => destination.name),
488
+ destinationSlugs: product.destinations.map((destination) => destination.slug),
489
+ locationTitles: product.locations.map((location) => location.title),
490
+ locationCities: product.locations
491
+ .map((location) => location.city)
492
+ .filter((value) => Boolean(value)),
493
+ locationCountryCodes: product.locations
494
+ .map((location) => location.countryCode)
495
+ .filter((value) => Boolean(value)),
496
+ coverMediaUrl: product.coverMedia?.url ?? null,
497
+ isFeatured: product.isFeatured,
498
+ createdAt: normalizeDateTime(rowById.get(product.id)?.createdAt),
499
+ updatedAt: normalizeDateTime(rowById.get(product.id)?.updatedAt),
500
+ })),
501
+ total: countResult[0]?.count ?? 0,
502
+ limit: query.limit,
503
+ offset: query.offset,
504
+ };
505
+ },
506
+ async getSearchDocumentByProductId(db, productId, query = {}) {
507
+ const result = await this.listSearchDocuments(db, {
508
+ visibility: query.visibility ?? "public",
509
+ status: query.status ?? "active",
510
+ ...query,
511
+ productIds: [productId],
512
+ limit: 1,
513
+ offset: 0,
514
+ });
515
+ return result.data[0] ?? null;
516
+ },
517
+ };