@voyant-travel/cruises 0.118.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 (210) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +50 -0
  3. package/dist/adapters/connect-compat.d.ts +20 -0
  4. package/dist/adapters/connect-compat.d.ts.map +1 -0
  5. package/dist/adapters/connect-compat.js +71 -0
  6. package/dist/adapters/contract-fixture.d.ts +32 -0
  7. package/dist/adapters/contract-fixture.d.ts.map +1 -0
  8. package/dist/adapters/contract-fixture.js +152 -0
  9. package/dist/adapters/index.d.ts +331 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +16 -0
  12. package/dist/adapters/memoize.d.ts +28 -0
  13. package/dist/adapters/memoize.d.ts.map +1 -0
  14. package/dist/adapters/memoize.js +131 -0
  15. package/dist/adapters/mock.d.ts +44 -0
  16. package/dist/adapters/mock.d.ts.map +1 -0
  17. package/dist/adapters/mock.js +192 -0
  18. package/dist/adapters/registry.d.ts +26 -0
  19. package/dist/adapters/registry.d.ts.map +1 -0
  20. package/dist/adapters/registry.js +42 -0
  21. package/dist/adapters/source-adapter-shim.d.ts +80 -0
  22. package/dist/adapters/source-adapter-shim.d.ts.map +1 -0
  23. package/dist/adapters/source-adapter-shim.js +390 -0
  24. package/dist/booking-engine/handler.d.ts +108 -0
  25. package/dist/booking-engine/handler.d.ts.map +1 -0
  26. package/dist/booking-engine/handler.js +225 -0
  27. package/dist/booking-engine/index.d.ts +9 -0
  28. package/dist/booking-engine/index.d.ts.map +1 -0
  29. package/dist/booking-engine/index.js +8 -0
  30. package/dist/booking-extension.d.ts +1179 -0
  31. package/dist/booking-extension.d.ts.map +1 -0
  32. package/dist/booking-extension.js +342 -0
  33. package/dist/cabin-features.d.ts +8 -0
  34. package/dist/cabin-features.d.ts.map +1 -0
  35. package/dist/cabin-features.js +7 -0
  36. package/dist/catalog-policy-cabins.d.ts +18 -0
  37. package/dist/catalog-policy-cabins.d.ts.map +1 -0
  38. package/dist/catalog-policy-cabins.js +96 -0
  39. package/dist/catalog-policy-core.d.ts +3 -0
  40. package/dist/catalog-policy-core.d.ts.map +1 -0
  41. package/dist/catalog-policy-core.js +247 -0
  42. package/dist/catalog-policy-structure.d.ts +3 -0
  43. package/dist/catalog-policy-structure.d.ts.map +1 -0
  44. package/dist/catalog-policy-structure.js +387 -0
  45. package/dist/catalog-policy.d.ts +15 -0
  46. package/dist/catalog-policy.d.ts.map +1 -0
  47. package/dist/catalog-policy.js +19 -0
  48. package/dist/content-shape.d.ts +5 -0
  49. package/dist/content-shape.d.ts.map +1 -0
  50. package/dist/content-shape.js +13 -0
  51. package/dist/draft-shape.d.ts +59 -0
  52. package/dist/draft-shape.d.ts.map +1 -0
  53. package/dist/draft-shape.js +98 -0
  54. package/dist/events.d.ts +21 -0
  55. package/dist/events.d.ts.map +1 -0
  56. package/dist/events.js +21 -0
  57. package/dist/index.d.ts +43 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +66 -0
  60. package/dist/lib/key.d.ts +41 -0
  61. package/dist/lib/key.d.ts.map +1 -0
  62. package/dist/lib/key.js +100 -0
  63. package/dist/routes-booking-payloads.d.ts +133 -0
  64. package/dist/routes-booking-payloads.d.ts.map +1 -0
  65. package/dist/routes-booking-payloads.js +142 -0
  66. package/dist/routes-content.d.ts +53 -0
  67. package/dist/routes-content.d.ts.map +1 -0
  68. package/dist/routes-content.js +158 -0
  69. package/dist/routes-core.d.ts +4 -0
  70. package/dist/routes-core.d.ts.map +1 -0
  71. package/dist/routes-core.js +68 -0
  72. package/dist/routes-detail.d.ts +4 -0
  73. package/dist/routes-detail.d.ts.map +1 -0
  74. package/dist/routes-detail.js +261 -0
  75. package/dist/routes-env.d.ts +13 -0
  76. package/dist/routes-env.d.ts.map +1 -0
  77. package/dist/routes-env.js +1 -0
  78. package/dist/routes-keying.d.ts +28 -0
  79. package/dist/routes-keying.d.ts.map +1 -0
  80. package/dist/routes-keying.js +70 -0
  81. package/dist/routes-public.d.ts +911 -0
  82. package/dist/routes-public.d.ts.map +1 -0
  83. package/dist/routes-public.js +252 -0
  84. package/dist/routes-sailings-prices.d.ts +4 -0
  85. package/dist/routes-sailings-prices.d.ts.map +1 -0
  86. package/dist/routes-sailings-prices.js +278 -0
  87. package/dist/routes-search-index.d.ts +4 -0
  88. package/dist/routes-search-index.d.ts.map +1 -0
  89. package/dist/routes-search-index.js +25 -0
  90. package/dist/routes-ships.d.ts +4 -0
  91. package/dist/routes-ships.d.ts.map +1 -0
  92. package/dist/routes-ships.js +147 -0
  93. package/dist/routes-voyage-groups.d.ts +4 -0
  94. package/dist/routes-voyage-groups.d.ts.map +1 -0
  95. package/dist/routes-voyage-groups.js +85 -0
  96. package/dist/routes.d.ts +5 -0
  97. package/dist/routes.d.ts.map +1 -0
  98. package/dist/routes.js +14 -0
  99. package/dist/schema-cabins.d.ts +1098 -0
  100. package/dist/schema-cabins.d.ts.map +1 -0
  101. package/dist/schema-cabins.js +105 -0
  102. package/dist/schema-content.d.ts +577 -0
  103. package/dist/schema-content.d.ts.map +1 -0
  104. package/dist/schema-content.js +63 -0
  105. package/dist/schema-core.d.ts +1790 -0
  106. package/dist/schema-core.d.ts.map +1 -0
  107. package/dist/schema-core.js +171 -0
  108. package/dist/schema-itinerary.d.ts +556 -0
  109. package/dist/schema-itinerary.d.ts.map +1 -0
  110. package/dist/schema-itinerary.js +50 -0
  111. package/dist/schema-pricing.d.ts +633 -0
  112. package/dist/schema-pricing.d.ts.map +1 -0
  113. package/dist/schema-pricing.js +73 -0
  114. package/dist/schema-search.d.ts +611 -0
  115. package/dist/schema-search.d.ts.map +1 -0
  116. package/dist/schema-search.js +64 -0
  117. package/dist/schema-shared.d.ts +23 -0
  118. package/dist/schema-shared.d.ts.map +1 -0
  119. package/dist/schema-shared.js +107 -0
  120. package/dist/schema-sourced-content.d.ts +247 -0
  121. package/dist/schema-sourced-content.d.ts.map +1 -0
  122. package/dist/schema-sourced-content.js +38 -0
  123. package/dist/schema.d.ts +10 -0
  124. package/dist/schema.d.ts.map +1 -0
  125. package/dist/schema.js +9 -0
  126. package/dist/service-booking-helpers.d.ts +12 -0
  127. package/dist/service-booking-helpers.d.ts.map +1 -0
  128. package/dist/service-booking-helpers.js +94 -0
  129. package/dist/service-booking-types.d.ts +101 -0
  130. package/dist/service-booking-types.d.ts.map +1 -0
  131. package/dist/service-booking-types.js +1 -0
  132. package/dist/service-bookings.d.ts +46 -0
  133. package/dist/service-bookings.d.ts.map +1 -0
  134. package/dist/service-bookings.js +420 -0
  135. package/dist/service-catalog-plane-cabins.d.ts +24 -0
  136. package/dist/service-catalog-plane-cabins.d.ts.map +1 -0
  137. package/dist/service-catalog-plane-cabins.js +90 -0
  138. package/dist/service-catalog-plane.d.ts +74 -0
  139. package/dist/service-catalog-plane.d.ts.map +1 -0
  140. package/dist/service-catalog-plane.js +194 -0
  141. package/dist/service-content-synthesizer.d.ts +42 -0
  142. package/dist/service-content-synthesizer.d.ts.map +1 -0
  143. package/dist/service-content-synthesizer.js +144 -0
  144. package/dist/service-content.d.ts +74 -0
  145. package/dist/service-content.d.ts.map +1 -0
  146. package/dist/service-content.js +315 -0
  147. package/dist/service-core.d.ts +134 -0
  148. package/dist/service-core.d.ts.map +1 -0
  149. package/dist/service-core.js +257 -0
  150. package/dist/service-detach.d.ts +18 -0
  151. package/dist/service-detach.d.ts.map +1 -0
  152. package/dist/service-detach.js +199 -0
  153. package/dist/service-enrichment.d.ts +11 -0
  154. package/dist/service-enrichment.d.ts.map +1 -0
  155. package/dist/service-enrichment.js +47 -0
  156. package/dist/service-external-refresh.d.ts +39 -0
  157. package/dist/service-external-refresh.d.ts.map +1 -0
  158. package/dist/service-external-refresh.js +47 -0
  159. package/dist/service-itinerary.d.ts +22 -0
  160. package/dist/service-itinerary.d.ts.map +1 -0
  161. package/dist/service-itinerary.js +34 -0
  162. package/dist/service-prices.d.ts +46 -0
  163. package/dist/service-prices.d.ts.map +1 -0
  164. package/dist/service-prices.js +89 -0
  165. package/dist/service-pricing.d.ts +97 -0
  166. package/dist/service-pricing.d.ts.map +1 -0
  167. package/dist/service-pricing.js +198 -0
  168. package/dist/service-sailings.d.ts +48 -0
  169. package/dist/service-sailings.d.ts.map +1 -0
  170. package/dist/service-sailings.js +145 -0
  171. package/dist/service-search-types.d.ts +54 -0
  172. package/dist/service-search-types.d.ts.map +1 -0
  173. package/dist/service-search-types.js +1 -0
  174. package/dist/service-search.d.ts +65 -0
  175. package/dist/service-search.d.ts.map +1 -0
  176. package/dist/service-search.js +467 -0
  177. package/dist/service-shared.d.ts +22 -0
  178. package/dist/service-shared.d.ts.map +1 -0
  179. package/dist/service-shared.js +22 -0
  180. package/dist/service-ships.d.ts +47 -0
  181. package/dist/service-ships.d.ts.map +1 -0
  182. package/dist/service-ships.js +156 -0
  183. package/dist/service.d.ts +255 -0
  184. package/dist/service.d.ts.map +1 -0
  185. package/dist/service.js +12 -0
  186. package/dist/validation-cabins.d.ts +267 -0
  187. package/dist/validation-cabins.d.ts.map +1 -0
  188. package/dist/validation-cabins.js +77 -0
  189. package/dist/validation-content.d.ts +123 -0
  190. package/dist/validation-content.d.ts.map +1 -0
  191. package/dist/validation-content.js +40 -0
  192. package/dist/validation-core.d.ts +393 -0
  193. package/dist/validation-core.d.ts.map +1 -0
  194. package/dist/validation-core.js +162 -0
  195. package/dist/validation-itinerary.d.ts +123 -0
  196. package/dist/validation-itinerary.d.ts.map +1 -0
  197. package/dist/validation-itinerary.js +47 -0
  198. package/dist/validation-pricing.d.ts +137 -0
  199. package/dist/validation-pricing.d.ts.map +1 -0
  200. package/dist/validation-pricing.js +49 -0
  201. package/dist/validation-search.d.ts +118 -0
  202. package/dist/validation-search.d.ts.map +1 -0
  203. package/dist/validation-search.js +60 -0
  204. package/dist/validation-shared.d.ts +123 -0
  205. package/dist/validation-shared.d.ts.map +1 -0
  206. package/dist/validation-shared.js +103 -0
  207. package/dist/validation.d.ts +8 -0
  208. package/dist/validation.d.ts.map +1 -0
  209. package/dist/validation.js +7 -0
  210. package/package.json +146 -0
@@ -0,0 +1,390 @@
1
+ // agent-quality: file-size exception -- owner: cruises; existing module stays co-located until a dedicated split preserves behavior and tests.
2
+ /**
3
+ * Adapt a vertical-shaped `CruiseAdapter` (with its multi-method
4
+ * `fetchCruise / fetchSailing / fetchShip / fetchSailingItinerary`
5
+ * surface) into a catalog-plane `SourceAdapter` so the cruises module
6
+ * can participate in:
7
+ *
8
+ * - The catalog plane's discovery / projection-capture pipeline
9
+ * (`sync.ts` writes a `catalog_sourced_entries` row per emitted
10
+ * projection), enabling the durable thin-content synthesizer
11
+ * fallback for cruises (sourced-content §2.5, §3.6).
12
+ * - The `getCruiseContent` SWR machinery (sourced-content §3.4) —
13
+ * the shim's `getContent` composes the cruise adapter's per-aspect
14
+ * fetches into one `CruiseContent` payload.
15
+ * - The catalog plane's snapshot content capture (sourced-content
16
+ * §5.1) — `bookEntity` calls this `getContent` at commit time.
17
+ *
18
+ * Per the doc's Phase E note: the cruise adapter retains its internal
19
+ * multi-call composition; only the public catalog surface narrows.
20
+ *
21
+ * Templates wire the shim by registering it into the catalog
22
+ * `SourceAdapterRegistry` AT PROCESS START, alongside the cruise
23
+ * adapter's own per-vertical registration. Both registrations are
24
+ * cheap — they share the same underlying `CruiseAdapter` instance.
25
+ */
26
+ import { CRUISES_CONTENT_SCHEMA_VERSION } from "../content-shape.js";
27
+ import { decodeSourceRef, encodeSourceRef } from "../lib/key.js";
28
+ /**
29
+ * Wrap a `CruiseAdapter` as a catalog `SourceAdapter`. The wrapped
30
+ * adapter is shared by reference — its internal state (HTTP clients,
31
+ * caches, credentials) is not duplicated.
32
+ */
33
+ export function cruiseAdapterToSourceAdapter(cruiseAdapter, options = {}) {
34
+ const sourceKind = options.sourceKind ?? `cruise:${cruiseAdapter.name}`;
35
+ const buildEntityId = options.buildEntityId ?? defaultBuildEntityId;
36
+ const pageSize = options.pageSize ?? 200;
37
+ const supportsContentFetch = options.supportsContentFetch ?? true;
38
+ const capabilities = {
39
+ verticals: ["cruises"],
40
+ supportsLiveResolution: true,
41
+ supportsDriftDetection: false,
42
+ supportsBookingForwarding: true,
43
+ postBookOperations: ["cancel", "status"],
44
+ supportsContentFetch,
45
+ supportedContentLocales: options.supportedContentLocales,
46
+ };
47
+ return {
48
+ cruiseAdapter,
49
+ kind: sourceKind,
50
+ capabilities,
51
+ async connect(_ctx) {
52
+ // CruiseAdapter has no explicit connect — the underlying HTTP
53
+ // client is constructed at adapter creation time.
54
+ },
55
+ async pause(_ctx) {
56
+ // No-op for the shim. Pause for cruise adapters typically means
57
+ // "stop the polling loop", which is template-orchestrated.
58
+ },
59
+ async disconnect(_ctx) {
60
+ // No-op. Templates revoke credentials at the cruise-adapter
61
+ // layer, not via the catalog-plane shim.
62
+ },
63
+ async getState(_ctx) {
64
+ // CruiseAdapter doesn't surface state. Default to "active";
65
+ // catalog-side disconnect detection happens through drift
66
+ // events, not state polling.
67
+ return "active";
68
+ },
69
+ async discover(_ctx, cursor) {
70
+ // Use `searchProjection` (the cruise vertical's bulk-stream
71
+ // surface) and translate each entry to a catalog
72
+ // `CatalogProjection`. Cursor handling is opaque — the shim
73
+ // walks the iterable up to `pageSize` items per page and
74
+ // encodes a small JSON cursor with the offset.
75
+ const offset = parseCursor(cursor);
76
+ const projections = [];
77
+ let scanned = 0;
78
+ let nextCursor;
79
+ const iterator = cruiseAdapter.searchProjection({})[Symbol.asyncIterator]();
80
+ // Skip past `offset` items so re-pagination is consistent.
81
+ for (let i = 0; i < offset; i += 1) {
82
+ const skip = await iterator.next();
83
+ if (skip.done) {
84
+ // Cursor advanced past end → return empty page.
85
+ return { projections: [], next_cursor: undefined };
86
+ }
87
+ }
88
+ while (scanned < pageSize) {
89
+ const next = await iterator.next();
90
+ if (next.done)
91
+ break;
92
+ const entry = next.value;
93
+ projections.push(toCatalogProjection(entry, sourceKind, buildEntityId));
94
+ scanned += 1;
95
+ }
96
+ const peek = await iterator.next();
97
+ if (!peek.done) {
98
+ nextCursor = encodeCursor(offset + scanned);
99
+ }
100
+ return { projections, next_cursor: nextCursor };
101
+ },
102
+ async liveResolve(_ctx, _request) {
103
+ // Cruise pricing flows through `fetchSailingPricing` per
104
+ // sailing — but the catalog `LiveResolveRequest` is keyed by
105
+ // entity_id (the cruise typeid), not by sailing. v1 leaves
106
+ // this as an explicit not-supported: callers should use the
107
+ // cruises module's per-sailing pricing routes directly. The
108
+ // catalog plane's quote engine still works because cruises'
109
+ // own quote path is exercised through the vertical's routes.
110
+ return {
111
+ values: {},
112
+ failed: {
113
+ /* nothing — empty result is the contract */
114
+ },
115
+ };
116
+ },
117
+ async getContent(_ctx, request) {
118
+ // Compose the cruise adapter's per-aspect fetches into one
119
+ // `CruiseContent` payload. Itinerary is per-sailing, so it stays
120
+ // attached to each sailing instead of being flattened onto the cruise.
121
+ const sourceRef = entityIdToSourceRef(request.entity_id);
122
+ const cruise = await cruiseAdapter.fetchCruise(sourceRef);
123
+ if (!cruise) {
124
+ throw new Error(`cruise content unavailable for ${request.entity_id} (adapter ${cruiseAdapter.name} returned null)`);
125
+ }
126
+ const ship = cruise.defaultShipRef
127
+ ? await cruiseAdapter.fetchShip(cruise.defaultShipRef)
128
+ : null;
129
+ const sailings = await cruiseAdapter.listSailingsForCruise(cruise.sourceRef);
130
+ const sailingsWithItinerary = await Promise.all(sailings.map(async (sailing) => cruiseSailingFrom(sailing, await cruiseAdapter.fetchSailingItinerary(sailing.sourceRef))));
131
+ const content = {
132
+ cruise: cruiseSummaryFrom(cruise),
133
+ ship: ship ? cruiseShipFrom(ship) : null,
134
+ sailings: sailingsWithItinerary,
135
+ cabin_categories: ship?.categories?.map(cruiseCabinCategoryFrom) ?? [],
136
+ itinerary_stops: [],
137
+ policies: cruisePoliciesFrom(cruise),
138
+ };
139
+ return {
140
+ entity_module: "cruises",
141
+ entity_id: request.entity_id,
142
+ source_ref: encodeSourceRef(cruise.sourceRef),
143
+ returned_locale: request.locale,
144
+ content,
145
+ content_schema_version: CRUISES_CONTENT_SCHEMA_VERSION,
146
+ };
147
+ },
148
+ async reserve(_ctx, _request) {
149
+ // Cruise reservations require per-sailing context (cabin
150
+ // category, occupancy, fare code, passengers). The catalog
151
+ // `ReserveRequest` doesn't carry that level of detail, so v1
152
+ // leaves cruise booking on the vertical's own commit path
153
+ // (`POST /v1/admin/cruises/:key/booking`). When the journey
154
+ // standardizes the descriptor, this shim can route through.
155
+ throw new Error(`cruise booking via catalog SourceAdapter is not supported in v1 — call the cruises vertical's commit path directly (POST /v1/admin/cruises/:key/booking)`);
156
+ },
157
+ async cancel(_ctx, _request) {
158
+ // Same reasoning as reserve — cancellation goes through the
159
+ // cruise vertical's own routes for now.
160
+ throw new Error("cruise cancellation via catalog SourceAdapter is not supported in v1");
161
+ },
162
+ };
163
+ }
164
+ // ─────────────────────────────────────────────────────────────────────────────
165
+ // Translators — External* shapes → CruiseContent fields
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ function cruiseSummaryFrom(c) {
168
+ return {
169
+ id: defaultBuildEntityId(c.sourceRef),
170
+ name: c.name,
171
+ status: c.status,
172
+ description: c.description ?? c.shortDescription ?? null,
173
+ cruise_type: c.cruiseType,
174
+ hero_image_url: c.heroImageUrl ?? null,
175
+ highlights: c.highlights ?? [],
176
+ cruise_line: c.lineName,
177
+ duration_nights: c.nights,
178
+ embarkation_port: c.embarkPortName ?? null,
179
+ disembarkation_port: c.disembarkPortName ?? null,
180
+ };
181
+ }
182
+ function cruiseShipFrom(s) {
183
+ return {
184
+ id: defaultBuildEntityId(s.sourceRef),
185
+ name: s.name,
186
+ ship_type: s.shipType ?? null,
187
+ description: s.description ?? null,
188
+ deck_plan_url: s.deckPlanUrl ?? null,
189
+ deck_plans: s.decks?.map((deck) => ({
190
+ name: deck.name,
191
+ level: deck.level ?? null,
192
+ image_url: deck.planImageUrl ?? null,
193
+ })) ?? [],
194
+ capacity: s.capacityGuests ?? null,
195
+ decks: s.deckCount ?? null,
196
+ year_built: s.yearBuilt ?? null,
197
+ gallery: s.gallery ?? [],
198
+ };
199
+ }
200
+ function cruiseSailingFrom(sail, itinerary = []) {
201
+ // Sailing duration: derived from departure→return when both are
202
+ // present. Handles the common case where the upstream ships dates
203
+ // but no explicit duration_nights.
204
+ const start = new Date(sail.departureDate);
205
+ const end = new Date(sail.returnDate);
206
+ const durationNights = Number.isFinite(start.getTime()) && Number.isFinite(end.getTime())
207
+ ? Math.max(0, Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)))
208
+ : null;
209
+ // The content schema requires lowest_price_cents + currency to be both-or-
210
+ // neither. Some adapters surface a price without its currency (or vice versa);
211
+ // an indicative "from" price without a currency isn't displayable anyway, so
212
+ // drop the pair rather than fail validation. Accurate per-cabin pricing comes
213
+ // from the live pricing endpoint, not this cached hint.
214
+ const hasPricePair = sail.lowestPriceCents != null && sail.currency != null;
215
+ return {
216
+ id: defaultBuildEntityId(sail.sourceRef),
217
+ source_ref: sail.sourceRef.externalId,
218
+ start_date: sail.departureDate,
219
+ end_date: sail.returnDate,
220
+ duration_nights: durationNights,
221
+ status: sail.salesStatus ?? null,
222
+ embarkation_port: sail.embarkPortName ?? null,
223
+ disembarkation_port: sail.disembarkPortName ?? null,
224
+ itinerary_stops: itinerary.map((day) => cruiseItineraryStopFrom(day)),
225
+ lowest_price_cents: hasPricePair ? (sail.lowestPriceCents ?? null) : null,
226
+ currency: hasPricePair ? (sail.currency ?? null) : null,
227
+ };
228
+ }
229
+ function cruiseCabinCategoryFrom(cat) {
230
+ return {
231
+ id: defaultBuildEntityId(cat.sourceRef),
232
+ code: cat.code,
233
+ name: cat.name,
234
+ description: cat.description ?? null,
235
+ type: cat.roomType,
236
+ capacity_min: cat.minOccupancy,
237
+ capacity_max: cat.maxOccupancy,
238
+ images: cat.images ?? [],
239
+ floorplan_images: cat.floorplanImages ?? [],
240
+ square_feet: cat.squareFeet ?? null,
241
+ grade_codes: cat.gradeCodes ?? [],
242
+ wheelchair_accessible: cat.wheelchairAccessible ?? false,
243
+ inclusions: cat.amenities ?? [],
244
+ feature_codes: cat.featureCodes ?? [],
245
+ bed_configurations: cat.bedConfigurations ?? [],
246
+ accessibility_features: cat.accessibilityFeatures ?? [],
247
+ view_type: cat.viewType ?? null,
248
+ };
249
+ }
250
+ function cruisePoliciesFrom(c) {
251
+ // ExternalCruise carries `inclusionsHtml` / `exclusionsHtml` as
252
+ // free-form HTML. Map to supplier_notes — the doc's CruisePolicy
253
+ // shape doesn't have a dedicated "inclusions" kind, and these
254
+ // fields are typically displayed as supplemental text rather than
255
+ // structural rules.
256
+ const out = [];
257
+ if (c.inclusionsHtml) {
258
+ out.push({ kind: "supplier_notes", body: c.inclusionsHtml });
259
+ }
260
+ if (c.exclusionsHtml) {
261
+ out.push({ kind: "supplier_notes", body: c.exclusionsHtml });
262
+ }
263
+ return out;
264
+ }
265
+ // Itinerary translator — exposed for tests / future per-sailing
266
+ // composition. The shim's `getContent` doesn't currently call this
267
+ // (itinerary is per-sailing), but verticals that wire a sailing-aware
268
+ // content path can use it.
269
+ export function cruiseItineraryStopFrom(day, date) {
270
+ return {
271
+ day_number: day.dayNumber,
272
+ date: date ?? null,
273
+ port_name: day.portName ?? "",
274
+ arrival_time: day.arrivalTime ?? null,
275
+ departure_time: day.departureTime ?? null,
276
+ description: day.description ?? null,
277
+ is_at_sea: day.isSeaDay ?? false,
278
+ };
279
+ }
280
+ // ─────────────────────────────────────────────────────────────────────────────
281
+ // Discovery → CatalogProjection
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ function toCatalogProjection(entry, sourceKind, buildEntityId) {
284
+ const provenance = {
285
+ source_kind: sourceKind,
286
+ source_provider: typeof entry.sourceRef.provider === "string"
287
+ ? entry.sourceRef.provider
288
+ : entry.sourceRef.connectionId,
289
+ source_connection_id: entry.sourceRef.connectionId,
290
+ source_ref: encodeSourceRef(entry.sourceRef),
291
+ source_freshness: "sync",
292
+ last_sourced_at: new Date(),
293
+ };
294
+ return {
295
+ entity_module: "cruises",
296
+ entity_id: buildEntityId(entry.sourceRef),
297
+ provenance,
298
+ fields: {
299
+ // Provenance — mirrors the owned-cruise builder (`cruiseRowToProjection`)
300
+ // so the catalog's Source column + sourced-row bookkeeping behave the
301
+ // same for owned and sourced cruises.
302
+ "source.kind": sourceKind,
303
+ "source.ref": encodeSourceRef(entry.sourceRef),
304
+ id: buildEntityId(entry.sourceRef),
305
+ name: entry.name,
306
+ slug: entry.slug,
307
+ // Structural scalars — keys MUST match the cruise field policy
308
+ // (`catalog-policy.ts`) and the catalog-ui columns, which are camelCase.
309
+ // The indexer drops any field whose key isn't a policy path, so emitting
310
+ // snake_case here silently blanked Type/Nights/etc. (issue #1466).
311
+ cruiseType: entry.cruiseType,
312
+ status: entry.salesStatus ?? null,
313
+ nights: entry.nights,
314
+ // Supplier / Ship columns facet on ids. connect-cruises ≥0.3.0 surfaces
315
+ // the upstream external ids; map them onto the policy's id fields (#1466
316
+ // fix 2). Falls back to null on older adapters that only carry names.
317
+ lineSupplierId: entry.lineExternalId ?? null,
318
+ defaultShipId: entry.shipExternalId ?? null,
319
+ heroImageUrl: entry.heroImageUrl ?? null,
320
+ thumbnailUrl: entry.heroImageUrl ?? null,
321
+ embarkPortFacilityId: entry.embarkPortFacilityId ?? null,
322
+ embarkPortCanonicalPlaceId: entry.embarkPortCanonicalPlaceId ?? null,
323
+ disembarkPortFacilityId: entry.disembarkPortFacilityId ?? null,
324
+ disembarkPortCanonicalPlaceId: entry.disembarkPortCanonicalPlaceId ?? null,
325
+ // Canonical geography — the policy paths for these arrays are snake_case
326
+ // (`region_ids[]` …), so the keys stay snake_case here (issue #1466).
327
+ region_ids: entry.regionIds ?? [],
328
+ waterway_ids: entry.waterwayIds ?? [],
329
+ port_ids: entry.portIds ?? [],
330
+ country_iso: entry.countryIso ?? [],
331
+ regions: entry.regions ?? [],
332
+ waterways: entry.waterways ?? [],
333
+ ports: entry.ports ?? [],
334
+ countries: entry.countries ?? [],
335
+ themes: entry.themes ?? [],
336
+ // Browse-time price + departure-window hints (Tier-1 indexed summaries;
337
+ // quote-time price is volatile-live and resolved elsewhere).
338
+ lowestPriceCached: entry.lowestPriceCents ?? null,
339
+ lowestPriceCurrencyCached: entry.lowestPriceCurrency ?? null,
340
+ lowestPriceUnit: "minor",
341
+ earliestDepartureCached: entry.earliestDeparture ?? null,
342
+ latestDepartureCached: entry.latestDeparture ?? null,
343
+ // Departure month facet + count — populated by the source enrichment
344
+ // (per-cruise sailing rollup). Default empty/null on adapters that
345
+ // don't supply them so the field is simply absent from the index doc.
346
+ departureMonths: entry.departureMonths ?? [],
347
+ departureCount: entry.departureCount ?? null,
348
+ },
349
+ };
350
+ }
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+ // Cursor encoding / entity-id translation
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ function parseCursor(cursor) {
355
+ if (!cursor)
356
+ return 0;
357
+ try {
358
+ const parsed = JSON.parse(cursor);
359
+ if (parsed && typeof parsed === "object" && typeof parsed.offset === "number") {
360
+ return parsed.offset;
361
+ }
362
+ }
363
+ catch {
364
+ // fall through
365
+ }
366
+ return 0;
367
+ }
368
+ function encodeCursor(offset) {
369
+ return JSON.stringify({ offset });
370
+ }
371
+ /**
372
+ * Default `entity_id` builder. Produces a stable URL-safe id that embeds the
373
+ * full SourceRef. Stability is load-bearing: `catalog_sourced_entries` is keyed
374
+ * on `entity_id`, so drift on the id maps to a new sourced row.
375
+ */
376
+ function defaultBuildEntityId(sourceRef) {
377
+ return `crus_${encodeSourceRef(sourceRef)}`;
378
+ }
379
+ /**
380
+ * Inverse of `defaultBuildEntityId`. The legacy `crus_<slug>` fallback keeps
381
+ * pre-encoded sourced rows readable, but new rows preserve the exact SourceRef.
382
+ */
383
+ function entityIdToSourceRef(entityId) {
384
+ const raw = entityId.startsWith("crus_") ? entityId.slice("crus_".length) : entityId;
385
+ const decoded = decodeSourceRef(raw);
386
+ if (decoded)
387
+ return decoded;
388
+ const externalId = raw;
389
+ return { externalId };
390
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Owned-arm booking handler for the `cruises` vertical (Phase F
3
+ * skeleton).
4
+ *
5
+ * Per `docs/architecture/booking-journey-architecture.md` §6 +
6
+ * §7 (cruise example) + §10 Phase F.
7
+ *
8
+ * computeQuote scope:
9
+ * - Reads cruise content via the caller-supplied loader
10
+ * (`getCruiseContent`).
11
+ * - Projects to a `BookingDraftShape` with cabin-category +
12
+ * occupancy sub-steps via `buildCruiseDraftShape`.
13
+ * - When a sailing + cabin category + occupancy are picked,
14
+ * looks up the per-occupancy `cruise_prices` row and returns
15
+ * pricing as `pricePerPerson × paxCount`.
16
+ *
17
+ * commit scope:
18
+ * - Returns `failed:not_yet_implemented`. Cruises need a
19
+ * vertical-specific commit primitive (cabin allocation +
20
+ * supplier hold + air-add-on routing) that doesn't exist
21
+ * today. The shell renders the descriptor cleanly; commit
22
+ * lands separately.
23
+ */
24
+ import type { OwnedBookingHandler, OwnedHandlerContext } from "@voyant-travel/catalog/booking-engine";
25
+ import type { CruiseContent } from "../content-shape.js";
26
+ export interface ResolvedCruisePrice {
27
+ /** Per-pax price in major units as a numeric string (matches
28
+ * cruise_prices.pricePerPerson). */
29
+ pricePerPerson: string;
30
+ currency: string;
31
+ fareCode?: string | null;
32
+ }
33
+ /**
34
+ * Caller-supplied loaders. Templates wire these to
35
+ * `getCruiseContent` and `pricingService.lowestAvailablePrice` /
36
+ * a custom per-(category, occupancy) lookup.
37
+ */
38
+ export interface CruiseHandlerLoaders {
39
+ loadContent: (ctx: OwnedHandlerContext, entityId: string) => Promise<CruiseContent | null>;
40
+ /**
41
+ * Resolve a price for the chosen sailing + category + occupancy.
42
+ * Returns null when no available row matches (e.g. cabin
43
+ * category is sold out at that occupancy).
44
+ */
45
+ loadPrice: (ctx: OwnedHandlerContext, args: {
46
+ entityId: string;
47
+ sailingId: string;
48
+ cabinCategoryId: string;
49
+ occupancy: number;
50
+ }) => Promise<ResolvedCruisePrice | null>;
51
+ }
52
+ /**
53
+ * Subset of `cruisesBookingService.createCruiseBooking`'s input —
54
+ * structural so the handler stays free of an
55
+ * `@voyant-travel/cruises/service-bookings` import (no workspace cycle).
56
+ */
57
+ export interface CruiseCommitBridgeInput {
58
+ sailingId: string;
59
+ cabinCategoryId: string;
60
+ cabinId?: string | null;
61
+ occupancy: number;
62
+ fareCode?: string | null;
63
+ personId?: string | null;
64
+ organizationId?: string | null;
65
+ contact: {
66
+ firstName: string;
67
+ lastName: string;
68
+ email?: string | null;
69
+ phone?: string | null;
70
+ };
71
+ passengers: Array<{
72
+ firstName: string;
73
+ lastName: string;
74
+ dateOfBirth?: string | null;
75
+ travelerCategory?: "adult" | "child" | "infant" | null;
76
+ }>;
77
+ /** Air-arrangement intent — see CreateCruiseBookingInput. */
78
+ airArrangement?: "cruise_line" | "independent" | "none" | null;
79
+ notes?: string | null;
80
+ }
81
+ export interface CruiseCommitBridgeResult {
82
+ status: "ok" | "failed";
83
+ bookingId?: string;
84
+ bookingNumber?: string;
85
+ reason?: string;
86
+ }
87
+ export type CruiseCommitBridge = (input: CruiseCommitBridgeInput, options?: {
88
+ userId?: string;
89
+ }) => Promise<CruiseCommitBridgeResult>;
90
+ export interface CreateCruiseBookingHandlerOptions extends CruiseHandlerLoaders {
91
+ /** Force the wizard to render a cabin-number sub-step even when
92
+ * the supplier doesn't surface a cabin map. Defaults to false. */
93
+ forceCabinNumberSubStep?: boolean;
94
+ /** Pass `true` when the deployment ships an insurance offer. */
95
+ includeInsurance?: boolean;
96
+ /**
97
+ * Caller-supplied bridge to `cruisesBookingService.createCruiseBooking`.
98
+ * When provided, `commit` calls into the cruise vertical's
99
+ * transactional booking path; when omitted, `commit` returns
100
+ * `failed:cruise_commit_not_yet_implemented`.
101
+ *
102
+ * Templates wire this with a small adapter:
103
+ * `(input, opts) => cruisesBookingService.createCruiseBooking(db, input, opts.userId)`
104
+ */
105
+ commitBridge?: CruiseCommitBridge;
106
+ }
107
+ export declare function createCruiseBookingHandler(options: CreateCruiseBookingHandlerOptions): OwnedBookingHandler;
108
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAMV,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,uCAAuC,CAAA;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAgCxD,MAAM,WAAW,mBAAmB;IAClC;yCACqC;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC1F;;;;OAIG;IACH,SAAS,EAAE,CACT,GAAG,EAAE,mBAAmB,EACxB,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,eAAe,EAAE,MAAM,CAAA;QACvB,SAAS,EAAE,MAAM,CAAA;KAClB,KACE,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACtB,CAAA;IACD,UAAU,EAAE,KAAK,CAAC;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,gBAAgB,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAA;KACvD,CAAC,CAAA;IACF,6DAA6D;IAC7D,cAAc,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,MAAM,GAAG,IAAI,CAAA;IAC9D,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAC/B,KAAK,EAAE,uBAAuB,EAC9B,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,wBAAwB,CAAC,CAAA;AAEtC,MAAM,WAAW,iCAAkC,SAAQ,oBAAoB;IAC7E;uEACmE;IACnE,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAClC;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,mBAAmB,CA4LrB"}