@voyant-travel/catalog 0.117.2

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 (243) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/dist/adapter/booking-forwarding.d.ts +2 -0
  4. package/dist/adapter/booking-forwarding.d.ts.map +1 -0
  5. package/dist/adapter/booking-forwarding.js +1 -0
  6. package/dist/adapter/channel-push-contracts.d.ts +2 -0
  7. package/dist/adapter/channel-push-contracts.d.ts.map +1 -0
  8. package/dist/adapter/channel-push-contracts.js +1 -0
  9. package/dist/adapter/contract.d.ts +2 -0
  10. package/dist/adapter/contract.d.ts.map +1 -0
  11. package/dist/adapter/contract.js +1 -0
  12. package/dist/adapter/contract.test.d.ts +2 -0
  13. package/dist/adapter/contract.test.d.ts.map +1 -0
  14. package/dist/adapter/contract.test.js +390 -0
  15. package/dist/adapter/provider-contracts.d.ts +2 -0
  16. package/dist/adapter/provider-contracts.d.ts.map +1 -0
  17. package/dist/adapter/provider-contracts.js +1 -0
  18. package/dist/adapter/provider-contracts.test.d.ts +2 -0
  19. package/dist/adapter/provider-contracts.test.d.ts.map +1 -0
  20. package/dist/adapter/provider-contracts.test.js +206 -0
  21. package/dist/adapter/schemas.d.ts +2 -0
  22. package/dist/adapter/schemas.d.ts.map +1 -0
  23. package/dist/adapter/schemas.js +1 -0
  24. package/dist/adapter/schemas.test.d.ts +2 -0
  25. package/dist/adapter/schemas.test.d.ts.map +1 -0
  26. package/dist/adapter/schemas.test.js +344 -0
  27. package/dist/booking-engine/book.d.ts +124 -0
  28. package/dist/booking-engine/book.d.ts.map +1 -0
  29. package/dist/booking-engine/book.js +311 -0
  30. package/dist/booking-engine/cancel.d.ts +40 -0
  31. package/dist/booking-engine/cancel.d.ts.map +1 -0
  32. package/dist/booking-engine/cancel.js +56 -0
  33. package/dist/booking-engine/checkout-finalize.d.ts +146 -0
  34. package/dist/booking-engine/checkout-finalize.d.ts.map +1 -0
  35. package/dist/booking-engine/checkout-finalize.js +132 -0
  36. package/dist/booking-engine/contracts.d.ts +9 -0
  37. package/dist/booking-engine/contracts.d.ts.map +1 -0
  38. package/dist/booking-engine/contracts.js +8 -0
  39. package/dist/booking-engine/contracts.test.d.ts +2 -0
  40. package/dist/booking-engine/contracts.test.d.ts.map +1 -0
  41. package/dist/booking-engine/contracts.test.js +116 -0
  42. package/dist/booking-engine/draft-shape.d.ts +10 -0
  43. package/dist/booking-engine/draft-shape.d.ts.map +1 -0
  44. package/dist/booking-engine/draft-shape.js +9 -0
  45. package/dist/booking-engine/draft-shape.test.d.ts +2 -0
  46. package/dist/booking-engine/draft-shape.test.d.ts.map +1 -0
  47. package/dist/booking-engine/draft-shape.test.js +74 -0
  48. package/dist/booking-engine/drafts-schema.d.ts +302 -0
  49. package/dist/booking-engine/drafts-schema.d.ts.map +1 -0
  50. package/dist/booking-engine/drafts-schema.js +53 -0
  51. package/dist/booking-engine/drafts-service.d.ts +41 -0
  52. package/dist/booking-engine/drafts-service.d.ts.map +1 -0
  53. package/dist/booking-engine/drafts-service.js +108 -0
  54. package/dist/booking-engine/errors.d.ts +81 -0
  55. package/dist/booking-engine/errors.d.ts.map +1 -0
  56. package/dist/booking-engine/errors.js +113 -0
  57. package/dist/booking-engine/index.d.ts +36 -0
  58. package/dist/booking-engine/index.d.ts.map +1 -0
  59. package/dist/booking-engine/index.js +34 -0
  60. package/dist/booking-engine/orders.d.ts +41 -0
  61. package/dist/booking-engine/orders.d.ts.map +1 -0
  62. package/dist/booking-engine/orders.js +49 -0
  63. package/dist/booking-engine/owned-handler.d.ts +166 -0
  64. package/dist/booking-engine/owned-handler.d.ts.map +1 -0
  65. package/dist/booking-engine/owned-handler.js +50 -0
  66. package/dist/booking-engine/owned-handler.test.d.ts +2 -0
  67. package/dist/booking-engine/owned-handler.test.d.ts.map +1 -0
  68. package/dist/booking-engine/owned-handler.test.js +63 -0
  69. package/dist/booking-engine/promotions-contract.d.ts +8 -0
  70. package/dist/booking-engine/promotions-contract.d.ts.map +1 -0
  71. package/dist/booking-engine/promotions-contract.js +7 -0
  72. package/dist/booking-engine/quote-enricher.test.d.ts +12 -0
  73. package/dist/booking-engine/quote-enricher.test.d.ts.map +1 -0
  74. package/dist/booking-engine/quote-enricher.test.js +138 -0
  75. package/dist/booking-engine/quote.d.ts +163 -0
  76. package/dist/booking-engine/quote.d.ts.map +1 -0
  77. package/dist/booking-engine/quote.js +259 -0
  78. package/dist/booking-engine/registry.d.ts +85 -0
  79. package/dist/booking-engine/registry.d.ts.map +1 -0
  80. package/dist/booking-engine/registry.js +118 -0
  81. package/dist/booking-engine/registry.test.d.ts +2 -0
  82. package/dist/booking-engine/registry.test.d.ts.map +1 -0
  83. package/dist/booking-engine/registry.test.js +132 -0
  84. package/dist/booking-engine/routes-contracts.d.ts +169 -0
  85. package/dist/booking-engine/routes-contracts.d.ts.map +1 -0
  86. package/dist/booking-engine/routes-contracts.js +63 -0
  87. package/dist/booking-engine/routes.d.ts +7 -0
  88. package/dist/booking-engine/routes.d.ts.map +1 -0
  89. package/dist/booking-engine/routes.js +443 -0
  90. package/dist/booking-engine/routes.test.d.ts +2 -0
  91. package/dist/booking-engine/routes.test.d.ts.map +1 -0
  92. package/dist/booking-engine/routes.test.js +304 -0
  93. package/dist/booking-engine/schema.d.ts +455 -0
  94. package/dist/booking-engine/schema.d.ts.map +1 -0
  95. package/dist/booking-engine/schema.js +75 -0
  96. package/dist/booking-engine/snapshot-content.d.ts +120 -0
  97. package/dist/booking-engine/snapshot-content.d.ts.map +1 -0
  98. package/dist/booking-engine/snapshot-content.js +110 -0
  99. package/dist/booking-engine/snapshot-content.test.d.ts +2 -0
  100. package/dist/booking-engine/snapshot-content.test.d.ts.map +1 -0
  101. package/dist/booking-engine/snapshot-content.test.js +213 -0
  102. package/dist/booking-engine/sync.d.ts +136 -0
  103. package/dist/booking-engine/sync.d.ts.map +1 -0
  104. package/dist/booking-engine/sync.js +177 -0
  105. package/dist/booking-engine/sync.test.d.ts +2 -0
  106. package/dist/booking-engine/sync.test.d.ts.map +1 -0
  107. package/dist/booking-engine/sync.test.js +377 -0
  108. package/dist/contract.d.ts +2 -0
  109. package/dist/contract.d.ts.map +1 -0
  110. package/dist/contract.js +1 -0
  111. package/dist/contract.test.d.ts +2 -0
  112. package/dist/contract.test.d.ts.map +1 -0
  113. package/dist/contract.test.js +107 -0
  114. package/dist/drift/events.d.ts +2 -0
  115. package/dist/drift/events.d.ts.map +1 -0
  116. package/dist/drift/events.js +1 -0
  117. package/dist/drift/events.test.d.ts +2 -0
  118. package/dist/drift/events.test.d.ts.map +1 -0
  119. package/dist/drift/events.test.js +100 -0
  120. package/dist/embeddings/contract.d.ts +85 -0
  121. package/dist/embeddings/contract.d.ts.map +1 -0
  122. package/dist/embeddings/contract.js +42 -0
  123. package/dist/embeddings/contract.test.d.ts +2 -0
  124. package/dist/embeddings/contract.test.d.ts.map +1 -0
  125. package/dist/embeddings/contract.test.js +30 -0
  126. package/dist/embeddings/gemini.d.ts +110 -0
  127. package/dist/embeddings/gemini.d.ts.map +1 -0
  128. package/dist/embeddings/gemini.js +118 -0
  129. package/dist/embeddings/gemini.test.d.ts +2 -0
  130. package/dist/embeddings/gemini.test.d.ts.map +1 -0
  131. package/dist/embeddings/gemini.test.js +132 -0
  132. package/dist/embeddings/model-registry.d.ts +62 -0
  133. package/dist/embeddings/model-registry.d.ts.map +1 -0
  134. package/dist/embeddings/model-registry.js +78 -0
  135. package/dist/embeddings/model-registry.test.d.ts +2 -0
  136. package/dist/embeddings/model-registry.test.d.ts.map +1 -0
  137. package/dist/embeddings/model-registry.test.js +81 -0
  138. package/dist/embeddings/openai.d.ts +81 -0
  139. package/dist/embeddings/openai.d.ts.map +1 -0
  140. package/dist/embeddings/openai.js +123 -0
  141. package/dist/embeddings/openai.test.d.ts +2 -0
  142. package/dist/embeddings/openai.test.d.ts.map +1 -0
  143. package/dist/embeddings/openai.test.js +164 -0
  144. package/dist/events/taxonomy.d.ts +158 -0
  145. package/dist/events/taxonomy.d.ts.map +1 -0
  146. package/dist/events/taxonomy.js +99 -0
  147. package/dist/events/taxonomy.test.d.ts +2 -0
  148. package/dist/events/taxonomy.test.d.ts.map +1 -0
  149. package/dist/events/taxonomy.test.js +48 -0
  150. package/dist/index.d.ts +27 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +39 -0
  153. package/dist/indexer/contract.d.ts +203 -0
  154. package/dist/indexer/contract.d.ts.map +1 -0
  155. package/dist/indexer/contract.js +16 -0
  156. package/dist/indexer/typesense-search-query.d.ts +31 -0
  157. package/dist/indexer/typesense-search-query.d.ts.map +1 -0
  158. package/dist/indexer/typesense-search-query.js +185 -0
  159. package/dist/indexer/typesense.d.ts +105 -0
  160. package/dist/indexer/typesense.d.ts.map +1 -0
  161. package/dist/indexer/typesense.js +394 -0
  162. package/dist/indexer/typesense.test.d.ts +2 -0
  163. package/dist/indexer/typesense.test.d.ts.map +1 -0
  164. package/dist/indexer/typesense.test.js +253 -0
  165. package/dist/overlay/resolver.d.ts +101 -0
  166. package/dist/overlay/resolver.d.ts.map +1 -0
  167. package/dist/overlay/resolver.js +167 -0
  168. package/dist/overlay/resolver.test.d.ts +2 -0
  169. package/dist/overlay/resolver.test.d.ts.map +1 -0
  170. package/dist/overlay/resolver.test.js +179 -0
  171. package/dist/overlay/schema.d.ts +266 -0
  172. package/dist/overlay/schema.d.ts.map +1 -0
  173. package/dist/overlay/schema.js +71 -0
  174. package/dist/provenance.d.ts +2 -0
  175. package/dist/provenance.d.ts.map +1 -0
  176. package/dist/provenance.js +1 -0
  177. package/dist/schema-sourced-entries.d.ts +344 -0
  178. package/dist/schema-sourced-entries.d.ts.map +1 -0
  179. package/dist/schema-sourced-entries.js +75 -0
  180. package/dist/schema.d.ts +21 -0
  181. package/dist/schema.d.ts.map +1 -0
  182. package/dist/schema.js +20 -0
  183. package/dist/search/federate.d.ts +58 -0
  184. package/dist/search/federate.d.ts.map +1 -0
  185. package/dist/search/federate.js +103 -0
  186. package/dist/search/federate.test.d.ts +2 -0
  187. package/dist/search/federate.test.d.ts.map +1 -0
  188. package/dist/search/federate.test.js +146 -0
  189. package/dist/search/rerank.d.ts +77 -0
  190. package/dist/search/rerank.d.ts.map +1 -0
  191. package/dist/search/rerank.js +68 -0
  192. package/dist/search/rerank.test.d.ts +2 -0
  193. package/dist/search/rerank.test.d.ts.map +1 -0
  194. package/dist/search/rerank.test.js +60 -0
  195. package/dist/search/routes.d.ts +144 -0
  196. package/dist/search/routes.d.ts.map +1 -0
  197. package/dist/search/routes.js +288 -0
  198. package/dist/search/routes.test.d.ts +2 -0
  199. package/dist/search/routes.test.d.ts.map +1 -0
  200. package/dist/search/routes.test.js +322 -0
  201. package/dist/search/semantic.d.ts +63 -0
  202. package/dist/search/semantic.d.ts.map +1 -0
  203. package/dist/search/semantic.js +75 -0
  204. package/dist/search/semantic.test.d.ts +2 -0
  205. package/dist/search/semantic.test.d.ts.map +1 -0
  206. package/dist/search/semantic.test.js +143 -0
  207. package/dist/services/build-indexer-document.test.d.ts +2 -0
  208. package/dist/services/build-indexer-document.test.d.ts.map +1 -0
  209. package/dist/services/build-indexer-document.test.js +102 -0
  210. package/dist/services/content-service.d.ts +125 -0
  211. package/dist/services/content-service.d.ts.map +1 -0
  212. package/dist/services/content-service.js +139 -0
  213. package/dist/services/content-service.test.d.ts +2 -0
  214. package/dist/services/content-service.test.d.ts.map +1 -0
  215. package/dist/services/content-service.test.js +322 -0
  216. package/dist/services/indexer-service.d.ts +109 -0
  217. package/dist/services/indexer-service.d.ts.map +1 -0
  218. package/dist/services/indexer-service.js +123 -0
  219. package/dist/services/indexer-service.test.d.ts +2 -0
  220. package/dist/services/indexer-service.test.d.ts.map +1 -0
  221. package/dist/services/indexer-service.test.js +176 -0
  222. package/dist/services/overlay-service.d.ts +108 -0
  223. package/dist/services/overlay-service.d.ts.map +1 -0
  224. package/dist/services/overlay-service.js +211 -0
  225. package/dist/services/overlay-service.test.d.ts +2 -0
  226. package/dist/services/overlay-service.test.d.ts.map +1 -0
  227. package/dist/services/overlay-service.test.js +79 -0
  228. package/dist/services/snapshot-builder.test.d.ts +2 -0
  229. package/dist/services/snapshot-builder.test.d.ts.map +1 -0
  230. package/dist/services/snapshot-builder.test.js +93 -0
  231. package/dist/services/snapshot-service.d.ts +78 -0
  232. package/dist/services/snapshot-service.d.ts.map +1 -0
  233. package/dist/services/snapshot-service.js +165 -0
  234. package/dist/services/sourced-entry-service.d.ts +142 -0
  235. package/dist/services/sourced-entry-service.d.ts.map +1 -0
  236. package/dist/services/sourced-entry-service.js +203 -0
  237. package/dist/services/sourced-entry-service.test.d.ts +10 -0
  238. package/dist/services/sourced-entry-service.test.d.ts.map +1 -0
  239. package/dist/services/sourced-entry-service.test.js +66 -0
  240. package/dist/snapshot/schema.d.ts +362 -0
  241. package/dist/snapshot/schema.d.ts.map +1 -0
  242. package/dist/snapshot/schema.js +102 -0
  243. package/package.json +210 -0
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSnapshotInputFromView, viewToFrozenPayload, viewToOverlayState, } from "./snapshot-service.js";
3
+ const sampleView = {
4
+ values: new Map([
5
+ ["title", "Resolved title"],
6
+ ["description", "Resolved description"],
7
+ ["status", "active"],
8
+ ]),
9
+ hidden: new Set(["internal_notes"]),
10
+ provenance: new Map([
11
+ ["title", { locale: "en-GB", audience: "customer", market: "default" }],
12
+ ["description", null],
13
+ ["status", null],
14
+ ]),
15
+ };
16
+ describe("viewToFrozenPayload", () => {
17
+ it("converts the resolved value Map into a JSONB-shaped object", () => {
18
+ const frozen = viewToFrozenPayload(sampleView);
19
+ expect(frozen).toEqual({
20
+ title: "Resolved title",
21
+ description: "Resolved description",
22
+ status: "active",
23
+ });
24
+ });
25
+ it("does not include hidden fields", () => {
26
+ const frozen = viewToFrozenPayload(sampleView);
27
+ expect(frozen).not.toHaveProperty("internal_notes");
28
+ });
29
+ });
30
+ describe("viewToOverlayState", () => {
31
+ it("includes only fields where an overlay was applied", () => {
32
+ const state = viewToOverlayState(sampleView);
33
+ expect(state).toHaveProperty("title");
34
+ expect(state).not.toHaveProperty("description");
35
+ expect(state).not.toHaveProperty("status");
36
+ });
37
+ it("records the variant slice that satisfied each overlay", () => {
38
+ const state = viewToOverlayState(sampleView);
39
+ expect(state.title).toEqual({
40
+ locale: "en-GB",
41
+ audience: "customer",
42
+ market: "default",
43
+ });
44
+ });
45
+ });
46
+ describe("buildSnapshotInputFromView", () => {
47
+ it("composes a CaptureSnapshotInput with frozen payload + overlay state", () => {
48
+ const input = buildSnapshotInputFromView(sampleView, {
49
+ entityModule: "products",
50
+ entityId: "prod_xyz",
51
+ sourceKind: "owned",
52
+ });
53
+ expect(input.entityModule).toBe("products");
54
+ expect(input.entityId).toBe("prod_xyz");
55
+ expect(input.sourceKind).toBe("owned");
56
+ expect(input.frozenPayload).toEqual({
57
+ title: "Resolved title",
58
+ description: "Resolved description",
59
+ status: "active",
60
+ });
61
+ expect(input.overlayStateAtCapture).toEqual({
62
+ title: { locale: "en-GB", audience: "customer", market: "default" },
63
+ });
64
+ });
65
+ it("passes through pricingBasis when supplied", () => {
66
+ const input = buildSnapshotInputFromView(sampleView, {
67
+ entityModule: "products",
68
+ entityId: "prod_xyz",
69
+ sourceKind: "owned",
70
+ pricingBasis: {
71
+ base_amount: 1000,
72
+ taxes: 100,
73
+ fees: 50,
74
+ surcharges: 0,
75
+ currency: "EUR",
76
+ },
77
+ });
78
+ expect(input.pricingBasis?.base_amount).toBe(1000);
79
+ expect(input.pricingBasis?.currency).toBe("EUR");
80
+ });
81
+ it("forwards source connection identifiers when provided", () => {
82
+ const input = buildSnapshotInputFromView(sampleView, {
83
+ entityModule: "cruises",
84
+ entityId: "crse_abc",
85
+ sourceKind: "voyant-connect",
86
+ sourceConnectionId: "conn_viking",
87
+ sourceRef: "WAVE2026-RHN-15D",
88
+ });
89
+ expect(input.sourceKind).toBe("voyant-connect");
90
+ expect(input.sourceConnectionId).toBe("conn_viking");
91
+ expect(input.sourceRef).toBe("WAVE2026-RHN-15D");
92
+ });
93
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * SnapshotService — drizzle-bound capture and read of booking snapshot
3
+ * graphs.
4
+ *
5
+ * One booking commit may produce multiple snapshot rows — one per
6
+ * participating CatalogEntry (a TUI package booking captures the package,
7
+ * the referenced hotel, each selected excursion, the chosen departure, the
8
+ * selected flight). Snapshots are immutable once written and preserved
9
+ * through source disconnection.
10
+ *
11
+ * Functions take an `AnyDrizzleDb` as their first parameter to match the
12
+ * existing voyant convention.
13
+ *
14
+ * See `docs/architecture/catalog-architecture.md` §5.3 for the design.
15
+ */
16
+ import type { CaptureSnapshotInput } from "@voyant-travel/catalog-contracts/snapshot";
17
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
18
+ import type { ResolvedView } from "../overlay/resolver.js";
19
+ import { type PricingBasis, type SelectBookingCatalogSnapshot } from "../snapshot/schema.js";
20
+ export type { CaptureSnapshotInput };
21
+ /**
22
+ * Capture a single booking-catalog-snapshot row. Idempotent on
23
+ * `(booking_id, entity_module, entity_id)` — re-capturing the same entity
24
+ * inside the same booking is a logic bug; this function lets the unique
25
+ * constraint catch it rather than silently overwriting.
26
+ */
27
+ export declare function captureSnapshot(db: AnyDrizzleDb, input: CaptureSnapshotInput): Promise<SelectBookingCatalogSnapshot>;
28
+ /**
29
+ * Capture multiple snapshot rows for a single booking in one transaction.
30
+ * Used by the booking-commit pipeline when a booking participates with
31
+ * multiple CatalogEntries (composite packages, cruises with selected
32
+ * cabins, flights with passengers).
33
+ *
34
+ * If any single capture fails, the whole transaction rolls back — the
35
+ * booking's snapshot graph is all-or-nothing.
36
+ */
37
+ export declare function captureSnapshotGraph(db: AnyDrizzleDb, bookingId: string, inputs: ReadonlyArray<Omit<CaptureSnapshotInput, "bookingId">>): Promise<SelectBookingCatalogSnapshot[]>;
38
+ /**
39
+ * Fetch all snapshot rows for a single booking. Returns the full snapshot
40
+ * graph — one row per participating CatalogEntry. Used by refunds, audits,
41
+ * post-book operations.
42
+ */
43
+ export declare function fetchSnapshotsForBooking(db: AnyDrizzleDb, bookingId: string): Promise<SelectBookingCatalogSnapshot[]>;
44
+ /**
45
+ * Fetch a specific entity's snapshot for a booking. Returns `null` if no
46
+ * snapshot was captured for the entity.
47
+ */
48
+ export declare function fetchEntitySnapshot(db: AnyDrizzleDb, bookingId: string, entityModule: string, entityId: string): Promise<SelectBookingCatalogSnapshot | null>;
49
+ /**
50
+ * Converts a `ResolvedView`'s value Map into a plain object suitable for
51
+ * storing as the JSONB `frozen_payload`. Field paths become keys.
52
+ */
53
+ export declare function viewToFrozenPayload(view: ResolvedView): Record<string, unknown>;
54
+ /**
55
+ * Converts a `ResolvedView`'s provenance Map into a plain object suitable
56
+ * for storing as the JSONB `overlay_state_at_capture`. Records which
57
+ * variant slice satisfied each overlayed field at capture time. Fields
58
+ * with `null` provenance (no overlay applied) are omitted.
59
+ */
60
+ export declare function viewToOverlayState(view: ResolvedView): Record<string, unknown>;
61
+ /**
62
+ * Composition helper: turn a `ResolvedView` plus identity / provenance /
63
+ * pricing context into a `CaptureSnapshotInput` ready to pass into
64
+ * `captureSnapshot` or `captureSnapshotGraph`.
65
+ *
66
+ * Verticals' `buildXSnapshotInput` helpers wrap this with the vertical-
67
+ * specific row fetching + resolution.
68
+ */
69
+ export declare function buildSnapshotInputFromView(view: ResolvedView, context: {
70
+ entityModule: string;
71
+ entityId: string;
72
+ sourceKind: string;
73
+ sourceProvider?: string;
74
+ sourceConnectionId?: string;
75
+ sourceRef?: string;
76
+ pricingBasis?: PricingBasis;
77
+ }): Omit<CaptureSnapshotInput, "bookingId">;
78
+ //# sourceMappingURL=snapshot-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-service.d.ts","sourceRoot":"","sources":["../../src/services/snapshot-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,4BAA4B,EAClC,MAAM,uBAAuB,CAAA;AAQ9B,YAAY,EAAE,oBAAoB,EAAE,CAAA;AAEpC;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,4BAA4B,CAAC,CAkCvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC,GAC7D,OAAO,CAAC,4BAA4B,EAAE,CAAC,CA2BzC;AAMD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,4BAA4B,EAAE,CAAC,CAMzC;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAa9C;AAMD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM/E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM9E;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE;IACP,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,GACA,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAYzC"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * SnapshotService — drizzle-bound capture and read of booking snapshot
3
+ * graphs.
4
+ *
5
+ * One booking commit may produce multiple snapshot rows — one per
6
+ * participating CatalogEntry (a TUI package booking captures the package,
7
+ * the referenced hotel, each selected excursion, the chosen departure, the
8
+ * selected flight). Snapshots are immutable once written and preserved
9
+ * through source disconnection.
10
+ *
11
+ * Functions take an `AnyDrizzleDb` as their first parameter to match the
12
+ * existing voyant convention.
13
+ *
14
+ * See `docs/architecture/catalog-architecture.md` §5.3 for the design.
15
+ */
16
+ import { newId } from "@voyant-travel/db/lib/typeid";
17
+ import { and, eq } from "drizzle-orm";
18
+ import { bookingCatalogSnapshotTable, } from "../snapshot/schema.js";
19
+ /**
20
+ * Capture a single booking-catalog-snapshot row. Idempotent on
21
+ * `(booking_id, entity_module, entity_id)` — re-capturing the same entity
22
+ * inside the same booking is a logic bug; this function lets the unique
23
+ * constraint catch it rather than silently overwriting.
24
+ */
25
+ export async function captureSnapshot(db, input) {
26
+ const inserted = await db
27
+ .insert(bookingCatalogSnapshotTable)
28
+ .values({
29
+ id: newId("booking_catalog_snapshot"),
30
+ booking_id: input.bookingId,
31
+ entity_module: input.entityModule,
32
+ entity_id: input.entityId,
33
+ source_kind: input.sourceKind,
34
+ source_provider: input.sourceProvider,
35
+ source_connection_id: input.sourceConnectionId,
36
+ source_ref: input.sourceRef,
37
+ frozen_payload: input.frozenPayload,
38
+ overlay_state_at_capture: input.overlayStateAtCapture,
39
+ pricing_base_amount: input.pricingBasis?.base_amount != null
40
+ ? String(input.pricingBasis.base_amount)
41
+ : undefined,
42
+ pricing_taxes: input.pricingBasis?.taxes != null ? String(input.pricingBasis.taxes) : undefined,
43
+ pricing_fees: input.pricingBasis?.fees != null ? String(input.pricingBasis.fees) : undefined,
44
+ pricing_surcharges: input.pricingBasis?.surcharges != null ? String(input.pricingBasis.surcharges) : undefined,
45
+ pricing_currency: input.pricingBasis?.currency,
46
+ pricing_breakdown: input.pricingBasis?.breakdown,
47
+ pricing_applied_offers: input.pricingBasis?.appliedOffers,
48
+ idempotency_key: input.idempotencyKey,
49
+ })
50
+ .returning();
51
+ if (!inserted[0]) {
52
+ throw new Error("captureSnapshot: insert returned no rows");
53
+ }
54
+ return inserted[0];
55
+ }
56
+ /**
57
+ * Capture multiple snapshot rows for a single booking in one transaction.
58
+ * Used by the booking-commit pipeline when a booking participates with
59
+ * multiple CatalogEntries (composite packages, cruises with selected
60
+ * cabins, flights with passengers).
61
+ *
62
+ * If any single capture fails, the whole transaction rolls back — the
63
+ * booking's snapshot graph is all-or-nothing.
64
+ */
65
+ export async function captureSnapshotGraph(db, bookingId, inputs) {
66
+ if (inputs.length === 0)
67
+ return [];
68
+ const rows = inputs.map((input) => ({
69
+ id: newId("booking_catalog_snapshot"),
70
+ booking_id: bookingId,
71
+ entity_module: input.entityModule,
72
+ entity_id: input.entityId,
73
+ source_kind: input.sourceKind,
74
+ source_provider: input.sourceProvider,
75
+ source_connection_id: input.sourceConnectionId,
76
+ source_ref: input.sourceRef,
77
+ frozen_payload: input.frozenPayload,
78
+ overlay_state_at_capture: input.overlayStateAtCapture,
79
+ pricing_base_amount: input.pricingBasis?.base_amount != null ? String(input.pricingBasis.base_amount) : undefined,
80
+ pricing_taxes: input.pricingBasis?.taxes != null ? String(input.pricingBasis.taxes) : undefined,
81
+ pricing_fees: input.pricingBasis?.fees != null ? String(input.pricingBasis.fees) : undefined,
82
+ pricing_surcharges: input.pricingBasis?.surcharges != null ? String(input.pricingBasis.surcharges) : undefined,
83
+ pricing_currency: input.pricingBasis?.currency,
84
+ pricing_breakdown: input.pricingBasis?.breakdown,
85
+ pricing_applied_offers: input.pricingBasis?.appliedOffers,
86
+ }));
87
+ const inserted = await db.insert(bookingCatalogSnapshotTable).values(rows).returning();
88
+ return inserted;
89
+ }
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ // Reads
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+ /**
94
+ * Fetch all snapshot rows for a single booking. Returns the full snapshot
95
+ * graph — one row per participating CatalogEntry. Used by refunds, audits,
96
+ * post-book operations.
97
+ */
98
+ export async function fetchSnapshotsForBooking(db, bookingId) {
99
+ const rows = await db
100
+ .select()
101
+ .from(bookingCatalogSnapshotTable)
102
+ .where(eq(bookingCatalogSnapshotTable.booking_id, bookingId));
103
+ return rows;
104
+ }
105
+ /**
106
+ * Fetch a specific entity's snapshot for a booking. Returns `null` if no
107
+ * snapshot was captured for the entity.
108
+ */
109
+ export async function fetchEntitySnapshot(db, bookingId, entityModule, entityId) {
110
+ const rows = await db
111
+ .select()
112
+ .from(bookingCatalogSnapshotTable)
113
+ .where(and(eq(bookingCatalogSnapshotTable.booking_id, bookingId), eq(bookingCatalogSnapshotTable.entity_module, entityModule), eq(bookingCatalogSnapshotTable.entity_id, entityId)))
114
+ .limit(1);
115
+ return (rows[0] ?? null);
116
+ }
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+ // View → snapshot input helpers
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ /**
121
+ * Converts a `ResolvedView`'s value Map into a plain object suitable for
122
+ * storing as the JSONB `frozen_payload`. Field paths become keys.
123
+ */
124
+ export function viewToFrozenPayload(view) {
125
+ const result = {};
126
+ for (const [path, value] of view.values) {
127
+ result[path] = value;
128
+ }
129
+ return result;
130
+ }
131
+ /**
132
+ * Converts a `ResolvedView`'s provenance Map into a plain object suitable
133
+ * for storing as the JSONB `overlay_state_at_capture`. Records which
134
+ * variant slice satisfied each overlayed field at capture time. Fields
135
+ * with `null` provenance (no overlay applied) are omitted.
136
+ */
137
+ export function viewToOverlayState(view) {
138
+ const result = {};
139
+ for (const [path, prov] of view.provenance) {
140
+ if (prov)
141
+ result[path] = prov;
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Composition helper: turn a `ResolvedView` plus identity / provenance /
147
+ * pricing context into a `CaptureSnapshotInput` ready to pass into
148
+ * `captureSnapshot` or `captureSnapshotGraph`.
149
+ *
150
+ * Verticals' `buildXSnapshotInput` helpers wrap this with the vertical-
151
+ * specific row fetching + resolution.
152
+ */
153
+ export function buildSnapshotInputFromView(view, context) {
154
+ return {
155
+ entityModule: context.entityModule,
156
+ entityId: context.entityId,
157
+ sourceKind: context.sourceKind,
158
+ sourceProvider: context.sourceProvider,
159
+ sourceConnectionId: context.sourceConnectionId,
160
+ sourceRef: context.sourceRef,
161
+ frozenPayload: viewToFrozenPayload(view),
162
+ overlayStateAtCapture: viewToOverlayState(view),
163
+ pricingBasis: context.pricingBasis,
164
+ };
165
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * SourcedEntryService — drizzle-bound entry point for the
3
+ * `catalog_sourced_entries` store.
4
+ *
5
+ * One row per sourced entity, written at `discover()` time. The row carries
6
+ * provenance, lifecycle markers, and the canonical local copy of the
7
+ * indexed projection (so the read service and the thin-content synthesizer
8
+ * can dispatch without round-tripping the search index).
9
+ *
10
+ * Three primitives:
11
+ *
12
+ * - `upsertSourcedEntry` — called from `sync.ts` for every projection;
13
+ * idempotent on (entity_module, entity_id) and on
14
+ * (source_kind, source_connection_id, source_ref).
15
+ * - `readSourcedEntry` — point-read by Voyant-side identity. Returns
16
+ * null for owned entities (which have no row here).
17
+ * - `createReadProvenance` — factory that composes `readSourcedEntry`
18
+ * with vertical-specific owned-checkers to produce a unified
19
+ * `readProvenance(db, entity_module, entity_id)` that returns one of:
20
+ * `{ kind: "owned" }`, `{ kind: "sourced", ... }`, or `null`.
21
+ *
22
+ * The factory pattern keeps this package neutral — it doesn't know how to
23
+ * read the products / cruises / hotels owned tables. Each vertical
24
+ * registers its owned-checker once when wiring its content service (Phase
25
+ * D and beyond).
26
+ *
27
+ * See `docs/architecture/catalog-sourced-content.md` §2.5.
28
+ */
29
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
30
+ import type { CatalogProjection } from "../adapter/contract.js";
31
+ import type { Provenance } from "../provenance.js";
32
+ import { type SelectCatalogSourcedEntry, type SourcedEntryStatus } from "../schema-sourced-entries.js";
33
+ /**
34
+ * Result of a `readProvenance` call. Three shapes:
35
+ *
36
+ * - `{ kind: "owned" }` — entity exists in the vertical's owned
37
+ * table; no sourced-entry row.
38
+ * - `{ kind: "sourced", ... }` — entity is sourced; carries the durable
39
+ * provenance row + entry id.
40
+ * - `null` — entity not found in either store.
41
+ *
42
+ * Callers pattern-match on `kind` to dispatch owned-vs-sourced reads.
43
+ */
44
+ export type ProvenanceReadResult = {
45
+ kind: "owned";
46
+ provenance: Provenance;
47
+ } | {
48
+ kind: "sourced";
49
+ provenance: Provenance;
50
+ entry_id: string;
51
+ status: SourcedEntryStatus;
52
+ projection: Record<string, unknown>;
53
+ projection_etag: string | null;
54
+ projection_seen_at: Date;
55
+ first_seen_at: Date;
56
+ last_seen_at: Date;
57
+ };
58
+ /**
59
+ * Vertical-specific owned-checker. Returns `true` iff the entity exists in
60
+ * the vertical's owned table (i.e. its id matches a row that has no source
61
+ * link). Implementations are tiny per-vertical helpers and live in the
62
+ * vertical package — the catalog plane doesn't import them directly.
63
+ */
64
+ export type OwnedChecker = (db: AnyDrizzleDb, entityId: string) => Promise<boolean>;
65
+ /**
66
+ * Read one sourced-entry row by Voyant-side identity. Returns `null` for
67
+ * entities that aren't in the sourced-entry store — owned entities, or
68
+ * sourced entities the deployment hasn't yet discovered.
69
+ */
70
+ export declare function readSourcedEntry(db: AnyDrizzleDb, entityModule: string, entityId: string): Promise<SelectCatalogSourcedEntry | null>;
71
+ /**
72
+ * Build a unified `readProvenance(db, entity_module, entity_id)` against a
73
+ * registry of vertical-specific owned-checkers. The returned function:
74
+ *
75
+ * 1. Calls the vertical's owned-checker. If it returns `true`, the entity
76
+ * is owned — return `{ kind: "owned", provenance: ... }` without
77
+ * touching the sourced-entry table.
78
+ * 2. Otherwise, look up the sourced-entry row. If found, return
79
+ * `{ kind: "sourced", ... }`.
80
+ * 3. If neither, return `null`.
81
+ *
82
+ * Verticals not in `ownedCheckers` skip the owned check (treated as
83
+ * sourced-only). This is intentional: not every vertical has an owned
84
+ * counterpart for every sourced entity.
85
+ */
86
+ export declare function createReadProvenance(options: {
87
+ ownedCheckers?: ReadonlyMap<string, OwnedChecker>;
88
+ }): (db: AnyDrizzleDb, entityModule: string, entityId: string) => Promise<ProvenanceReadResult | null>;
89
+ /**
90
+ * Input for `upsertSourcedEntry`. Accepts a `CatalogProjection` (the shape
91
+ * `discover()` emits) plus optional metadata the adapter chose not to put
92
+ * on the projection (etag, freshness override).
93
+ */
94
+ export interface UpsertSourcedEntryInput {
95
+ /** The projection emitted by `adapter.discover()`. */
96
+ projection: CatalogProjection;
97
+ /**
98
+ * Optional ETag-style marker for the projection itself. Distinct from
99
+ * the content cache's etag — this one stamps the indexed projection.
100
+ */
101
+ projectionEtag?: string;
102
+ /**
103
+ * When the upstream said this projection was last sourced. Defaults to
104
+ * `provenance.last_sourced_at` if set, otherwise `new Date()`.
105
+ */
106
+ lastSourcedAt?: Date;
107
+ /**
108
+ * Optional override for the lifecycle status. Withdrawal sweepers set
109
+ * this to `"withdrawn"` for rows the upstream stopped emitting.
110
+ */
111
+ status?: SourcedEntryStatus;
112
+ }
113
+ /**
114
+ * Upsert a sourced-entry row. Idempotent on `(entity_module, entity_id)`
115
+ * — repeated calls update `projection`, `projection_etag`,
116
+ * `projection_seen_at`, `last_seen_at`, `last_sourced_at`,
117
+ * `source_freshness`, and `updated_at`. The first-seen timestamp is
118
+ * preserved.
119
+ *
120
+ * Owned projections are rejected — `provenance.source_kind === "owned"`
121
+ * has no place in the sourced-entry store. Callers in `sync.ts` should
122
+ * already filter these out, but this guard makes the invariant explicit.
123
+ */
124
+ export declare function upsertSourcedEntry(db: AnyDrizzleDb, input: UpsertSourcedEntryInput): Promise<SelectCatalogSourcedEntry>;
125
+ /**
126
+ * Mark a sourced-entry row as withdrawn (the upstream stopped emitting
127
+ * it). Used by the periodic withdrawal sweeper or by drift events of kind
128
+ * `entity_archived`. Does not delete the row — withdrawals are auditable.
129
+ */
130
+ export declare function markSourcedEntryWithdrawn(db: AnyDrizzleDb, entityModule: string, entityId: string): Promise<void>;
131
+ /**
132
+ * Mark active sourced rows missing from a successful full-source discovery pass
133
+ * as withdrawn. Callers should invoke this only after an adapter completed its
134
+ * projection stream; failed refreshes must leave existing rows untouched.
135
+ */
136
+ export declare function markMissingSourcedEntriesWithdrawn(db: AnyDrizzleDb, input: {
137
+ entityModule: string;
138
+ sourceKind: string;
139
+ sourceConnectionId?: string | null;
140
+ seenEntityIds: ReadonlySet<string>;
141
+ }): Promise<SelectCatalogSourcedEntry[]>;
142
+ //# sourceMappingURL=sourced-entry-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourced-entry-service.d.ts","sourceRoot":"","sources":["../../src/services/sourced-entry-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAA;AAC9D,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACxB,MAAM,8BAA8B,CAAA;AAMrC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GACzC;IACE,IAAI,EAAE,SAAS,CAAA;IACf,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,kBAAkB,EAAE,IAAI,CAAA;IACxB,aAAa,EAAE,IAAI,CAAA;IACnB,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AAEL;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAMnF;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,YAAY,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAY3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CAClD,GAAG,CACF,EAAE,EAAE,YAAY,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAyCxC;AAMD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,UAAU,EAAE,iBAAiB,CAAA;IAC7B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,IAAI,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAC5B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,yBAAyB,CAAC,CA2DpC;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IACL,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACnC,GACA,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAkCtC"}