@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,431 @@
1
+ // agent-quality: file-size exception -- owner: inventory; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ /**
3
+ * Catalog-plane integration for the products service.
4
+ *
5
+ * Adds catalog-aware service methods alongside the existing `productsService`
6
+ * surface in `service.ts`. Routes opt in: the original `getProductById` /
7
+ * `listProducts` continue to return raw DB rows; the methods here return
8
+ * resolved CatalogEntry views with overlays + visibility filtering applied.
9
+ *
10
+ * Existing service code is untouched. Migration is per-route, gradual.
11
+ *
12
+ * Naming note: this file is `service-catalog-plane.ts` (not `service-catalog.ts`)
13
+ * because the existing `service-catalog.ts` handles the products module's own
14
+ * catalog management (categories, tags, types). The "catalog plane" is the
15
+ * cross-vertical projection / overlay / snapshot infrastructure from
16
+ * `@voyant-travel/catalog`.
17
+ *
18
+ * See `docs/architecture/catalog-architecture.md` §9.1 for the integration
19
+ * pattern this file establishes (replicated for cruises, accommodations, etc.
20
+ * in their own service-catalog-plane.ts files).
21
+ */
22
+ import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, fetchOverlaysForEntities, resolveEntityView, resolveEntityViewWithOverlays, } from "@voyant-travel/catalog";
23
+ import { asc, eq, sql } from "drizzle-orm";
24
+ import { productCatalogPolicy } from "./catalog-policy.js";
25
+ import { products } from "./schema-core.js";
26
+ import { productDays, productItineraries, productMedia } from "./schema-itinerary.js";
27
+ import { productLocations, productTranslations } from "./schema-settings.js";
28
+ /**
29
+ * Lazy-initialized registry. Built once per process; the field-policy file
30
+ * is static so this is safe to memoize.
31
+ */
32
+ let _registry;
33
+ function getProductsRegistry() {
34
+ if (!_registry) {
35
+ _registry = createFieldPolicyRegistry(productCatalogPolicy);
36
+ }
37
+ return _registry;
38
+ }
39
+ /**
40
+ * Maps a product row to a field-keyed projection consumable by the catalog
41
+ * resolver. Field paths match the policy registry declarations in
42
+ * `catalog-policy.ts`.
43
+ *
44
+ * Provenance fields (`source.kind`, `source.ref`, `seller.operator_id`) are
45
+ * synthesized: today's products module models operator-owned inventory
46
+ * exclusively, so `source.kind = "owned"` and `source.ref = undefined`.
47
+ * When sourced products land (e.g. via Voyant Connect), this helper picks
48
+ * up the provenance from a parallel provenance row instead.
49
+ */
50
+ export function productRowToProjection(row, context) {
51
+ const projection = new Map([
52
+ // Provenance — synthesized for owned products.
53
+ ["source.kind", "owned"],
54
+ ["seller.operator_id", context.sellerOperatorId],
55
+ // Identity
56
+ ["id", row.id],
57
+ ["createdAt", row.createdAt],
58
+ ["updatedAt", row.updatedAt],
59
+ // Merchandisable
60
+ ["name", row.name],
61
+ ["description", row.description],
62
+ ["inclusionsHtml", row.inclusionsHtml],
63
+ ["exclusionsHtml", row.exclusionsHtml],
64
+ ["termsHtml", row.termsHtml],
65
+ ["tags[]", row.tags],
66
+ // Structural
67
+ ["status", row.status],
68
+ ["bookingMode", row.bookingMode],
69
+ ["capacityMode", row.capacityMode],
70
+ ["visibility", row.visibility],
71
+ ["activated", row.activated],
72
+ ["productTypeId", row.productTypeId],
73
+ ["facilityId", row.facilityId],
74
+ ["supplierId", row.supplierId],
75
+ ["pax", row.pax],
76
+ ["startDate", row.startDate],
77
+ ["endDate", row.endDate],
78
+ ["startDateEpochDays", dateToEpochDays(row.startDate)],
79
+ ["endDateEpochDays", dateToEpochDays(row.endDate)],
80
+ ["timezone", row.timezone],
81
+ ["reservationTimeoutMinutes", row.reservationTimeoutMinutes],
82
+ ["termsShowOnContract", row.termsShowOnContract],
83
+ // Pricing (configured defaults — quote-time prices come from pricing module)
84
+ ["sellAmountCents", row.sellAmountCents],
85
+ ["sellCurrency", row.sellCurrency],
86
+ // Internal / staff-only
87
+ ["costAmountCents", row.costAmountCents],
88
+ ["marginPercent", row.marginPercent],
89
+ ]);
90
+ return projection;
91
+ }
92
+ /**
93
+ * Returns the Provenance tuple for a product row. Owned products synthesize
94
+ * a `source.kind: "owned"` provenance with `static` freshness; sourced
95
+ * products (Voyant Connect / GDS / direct API) carry their actual source
96
+ * connection identity. Phase 1 ships only the owned form.
97
+ */
98
+ export function productProvenance(_row, _context) {
99
+ return {
100
+ source_kind: "owned",
101
+ source_freshness: "static",
102
+ };
103
+ }
104
+ /**
105
+ * Catalog-aware product fetch. Returns the resolved view (source projection
106
+ * + active overlays + visibility filtering) instead of the raw DB row.
107
+ *
108
+ * The original `productsService.getProductById` continues to return raw
109
+ * rows — routes that haven't migrated to the catalog plane keep working.
110
+ *
111
+ * Returns `null` if no product with `id` exists.
112
+ */
113
+ export async function getResolvedProductById(db, id, context) {
114
+ const rows = await db.select().from(products).where(eq(products.id, id)).limit(1);
115
+ const row = rows[0];
116
+ if (!row)
117
+ return null;
118
+ const projection = productRowToProjection(row, {
119
+ sellerOperatorId: context.sellerOperatorId,
120
+ });
121
+ return resolveEntityView(db, getProductsRegistry(), "products", id, projection, context.scope);
122
+ }
123
+ /**
124
+ * Catalog-aware product list. Returns resolved views per row.
125
+ *
126
+ * Caller fetches the rows (typically via the existing `productsService.listProducts`
127
+ * with whatever filtering / pagination / sort the route applies) and passes
128
+ * them in. This keeps query construction in the existing service layer and
129
+ * adds the catalog overlay step on top.
130
+ *
131
+ * Overlays for the whole page are fetched in ONE query via
132
+ * `fetchOverlaysForEntities` and applied in-memory per product — the
133
+ * per-product output is byte-identical to calling `resolveEntityView`
134
+ * once per row, minus the N-1 sequential round trips.
135
+ *
136
+ * Real high-volume list paths (storefront browse, admin search) should
137
+ * still go through the search index instead — `IndexerService.search` is
138
+ * already wired for that purpose. Use this method for small admin-facing
139
+ * lists or detail-page composition where the index isn't on the read path.
140
+ */
141
+ export async function listResolvedProducts(db, rows, context) {
142
+ const registry = getProductsRegistry();
143
+ const overlaysByEntity = await fetchOverlaysForEntities(db, "products", rows.map((row) => row.id));
144
+ return rows.map((row) => {
145
+ const projection = productRowToProjection(row, {
146
+ sellerOperatorId: context.sellerOperatorId,
147
+ });
148
+ return resolveEntityViewWithOverlays(registry, projection, overlaysByEntity.get(row.id) ?? [], context.scope);
149
+ });
150
+ }
151
+ /**
152
+ * Build a `CaptureSnapshotInput` for a product to feed into the catalog
153
+ * plane's `captureSnapshot` / `captureSnapshotGraph` helpers at booking
154
+ * commit time. Fetches the product, resolves its view (overlays applied,
155
+ * visibility filter for the supplied scope), and returns the snapshot
156
+ * input shape.
157
+ *
158
+ * Returns `null` if the product doesn't exist.
159
+ *
160
+ * Composition: a single-product booking calls this once and passes the
161
+ * result to `captureSnapshot`. A composite booking (e.g. a tour-package
162
+ * booking with referenced accommodations + excursions) calls this and the
163
+ * other verticals' equivalents, collects the inputs, and passes them all
164
+ * to `captureSnapshotGraph` in one transaction.
165
+ */
166
+ export async function buildProductSnapshotInput(db, productId, context) {
167
+ const view = await getResolvedProductById(db, productId, context);
168
+ if (!view)
169
+ return null;
170
+ return buildSnapshotInputFromView(view, {
171
+ entityModule: "products",
172
+ entityId: productId,
173
+ sourceKind: "owned",
174
+ pricingBasis: context.pricingBasis,
175
+ });
176
+ }
177
+ /**
178
+ * Compose the registry from the base product policy plus any contributing
179
+ * extensions' policies. Templates wire this when they enable child-entity
180
+ * registries.
181
+ */
182
+ export function createProductsRegistry(...extensionPolicies) {
183
+ if (extensionPolicies.length === 0)
184
+ return getProductsRegistry();
185
+ const composed = [...productCatalogPolicy];
186
+ for (const policies of extensionPolicies) {
187
+ composed.push(...policies);
188
+ }
189
+ return createFieldPolicyRegistry(composed);
190
+ }
191
+ /**
192
+ * Construct a sync `DocumentEmitter` for products. The emitter takes a
193
+ * pre-fetched product row + a slice and returns the indexer document
194
+ * (filtered by visibility, with blob-only fields skipped).
195
+ *
196
+ * Bulk-reindex pipelines that already have rows in hand call this directly.
197
+ * Live reindex paths use `createProductDocumentBuilder` below, which fetches
198
+ * the row before emitting.
199
+ *
200
+ * Pass a custom `registry` when the deployment composes additional
201
+ * child-entity policies; otherwise the default products registry is used.
202
+ */
203
+ export function createProductDocumentEmitter(context) {
204
+ const registry = context.registry ?? getProductsRegistry();
205
+ return {
206
+ vertical: "products",
207
+ emit(source, slice) {
208
+ const projection = productRowToProjection(source, {
209
+ sellerOperatorId: context.sellerOperatorId,
210
+ });
211
+ return buildIndexerDocument(registry, projection, slice, source.id);
212
+ },
213
+ };
214
+ }
215
+ function isPublicStorefrontProduct(row) {
216
+ return row.status === "active" && row.activated === true && row.visibility === "public";
217
+ }
218
+ function shouldEmitForSlice(row, slice) {
219
+ // The catalog is a "bookable now" surface — draft and archived
220
+ // products don't belong there, regardless of audience. Operators
221
+ // browsing /catalog get the same active-only set the storefront sees,
222
+ // just with staff-visible attribute columns.
223
+ if (row.status !== "active")
224
+ return false;
225
+ if (slice.audience === "customer" ||
226
+ slice.audience === "partner" ||
227
+ slice.audience === "supplier") {
228
+ return isPublicStorefrontProduct(row);
229
+ }
230
+ return true;
231
+ }
232
+ /**
233
+ * Async `DocumentBuilder` for products — fetches the row by id, then emits.
234
+ * Plug this into `IndexerService.reindexEntity` for live reindex events.
235
+ *
236
+ * Returns `null` if the product no longer exists (e.g. it was deleted
237
+ * between the reindex enqueue and the worker picking it up). Callers can
238
+ * treat `null` as a delete signal.
239
+ *
240
+ * `extensions` denormalize child-entity fields onto the product doc. They
241
+ * run in parallel after the base row is fetched. An extension that throws
242
+ * fails the whole build — failures here would otherwise produce silently
243
+ * incomplete documents.
244
+ *
245
+ * Pass a custom `registry` (composed via `createProductsRegistry`) when
246
+ * extensions contribute fields beyond the base products policy.
247
+ */
248
+ export function createProductDocumentBuilder(db, context) {
249
+ const registry = context.registry ?? getProductsRegistry();
250
+ const extensions = context.extensions ?? [];
251
+ return async (entityId, slice) => {
252
+ const rows = await db.select().from(products).where(eq(products.id, entityId)).limit(1);
253
+ const row = rows[0];
254
+ if (!row)
255
+ return null;
256
+ if (!shouldEmitForSlice(row, slice))
257
+ return null;
258
+ const baseProjection = productRowToProjection(row, {
259
+ sellerOperatorId: context.sellerOperatorId,
260
+ });
261
+ if (extensions.length === 0) {
262
+ return buildIndexerDocument(registry, baseProjection, slice, entityId);
263
+ }
264
+ const extensionProjections = await Promise.all(extensions.map((ext) => ext.project(db, entityId, slice)));
265
+ const merged = new Map(baseProjection);
266
+ for (const projection of extensionProjections) {
267
+ for (const [path, value] of projection) {
268
+ merged.set(path, value);
269
+ }
270
+ }
271
+ return buildIndexerDocument(registry, merged, slice, entityId);
272
+ };
273
+ }
274
+ /**
275
+ * Product-owned storefront-card projection. This extension keeps the
276
+ * customer catalog slice directly renderable by denormalizing localized
277
+ * routing, card media, duration, and map coordinates into the search doc.
278
+ */
279
+ export function createProductStorefrontCardProjectionExtension() {
280
+ return {
281
+ name: "products:storefront-card",
282
+ async project(db, productId, slice) {
283
+ // Wave 1: everything keyed by productId alone runs concurrently —
284
+ // including the departures count, which used to trail sequentially.
285
+ const [translations, mediaRows, locationRows, itineraryRows, availableDeparturesCount] = await Promise.all([
286
+ db
287
+ .select({
288
+ languageTag: productTranslations.languageTag,
289
+ name: productTranslations.name,
290
+ slug: productTranslations.slug,
291
+ shortDescription: productTranslations.shortDescription,
292
+ inclusionsHtml: productTranslations.inclusionsHtml,
293
+ exclusionsHtml: productTranslations.exclusionsHtml,
294
+ termsHtml: productTranslations.termsHtml,
295
+ })
296
+ .from(productTranslations)
297
+ .where(eq(productTranslations.productId, productId))
298
+ .orderBy(asc(productTranslations.updatedAt)),
299
+ db
300
+ .select({
301
+ url: productMedia.url,
302
+ mediaType: productMedia.mediaType,
303
+ isCover: productMedia.isCover,
304
+ isBrochure: productMedia.isBrochure,
305
+ sortOrder: productMedia.sortOrder,
306
+ createdAt: productMedia.createdAt,
307
+ })
308
+ .from(productMedia)
309
+ .where(eq(productMedia.productId, productId))
310
+ .orderBy(asc(productMedia.sortOrder), asc(productMedia.createdAt)),
311
+ db
312
+ .select({
313
+ latitude: productLocations.latitude,
314
+ longitude: productLocations.longitude,
315
+ sortOrder: productLocations.sortOrder,
316
+ createdAt: productLocations.createdAt,
317
+ })
318
+ .from(productLocations)
319
+ .where(eq(productLocations.productId, productId))
320
+ .orderBy(asc(productLocations.sortOrder), asc(productLocations.createdAt)),
321
+ db
322
+ .select({ id: productItineraries.id, isDefault: productItineraries.isDefault })
323
+ .from(productItineraries)
324
+ .where(eq(productItineraries.productId, productId))
325
+ .orderBy(asc(productItineraries.sortOrder)),
326
+ countAvailableDepartures(db, productId),
327
+ ]);
328
+ const translation = pickTranslation(translations, slice.locale);
329
+ const imageMediaRows = mediaRows.filter((m) => !m.isBrochure && m.mediaType === "image");
330
+ const cover = imageMediaRows.find((m) => m.isCover);
331
+ const primaryMedia = cover ?? imageMediaRows[0] ?? null;
332
+ const coordinateLocation = locationRows.find((l) => l.latitude != null && l.longitude != null) ?? null;
333
+ const defaultItinerary = itineraryRows.find((it) => it.isDefault) ?? itineraryRows[0];
334
+ // Wave 2: the duration estimate needs `defaultItinerary`, which is only
335
+ // known after the itinerary rows land — it cannot join wave 1.
336
+ const durationDays = defaultItinerary
337
+ ? await estimateItineraryDurationDays(db, defaultItinerary.id)
338
+ : null;
339
+ const out = new Map([
340
+ ["slug", translation?.slug ?? null],
341
+ ["shortDescription", translation?.shortDescription ?? null],
342
+ ["primaryMediaUrl", primaryMedia?.url ?? null],
343
+ ["thumbnailUrl", primaryMedia?.url ?? null],
344
+ ["coverMediaUrl", primaryMedia?.url ?? null],
345
+ ["durationDays", durationDays],
346
+ ["availableDeparturesCount", availableDeparturesCount],
347
+ ["latitude", coordinateLocation?.latitude ?? null],
348
+ ["longitude", coordinateLocation?.longitude ?? null],
349
+ ]);
350
+ if (translation?.name) {
351
+ out.set("name", translation.name);
352
+ }
353
+ if (translation?.inclusionsHtml != null) {
354
+ out.set("inclusionsHtml", translation.inclusionsHtml);
355
+ }
356
+ if (translation?.exclusionsHtml != null) {
357
+ out.set("exclusionsHtml", translation.exclusionsHtml);
358
+ }
359
+ if (translation?.termsHtml != null) {
360
+ out.set("termsHtml", translation.termsHtml);
361
+ }
362
+ return out;
363
+ },
364
+ };
365
+ }
366
+ function pickTranslation(rows, locale) {
367
+ return (rows.find((row) => row.languageTag === locale) ??
368
+ rows.find((row) => row.languageTag.toLowerCase() === locale.toLowerCase()) ??
369
+ rows.find((row) => row.languageTag.split("-")[0] === locale.split("-")[0]) ??
370
+ rows[0] ??
371
+ null);
372
+ }
373
+ /**
374
+ * Counts future, open availability slots for a product — surfaces in the
375
+ * catalog index as `availableDeparturesCount` so the operator catalog
376
+ * table can show booking-ready stock at a glance without a separate
377
+ * round-trip. Owned-products only; sourced products carry departures via
378
+ * the upstream feed and don't write to `availability_slots`.
379
+ *
380
+ * Cross-package boundary: queries `availability_slots` by raw table name
381
+ * via `sql` so the products module doesn't take a hard dependency on the
382
+ * `@voyant-travel/operations` schema.
383
+ */
384
+ async function countAvailableDepartures(db, productId) {
385
+ try {
386
+ const result = await db.execute(sql `
387
+ SELECT COUNT(*)::int AS count
388
+ FROM availability_slots
389
+ WHERE product_id = ${productId}
390
+ AND status = 'open'
391
+ AND starts_at >= NOW()
392
+ `);
393
+ // postgres-js + neon-serverless both return `{ count }` on the first row;
394
+ // shape-defensive read in case a driver wraps it differently.
395
+ const rows = Array.isArray(result) ? result : (result.rows ?? []);
396
+ const row = rows[0];
397
+ const value = row?.count;
398
+ if (typeof value === "number")
399
+ return value;
400
+ if (typeof value === "string") {
401
+ const n = Number(value);
402
+ return Number.isFinite(n) ? n : 0;
403
+ }
404
+ return 0;
405
+ }
406
+ catch {
407
+ // availability_slots may not exist in slim test fixtures; treat as 0
408
+ // so reindex doesn't fail.
409
+ return 0;
410
+ }
411
+ }
412
+ async function estimateItineraryDurationDays(db, itineraryId) {
413
+ const rows = await db
414
+ .select({ dayNumber: productDays.dayNumber })
415
+ .from(productDays)
416
+ .where(eq(productDays.itineraryId, itineraryId))
417
+ .orderBy(asc(productDays.dayNumber));
418
+ if (rows.length === 0)
419
+ return null;
420
+ const max = Math.max(...rows.map((row) => row.dayNumber));
421
+ return Number.isFinite(max) && max > 0 ? max : null;
422
+ }
423
+ function dateToEpochDays(value) {
424
+ if (!value)
425
+ return null;
426
+ const date = typeof value === "string" ? new Date(value) : value;
427
+ const time = date.getTime();
428
+ if (Number.isNaN(time))
429
+ return null;
430
+ return Math.floor(time / (24 * 60 * 60 * 1000));
431
+ }
@@ -0,0 +1,251 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import { products } from "./schema.js";
3
+ import type { CatalogSearchDocument, CatalogSearchDocumentListQuery } from "./validation-catalog.js";
4
+ type CatalogProductRow = typeof products.$inferSelect;
5
+ type HydrateCatalogProductOptions = {
6
+ includeContent?: boolean;
7
+ languageTag?: string | null;
8
+ fallbackLanguageTags?: string[];
9
+ };
10
+ export declare const catalogProductsService: {
11
+ hydrateProducts(db: PostgresJsDatabase, productRows: CatalogProductRow[], options?: HydrateCatalogProductOptions): Promise<({
12
+ description: string | null;
13
+ inclusionsHtml: null;
14
+ exclusionsHtml: null;
15
+ termsHtml: null;
16
+ id: string;
17
+ name: string;
18
+ contentLanguageTag: string | null;
19
+ slug: string | null;
20
+ shortDescription: string | null;
21
+ seoTitle: string | null;
22
+ seoDescription: string | null;
23
+ bookingMode: "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary" | "other";
24
+ capacityMode: "free_sale" | "limited" | "on_request";
25
+ visibility: "public" | "private" | "hidden";
26
+ sellCurrency: string;
27
+ sellAmountCents: number | null;
28
+ startDate: string | null;
29
+ endDate: string | null;
30
+ pax: number | null;
31
+ contractTemplateId: string | null;
32
+ productType: {
33
+ id: string;
34
+ code: string;
35
+ name: string;
36
+ description: string | null;
37
+ } | null;
38
+ categories: {
39
+ id: string;
40
+ parentId: string | null;
41
+ name: string;
42
+ slug: string;
43
+ description: string | null;
44
+ sortOrder: number;
45
+ }[];
46
+ tags: {
47
+ id: string;
48
+ name: string;
49
+ }[];
50
+ capabilities: string[];
51
+ destinations: {
52
+ id: string;
53
+ parentId: string | null;
54
+ slug: string;
55
+ canonicalPlaceId: string | null;
56
+ name: string;
57
+ description: string | null;
58
+ seoTitle: string | null;
59
+ seoDescription: string | null;
60
+ destinationType: string;
61
+ latitude: number | null;
62
+ longitude: number | null;
63
+ sortOrder: number;
64
+ }[];
65
+ locations: {
66
+ id: string;
67
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
68
+ title: string;
69
+ address: string | null;
70
+ city: string | null;
71
+ countryCode: string | null;
72
+ latitude: number | null;
73
+ longitude: number | null;
74
+ sortOrder: number;
75
+ }[];
76
+ coverMedia: {
77
+ id: string;
78
+ mediaType: "image" | "video" | "document";
79
+ name: string;
80
+ url: string;
81
+ mimeType: string | null;
82
+ altText: string | null;
83
+ sortOrder: number;
84
+ isCover: boolean;
85
+ isBrochure: boolean;
86
+ isBrochureCurrent: boolean;
87
+ brochureVersion: number | null;
88
+ } | null;
89
+ isFeatured: boolean;
90
+ } | {
91
+ brochure: {
92
+ id: string;
93
+ mediaType: "image" | "video" | "document";
94
+ name: string;
95
+ url: string;
96
+ mimeType: string | null;
97
+ altText: string | null;
98
+ sortOrder: number;
99
+ isCover: boolean;
100
+ isBrochure: boolean;
101
+ isBrochureCurrent: boolean;
102
+ brochureVersion: number | null;
103
+ } | null;
104
+ media: {
105
+ id: string;
106
+ mediaType: "image" | "video" | "document";
107
+ name: string;
108
+ url: string;
109
+ mimeType: string | null;
110
+ altText: string | null;
111
+ sortOrder: number;
112
+ isCover: boolean;
113
+ isBrochure: boolean;
114
+ isBrochureCurrent: boolean;
115
+ brochureVersion: number | null;
116
+ }[];
117
+ features: {
118
+ id: string;
119
+ featureType: "other" | "inclusion" | "exclusion" | "highlight" | "important_information";
120
+ title: string;
121
+ description: string | null;
122
+ sortOrder: number;
123
+ }[];
124
+ faqs: {
125
+ id: string;
126
+ question: string;
127
+ answer: string;
128
+ sortOrder: number;
129
+ }[];
130
+ id: string;
131
+ name: string;
132
+ description: string | null;
133
+ inclusionsHtml: string | null;
134
+ exclusionsHtml: string | null;
135
+ termsHtml: string | null;
136
+ contentLanguageTag: string | null;
137
+ slug: string | null;
138
+ shortDescription: string | null;
139
+ seoTitle: string | null;
140
+ seoDescription: string | null;
141
+ bookingMode: "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary" | "other";
142
+ capacityMode: "free_sale" | "limited" | "on_request";
143
+ visibility: "public" | "private" | "hidden";
144
+ sellCurrency: string;
145
+ sellAmountCents: number | null;
146
+ startDate: string | null;
147
+ endDate: string | null;
148
+ pax: number | null;
149
+ contractTemplateId: string | null;
150
+ productType: {
151
+ id: string;
152
+ code: string;
153
+ name: string;
154
+ description: string | null;
155
+ } | null;
156
+ categories: {
157
+ id: string;
158
+ parentId: string | null;
159
+ name: string;
160
+ slug: string;
161
+ description: string | null;
162
+ sortOrder: number;
163
+ }[];
164
+ tags: {
165
+ id: string;
166
+ name: string;
167
+ }[];
168
+ capabilities: string[];
169
+ destinations: {
170
+ id: string;
171
+ parentId: string | null;
172
+ slug: string;
173
+ canonicalPlaceId: string | null;
174
+ name: string;
175
+ description: string | null;
176
+ seoTitle: string | null;
177
+ seoDescription: string | null;
178
+ destinationType: string;
179
+ latitude: number | null;
180
+ longitude: number | null;
181
+ sortOrder: number;
182
+ }[];
183
+ locations: {
184
+ id: string;
185
+ locationType: "other" | "start" | "end" | "meeting_point" | "pickup" | "dropoff" | "point_of_interest";
186
+ title: string;
187
+ address: string | null;
188
+ city: string | null;
189
+ countryCode: string | null;
190
+ latitude: number | null;
191
+ longitude: number | null;
192
+ sortOrder: number;
193
+ }[];
194
+ coverMedia: {
195
+ id: string;
196
+ mediaType: "image" | "video" | "document";
197
+ name: string;
198
+ url: string;
199
+ mimeType: string | null;
200
+ altText: string | null;
201
+ sortOrder: number;
202
+ isCover: boolean;
203
+ isBrochure: boolean;
204
+ isBrochureCurrent: boolean;
205
+ brochureVersion: number | null;
206
+ } | null;
207
+ isFeatured: boolean;
208
+ })[]>;
209
+ listSearchDocuments(db: PostgresJsDatabase, query: CatalogSearchDocumentListQuery): Promise<{
210
+ data: CatalogSearchDocument[];
211
+ total: number;
212
+ limit: number;
213
+ offset: number;
214
+ }>;
215
+ getSearchDocumentByProductId(db: PostgresJsDatabase, productId: string, query?: Partial<Omit<CatalogSearchDocumentListQuery, "productIds" | "limit" | "offset">>): Promise<{
216
+ id: string;
217
+ productId: string;
218
+ languageTag: string | null;
219
+ name: string;
220
+ slug: string | null;
221
+ shortDescription: string | null;
222
+ description: string | null;
223
+ seoTitle: string | null;
224
+ seoDescription: string | null;
225
+ sellCurrency: string;
226
+ sellAmountCents: number | null;
227
+ startDate: string | null;
228
+ endDate: string | null;
229
+ pax: number | null;
230
+ productTypeCode: string | null;
231
+ productTypeName: string | null;
232
+ categoryIds: string[];
233
+ categoryNames: string[];
234
+ categorySlugs: string[];
235
+ tagIds: string[];
236
+ tagNames: string[];
237
+ capabilities: string[];
238
+ destinationIds: string[];
239
+ destinationNames: string[];
240
+ destinationSlugs: string[];
241
+ locationTitles: string[];
242
+ locationCities: string[];
243
+ locationCountryCodes: string[];
244
+ coverMediaUrl: string | null;
245
+ isFeatured: boolean;
246
+ createdAt: string | null;
247
+ updatedAt: string | null;
248
+ } | null>;
249
+ };
250
+ export {};
251
+ //# sourceMappingURL=service-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog.d.ts","sourceRoot":"","sources":["../src/service-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAWL,QAAQ,EAMT,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACV,qBAAqB,EACrB,8BAA8B,EAE/B,MAAM,yBAAyB,CAAA;AAEhC,KAAK,iBAAiB,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAErD,KAAK,4BAA4B,GAAG;IAClC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC,CAAA;AA2aD,eAAO,MAAM,sBAAsB;wBAE3B,kBAAkB,eACT,iBAAiB,EAAE,YACvB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAkIjC,kBAAkB,SACf,8BAA8B,GACpC,OAAO,CAAC;QACT,IAAI,EAAE,qBAAqB,EAAE,CAAA;QAC7B,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAC;qCAkFI,kBAAkB,aACX,MAAM,UACV,OAAO,CAAC,IAAI,CAAC,8BAA8B,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAa1F,CAAA"}