@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise cabin/deck projection extension.
|
|
3
|
+
*
|
|
4
|
+
* Joins a cruise's default ship to cabin categories, specific cabins, and
|
|
5
|
+
* decks, then denormalizes machine-filterable cabin/deck facets onto the
|
|
6
|
+
* cruise search document.
|
|
7
|
+
*/
|
|
8
|
+
import { asc, eq } from "drizzle-orm";
|
|
9
|
+
import { cruiseCabinCategories, cruiseCabins, cruiseDecks } from "./schema-cabins.js";
|
|
10
|
+
import { cruises } from "./schema-core.js";
|
|
11
|
+
export function projectCruiseCabinFacetRows(rows) {
|
|
12
|
+
const cabinCategoryIds = [];
|
|
13
|
+
const cabinCategoryCodes = [];
|
|
14
|
+
const cabinGradeCodes = [];
|
|
15
|
+
const cabinRoomTypes = [];
|
|
16
|
+
const cabinFeatureCodes = [];
|
|
17
|
+
const cabinBedConfigurations = [];
|
|
18
|
+
const cabinAccessibilityFeatures = [];
|
|
19
|
+
const cabinViewTypes = [];
|
|
20
|
+
const deckIds = [];
|
|
21
|
+
const deckNames = [];
|
|
22
|
+
const deckLevels = [];
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
pushUnique(cabinCategoryIds, row.cabinCategoryId);
|
|
25
|
+
pushUnique(cabinCategoryCodes, row.cabinCategoryCode);
|
|
26
|
+
pushUnique(cabinRoomTypes, row.roomType);
|
|
27
|
+
for (const code of row.featureCodes ?? [])
|
|
28
|
+
pushUnique(cabinFeatureCodes, code);
|
|
29
|
+
for (const bed of row.bedConfigurations ?? [])
|
|
30
|
+
pushUnique(cabinBedConfigurations, bed);
|
|
31
|
+
for (const feature of row.accessibilityFeatures ?? []) {
|
|
32
|
+
pushUnique(cabinAccessibilityFeatures, feature);
|
|
33
|
+
}
|
|
34
|
+
if (row.viewType)
|
|
35
|
+
pushUnique(cabinViewTypes, row.viewType);
|
|
36
|
+
for (const grade of row.gradeCodes ?? [])
|
|
37
|
+
pushUnique(cabinGradeCodes, grade);
|
|
38
|
+
if (row.deckId)
|
|
39
|
+
pushUnique(deckIds, row.deckId);
|
|
40
|
+
if (row.deckName)
|
|
41
|
+
pushUnique(deckNames, row.deckName);
|
|
42
|
+
if (row.deckLevel !== null)
|
|
43
|
+
pushUnique(deckLevels, row.deckLevel);
|
|
44
|
+
}
|
|
45
|
+
return new Map([
|
|
46
|
+
["cabinCategoryIds[]", cabinCategoryIds],
|
|
47
|
+
["cabinCategoryCodes[]", cabinCategoryCodes],
|
|
48
|
+
["cabinGradeCodes[]", cabinGradeCodes],
|
|
49
|
+
["cabinRoomTypes[]", cabinRoomTypes],
|
|
50
|
+
["cabinFeatureCodes[]", cabinFeatureCodes],
|
|
51
|
+
["cabinBedConfigurations[]", cabinBedConfigurations],
|
|
52
|
+
["cabinAccessibilityFeatures[]", cabinAccessibilityFeatures],
|
|
53
|
+
["cabinViewTypes[]", cabinViewTypes],
|
|
54
|
+
["deckIds[]", deckIds],
|
|
55
|
+
["deckNames[]", deckNames],
|
|
56
|
+
["deckLevels[]", deckLevels],
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
export function createCruiseCabinFacetProjectionExtension() {
|
|
60
|
+
return {
|
|
61
|
+
name: "cruises:cabin-facets",
|
|
62
|
+
async project(db, cruiseId, _slice) {
|
|
63
|
+
const rows = await db
|
|
64
|
+
.select({
|
|
65
|
+
cabinCategoryId: cruiseCabinCategories.id,
|
|
66
|
+
cabinCategoryCode: cruiseCabinCategories.code,
|
|
67
|
+
roomType: cruiseCabinCategories.roomType,
|
|
68
|
+
featureCodes: cruiseCabinCategories.featureCodes,
|
|
69
|
+
bedConfigurations: cruiseCabinCategories.bedConfigurations,
|
|
70
|
+
accessibilityFeatures: cruiseCabinCategories.accessibilityFeatures,
|
|
71
|
+
viewType: cruiseCabinCategories.viewType,
|
|
72
|
+
gradeCodes: cruiseCabinCategories.gradeCodes,
|
|
73
|
+
deckId: cruiseDecks.id,
|
|
74
|
+
deckName: cruiseDecks.name,
|
|
75
|
+
deckLevel: cruiseDecks.level,
|
|
76
|
+
})
|
|
77
|
+
.from(cruises)
|
|
78
|
+
.innerJoin(cruiseCabinCategories, eq(cruiseCabinCategories.shipId, cruises.defaultShipId))
|
|
79
|
+
.leftJoin(cruiseCabins, eq(cruiseCabins.categoryId, cruiseCabinCategories.id))
|
|
80
|
+
.leftJoin(cruiseDecks, eq(cruiseCabins.deckId, cruiseDecks.id))
|
|
81
|
+
.where(eq(cruises.id, cruiseId))
|
|
82
|
+
.orderBy(asc(cruiseCabinCategories.code), asc(cruiseDecks.level), asc(cruiseDecks.name));
|
|
83
|
+
return projectCruiseCabinFacetRows(rows);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function pushUnique(values, value) {
|
|
88
|
+
if (!values.includes(value))
|
|
89
|
+
values.push(value);
|
|
90
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the cruises service.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the pattern in `packages/products/src/service-catalog-plane.ts`.
|
|
5
|
+
* Adds catalog-aware methods alongside the existing service surface.
|
|
6
|
+
*
|
|
7
|
+
* See `docs/architecture/catalog-architecture.md` §9.1.
|
|
8
|
+
*/
|
|
9
|
+
import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type FieldPolicy, type FieldPolicyRegistry, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope } from "@voyant-travel/catalog";
|
|
10
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
11
|
+
import { cruises } from "./schema-core.js";
|
|
12
|
+
export interface CruiseProjectionExtension {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
project(db: AnyDrizzleDb, cruiseId: string, slice: IndexerSlice): Promise<ReadonlyMap<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createCruisesRegistry(...extensionPolicies: ReadonlyArray<ReadonlyArray<FieldPolicy>>): FieldPolicyRegistry;
|
|
17
|
+
/**
|
|
18
|
+
* Maps a cruise row to a field-keyed projection. Provenance is synthesized:
|
|
19
|
+
* cruises sourced from a line via Voyant Connect would carry their actual
|
|
20
|
+
* source connection; today's mapper assumes operator-owned cruises (Phase 1
|
|
21
|
+
* baseline). When sourced cruises land, this helper picks up the parallel
|
|
22
|
+
* provenance row.
|
|
23
|
+
*/
|
|
24
|
+
export declare function cruiseRowToProjection(row: typeof cruises.$inferSelect, context: {
|
|
25
|
+
sellerOperatorId: string;
|
|
26
|
+
sourceKind?: string;
|
|
27
|
+
sourceRef?: string;
|
|
28
|
+
}): ReadonlyMap<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Returns the Provenance tuple for a cruise. Owned cruises synthesize an
|
|
31
|
+
* `owned` source; sourced cruises (e.g. via Voyant Connect from a cruise
|
|
32
|
+
* line) carry the actual connection identity from their parallel provenance
|
|
33
|
+
* row.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cruiseProvenance(_row: typeof cruises.$inferSelect, context: {
|
|
36
|
+
sellerOperatorId: string;
|
|
37
|
+
sourceKind?: string;
|
|
38
|
+
sourceRef?: string;
|
|
39
|
+
}): Provenance;
|
|
40
|
+
export interface CruiseCatalogContext {
|
|
41
|
+
sellerOperatorId: string;
|
|
42
|
+
scope: ResolverScope;
|
|
43
|
+
/** When the cruise comes from an upstream source, populate these. */
|
|
44
|
+
sourceKind?: string;
|
|
45
|
+
sourceRef?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function getResolvedCruiseById(db: AnyDrizzleDb, id: string, context: CruiseCatalogContext): Promise<ResolvedView | null>;
|
|
48
|
+
export declare function listResolvedCruises(db: AnyDrizzleDb, rows: ReadonlyArray<typeof cruises.$inferSelect>, context: CruiseCatalogContext): Promise<ResolvedView[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Build a `CaptureSnapshotInput` for a cruise. Fetches the cruise, resolves
|
|
51
|
+
* its view, and returns the snapshot input shape ready to pass into
|
|
52
|
+
* `captureSnapshot` or `captureSnapshotGraph`.
|
|
53
|
+
*
|
|
54
|
+
* Sourced cruises (Voyant Connect from a cruise line) carry their actual
|
|
55
|
+
* source kind / ref through the context.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildCruiseSnapshotInput(db: AnyDrizzleDb, cruiseId: string, context: CruiseCatalogContext & {
|
|
58
|
+
pricingBasis?: PricingBasis;
|
|
59
|
+
}): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
|
|
60
|
+
export declare function createCruiseDocumentEmitter(context: {
|
|
61
|
+
sellerOperatorId: string;
|
|
62
|
+
sourceKind?: string;
|
|
63
|
+
sourceRef?: string;
|
|
64
|
+
registry?: FieldPolicyRegistry;
|
|
65
|
+
}): DocumentEmitter<typeof cruises.$inferSelect>;
|
|
66
|
+
export declare function createCruiseDocumentBuilder(db: AnyDrizzleDb, context: {
|
|
67
|
+
sellerOperatorId: string;
|
|
68
|
+
sourceKind?: string;
|
|
69
|
+
sourceRef?: string;
|
|
70
|
+
extensions?: ReadonlyArray<CruiseProjectionExtension>;
|
|
71
|
+
registry?: FieldPolicyRegistry;
|
|
72
|
+
}): DocumentBuilder;
|
|
73
|
+
export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, };
|
|
74
|
+
//# sourceMappingURL=service-catalog-plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAU1C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,CACL,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC;AAED,wBAAgB,qBAAqB,CACnC,GAAG,iBAAiB,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,GAC9D,mBAAmB,CAOrB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,OAAO,CAAC,YAAY,EAChC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAoD9B;AASD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,OAAO,CAAC,YAAY,EACjC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,UAAU,CAMZ;AAED,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,aAAa,CAAA;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAW9B;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,OAAO,CAAC,YAAY,CAAC,EAChD,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,YAAY,EAAE,CAAC,CAazB;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC9D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CAUzD;AAMD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE;IACnD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GAAG,eAAe,CAAC,OAAO,OAAO,CAAC,YAAY,CAAC,CAa/C;AAED,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IACP,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,aAAa,CAAC,yBAAyB,CAAC,CAAA;IACrD,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAC/B,GACA,eAAe,CA0BjB;AAED,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,GACd,CAAA"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the cruises service.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the pattern in `packages/products/src/service-catalog-plane.ts`.
|
|
5
|
+
* Adds catalog-aware methods alongside the existing service surface.
|
|
6
|
+
*
|
|
7
|
+
* See `docs/architecture/catalog-architecture.md` §9.1.
|
|
8
|
+
*/
|
|
9
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyant-travel/catalog";
|
|
10
|
+
import { eq } from "drizzle-orm";
|
|
11
|
+
import { cruiseCatalogPolicy } from "./catalog-policy.js";
|
|
12
|
+
import { cruises } from "./schema-core.js";
|
|
13
|
+
let _registry;
|
|
14
|
+
function getCruisesRegistry() {
|
|
15
|
+
if (!_registry) {
|
|
16
|
+
_registry = createFieldPolicyRegistry(cruiseCatalogPolicy);
|
|
17
|
+
}
|
|
18
|
+
return _registry;
|
|
19
|
+
}
|
|
20
|
+
export function createCruisesRegistry(...extensionPolicies) {
|
|
21
|
+
if (extensionPolicies.length === 0)
|
|
22
|
+
return getCruisesRegistry();
|
|
23
|
+
const composed = [...cruiseCatalogPolicy];
|
|
24
|
+
for (const policies of extensionPolicies) {
|
|
25
|
+
composed.push(...policies);
|
|
26
|
+
}
|
|
27
|
+
return createFieldPolicyRegistry(composed);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Maps a cruise row to a field-keyed projection. Provenance is synthesized:
|
|
31
|
+
* cruises sourced from a line via Voyant Connect would carry their actual
|
|
32
|
+
* source connection; today's mapper assumes operator-owned cruises (Phase 1
|
|
33
|
+
* baseline). When sourced cruises land, this helper picks up the parallel
|
|
34
|
+
* provenance row.
|
|
35
|
+
*/
|
|
36
|
+
export function cruiseRowToProjection(row, context) {
|
|
37
|
+
return new Map([
|
|
38
|
+
// Provenance
|
|
39
|
+
["source.kind", context.sourceKind ?? "owned"],
|
|
40
|
+
["source.ref", context.sourceRef],
|
|
41
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
42
|
+
// Identity
|
|
43
|
+
["id", row.id],
|
|
44
|
+
["slug", row.slug],
|
|
45
|
+
["createdAt", row.createdAt],
|
|
46
|
+
["updatedAt", row.updatedAt],
|
|
47
|
+
["externalRefs", row.externalRefs],
|
|
48
|
+
// Merchandisable
|
|
49
|
+
["name", row.name],
|
|
50
|
+
["description", row.description],
|
|
51
|
+
["shortDescription", row.shortDescription],
|
|
52
|
+
["highlights", row.highlights],
|
|
53
|
+
["inclusionsHtml", row.inclusionsHtml],
|
|
54
|
+
["exclusionsHtml", row.exclusionsHtml],
|
|
55
|
+
["heroImageUrl", row.heroImageUrl],
|
|
56
|
+
["thumbnailUrl", row.heroImageUrl],
|
|
57
|
+
["mapImageUrl", row.mapImageUrl],
|
|
58
|
+
// Structural
|
|
59
|
+
["cruiseType", row.cruiseType],
|
|
60
|
+
["status", row.status],
|
|
61
|
+
["lineSupplierId", row.lineSupplierId],
|
|
62
|
+
["defaultShipId", row.defaultShipId],
|
|
63
|
+
["nights", row.nights],
|
|
64
|
+
["embarkPortFacilityId", row.embarkPortFacilityId],
|
|
65
|
+
["embarkPortCanonicalPlaceId", row.embarkPortCanonicalPlaceId],
|
|
66
|
+
["disembarkPortFacilityId", row.disembarkPortFacilityId],
|
|
67
|
+
["disembarkPortCanonicalPlaceId", row.disembarkPortCanonicalPlaceId],
|
|
68
|
+
["region_ids[]", row.regionIds],
|
|
69
|
+
["waterway_ids[]", row.waterwayIds],
|
|
70
|
+
["port_ids[]", row.portIds],
|
|
71
|
+
["country_iso[]", row.countryIso],
|
|
72
|
+
["regions[]", row.regions],
|
|
73
|
+
["waterways[]", row.waterways],
|
|
74
|
+
["ports[]", row.ports],
|
|
75
|
+
["countries[]", row.countries],
|
|
76
|
+
["themes[]", row.themes],
|
|
77
|
+
// Volatile-indexed (browse-time approximations)
|
|
78
|
+
["lowestPriceCached", moneyStringToCents(row.lowestPriceCached)],
|
|
79
|
+
["lowestPriceCurrencyCached", row.lowestPriceCurrencyCached],
|
|
80
|
+
["lowestPriceUnit", "minor"],
|
|
81
|
+
["earliestDepartureCached", row.earliestDepartureCached],
|
|
82
|
+
["latestDepartureCached", row.latestDepartureCached],
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
function moneyStringToCents(value) {
|
|
86
|
+
if (!value)
|
|
87
|
+
return null;
|
|
88
|
+
const major = Number.parseFloat(value);
|
|
89
|
+
if (!Number.isFinite(major))
|
|
90
|
+
return null;
|
|
91
|
+
return Math.round(major * 100);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the Provenance tuple for a cruise. Owned cruises synthesize an
|
|
95
|
+
* `owned` source; sourced cruises (e.g. via Voyant Connect from a cruise
|
|
96
|
+
* line) carry the actual connection identity from their parallel provenance
|
|
97
|
+
* row.
|
|
98
|
+
*/
|
|
99
|
+
export function cruiseProvenance(_row, context) {
|
|
100
|
+
return {
|
|
101
|
+
source_kind: context.sourceKind ?? "owned",
|
|
102
|
+
source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
|
|
103
|
+
source_ref: context.sourceRef,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function getResolvedCruiseById(db, id, context) {
|
|
107
|
+
const rows = await db.select().from(cruises).where(eq(cruises.id, id)).limit(1);
|
|
108
|
+
const row = rows[0];
|
|
109
|
+
if (!row)
|
|
110
|
+
return null;
|
|
111
|
+
const projection = cruiseRowToProjection(row, {
|
|
112
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
113
|
+
sourceKind: context.sourceKind,
|
|
114
|
+
sourceRef: context.sourceRef,
|
|
115
|
+
});
|
|
116
|
+
return resolveEntityView(db, getCruisesRegistry(), "cruises", id, projection, context.scope);
|
|
117
|
+
}
|
|
118
|
+
export async function listResolvedCruises(db, rows, context) {
|
|
119
|
+
const registry = getCruisesRegistry();
|
|
120
|
+
const views = [];
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
const projection = cruiseRowToProjection(row, {
|
|
123
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
124
|
+
sourceKind: context.sourceKind,
|
|
125
|
+
sourceRef: context.sourceRef,
|
|
126
|
+
});
|
|
127
|
+
const view = await resolveEntityView(db, registry, "cruises", row.id, projection, context.scope);
|
|
128
|
+
views.push(view);
|
|
129
|
+
}
|
|
130
|
+
return views;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build a `CaptureSnapshotInput` for a cruise. Fetches the cruise, resolves
|
|
134
|
+
* its view, and returns the snapshot input shape ready to pass into
|
|
135
|
+
* `captureSnapshot` or `captureSnapshotGraph`.
|
|
136
|
+
*
|
|
137
|
+
* Sourced cruises (Voyant Connect from a cruise line) carry their actual
|
|
138
|
+
* source kind / ref through the context.
|
|
139
|
+
*/
|
|
140
|
+
export async function buildCruiseSnapshotInput(db, cruiseId, context) {
|
|
141
|
+
const view = await getResolvedCruiseById(db, cruiseId, context);
|
|
142
|
+
if (!view)
|
|
143
|
+
return null;
|
|
144
|
+
return buildSnapshotInputFromView(view, {
|
|
145
|
+
entityModule: "cruises",
|
|
146
|
+
entityId: cruiseId,
|
|
147
|
+
sourceKind: context.sourceKind ?? "owned",
|
|
148
|
+
sourceRef: context.sourceRef,
|
|
149
|
+
pricingBasis: context.pricingBasis,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
// Indexer document emission
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
export function createCruiseDocumentEmitter(context) {
|
|
156
|
+
const registry = context.registry ?? getCruisesRegistry();
|
|
157
|
+
return {
|
|
158
|
+
vertical: "cruises",
|
|
159
|
+
emit(source, slice) {
|
|
160
|
+
const projection = cruiseRowToProjection(source, {
|
|
161
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
162
|
+
sourceKind: context.sourceKind,
|
|
163
|
+
sourceRef: context.sourceRef,
|
|
164
|
+
});
|
|
165
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function createCruiseDocumentBuilder(db, context) {
|
|
170
|
+
const registry = context.registry ?? getCruisesRegistry();
|
|
171
|
+
const extensions = context.extensions ?? [];
|
|
172
|
+
const emitter = createCruiseDocumentEmitter({ ...context, registry });
|
|
173
|
+
return async (entityId, slice) => {
|
|
174
|
+
const rows = await db.select().from(cruises).where(eq(cruises.id, entityId)).limit(1);
|
|
175
|
+
const row = rows[0];
|
|
176
|
+
if (!row)
|
|
177
|
+
return null;
|
|
178
|
+
if (extensions.length === 0)
|
|
179
|
+
return emitter.emit(row, slice);
|
|
180
|
+
const baseProjection = cruiseRowToProjection(row, {
|
|
181
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
182
|
+
sourceKind: context.sourceKind,
|
|
183
|
+
sourceRef: context.sourceRef,
|
|
184
|
+
});
|
|
185
|
+
const extensionProjections = await Promise.all(extensions.map((ext) => ext.project(db, entityId, slice)));
|
|
186
|
+
const merged = new Map(baseProjection);
|
|
187
|
+
for (const projection of extensionProjections) {
|
|
188
|
+
for (const [path, value] of projection) {
|
|
189
|
+
merged.set(path, value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return buildIndexerDocument(registry, merged, slice, entityId);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise content synthesizer — fallback for thin adapters that declare
|
|
3
|
+
* `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `CruiseContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Fields the projection doesn't
|
|
8
|
+
* carry render as typed empty states (`sailings: []`,
|
|
9
|
+
* `cabin_categories: []`).
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
12
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
13
|
+
* output.
|
|
14
|
+
*/
|
|
15
|
+
import { type ProvenanceReadResult } from "@voyant-travel/catalog";
|
|
16
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
17
|
+
import { type CruiseContent } from "./content-shape.js";
|
|
18
|
+
export interface SynthesizeCruiseContentOptions {
|
|
19
|
+
provenance: Extract<ProvenanceReadResult, {
|
|
20
|
+
kind: "sourced";
|
|
21
|
+
}>;
|
|
22
|
+
overlays?: ReadonlyArray<{
|
|
23
|
+
field_path: string;
|
|
24
|
+
value: unknown;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export interface SynthesizedCruiseContent {
|
|
28
|
+
content: CruiseContent;
|
|
29
|
+
content_schema_version: string;
|
|
30
|
+
served_locale: string;
|
|
31
|
+
source_kind: string;
|
|
32
|
+
source_provider?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function synthesizeCruiseContent(scope: {
|
|
35
|
+
locale: string;
|
|
36
|
+
}, options: SynthesizeCruiseContentOptions): SynthesizedCruiseContent;
|
|
37
|
+
export declare function synthesizeCruiseContentFromDb(db: AnyDrizzleDb, scope: {
|
|
38
|
+
locale: string;
|
|
39
|
+
}, provenance: Extract<ProvenanceReadResult, {
|
|
40
|
+
kind: "sourced";
|
|
41
|
+
}>): Promise<SynthesizedCruiseContent>;
|
|
42
|
+
//# sourceMappingURL=service-content-synthesizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../src/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,aAAa,CAAA;IACtB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,8BAA8B,GACtC,wBAAwB,CAoC1B;AAED,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,wBAAwB,CAAC,CAOnC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise content synthesizer — fallback for thin adapters that declare
|
|
3
|
+
* `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `CruiseContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Fields the projection doesn't
|
|
8
|
+
* carry render as typed empty states (`sailings: []`,
|
|
9
|
+
* `cabin_categories: []`).
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
12
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
13
|
+
* output.
|
|
14
|
+
*/
|
|
15
|
+
import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyant-travel/catalog";
|
|
16
|
+
import { CRUISES_CONTENT_SCHEMA_VERSION, cruiseContentSchema, } from "./content-shape.js";
|
|
17
|
+
export function synthesizeCruiseContent(scope, options) {
|
|
18
|
+
const projection = options.provenance.projection;
|
|
19
|
+
const cruise = pickCruiseSummary(projection, options.provenance);
|
|
20
|
+
const ship = pickShip(projection);
|
|
21
|
+
const itinerary = pickItineraryStops(projection);
|
|
22
|
+
const policies = pickPolicies(projection);
|
|
23
|
+
const baseContent = {
|
|
24
|
+
cruise,
|
|
25
|
+
ship,
|
|
26
|
+
sailings: [],
|
|
27
|
+
cabin_categories: [],
|
|
28
|
+
itinerary_stops: itinerary,
|
|
29
|
+
policies,
|
|
30
|
+
};
|
|
31
|
+
let merged = baseContent;
|
|
32
|
+
if (options.overlays && options.overlays.length > 0) {
|
|
33
|
+
const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
|
|
34
|
+
validate(p) {
|
|
35
|
+
const r = cruiseContentSchema.safeParse(p);
|
|
36
|
+
return r.success
|
|
37
|
+
? { valid: true }
|
|
38
|
+
: { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
merged = cruiseContentSchema.parse(result);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: merged,
|
|
45
|
+
content_schema_version: CRUISES_CONTENT_SCHEMA_VERSION,
|
|
46
|
+
served_locale: scope.locale,
|
|
47
|
+
source_kind: options.provenance.provenance.source_kind,
|
|
48
|
+
source_provider: options.provenance.provenance.source_provider,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function synthesizeCruiseContentFromDb(db, scope, provenance) {
|
|
52
|
+
const entityId = entityIdFromProvenance(provenance);
|
|
53
|
+
const overlays = await fetchOverlaysForEntity(db, "cruises", entityId);
|
|
54
|
+
return synthesizeCruiseContent(scope, {
|
|
55
|
+
provenance,
|
|
56
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function entityIdFromProvenance(provenance) {
|
|
60
|
+
const fromProjection = provenance.projection.id;
|
|
61
|
+
if (typeof fromProjection === "string" && fromProjection.length > 0) {
|
|
62
|
+
return fromProjection;
|
|
63
|
+
}
|
|
64
|
+
return provenance.entry_id;
|
|
65
|
+
}
|
|
66
|
+
function pickCruiseSummary(projection, provenance) {
|
|
67
|
+
return {
|
|
68
|
+
id: stringOr(projection.id, "") || provenance.entry_id,
|
|
69
|
+
name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed cruise",
|
|
70
|
+
status: stringOr(projection.status, undefined),
|
|
71
|
+
description: stringOr(projection.description, null),
|
|
72
|
+
cruise_type: stringOr(projection.cruise_type, null),
|
|
73
|
+
hero_image_url: stringOr(projection.hero_image_url, null),
|
|
74
|
+
highlights: stringArrayOr(projection.highlights, []),
|
|
75
|
+
cruise_line: stringOr(projection.cruise_line, null) ??
|
|
76
|
+
stringOr(projection.line_name, null) ??
|
|
77
|
+
provenance.provenance.source_provider ??
|
|
78
|
+
null,
|
|
79
|
+
duration_nights: numberOr(projection.duration_nights, null),
|
|
80
|
+
embarkation_port: stringOr(projection.embarkation_port, null),
|
|
81
|
+
disembarkation_port: stringOr(projection.disembarkation_port, null),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function pickShip(projection) {
|
|
85
|
+
const shipName = stringOr(projection.ship_name, null) ?? stringOr(projection.ship, null);
|
|
86
|
+
if (!shipName)
|
|
87
|
+
return null;
|
|
88
|
+
return {
|
|
89
|
+
name: shipName,
|
|
90
|
+
description: stringOr(projection.ship_description, null),
|
|
91
|
+
deck_plan_url: stringOr(projection.ship_deck_plan_url, null),
|
|
92
|
+
deck_plans: [],
|
|
93
|
+
capacity: numberOr(projection.ship_capacity, null),
|
|
94
|
+
decks: numberOr(projection.ship_decks, null),
|
|
95
|
+
gallery: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function pickItineraryStops(projection) {
|
|
99
|
+
const stops = projection.itinerary;
|
|
100
|
+
if (!Array.isArray(stops))
|
|
101
|
+
return [];
|
|
102
|
+
const result = [];
|
|
103
|
+
for (const item of stops) {
|
|
104
|
+
if (!item || typeof item !== "object")
|
|
105
|
+
continue;
|
|
106
|
+
const obj = item;
|
|
107
|
+
const portName = stringOr(obj.port_name, null) ?? stringOr(obj.port, null);
|
|
108
|
+
const dayNumber = numberOr(obj.day_number, null) ?? numberOr(obj.day, null);
|
|
109
|
+
if (!portName || dayNumber === null || dayNumber <= 0)
|
|
110
|
+
continue;
|
|
111
|
+
result.push({
|
|
112
|
+
day_number: dayNumber,
|
|
113
|
+
date: stringOr(obj.date, null),
|
|
114
|
+
port_name: portName,
|
|
115
|
+
arrival_time: stringOr(obj.arrival_time, null),
|
|
116
|
+
departure_time: stringOr(obj.departure_time, null),
|
|
117
|
+
description: stringOr(obj.description, null),
|
|
118
|
+
is_at_sea: typeof obj.is_at_sea === "boolean" ? obj.is_at_sea : false,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function pickPolicies(projection) {
|
|
124
|
+
const out = [];
|
|
125
|
+
const cancel = stringOr(projection.cancellation_policy, null);
|
|
126
|
+
if (cancel)
|
|
127
|
+
out.push({ kind: "cancellation", body: cancel });
|
|
128
|
+
const payment = stringOr(projection.payment_terms, null);
|
|
129
|
+
if (payment)
|
|
130
|
+
out.push({ kind: "payment", body: payment });
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function stringOr(value, fallback) {
|
|
134
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
135
|
+
}
|
|
136
|
+
function numberOr(value, fallback) {
|
|
137
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
138
|
+
}
|
|
139
|
+
function stringArrayOr(value, fallback) {
|
|
140
|
+
if (!Array.isArray(value))
|
|
141
|
+
return fallback;
|
|
142
|
+
const out = value.filter((v) => typeof v === "string");
|
|
143
|
+
return out.length > 0 ? out : fallback;
|
|
144
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise content service — `getCruiseContent` with locale-resolved
|
|
3
|
+
* cache reads, SWR refresh, and synthesizer fallback.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `service-content.ts` in the products package but cruise-
|
|
6
|
+
* shaped. The cruise content aggregate (§3.2 / §E) is `{ cruise, ship,
|
|
7
|
+
* sailings[], cabin_categories[], itinerary_stops[], policies[] }` —
|
|
8
|
+
* one payload returned by a single getContent. The cruise adapter's
|
|
9
|
+
* existing internal multi-method API
|
|
10
|
+
* (`fetchCruise/fetchSailing/fetchShip/fetchItinerary`) composes
|
|
11
|
+
* internally to produce this blob; the public catalog SourceAdapter
|
|
12
|
+
* contract gets one method, not five.
|
|
13
|
+
*
|
|
14
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.3, §3.4, §3.6.
|
|
15
|
+
*/
|
|
16
|
+
import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyant-travel/catalog";
|
|
17
|
+
import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
|
|
18
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
19
|
+
import { type CruiseContent } from "./content-shape.js";
|
|
20
|
+
export interface CruiseContentScope {
|
|
21
|
+
preferredLocales: ReadonlyArray<string>;
|
|
22
|
+
market?: string;
|
|
23
|
+
currency?: string;
|
|
24
|
+
acceptMachineTranslated?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface GetCruiseContentOptions {
|
|
27
|
+
registry: SourceAdapterRegistry;
|
|
28
|
+
buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
|
|
29
|
+
onOverlayError?: (event: {
|
|
30
|
+
field_path: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface ResolvedCruiseContent {
|
|
35
|
+
content: CruiseContent;
|
|
36
|
+
resolution: ContentLocaleResolution<{
|
|
37
|
+
locale: string;
|
|
38
|
+
payload: CruiseContent;
|
|
39
|
+
}>;
|
|
40
|
+
source: "sourced-cache" | "sourced-fresh" | "synthesized";
|
|
41
|
+
served_stale: boolean;
|
|
42
|
+
synthesized: boolean;
|
|
43
|
+
machine_translated: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function getCruiseContent(db: AnyDrizzleDb, entityId: string, scope: CruiseContentScope, options: GetCruiseContentOptions): Promise<ResolvedCruiseContent | null>;
|
|
46
|
+
export interface CruiseSailingPricingRow {
|
|
47
|
+
/** Upstream cabin-category external id (e.g. `<ship>_<grade>`). */
|
|
48
|
+
cabinExternalId: string;
|
|
49
|
+
occupancy: number;
|
|
50
|
+
fareCode: string | null;
|
|
51
|
+
fareName: string | null;
|
|
52
|
+
currency: string;
|
|
53
|
+
/** Major-unit price string as the adapter returns it (e.g. "12959.00"). */
|
|
54
|
+
pricePerPerson: string;
|
|
55
|
+
availability: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Live per-sailing cabin pricing for a sourced cruise. Pricing is volatile-live
|
|
59
|
+
* (architecture §5.4) so it is fetched fresh from the adapter per call rather
|
|
60
|
+
* than baked into the cached content — the catalog detail sheet calls this
|
|
61
|
+
* lazily when a departure row is expanded.
|
|
62
|
+
*
|
|
63
|
+
* Returns `null` when the entity has no sourced row or the registered adapter
|
|
64
|
+
* can't price sailings (e.g. a thin adapter without `fetchSailingPricing`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function getCruiseSailingPricing(db: AnyDrizzleDb, entityId: string, sailingExternalId: string, options: {
|
|
67
|
+
registry: SourceAdapterRegistry;
|
|
68
|
+
}): Promise<CruiseSailingPricingRow[] | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Drift event consumer for the cruises content cache. Per sourced-
|
|
71
|
+
* content §3.4.1.
|
|
72
|
+
*/
|
|
73
|
+
export declare const invalidateCruiseContentOnDrift: InvalidateOnDrift;
|
|
74
|
+
//# sourceMappingURL=service-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../src/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,oBAAoB,CAAA;AAa3B,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,aAAa,CAAA;IACtB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC,CAAA;IAC/E,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CA2GvC;AAED,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,MAAM,EACzB,OAAO,EAAE;IAAE,QAAQ,EAAE,qBAAqB,CAAA;CAAE,GAC3C,OAAO,CAAC,uBAAuB,EAAE,GAAG,IAAI,CAAC,CA6B3C;AA0OD;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,iBAG5C,CAAA"}
|