@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.
- package/LICENSE +201 -0
- package/README.md +50 -0
- package/dist/adapters/connect-compat.d.ts +20 -0
- package/dist/adapters/connect-compat.d.ts.map +1 -0
- package/dist/adapters/connect-compat.js +71 -0
- package/dist/adapters/contract-fixture.d.ts +32 -0
- package/dist/adapters/contract-fixture.d.ts.map +1 -0
- package/dist/adapters/contract-fixture.js +152 -0
- package/dist/adapters/index.d.ts +331 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/memoize.d.ts +28 -0
- package/dist/adapters/memoize.d.ts.map +1 -0
- package/dist/adapters/memoize.js +131 -0
- package/dist/adapters/mock.d.ts +44 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +192 -0
- package/dist/adapters/registry.d.ts +26 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +42 -0
- package/dist/adapters/source-adapter-shim.d.ts +80 -0
- package/dist/adapters/source-adapter-shim.d.ts.map +1 -0
- package/dist/adapters/source-adapter-shim.js +390 -0
- package/dist/booking-engine/handler.d.ts +108 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +225 -0
- package/dist/booking-engine/index.d.ts +9 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +8 -0
- package/dist/booking-extension.d.ts +1179 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +342 -0
- package/dist/cabin-features.d.ts +8 -0
- package/dist/cabin-features.d.ts.map +1 -0
- package/dist/cabin-features.js +7 -0
- package/dist/catalog-policy-cabins.d.ts +18 -0
- package/dist/catalog-policy-cabins.d.ts.map +1 -0
- package/dist/catalog-policy-cabins.js +96 -0
- package/dist/catalog-policy-core.d.ts +3 -0
- package/dist/catalog-policy-core.d.ts.map +1 -0
- package/dist/catalog-policy-core.js +247 -0
- package/dist/catalog-policy-structure.d.ts +3 -0
- package/dist/catalog-policy-structure.d.ts.map +1 -0
- package/dist/catalog-policy-structure.js +387 -0
- package/dist/catalog-policy.d.ts +15 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +19 -0
- package/dist/content-shape.d.ts +5 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +13 -0
- package/dist/draft-shape.d.ts +59 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +98 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +21 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/lib/key.d.ts +41 -0
- package/dist/lib/key.d.ts.map +1 -0
- package/dist/lib/key.js +100 -0
- package/dist/routes-booking-payloads.d.ts +133 -0
- package/dist/routes-booking-payloads.d.ts.map +1 -0
- package/dist/routes-booking-payloads.js +142 -0
- package/dist/routes-content.d.ts +53 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +158 -0
- package/dist/routes-core.d.ts +4 -0
- package/dist/routes-core.d.ts.map +1 -0
- package/dist/routes-core.js +68 -0
- package/dist/routes-detail.d.ts +4 -0
- package/dist/routes-detail.d.ts.map +1 -0
- package/dist/routes-detail.js +261 -0
- package/dist/routes-env.d.ts +13 -0
- package/dist/routes-env.d.ts.map +1 -0
- package/dist/routes-env.js +1 -0
- package/dist/routes-keying.d.ts +28 -0
- package/dist/routes-keying.d.ts.map +1 -0
- package/dist/routes-keying.js +70 -0
- package/dist/routes-public.d.ts +911 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +252 -0
- package/dist/routes-sailings-prices.d.ts +4 -0
- package/dist/routes-sailings-prices.d.ts.map +1 -0
- package/dist/routes-sailings-prices.js +278 -0
- package/dist/routes-search-index.d.ts +4 -0
- package/dist/routes-search-index.d.ts.map +1 -0
- package/dist/routes-search-index.js +25 -0
- package/dist/routes-ships.d.ts +4 -0
- package/dist/routes-ships.d.ts.map +1 -0
- package/dist/routes-ships.js +147 -0
- package/dist/routes-voyage-groups.d.ts +4 -0
- package/dist/routes-voyage-groups.d.ts.map +1 -0
- package/dist/routes-voyage-groups.js +85 -0
- package/dist/routes.d.ts +5 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +14 -0
- package/dist/schema-cabins.d.ts +1098 -0
- package/dist/schema-cabins.d.ts.map +1 -0
- package/dist/schema-cabins.js +105 -0
- package/dist/schema-content.d.ts +577 -0
- package/dist/schema-content.d.ts.map +1 -0
- package/dist/schema-content.js +63 -0
- package/dist/schema-core.d.ts +1790 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +171 -0
- package/dist/schema-itinerary.d.ts +556 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +50 -0
- package/dist/schema-pricing.d.ts +633 -0
- package/dist/schema-pricing.d.ts.map +1 -0
- package/dist/schema-pricing.js +73 -0
- package/dist/schema-search.d.ts +611 -0
- package/dist/schema-search.d.ts.map +1 -0
- package/dist/schema-search.js +64 -0
- package/dist/schema-shared.d.ts +23 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +107 -0
- package/dist/schema-sourced-content.d.ts +247 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +38 -0
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/service-booking-helpers.d.ts +12 -0
- package/dist/service-booking-helpers.d.ts.map +1 -0
- package/dist/service-booking-helpers.js +94 -0
- package/dist/service-booking-types.d.ts +101 -0
- package/dist/service-booking-types.d.ts.map +1 -0
- package/dist/service-booking-types.js +1 -0
- package/dist/service-bookings.d.ts +46 -0
- package/dist/service-bookings.d.ts.map +1 -0
- package/dist/service-bookings.js +420 -0
- package/dist/service-catalog-plane-cabins.d.ts +24 -0
- package/dist/service-catalog-plane-cabins.d.ts.map +1 -0
- package/dist/service-catalog-plane-cabins.js +90 -0
- package/dist/service-catalog-plane.d.ts +74 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +194 -0
- package/dist/service-content-synthesizer.d.ts +42 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +144 -0
- package/dist/service-content.d.ts +74 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +315 -0
- package/dist/service-core.d.ts +134 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +257 -0
- package/dist/service-detach.d.ts +18 -0
- package/dist/service-detach.d.ts.map +1 -0
- package/dist/service-detach.js +199 -0
- package/dist/service-enrichment.d.ts +11 -0
- package/dist/service-enrichment.d.ts.map +1 -0
- package/dist/service-enrichment.js +47 -0
- package/dist/service-external-refresh.d.ts +39 -0
- package/dist/service-external-refresh.d.ts.map +1 -0
- package/dist/service-external-refresh.js +47 -0
- package/dist/service-itinerary.d.ts +22 -0
- package/dist/service-itinerary.d.ts.map +1 -0
- package/dist/service-itinerary.js +34 -0
- package/dist/service-prices.d.ts +46 -0
- package/dist/service-prices.d.ts.map +1 -0
- package/dist/service-prices.js +89 -0
- package/dist/service-pricing.d.ts +97 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +198 -0
- package/dist/service-sailings.d.ts +48 -0
- package/dist/service-sailings.d.ts.map +1 -0
- package/dist/service-sailings.js +145 -0
- package/dist/service-search-types.d.ts +54 -0
- package/dist/service-search-types.d.ts.map +1 -0
- package/dist/service-search-types.js +1 -0
- package/dist/service-search.d.ts +65 -0
- package/dist/service-search.d.ts.map +1 -0
- package/dist/service-search.js +467 -0
- package/dist/service-shared.d.ts +22 -0
- package/dist/service-shared.d.ts.map +1 -0
- package/dist/service-shared.js +22 -0
- package/dist/service-ships.d.ts +47 -0
- package/dist/service-ships.d.ts.map +1 -0
- package/dist/service-ships.js +156 -0
- package/dist/service.d.ts +255 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +12 -0
- package/dist/validation-cabins.d.ts +267 -0
- package/dist/validation-cabins.d.ts.map +1 -0
- package/dist/validation-cabins.js +77 -0
- package/dist/validation-content.d.ts +123 -0
- package/dist/validation-content.d.ts.map +1 -0
- package/dist/validation-content.js +40 -0
- package/dist/validation-core.d.ts +393 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +162 -0
- package/dist/validation-itinerary.d.ts +123 -0
- package/dist/validation-itinerary.d.ts.map +1 -0
- package/dist/validation-itinerary.js +47 -0
- package/dist/validation-pricing.d.ts +137 -0
- package/dist/validation-pricing.d.ts.map +1 -0
- package/dist/validation-pricing.js +49 -0
- package/dist/validation-search.d.ts +118 -0
- package/dist/validation-search.d.ts.map +1 -0
- package/dist/validation-search.js +60 -0
- package/dist/validation-shared.d.ts +123 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +103 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- 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"}
|