@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,138 @@
1
+ /**
2
+ * Focused tests for the `contentEnricher` hook on `quoteEntity`. The
3
+ * full quote path needs a real DB — these tests stub the drizzle
4
+ * insert chain just enough to verify the hook's call contract:
5
+ * - Called with the right input when wired and live-resolve succeeds.
6
+ * - Result attached to the quote response as `shape`.
7
+ * - Errors swallowed + reported via `onEnricherError` instead of
8
+ * failing the quote.
9
+ * - Skipped when the entity is not available (failed live-resolve).
10
+ */
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, } from "./draft-shape.js";
13
+ import { quoteEntity } from "./quote.js";
14
+ import { createSourceAdapterRegistry } from "./registry.js";
15
+ function makeStubDb() {
16
+ // Mirrors enough of the drizzle chain for `.insert(...).values(...).returning()`.
17
+ return {
18
+ insert() {
19
+ return {
20
+ values(_v) {
21
+ return {
22
+ async returning() {
23
+ return [{ id: "cquo_x" }];
24
+ },
25
+ };
26
+ },
27
+ };
28
+ },
29
+ };
30
+ }
31
+ function makeAdapter(liveResolve) {
32
+ return {
33
+ kind: "stub",
34
+ capabilities: {
35
+ verticals: ["products"],
36
+ supportsLiveResolution: true,
37
+ supportsDriftDetection: false,
38
+ supportsBookingForwarding: false,
39
+ postBookOperations: [],
40
+ },
41
+ connect: async () => undefined,
42
+ pause: async () => undefined,
43
+ disconnect: async () => undefined,
44
+ getState: async () => "active",
45
+ discover: async () => ({ projections: [], next_cursor: undefined }),
46
+ liveResolve,
47
+ };
48
+ }
49
+ const baseRequest = {
50
+ entityModule: "products",
51
+ entityId: "prod_abc",
52
+ sourceKind: "stub",
53
+ scope: { locale: "en-GB", audience: "customer", market: "GB", currency: "GBP" },
54
+ adapterContext: { connection_id: "conn_1" },
55
+ };
56
+ const okLiveResolve = async (_ctx, _req) => ({
57
+ values: { prod_abc: { priceCents: 10000, currency: "GBP" } },
58
+ });
59
+ const failLiveResolve = async (_ctx, _req) => ({
60
+ values: {},
61
+ failed: { prod_abc: "not_found" },
62
+ });
63
+ const sampleShape = {
64
+ ...defaultDraftShapeFlags(),
65
+ paxBands: DEFAULT_PAX_BANDS,
66
+ paxBandsAllowedTotal: { min: 1, max: 8 },
67
+ travelerFields: defaultTravelerFields(),
68
+ bookingFields: defaultBookingFields(),
69
+ paymentIntents: ["hold", "card"],
70
+ };
71
+ describe("quoteEntity — contentEnricher hook", () => {
72
+ it("attaches the enricher's BookingDraftShape to the quote result", async () => {
73
+ const enricher = vi.fn(async () => sampleShape);
74
+ const registry = createSourceAdapterRegistry();
75
+ registry.register(makeAdapter(okLiveResolve));
76
+ const result = await quoteEntity(makeStubDb(), { registry, contentEnricher: enricher }, baseRequest);
77
+ expect(result.available).toBe(true);
78
+ expect(result.shape).toEqual(sampleShape);
79
+ expect(enricher).toHaveBeenCalledTimes(1);
80
+ });
81
+ it("threads entity identity + scope into the enricher input", async () => {
82
+ let captured = null;
83
+ const enricher = async (input) => {
84
+ captured = input;
85
+ return sampleShape;
86
+ };
87
+ const registry = createSourceAdapterRegistry();
88
+ registry.register(makeAdapter(okLiveResolve));
89
+ await quoteEntity(makeStubDb(), { registry, contentEnricher: enricher }, baseRequest);
90
+ expect(captured).not.toBeNull();
91
+ expect(captured.entityModule).toBe("products");
92
+ expect(captured.entityId).toBe("prod_abc");
93
+ expect(captured.sourceKind).toBe("stub");
94
+ expect(captured.scope.locale).toBe("en-GB");
95
+ expect(captured.scope.market).toBe("GB");
96
+ expect(captured.scope.currency).toBe("GBP");
97
+ });
98
+ it("omits shape when no enricher is wired (today's default)", async () => {
99
+ const registry = createSourceAdapterRegistry();
100
+ registry.register(makeAdapter(okLiveResolve));
101
+ const result = await quoteEntity(makeStubDb(), { registry }, baseRequest);
102
+ expect(result.shape).toBeUndefined();
103
+ });
104
+ it("skips the enricher when live-resolve fails (entity not available)", async () => {
105
+ const enricher = vi.fn(async () => sampleShape);
106
+ const registry = createSourceAdapterRegistry();
107
+ registry.register(makeAdapter(failLiveResolve));
108
+ const result = await quoteEntity(makeStubDb(), { registry, contentEnricher: enricher }, baseRequest);
109
+ expect(result.available).toBe(false);
110
+ expect(result.shape).toBeUndefined();
111
+ expect(enricher).not.toHaveBeenCalled();
112
+ });
113
+ it("swallows enricher errors and reports via onEnricherError — quote still succeeds", async () => {
114
+ const enricher = async () => {
115
+ throw new Error("content-service unreachable");
116
+ };
117
+ const errors = [];
118
+ const registry = createSourceAdapterRegistry();
119
+ registry.register(makeAdapter(okLiveResolve));
120
+ const result = await quoteEntity(makeStubDb(), { registry, contentEnricher: enricher, onEnricherError: (e) => errors.push(e) }, baseRequest);
121
+ // Quote succeeded — the enricher error didn't block.
122
+ expect(result.available).toBe(true);
123
+ expect(result.shape).toBeUndefined();
124
+ // Diagnostic surfaced.
125
+ expect(errors).toHaveLength(1);
126
+ expect(errors[0]?.entityModule).toBe("products");
127
+ expect(errors[0]?.entityId).toBe("prod_abc");
128
+ expect(errors[0]?.reason).toContain("content-service unreachable");
129
+ });
130
+ it("returns shape: undefined when the enricher returns null (entity has no content)", async () => {
131
+ const enricher = async () => null;
132
+ const registry = createSourceAdapterRegistry();
133
+ registry.register(makeAdapter(okLiveResolve));
134
+ const result = await quoteEntity(makeStubDb(), { registry, contentEnricher: enricher }, baseRequest);
135
+ expect(result.available).toBe(true);
136
+ expect(result.shape).toBeUndefined();
137
+ });
138
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * `quoteEntity` — the first step in the booking engine lifecycle.
3
+ *
4
+ * Asks the registered adapter "is this row still bookable, and at what
5
+ * price right now?", persists the answer in `catalog_quotes` with an
6
+ * expiry, and returns a stable `quoteId` the subsequent `bookEntity`
7
+ * call can validate against.
8
+ *
9
+ * Quotes are short-lived (default TTL: 10 minutes) and not de-duped.
10
+ * Re-quoting the same row produces a new quote row so the audit trail
11
+ * shows every quote attempt.
12
+ */
13
+ import type { AnyDrizzleDb } from "@voyant-travel/db";
14
+ import type { SourceAdapterContext } from "../adapter/contract.js";
15
+ import type { PricingBasis } from "../snapshot/schema.js";
16
+ import type { BookingDraftShape } from "./draft-shape.js";
17
+ import type { OwnedBookingHandlerRegistry } from "./owned-handler.js";
18
+ import type { PromotionEvaluationInput, PromotionEvaluationOutput } from "./promotions-contract.js";
19
+ import type { SourceAdapterRegistry } from "./registry.js";
20
+ /** Default time-to-live for a quote. */
21
+ export declare const DEFAULT_QUOTE_TTL_MS: number;
22
+ export interface QuoteScope {
23
+ locale: string;
24
+ audience: string;
25
+ market: string;
26
+ currency?: string;
27
+ }
28
+ export interface QuoteEntityRequest {
29
+ /** The catalog row to quote. */
30
+ entityModule: string;
31
+ entityId: string;
32
+ /** Source pointer, read from the row's provenance. */
33
+ sourceKind: string;
34
+ sourceProvider?: string;
35
+ sourceConnectionId?: string;
36
+ sourceRef?: string;
37
+ /** Variant scope for the quote. */
38
+ scope: QuoteScope;
39
+ /** Vertical-specific parameters echoed to the adapter (date range, pax, etc.). */
40
+ parameters?: Record<string, unknown>;
41
+ /** Override the TTL (rare — defaults to `DEFAULT_QUOTE_TTL_MS`). */
42
+ ttlMs?: number;
43
+ /** Adapter context (connection_id, credentials, correlation_id). */
44
+ adapterContext: SourceAdapterContext;
45
+ }
46
+ export interface QuoteEntityResult {
47
+ quoteId: string;
48
+ quotedAt: Date;
49
+ expiresAt: Date;
50
+ available: boolean;
51
+ invalidReason?: string;
52
+ pricing?: PricingBasis;
53
+ upstreamPayload?: Record<string, unknown>;
54
+ /**
55
+ * The journey wizard descriptor — populated when
56
+ * `deps.contentEnricher` is wired and the entity is sourced. Tells
57
+ * the wizard which steps + sub-steps to render. Per
58
+ * `docs/architecture/booking-journey-architecture.md` §3, this is
59
+ * returned alongside the quote so the journey can render the
60
+ * correct shape without a follow-up call.
61
+ *
62
+ * Undefined when no enricher is wired (today's behavior — the
63
+ * journey hardcodes a minimal shape until templates wire content).
64
+ */
65
+ shape?: BookingDraftShape;
66
+ }
67
+ /**
68
+ * Input the content enricher receives — quote + scope + parameters.
69
+ * The enricher reads cached content for the entity and projects a
70
+ * `BookingDraftShape` that drives the wizard. Verticals compose their
71
+ * `build*DraftShape` builders into one enricher routed by
72
+ * `entity_module`.
73
+ */
74
+ export interface QuoteContentEnrichmentInput {
75
+ db: AnyDrizzleDb;
76
+ entityModule: string;
77
+ entityId: string;
78
+ sourceKind: string;
79
+ sourceConnectionId?: string;
80
+ sourceRef?: string;
81
+ scope: QuoteScope;
82
+ parameters?: Record<string, unknown>;
83
+ adapterContext: SourceAdapterContext;
84
+ }
85
+ /**
86
+ * Hook called by `quoteEntity` after the live-resolve step succeeds.
87
+ * Receives entity identity + scope; returns a `BookingDraftShape` (or
88
+ * null when content is unavailable / the entity is owned and the
89
+ * enricher chooses not to surface a shape).
90
+ *
91
+ * Templates compose this from per-vertical content services, e.g.:
92
+ *
93
+ * const enricher: QuoteContentEnricher = async (input) => {
94
+ * const content = await readContentByModule(input)
95
+ * return content ? buildDraftShape(input.entityModule, content, input.scope) : null
96
+ * }
97
+ */
98
+ export type QuoteContentEnricher = (input: QuoteContentEnrichmentInput) => Promise<BookingDraftShape | null>;
99
+ export interface QuoteEntityDeps {
100
+ registry: SourceAdapterRegistry;
101
+ /**
102
+ * Owned-arm dispatch — when set and the request's source kind is
103
+ * `"owned"`, the engine routes to a handler keyed by
104
+ * `entityModule` instead of the SourceAdapterRegistry. Per
105
+ * booking-journey-architecture §6.
106
+ *
107
+ * Templates that ship owned products MUST wire this; templates
108
+ * that only proxy sourced rows can leave it undefined and the
109
+ * engine falls through to the legacy adapter path.
110
+ */
111
+ ownedHandlers?: OwnedBookingHandlerRegistry;
112
+ /**
113
+ * Optional content-aware enricher. When wired, called after the
114
+ * adapter's `liveResolve` step succeeds; the returned
115
+ * `BookingDraftShape` is attached to the quote result so the
116
+ * journey wizard can render the correct shape without a follow-up
117
+ * call.
118
+ *
119
+ * When not wired (today's default), the quote response omits
120
+ * `shape` and the journey falls back to its hardcoded minimal
121
+ * descriptor.
122
+ *
123
+ * Errors from the enricher are caught and logged via
124
+ * `onEnricherError` (defaults to silent) — they MUST NOT fail the
125
+ * quote because the wizard can render the minimal shape on its
126
+ * own.
127
+ */
128
+ contentEnricher?: QuoteContentEnricher;
129
+ /** Optional sink for enricher errors. */
130
+ onEnricherError?: (event: {
131
+ entityModule: string;
132
+ entityId: string;
133
+ reason: string;
134
+ }) => void;
135
+ /**
136
+ * Optional promotion-evaluator hook. When wired, called after the
137
+ * adapter's `liveResolve` succeeds (only for `entity_module ==
138
+ * "products"` in v1). Discounts apply to `pricing.base_amount`
139
+ * pre-tax; the operator starter's `applyOperatorTaxToQuoteResult`
140
+ * step downstream recomputes taxes against the new base.
141
+ *
142
+ * When the customer-supplied code fails validation, the engine
143
+ * surfaces the result as a `code_*` `invalidReason` on the quote
144
+ * (`code_not_found`, `code_expired`, `code_not_yet_valid`,
145
+ * `code_not_applicable`). Auto offers do NOT apply when a bad code
146
+ * is supplied — the quote is short-circuited to unavailable so the
147
+ * customer gets clear feedback.
148
+ *
149
+ * Per `docs/architecture/promotions-architecture.md` §3.6 + §7.1.
150
+ */
151
+ evaluatePromotions?: (input: PromotionEvaluationInput) => Promise<PromotionEvaluationOutput>;
152
+ }
153
+ /**
154
+ * Quote the row. Calls `adapter.liveResolve` (sourced) or interprets
155
+ * `available = true` from a stub (owned, when no adapter is registered
156
+ * for the `"owned"` kind in this MVP cut).
157
+ *
158
+ * Throws `NoAdapterRegisteredError` if the registry has no entry for
159
+ * `sourceKind`. Persists the quote either way — a `failed` lookup is
160
+ * still recorded so subsequent diagnostics can see the attempt.
161
+ */
162
+ export declare function quoteEntity(db: AnyDrizzleDb, deps: QuoteEntityDeps, request: QuoteEntityRequest): Promise<QuoteEntityResult>;
163
+ //# sourceMappingURL=quote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.d.ts","sourceRoot":"","sources":["../../src/booking-engine/quote.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `quoteEntity` — the first step in the booking engine lifecycle.
3
+ *
4
+ * Asks the registered adapter "is this row still bookable, and at what
5
+ * price right now?", persists the answer in `catalog_quotes` with an
6
+ * expiry, and returns a stable `quoteId` the subsequent `bookEntity`
7
+ * call can validate against.
8
+ *
9
+ * Quotes are short-lived (default TTL: 10 minutes) and not de-duped.
10
+ * Re-quoting the same row produces a new quote row so the audit trail
11
+ * shows every quote attempt.
12
+ */
13
+ import { newId } from "@voyant-travel/db/lib/typeid";
14
+ import { OWNED_SOURCE_KIND } from "./owned-handler.js";
15
+ import { catalogQuotesTable } from "./schema.js";
16
+ /** Default time-to-live for a quote. */
17
+ export const DEFAULT_QUOTE_TTL_MS = 10 * 60 * 1000;
18
+ /**
19
+ * Quote the row. Calls `adapter.liveResolve` (sourced) or interprets
20
+ * `available = true` from a stub (owned, when no adapter is registered
21
+ * for the `"owned"` kind in this MVP cut).
22
+ *
23
+ * Throws `NoAdapterRegisteredError` if the registry has no entry for
24
+ * `sourceKind`. Persists the quote either way — a `failed` lookup is
25
+ * still recorded so subsequent diagnostics can see the attempt.
26
+ */
27
+ export async function quoteEntity(db, deps, request) {
28
+ const ttlMs = request.ttlMs ?? DEFAULT_QUOTE_TTL_MS;
29
+ const quotedAt = new Date();
30
+ const expiresAt = new Date(quotedAt.getTime() + ttlMs);
31
+ // Two dispatch arms:
32
+ // - Owned: handler registry keyed by entity_module. Returns a
33
+ // ComputeQuoteResult directly — pricing, shape, availability.
34
+ // - Sourced: SourceAdapterRegistry keyed by connection_id (with
35
+ // a kind-only fallback for legacy single-connection-per-kind).
36
+ let available;
37
+ let failedReason;
38
+ let pricing;
39
+ let upstreamPayload;
40
+ let ownedShape;
41
+ if (request.sourceKind === OWNED_SOURCE_KIND && deps.ownedHandlers) {
42
+ const handler = deps.ownedHandlers.resolveOrThrow(request.entityModule);
43
+ const result = await handler.computeQuote({ db, adapterContext: request.adapterContext }, {
44
+ entityModule: request.entityModule,
45
+ entityId: request.entityId,
46
+ scope: request.scope,
47
+ parameters: request.parameters,
48
+ draft: request.parameters?.draft,
49
+ });
50
+ available = result.available;
51
+ failedReason = result.invalidReason;
52
+ pricing = result.pricing;
53
+ upstreamPayload = result.upstreamPayload;
54
+ ownedShape = result.shape;
55
+ }
56
+ else {
57
+ const adapter = request.sourceConnectionId
58
+ ? (deps.registry.resolveByConnection(request.sourceConnectionId) ??
59
+ deps.registry.resolveOrThrow(request.sourceKind))
60
+ : deps.registry.resolveOrThrow(request.sourceKind);
61
+ let liveResolve = { values: {} };
62
+ if (adapter.liveResolve) {
63
+ liveResolve = await adapter.liveResolve(request.adapterContext, {
64
+ ids: [request.entityId],
65
+ scope: {
66
+ locale: request.scope.locale,
67
+ audience: request.scope.audience,
68
+ market: request.scope.market,
69
+ currency: request.scope.currency,
70
+ },
71
+ parameters: request.parameters,
72
+ });
73
+ }
74
+ failedReason = liveResolve.failed?.[request.entityId];
75
+ const liveValues = liveResolve.values[request.entityId];
76
+ available = !failedReason && liveValues !== undefined;
77
+ pricing = available ? liveValuesToPricing(liveValues, request.scope.currency) : undefined;
78
+ upstreamPayload = liveValues;
79
+ }
80
+ // Promotion evaluation — runs only for the products vertical in v1
81
+ // (other verticals would need their own bridge to the evaluator).
82
+ // Discounts apply to `pricing.base_amount` pre-tax; operator starter
83
+ // tax recompute downstream picks up the new base.
84
+ let appliedOffers;
85
+ if (deps.evaluatePromotions && available && pricing && request.entityModule === "products") {
86
+ const params = request.parameters;
87
+ const promotionCode = readString(params?.promotionCode);
88
+ // Read `paxCount` first — `engineParametersFromDraft` (in this file's
89
+ // sibling `routes.ts`) writes the summed traveler count under that key
90
+ // when a draft drives the quote, and the products owned-handler reads
91
+ // `paxCount` too. Fall back to `pax` for callers that build parameters
92
+ // directly without going through the draft pipeline.
93
+ const pax = readNumber(params?.paxCount) ?? readNumber(params?.pax);
94
+ const offerEval = await deps.evaluatePromotions({
95
+ productId: request.entityId,
96
+ slice: { audience: narrowAudience(request.scope.audience), market: request.scope.market },
97
+ pax,
98
+ date: quotedAt,
99
+ code: promotionCode,
100
+ basePriceCents: Math.round(pricing.base_amount),
101
+ baseCurrency: pricing.currency,
102
+ });
103
+ const codeStatus = offerEval.codeStatus;
104
+ if (codeStatus && codeStatus.kind !== "code_valid") {
105
+ // Customer-supplied code failed validation. Surface as a quote-
106
+ // level invalidReason and short-circuit; auto offers don't apply
107
+ // either, so the customer gets unambiguous feedback.
108
+ available = false;
109
+ failedReason = codeStatusToReason(codeStatus);
110
+ }
111
+ else if (offerEval.applied.length > 0) {
112
+ // Subtract the discount from base_amount in cents. The operator
113
+ // template's `applyOperatorTaxToQuoteResult` step downstream
114
+ // sees the new base and recomputes taxes accordingly.
115
+ pricing.base_amount = Math.round(pricing.base_amount) - offerEval.total.discountAppliedCents;
116
+ pricing.appliedOffers = offerEval.applied;
117
+ appliedOffers = offerEval.applied;
118
+ // Invalidate any taxes / breakdown that the source (sourced
119
+ // adapter or owned handler) computed against the un-discounted
120
+ // base — they're stale now. Setting `taxes = 0` and clearing
121
+ // `breakdown` is the explicit signal the operator-side transform
122
+ // (`applyOperatorTaxToQuoteResult`) reads to recompute. Without
123
+ // this, the API serializer would echo a breakdown total that
124
+ // doesn't match `appliedOffers`.
125
+ pricing.taxes = 0;
126
+ pricing.breakdown = undefined;
127
+ }
128
+ }
129
+ const quoteId = newId("catalog_quotes");
130
+ const inserted = (await db
131
+ .insert(catalogQuotesTable)
132
+ .values({
133
+ id: quoteId,
134
+ entity_module: request.entityModule,
135
+ entity_id: request.entityId,
136
+ source_kind: request.sourceKind,
137
+ source_provider: request.sourceProvider,
138
+ source_connection_id: request.sourceConnectionId,
139
+ source_ref: request.sourceRef,
140
+ available,
141
+ invalid_reason: failedReason,
142
+ locale: request.scope.locale,
143
+ audience: request.scope.audience,
144
+ market: request.scope.market,
145
+ currency: request.scope.currency,
146
+ pricing_base_amount: pricing?.base_amount != null ? String(pricing.base_amount) : undefined,
147
+ pricing_taxes: pricing?.taxes != null ? String(pricing.taxes) : undefined,
148
+ pricing_fees: pricing?.fees != null ? String(pricing.fees) : undefined,
149
+ pricing_surcharges: pricing?.surcharges != null ? String(pricing.surcharges) : undefined,
150
+ pricing_currency: pricing?.currency,
151
+ pricing_breakdown: pricing?.breakdown,
152
+ pricing_applied_offers: appliedOffers,
153
+ upstream_payload: upstreamPayload ?? null,
154
+ created_at: quotedAt,
155
+ expires_at: expiresAt,
156
+ })
157
+ .returning());
158
+ if (!inserted[0])
159
+ throw new Error("quoteEntity: insert returned no rows");
160
+ // Optional content enrichment — per booking-journey-architecture
161
+ // §3, the quote response carries a BookingDraftShape descriptor
162
+ // when the engine has the content in front of it. When the hook
163
+ // throws, we swallow the error (the wizard's minimal-shape fallback
164
+ // covers it) but surface via onEnricherError for diagnostics.
165
+ //
166
+ // Owned handlers are authoritative for their own products — when
167
+ // they returned a shape, the enricher is not consulted.
168
+ let shape = ownedShape;
169
+ if (!shape && deps.contentEnricher && available) {
170
+ try {
171
+ const enriched = await deps.contentEnricher({
172
+ db,
173
+ entityModule: request.entityModule,
174
+ entityId: request.entityId,
175
+ sourceKind: request.sourceKind,
176
+ sourceConnectionId: request.sourceConnectionId,
177
+ sourceRef: request.sourceRef,
178
+ scope: request.scope,
179
+ parameters: request.parameters,
180
+ adapterContext: request.adapterContext,
181
+ });
182
+ shape = enriched ?? undefined;
183
+ }
184
+ catch (err) {
185
+ deps.onEnricherError?.({
186
+ entityModule: request.entityModule,
187
+ entityId: request.entityId,
188
+ reason: err instanceof Error ? err.message : String(err),
189
+ });
190
+ }
191
+ }
192
+ return {
193
+ quoteId,
194
+ quotedAt,
195
+ expiresAt,
196
+ available,
197
+ invalidReason: failedReason,
198
+ pricing,
199
+ upstreamPayload,
200
+ shape,
201
+ };
202
+ }
203
+ /**
204
+ * Convert an adapter's per-entity `liveResolve` value into the catalog
205
+ * plane's `PricingBasis` shape. The adapter's payload is opaque, so this
206
+ * helper looks for the conventional fields (`priceCents`, `currency`,
207
+ * `taxesCents`, `feesCents`, `surchargesCents`) and falls back to a
208
+ * single-line "all in base_amount" basis when only a price is provided.
209
+ */
210
+ function liveValuesToPricing(values, fallbackCurrency) {
211
+ if (!values)
212
+ return undefined;
213
+ const priceCents = readNumber(values.priceCents) ?? readNumber(values.price);
214
+ if (priceCents == null)
215
+ return undefined;
216
+ const currency = readString(values.currency) ?? fallbackCurrency;
217
+ if (!currency)
218
+ return undefined;
219
+ return {
220
+ base_amount: priceCents,
221
+ taxes: readNumber(values.taxesCents) ?? 0,
222
+ fees: readNumber(values.feesCents) ?? 0,
223
+ surcharges: readNumber(values.surchargesCents) ?? 0,
224
+ currency,
225
+ };
226
+ }
227
+ function readNumber(v) {
228
+ if (typeof v === "number" && Number.isFinite(v))
229
+ return v;
230
+ if (typeof v === "string") {
231
+ const n = Number.parseFloat(v);
232
+ return Number.isFinite(n) ? n : undefined;
233
+ }
234
+ return undefined;
235
+ }
236
+ function readString(v) {
237
+ return typeof v === "string" ? v : undefined;
238
+ }
239
+ const KNOWN_AUDIENCES = new Set(["staff", "customer", "partner", "supplier"]);
240
+ /**
241
+ * Narrow the QuoteScope.audience (typed `string` for legacy reasons)
242
+ * to the evaluator's `Visibility` enum. Unknown audiences fall back to
243
+ * `"customer"` — the most permissive storefront default; exotic audience
244
+ * tokens (e.g., `"staff-admin"`) get no special treatment beyond visibility
245
+ * rules the offer's scope already encodes.
246
+ */
247
+ function narrowAudience(audience) {
248
+ if (audience === "staff-admin")
249
+ return "staff";
250
+ return KNOWN_AUDIENCES.has(audience) ? audience : "customer";
251
+ }
252
+ /**
253
+ * Map a non-valid `CodeStatus` to a quote `invalidReason` string. The
254
+ * `code_valid` case is filtered upstream so this only sees the failure
255
+ * variants.
256
+ */
257
+ function codeStatusToReason(codeStatus) {
258
+ return codeStatus.kind;
259
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `SourceAdapterRegistry` — process-local map keyed by `connection_id` that
3
+ * the booking engine and channel-push pipeline consult when dispatching a
4
+ * quote / book / cancel / push call.
5
+ *
6
+ * Templates wire the registry at process start, registering one adapter
7
+ * instance per upstream connection. Two connections of the same kind
8
+ * (e.g. TUI dev + TUI prod, or two different Voyant Connect peers) get
9
+ * two registry entries with two distinct `connection_id`s — different
10
+ * credentials, different rate buckets, different `channel_id` mappings.
11
+ *
12
+ * `kind` remains a useful secondary index — "list all adapters of this
13
+ * kind" supports rotate-to-next-available policies and admin debug
14
+ * surfaces. But routing dispatches by `connection_id` because that's
15
+ * what the data carries (provenance.source_connection_id on sourced
16
+ * rows, channels.id on outbound mappings).
17
+ *
18
+ * Per channel-push-architecture §3.1 and catalog-booking-engine §4.
19
+ */
20
+ import type { SourceAdapter } from "../adapter/contract.js";
21
+ /**
22
+ * One registry entry. `connectionId` is the typed-id key (the row in
23
+ * whichever table holds the connection record — `channels` for outbound,
24
+ * the catalog plane's connection store for inbound). For adapters with
25
+ * no upstream connection record (e.g. the demo adapter at boot), pass a
26
+ * stable synthetic id like `"default:<kind>"`.
27
+ */
28
+ export interface RegisteredAdapter {
29
+ connectionId: string;
30
+ adapter: SourceAdapter;
31
+ }
32
+ export interface SourceAdapterRegistry {
33
+ /**
34
+ * Register an adapter under a connection id. The connection id is the
35
+ * primary key — re-registering the same connection id replaces the
36
+ * previous adapter (used at hot-reload time). Production registrations
37
+ * happen once at process start, one entry per upstream connection.
38
+ */
39
+ register(connectionId: string, adapter: SourceAdapter): void;
40
+ /**
41
+ * Backward-compat overload — register an adapter without an explicit
42
+ * connection id. Stored under the synthetic id `"default:<kind>"`.
43
+ * Use this for single-deployment adapters where there's no separate
44
+ * connection record (e.g. demo adapters, single-tenant integrations).
45
+ * New code paths that route per-connection should prefer the explicit
46
+ * `register(connectionId, adapter)` form.
47
+ */
48
+ register(adapter: SourceAdapter): void;
49
+ /**
50
+ * Resolve by connection id. Hot path for the booking engine (sourced
51
+ * bookings) and the channel-push pipeline (outbound dispatches).
52
+ * Returns `undefined` when no adapter is registered.
53
+ */
54
+ resolveByConnection(connectionId: string): SourceAdapter | undefined;
55
+ /** Like `resolveByConnection` but throws `NoAdapterRegisteredError` on miss. */
56
+ resolveByConnectionOrThrow(connectionId: string): SourceAdapter;
57
+ /**
58
+ * Resolve by source kind. Returns the FIRST adapter registered for this
59
+ * kind. Useful for legacy dispatch paths that don't yet thread a
60
+ * connection id, and for the common single-connection-per-kind case.
61
+ * New code that supports multiple connections per kind should use
62
+ * `byKind` to pick deliberately.
63
+ */
64
+ resolveOrThrow(sourceKind: string): SourceAdapter;
65
+ /**
66
+ * Returns every adapter registered for this kind, paired with its
67
+ * connection id. Order is registration order. Use for "rotate to next
68
+ * available connection" policies and admin debug surfaces.
69
+ */
70
+ byKind(sourceKind: string): ReadonlyArray<RegisteredAdapter>;
71
+ /** Returns the registered connection ids. */
72
+ connections(): ReadonlyArray<string>;
73
+ /** Returns the registered source kinds. */
74
+ kinds(): ReadonlyArray<string>;
75
+ /** True iff a connection id is registered. */
76
+ has(connectionId: string): boolean;
77
+ /** True iff at least one adapter of this kind is registered. */
78
+ hasKind(sourceKind: string): boolean;
79
+ }
80
+ /**
81
+ * Construct a fresh registry. Templates create one at process start and
82
+ * pass it to the booking-engine route handlers + channel-push wiring.
83
+ */
84
+ export declare function createSourceAdapterRegistry(): SourceAdapterRegistry;
85
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/booking-engine/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAI3D;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,aAAa,CAAA;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAE5D;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAEtC;;;;OAIG;IACH,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAEpE,gFAAgF;IAChF,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,CAAA;IAE/D;;;;;;OAMG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,CAAA;IAEjD;;;;OAIG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;IAE5D,6CAA6C;IAC7C,WAAW,IAAI,aAAa,CAAC,MAAM,CAAC,CAAA;IAEpC,2CAA2C;IAC3C,KAAK,IAAI,aAAa,CAAC,MAAM,CAAC,CAAA;IAE9B,8CAA8C;IAC9C,GAAG,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAA;IAElC,gEAAgE;IAChE,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;CACrC;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,qBAAqB,CA6FnE"}