@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,87 @@
1
+ import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
2
+ import { relations } from "drizzle-orm";
3
+ import { boolean, index, integer, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, } from "drizzle-orm/pg-core";
4
+ export const extraSelectionTypeEnum = pgEnum("extra_selection_type", [
5
+ "optional",
6
+ "required",
7
+ "default_selected",
8
+ "unavailable",
9
+ ]);
10
+ export const extraPricingModeEnum = pgEnum("extra_pricing_mode", [
11
+ "included",
12
+ "per_person",
13
+ "per_booking",
14
+ "quantity_based",
15
+ "on_request",
16
+ "free",
17
+ ]);
18
+ export const extraCollectionModeEnum = pgEnum("extra_collection_mode", [
19
+ "booking_total",
20
+ "cash_on_trip",
21
+ "external",
22
+ "included",
23
+ "none",
24
+ ]);
25
+ export const productExtras = pgTable("product_extras", {
26
+ id: typeId("product_extras"),
27
+ productId: text("product_id").notNull(),
28
+ supplierId: text("supplier_id"),
29
+ code: text("code"),
30
+ name: text("name").notNull(),
31
+ description: text("description"),
32
+ selectionType: extraSelectionTypeEnum("selection_type").notNull().default("optional"),
33
+ pricingMode: extraPricingModeEnum("pricing_mode").notNull().default("per_booking"),
34
+ pricedPerPerson: boolean("priced_per_person").notNull().default(false),
35
+ collectionMode: extraCollectionModeEnum("collection_mode").notNull().default("booking_total"),
36
+ showOnSlotManifest: boolean("show_on_slot_manifest").notNull().default(true),
37
+ minQuantity: integer("min_quantity"),
38
+ maxQuantity: integer("max_quantity"),
39
+ defaultQuantity: integer("default_quantity"),
40
+ active: boolean("active").notNull().default(true),
41
+ sortOrder: integer("sort_order").notNull().default(0),
42
+ metadata: jsonb("metadata").$type(),
43
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
44
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
45
+ }, (table) => [
46
+ index("idx_product_extras_sort_name").on(table.sortOrder, table.name),
47
+ index("idx_product_extras_product_sort_name").on(table.productId, table.sortOrder, table.name),
48
+ index("idx_product_extras_supplier_sort_name").on(table.supplierId, table.sortOrder, table.name),
49
+ index("idx_product_extras_active_sort_name").on(table.active, table.sortOrder, table.name),
50
+ uniqueIndex("uidx_product_extras_product_code").on(table.productId, table.code),
51
+ ]);
52
+ export const optionExtraConfigs = pgTable("option_extra_configs", {
53
+ id: typeId("option_extra_configs"),
54
+ optionId: text("option_id").notNull(),
55
+ productExtraId: typeIdRef("product_extra_id")
56
+ .notNull()
57
+ .references(() => productExtras.id, { onDelete: "cascade" }),
58
+ selectionType: extraSelectionTypeEnum("selection_type"),
59
+ pricingMode: extraPricingModeEnum("pricing_mode"),
60
+ pricedPerPerson: boolean("priced_per_person"),
61
+ minQuantity: integer("min_quantity"),
62
+ maxQuantity: integer("max_quantity"),
63
+ defaultQuantity: integer("default_quantity"),
64
+ isDefault: boolean("is_default").notNull().default(false),
65
+ active: boolean("active").notNull().default(true),
66
+ sortOrder: integer("sort_order").notNull().default(0),
67
+ notes: text("notes"),
68
+ metadata: jsonb("metadata").$type(),
69
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
70
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
71
+ }, (table) => [
72
+ index("idx_option_extra_configs_sort_default").on(table.sortOrder, table.isDefault),
73
+ index("idx_option_extra_configs_option_sort_default").on(table.optionId, table.sortOrder, table.isDefault),
74
+ index("idx_option_extra_configs_extra_sort_default").on(table.productExtraId, table.sortOrder, table.isDefault),
75
+ index("idx_option_extra_configs_active_sort_default").on(table.active, table.sortOrder, table.isDefault),
76
+ uniqueIndex("uidx_option_extra_configs_option_extra").on(table.optionId, table.productExtraId),
77
+ ]);
78
+ export const productExtrasRelations = relations(productExtras, ({ many }) => ({
79
+ optionConfigs: many(optionExtraConfigs),
80
+ }));
81
+ export const optionExtraConfigsRelations = relations(optionExtraConfigs, ({ one }) => ({
82
+ productExtra: one(productExtras, {
83
+ fields: [optionExtraConfigs.productExtraId],
84
+ references: [productExtras.id],
85
+ }),
86
+ }));
87
+ export { EXTRAS_CONTENT_MARKET_ANY, extrasSourcedContentTable, } from "./schema-sourced-content.js";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Catalog-plane integration for the extras service.
3
+ *
4
+ * Extras are a **partial-adoption vertical** per architecture §3.3.1: they
5
+ * participate in provenance + booking snapshot + catalog event taxonomy,
6
+ * but skip the search index and overlay store. The service-plane integration
7
+ * here reflects that — `getResolvedExtraById` returns a resolved view but
8
+ * the resolver always sees an empty overlay set (extras have no overlay
9
+ * rows; the catalog-policy file declares no merchandisable fields).
10
+ *
11
+ * The value of running through the catalog-plane resolver anyway is
12
+ * uniformity: extras' projection and visibility filtering use the same
13
+ * machinery as every other vertical, and snapshot capture at booking commit
14
+ * (the most important participation surface for extras) reuses
15
+ * `productExtraRowToProjection` to build the frozen payload.
16
+ *
17
+ * See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
18
+ */
19
+ import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope } from "@voyant-travel/catalog";
20
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
21
+ import { productExtras } from "./schema.js";
22
+ /**
23
+ * Maps a product-extra row to a field-keyed projection. Extras almost
24
+ * always inherit their provenance from the parent product they attach to;
25
+ * the caller passes the parent's source kind / ref through, defaulting to
26
+ * `owned` for operator-defined extras.
27
+ */
28
+ export declare function productExtraRowToProjection(row: typeof productExtras.$inferSelect, context: {
29
+ sellerOperatorId: string;
30
+ sourceKind?: string;
31
+ sourceRef?: string;
32
+ }): ReadonlyMap<string, unknown>;
33
+ export declare function productExtraProvenance(_row: typeof productExtras.$inferSelect, context: {
34
+ sellerOperatorId: string;
35
+ sourceKind?: string;
36
+ sourceRef?: string;
37
+ }): Provenance;
38
+ export interface ProductExtraCatalogContext {
39
+ sellerOperatorId: string;
40
+ scope: ResolverScope;
41
+ sourceKind?: string;
42
+ sourceRef?: string;
43
+ }
44
+ /**
45
+ * Catalog-aware extra fetch. The catalog-policy declares no merchandisable
46
+ * fields for extras (per §3.3.1 partial adoption), so the resolver acts as
47
+ * a pure visibility filter rather than an overlay-merge engine. Useful
48
+ * primarily for snapshot capture at booking time.
49
+ */
50
+ export declare function getResolvedExtraById(db: AnyDrizzleDb, id: string, context: ProductExtraCatalogContext): Promise<ResolvedView | null>;
51
+ export declare function listResolvedExtras(db: AnyDrizzleDb, rows: ReadonlyArray<typeof productExtras.$inferSelect>, context: ProductExtraCatalogContext): Promise<ResolvedView[]>;
52
+ /**
53
+ * Build a `CaptureSnapshotInput` for a product extra. Extras participate
54
+ * in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
55
+ * exactly what add-on the customer purchased and what selectionType /
56
+ * pricingMode applied at booking time.
57
+ */
58
+ export declare function buildExtraSnapshotInput(db: AnyDrizzleDb, extraId: string, context: ProductExtraCatalogContext & {
59
+ pricingBasis?: PricingBasis;
60
+ }): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
61
+ /**
62
+ * Note: per architecture §3.3.1, extras are a partial-adoption vertical.
63
+ * Most fields remain snapshot-oriented, but deployments may opt extras into
64
+ * the index for ops-side keyword search and thumbnail rendering.
65
+ */
66
+ export declare function createExtraDocumentEmitter(context: {
67
+ sellerOperatorId: string;
68
+ sourceKind?: string;
69
+ sourceRef?: string;
70
+ }): DocumentEmitter<typeof productExtras.$inferSelect>;
71
+ export declare function createExtraDocumentBuilder(db: AnyDrizzleDb, context: {
72
+ sellerOperatorId: string;
73
+ sourceKind?: string;
74
+ sourceRef?: string;
75
+ }): DocumentBuilder;
76
+ export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, };
77
+ //# sourceMappingURL=service-catalog-plane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../../src/extras/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAW3C;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,aAAa,CAAC,YAAY,EACtC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAgC9B;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,aAAa,CAAC,YAAY,EACvC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,UAAU,CAMZ;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,aAAa,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAW9B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,EACtD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAazB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,0BAA0B,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CAUzD;AAMD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,eAAe,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,CAarD;AAED,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,eAAe,CAuBjB;AA0ED,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,GACd,CAAA"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Catalog-plane integration for the extras service.
3
+ *
4
+ * Extras are a **partial-adoption vertical** per architecture §3.3.1: they
5
+ * participate in provenance + booking snapshot + catalog event taxonomy,
6
+ * but skip the search index and overlay store. The service-plane integration
7
+ * here reflects that — `getResolvedExtraById` returns a resolved view but
8
+ * the resolver always sees an empty overlay set (extras have no overlay
9
+ * rows; the catalog-policy file declares no merchandisable fields).
10
+ *
11
+ * The value of running through the catalog-plane resolver anyway is
12
+ * uniformity: extras' projection and visibility filtering use the same
13
+ * machinery as every other vertical, and snapshot capture at booking commit
14
+ * (the most important participation surface for extras) reuses
15
+ * `productExtraRowToProjection` to build the frozen payload.
16
+ *
17
+ * See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
18
+ */
19
+ import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyant-travel/catalog";
20
+ import { and, eq } from "drizzle-orm";
21
+ import { extrasCatalogPolicy } from "./catalog-policy.js";
22
+ import { productExtras } from "./schema.js";
23
+ import { EXTRAS_CONTENT_MARKET_ANY, extrasSourcedContentTable } from "./schema-sourced-content.js";
24
+ let _registry;
25
+ function getExtrasRegistry() {
26
+ if (!_registry) {
27
+ _registry = createFieldPolicyRegistry(extrasCatalogPolicy);
28
+ }
29
+ return _registry;
30
+ }
31
+ /**
32
+ * Maps a product-extra row to a field-keyed projection. Extras almost
33
+ * always inherit their provenance from the parent product they attach to;
34
+ * the caller passes the parent's source kind / ref through, defaulting to
35
+ * `owned` for operator-defined extras.
36
+ */
37
+ export function productExtraRowToProjection(row, context) {
38
+ return new Map([
39
+ // Provenance
40
+ ["source.kind", context.sourceKind ?? "owned"],
41
+ ["source.ref", context.sourceRef],
42
+ ["seller.operator_id", context.sellerOperatorId],
43
+ // Identity + cross-module reference
44
+ ["id", row.id],
45
+ ["code", row.code],
46
+ ["productId", row.productId],
47
+ ["supplierId", row.supplierId],
48
+ ["createdAt", row.createdAt],
49
+ ["updatedAt", row.updatedAt],
50
+ // Snapshot-relevant managed fields
51
+ ["name", row.name],
52
+ ["description", row.description],
53
+ ["thumbnailUrl", pickThumbnailUrl(row.metadata)],
54
+ // Selection / pricing structure
55
+ ["selectionType", row.selectionType],
56
+ ["pricingMode", row.pricingMode],
57
+ ["pricedPerPerson", row.pricedPerPerson],
58
+ ["collectionMode", row.collectionMode],
59
+ ["showOnSlotManifest", row.showOnSlotManifest],
60
+ ["minQuantity", row.minQuantity],
61
+ ["maxQuantity", row.maxQuantity],
62
+ ["defaultQuantity", row.defaultQuantity],
63
+ ["active", row.active],
64
+ ["sortOrder", row.sortOrder],
65
+ ]);
66
+ }
67
+ export function productExtraProvenance(_row, context) {
68
+ return {
69
+ source_kind: context.sourceKind ?? "owned",
70
+ source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
71
+ source_ref: context.sourceRef,
72
+ };
73
+ }
74
+ /**
75
+ * Catalog-aware extra fetch. The catalog-policy declares no merchandisable
76
+ * fields for extras (per §3.3.1 partial adoption), so the resolver acts as
77
+ * a pure visibility filter rather than an overlay-merge engine. Useful
78
+ * primarily for snapshot capture at booking time.
79
+ */
80
+ export async function getResolvedExtraById(db, id, context) {
81
+ const rows = await db.select().from(productExtras).where(eq(productExtras.id, id)).limit(1);
82
+ const row = rows[0];
83
+ if (!row)
84
+ return null;
85
+ const projection = productExtraRowToProjection(row, {
86
+ sellerOperatorId: context.sellerOperatorId,
87
+ sourceKind: context.sourceKind,
88
+ sourceRef: context.sourceRef,
89
+ });
90
+ return resolveEntityView(db, getExtrasRegistry(), "extras", id, projection, context.scope);
91
+ }
92
+ export async function listResolvedExtras(db, rows, context) {
93
+ const registry = getExtrasRegistry();
94
+ const views = [];
95
+ for (const row of rows) {
96
+ const projection = productExtraRowToProjection(row, {
97
+ sellerOperatorId: context.sellerOperatorId,
98
+ sourceKind: context.sourceKind,
99
+ sourceRef: context.sourceRef,
100
+ });
101
+ const view = await resolveEntityView(db, registry, "extras", row.id, projection, context.scope);
102
+ views.push(view);
103
+ }
104
+ return views;
105
+ }
106
+ /**
107
+ * Build a `CaptureSnapshotInput` for a product extra. Extras participate
108
+ * in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
109
+ * exactly what add-on the customer purchased and what selectionType /
110
+ * pricingMode applied at booking time.
111
+ */
112
+ export async function buildExtraSnapshotInput(db, extraId, context) {
113
+ const view = await getResolvedExtraById(db, extraId, context);
114
+ if (!view)
115
+ return null;
116
+ return buildSnapshotInputFromView(view, {
117
+ entityModule: "extras",
118
+ entityId: extraId,
119
+ sourceKind: context.sourceKind ?? "owned",
120
+ sourceRef: context.sourceRef,
121
+ pricingBasis: context.pricingBasis,
122
+ });
123
+ }
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // Indexer document emission
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ /**
128
+ * Note: per architecture §3.3.1, extras are a partial-adoption vertical.
129
+ * Most fields remain snapshot-oriented, but deployments may opt extras into
130
+ * the index for ops-side keyword search and thumbnail rendering.
131
+ */
132
+ export function createExtraDocumentEmitter(context) {
133
+ const registry = getExtrasRegistry();
134
+ return {
135
+ vertical: "extras",
136
+ emit(source, slice) {
137
+ const projection = productExtraRowToProjection(source, {
138
+ sellerOperatorId: context.sellerOperatorId,
139
+ sourceKind: context.sourceKind,
140
+ sourceRef: context.sourceRef,
141
+ });
142
+ return buildIndexerDocument(registry, projection, slice, source.id);
143
+ },
144
+ };
145
+ }
146
+ export function createExtraDocumentBuilder(db, context) {
147
+ const registry = getExtrasRegistry();
148
+ return async (entityId, slice) => {
149
+ const rows = await db
150
+ .select()
151
+ .from(productExtras)
152
+ .where(eq(productExtras.id, entityId))
153
+ .limit(1);
154
+ const row = rows[0];
155
+ if (!row)
156
+ return null;
157
+ const projection = new Map(productExtraRowToProjection(row, {
158
+ sellerOperatorId: context.sellerOperatorId,
159
+ sourceKind: context.sourceKind,
160
+ sourceRef: context.sourceRef,
161
+ }));
162
+ const sourcedThumbnailUrl = await fetchSourcedContentThumbnailUrl(db, entityId, slice);
163
+ if (sourcedThumbnailUrl) {
164
+ projection.set("thumbnailUrl", sourcedThumbnailUrl);
165
+ }
166
+ return buildIndexerDocument(registry, projection, slice, entityId);
167
+ };
168
+ }
169
+ async function fetchSourcedContentThumbnailUrl(db, entityId, slice) {
170
+ const rows = await db
171
+ .select({
172
+ market: extrasSourcedContentTable.market,
173
+ payload: extrasSourcedContentTable.payload,
174
+ })
175
+ .from(extrasSourcedContentTable)
176
+ .where(and(eq(extrasSourcedContentTable.entity_id, entityId), eq(extrasSourcedContentTable.locale, slice.locale)));
177
+ const row = rows.find((candidate) => candidate.market === slice.market) ??
178
+ rows.find((candidate) => candidate.market === EXTRAS_CONTENT_MARKET_ANY) ??
179
+ rows[0];
180
+ return pickThumbnailUrl(row?.payload);
181
+ }
182
+ function pickThumbnailUrl(value) {
183
+ const record = asRecord(value);
184
+ if (!record)
185
+ return null;
186
+ return (firstString(record.thumbnailUrl, record.heroImageUrl, record.hero_image_url, record.imageUrl, record.image_url) ??
187
+ firstMediaUrl(record.media) ??
188
+ firstStringFromArray(record.images) ??
189
+ firstStringFromArray(record.galleryUrls));
190
+ }
191
+ function firstMediaUrl(value) {
192
+ if (!Array.isArray(value))
193
+ return null;
194
+ for (const item of value) {
195
+ const media = asRecord(item);
196
+ if (!media)
197
+ continue;
198
+ const type = typeof media.type === "string" ? media.type : null;
199
+ if (type && type !== "image")
200
+ continue;
201
+ const url = firstString(media.url, media.src);
202
+ if (url)
203
+ return url;
204
+ }
205
+ return null;
206
+ }
207
+ function firstStringFromArray(value) {
208
+ if (!Array.isArray(value))
209
+ return null;
210
+ return value.find((item) => typeof item === "string" && item.length > 0) ?? null;
211
+ }
212
+ function firstString(...values) {
213
+ return (values.find((value) => typeof value === "string" && value.length > 0) ?? null);
214
+ }
215
+ function asRecord(value) {
216
+ return value && typeof value === "object" && !Array.isArray(value)
217
+ ? value
218
+ : null;
219
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Extras content synthesizer — fallback for thin adapters that declare
3
+ * `supportsContentFetch: false`.
4
+ *
5
+ * Produces the most complete `ExtraContent` blob we can legitimately
6
+ * synthesize from the durable sourced-entry projection + locale-aware
7
+ * overlays + plane-level provenance. Sub-options render as a typed
8
+ * empty state unless the projection genuinely carries them.
9
+ *
10
+ * Per §3.6: never invents plausible-but-unverified fields, never
11
+ * machine-translates, never mines snapshots, never caches its own
12
+ * output.
13
+ */
14
+ import { type ProvenanceReadResult } from "@voyant-travel/catalog";
15
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
16
+ import { type ExtraContent } from "./content-shape.js";
17
+ export interface SynthesizeExtraContentOptions {
18
+ provenance: Extract<ProvenanceReadResult, {
19
+ kind: "sourced";
20
+ }>;
21
+ overlays?: ReadonlyArray<{
22
+ field_path: string;
23
+ value: unknown;
24
+ }>;
25
+ }
26
+ export interface SynthesizedExtraContent {
27
+ content: ExtraContent;
28
+ content_schema_version: string;
29
+ served_locale: string;
30
+ source_kind: string;
31
+ source_provider?: string;
32
+ }
33
+ export declare function synthesizeExtraContent(scope: {
34
+ locale: string;
35
+ }, options: SynthesizeExtraContentOptions): SynthesizedExtraContent;
36
+ export declare function synthesizeExtraContentFromDb(db: AnyDrizzleDb, scope: {
37
+ locale: string;
38
+ }, provenance: Extract<ProvenanceReadResult, {
39
+ kind: "sourced";
40
+ }>): Promise<SynthesizedExtraContent>;
41
+ //# sourceMappingURL=service-content-synthesizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../../src/extras/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,YAAY,CAAA;IACrB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,6BAA6B,GACrC,uBAAuB,CAiCzB;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,uBAAuB,CAAC,CAOlC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Extras content synthesizer — fallback for thin adapters that declare
3
+ * `supportsContentFetch: false`.
4
+ *
5
+ * Produces the most complete `ExtraContent` blob we can legitimately
6
+ * synthesize from the durable sourced-entry projection + locale-aware
7
+ * overlays + plane-level provenance. Sub-options render as a typed
8
+ * empty state unless the projection genuinely carries them.
9
+ *
10
+ * Per §3.6: never invents plausible-but-unverified fields, never
11
+ * machine-translates, never mines snapshots, never caches its own
12
+ * output.
13
+ */
14
+ import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyant-travel/catalog";
15
+ import { EXTRAS_CONTENT_SCHEMA_VERSION, extraContentSchema, } from "./content-shape.js";
16
+ export function synthesizeExtraContent(scope, options) {
17
+ const projection = options.provenance.projection;
18
+ const extra = pickExtraSummary(projection, options.provenance);
19
+ const media = pickMedia(projection);
20
+ const policies = pickPolicies(projection);
21
+ const baseContent = {
22
+ extra,
23
+ options: [],
24
+ media,
25
+ policies,
26
+ };
27
+ let merged = baseContent;
28
+ if (options.overlays && options.overlays.length > 0) {
29
+ const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
30
+ validate(p) {
31
+ const r = extraContentSchema.safeParse(p);
32
+ return r.success
33
+ ? { valid: true }
34
+ : { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
35
+ },
36
+ });
37
+ merged = extraContentSchema.parse(result);
38
+ }
39
+ return {
40
+ content: merged,
41
+ content_schema_version: EXTRAS_CONTENT_SCHEMA_VERSION,
42
+ served_locale: scope.locale,
43
+ source_kind: options.provenance.provenance.source_kind,
44
+ source_provider: options.provenance.provenance.source_provider,
45
+ };
46
+ }
47
+ export async function synthesizeExtraContentFromDb(db, scope, provenance) {
48
+ const entityId = entityIdFromProvenance(provenance);
49
+ const overlays = await fetchOverlaysForEntity(db, "extras", entityId);
50
+ return synthesizeExtraContent(scope, {
51
+ provenance,
52
+ overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
53
+ });
54
+ }
55
+ function entityIdFromProvenance(provenance) {
56
+ const fromProjection = provenance.projection.id;
57
+ if (typeof fromProjection === "string" && fromProjection.length > 0) {
58
+ return fromProjection;
59
+ }
60
+ return provenance.entry_id;
61
+ }
62
+ function pickExtraSummary(projection, provenance) {
63
+ return {
64
+ id: stringOr(projection.id, "") || provenance.entry_id,
65
+ name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed extra",
66
+ status: stringOr(projection.status, undefined),
67
+ description: stringOr(projection.description, null),
68
+ selection_type: stringOr(projection.selection_type, "optional"),
69
+ pricing_mode: stringOr(projection.pricing_mode, "per_booking"),
70
+ priced_per_person: typeof projection.priced_per_person === "boolean" ? projection.priced_per_person : undefined,
71
+ category: stringOr(projection.category, null),
72
+ hero_image_url: stringOr(projection.hero_image_url, null),
73
+ highlights: stringArrayOr(projection.highlights, []),
74
+ supplier: stringOr(projection.supplier, null) ??
75
+ stringOr(projection.supplier_name, null) ??
76
+ provenance.provenance.source_provider ??
77
+ null,
78
+ duration_minutes: numberOr(projection.duration_minutes, null),
79
+ requirements_summary: stringOr(projection.requirements_summary, null),
80
+ };
81
+ }
82
+ function pickMedia(projection) {
83
+ const heroUrl = stringOr(projection.hero_image_url, null);
84
+ const out = [];
85
+ if (heroUrl) {
86
+ out.push({ url: heroUrl, type: "image", caption: null, alt: null });
87
+ }
88
+ const additional = projection.media;
89
+ if (Array.isArray(additional)) {
90
+ for (const item of additional) {
91
+ if (item && typeof item === "object") {
92
+ const obj = item;
93
+ const url = stringOr(obj.url, null);
94
+ if (!url)
95
+ continue;
96
+ out.push({
97
+ url,
98
+ type: pickMediaType(obj.type),
99
+ caption: stringOr(obj.caption, null),
100
+ alt: stringOr(obj.alt, null),
101
+ });
102
+ }
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ function pickMediaType(value) {
108
+ if (value === "video")
109
+ return "video";
110
+ if (value === "document")
111
+ return "document";
112
+ return "image";
113
+ }
114
+ function pickPolicies(projection) {
115
+ const out = [];
116
+ const cancel = stringOr(projection.cancellation_policy, null);
117
+ if (cancel)
118
+ out.push({ kind: "cancellation", body: cancel });
119
+ const payment = stringOr(projection.payment_terms, null);
120
+ if (payment)
121
+ out.push({ kind: "payment", body: payment });
122
+ const requirements = stringOr(projection.requirements, null);
123
+ if (requirements)
124
+ out.push({ kind: "requirements", body: requirements });
125
+ return out;
126
+ }
127
+ function stringOr(value, fallback) {
128
+ return typeof value === "string" && value.length > 0 ? value : fallback;
129
+ }
130
+ function numberOr(value, fallback) {
131
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
132
+ }
133
+ function stringArrayOr(value, fallback) {
134
+ if (!Array.isArray(value))
135
+ return fallback;
136
+ const out = value.filter((v) => typeof v === "string");
137
+ return out.length > 0 ? out : fallback;
138
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Extras content service — `getExtraContent` with locale-resolved
3
+ * cache reads, SWR refresh, and synthesizer fallback.
4
+ *
5
+ * Mirrors `service-content.ts` in the products / cruises / accommodations
6
+ * / charters packages but extras-shaped. The extras content aggregate
7
+ * (§3.2 / §3.6) is `{ extra, options[], media[], policies[] }` — one
8
+ * payload returned by a single getContent. Pricing stays out (volatile-
9
+ * live, flows through `liveResolve`).
10
+ *
11
+ * Extras don't appear in the search index (per the vertical's catalog-
12
+ * policy.ts), but sourced extras still need rich content for the
13
+ * booking-flow's add-on selection UI. The cache layer covers exactly
14
+ * that surface.
15
+ */
16
+ import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyant-travel/catalog";
17
+ import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
18
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
19
+ import { type ExtraContent } from "./content-shape.js";
20
+ export interface ExtraContentScope {
21
+ preferredLocales: ReadonlyArray<string>;
22
+ market?: string;
23
+ currency?: string;
24
+ acceptMachineTranslated?: boolean;
25
+ }
26
+ export interface GetExtraContentOptions {
27
+ registry: SourceAdapterRegistry;
28
+ buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
29
+ onOverlayError?: (event: {
30
+ field_path: string;
31
+ reason: string;
32
+ }) => void;
33
+ }
34
+ export interface ResolvedExtraContent {
35
+ content: ExtraContent;
36
+ resolution: ContentLocaleResolution<{
37
+ locale: string;
38
+ payload: ExtraContent;
39
+ }>;
40
+ source: "sourced-cache" | "sourced-fresh" | "synthesized";
41
+ served_stale: boolean;
42
+ synthesized: boolean;
43
+ machine_translated: boolean;
44
+ }
45
+ export declare function getExtraContent(db: AnyDrizzleDb, entityId: string, scope: ExtraContentScope, options: GetExtraContentOptions): Promise<ResolvedExtraContent | null>;
46
+ /** Drift event consumer for the extras content cache. Per §3.4.1. */
47
+ export declare const invalidateExtraContentOnDrift: InvalidateOnDrift;
48
+ //# sourceMappingURL=service-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../../src/extras/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,oBAAoB,CAAA;AAc3B,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAAA;IAC9E,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA0FtC;AA2ND,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,iBAG3C,CAAA"}