@voyantjs/cruises-contracts 0.90.0

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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @voyantjs/cruises-contracts
2
+
3
+ Pure cruise content contracts for adapter implementers and external consumers
4
+ that need to validate `cruises/v1` rich content payloads without installing the
5
+ full cruises runtime package.
6
+
7
+ Use this package for `CRUISES_CONTENT_SCHEMA_VERSION`, `cruiseContentSchema`,
8
+ `CruiseContent`, nested content types, `validateCruiseContent`, and the cabin
9
+ facet vocabularies (`CABIN_BED_CONFIGURATIONS`, `CABIN_ACCESSIBILITY_FEATURES`,
10
+ `CABIN_VIEW_TYPES`, exported from `@voyantjs/cruises-contracts/cabin-features`).
11
+ Use `@voyantjs/cruises` when you also need Drizzle schema, routes, services,
12
+ booking integration, adapter registry helpers, or runtime content resolution.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @voyantjs/cruises-contracts zod
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import {
24
+ CRUISES_CONTENT_SCHEMA_VERSION,
25
+ cruiseContentSchema,
26
+ type CruiseContent,
27
+ } from "@voyantjs/cruises-contracts"
28
+ ```
29
+
30
+ Existing `@voyantjs/cruises/content-shape` imports remain available for
31
+ applications that already depend on the full runtime package.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Cabin facet vocabularies for the cruises content contract.
3
+ *
4
+ * Pure, dependency-free enumerations shared between the rich content
5
+ * schema (`cabin_categories[].{bed_configurations,accessibility_features,
6
+ * view_type}`) and the runtime cruises catalog facets. They live in the
7
+ * contracts package so external adapter authors can emit and validate
8
+ * cabin facets without pulling in the cruises runtime.
9
+ */
10
+ export declare const CABIN_BED_CONFIGURATIONS: readonly ["single", "twin", "double", "queen", "king", "convertible_twins", "sofa_bed", "pullman", "bunk", "murphy"];
11
+ export type CabinBedConfiguration = (typeof CABIN_BED_CONFIGURATIONS)[number];
12
+ export declare const CABIN_ACCESSIBILITY_FEATURES: readonly ["wheelchair_accessible", "step_free_access", "roll_in_shower", "grab_bars", "visual_alarm", "hearing_loop", "accessible_balcony", "accessible_bathroom"];
13
+ export type CabinAccessibilityFeature = (typeof CABIN_ACCESSIBILITY_FEATURES)[number];
14
+ export declare const CABIN_VIEW_TYPES: readonly ["none", "interior", "virtual", "porthole", "window", "oceanview", "river_view", "balcony", "french_balcony", "promenade", "obstructed"];
15
+ export type CabinViewType = (typeof CABIN_VIEW_TYPES)[number];
16
+ //# sourceMappingURL=cabin-features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cabin-features.d.ts","sourceRoot":"","sources":["../src/cabin-features.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,wBAAwB,sHAW3B,CAAA;AAEV,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7E,eAAO,MAAM,4BAA4B,oKAS/B,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAA;AAErF,eAAO,MAAM,gBAAgB,mJAYnB,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cabin facet vocabularies for the cruises content contract.
3
+ *
4
+ * Pure, dependency-free enumerations shared between the rich content
5
+ * schema (`cabin_categories[].{bed_configurations,accessibility_features,
6
+ * view_type}`) and the runtime cruises catalog facets. They live in the
7
+ * contracts package so external adapter authors can emit and validate
8
+ * cabin facets without pulling in the cruises runtime.
9
+ */
10
+ export const CABIN_BED_CONFIGURATIONS = [
11
+ "single",
12
+ "twin",
13
+ "double",
14
+ "queen",
15
+ "king",
16
+ "convertible_twins",
17
+ "sofa_bed",
18
+ "pullman",
19
+ "bunk",
20
+ "murphy",
21
+ ];
22
+ export const CABIN_ACCESSIBILITY_FEATURES = [
23
+ "wheelchair_accessible",
24
+ "step_free_access",
25
+ "roll_in_shower",
26
+ "grab_bars",
27
+ "visual_alarm",
28
+ "hearing_loop",
29
+ "accessible_balcony",
30
+ "accessible_bathroom",
31
+ ];
32
+ export const CABIN_VIEW_TYPES = [
33
+ "none",
34
+ "interior",
35
+ "virtual",
36
+ "porthole",
37
+ "window",
38
+ "oceanview",
39
+ "river_view",
40
+ "balcony",
41
+ "french_balcony",
42
+ "promenade",
43
+ "obstructed",
44
+ ];
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Cruises content shape — the rich detail-page content shape returned
3
+ * by `getContent` for sourced cruises.
4
+ *
5
+ * The doc's "cruise content aggregate" (§E in the migration plan +
6
+ * §3.2): `{ cruise, ship, sailings[], cabinCategories[], itineraryStops[],
7
+ * policies[] }` is one content payload returned by a single getContent.
8
+ * The cruise adapter's existing internal multi-call composition
9
+ * (`fetchCruise / fetchSailing / fetchShip / fetchItinerary`) flattens
10
+ * to one `GetContentResult.content` blob; the public adapter contract
11
+ * gets one method, not five.
12
+ *
13
+ * Full pricing stays out — it's volatile and continues to flow through
14
+ * `liveResolve`. The content blob carries structural cabin categories and
15
+ * itinerary stops, plus optional per-sailing browse price summaries as
16
+ * integer minor units paired with currency.
17
+ *
18
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §E.
19
+ */
20
+ import { z } from "zod";
21
+ export declare const CRUISES_CONTENT_SCHEMA_VERSION = "cruises/v1";
22
+ export declare const cruiseSummarySchema: z.ZodObject<{
23
+ id: z.ZodString;
24
+ name: z.ZodString;
25
+ status: z.ZodOptional<z.ZodString>;
26
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ cruise_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
29
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
+ cruise_line: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ duration_nights: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
32
+ embarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ disembarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
+ }, z.core.$strip>;
35
+ export declare const cruiseShipSchema: z.ZodObject<{
36
+ id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
37
+ name: z.ZodString;
38
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
40
+ decks: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
41
+ year_built: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
42
+ }, z.core.$strip>;
43
+ export declare const cruiseItineraryStopSchema: z.ZodObject<{
44
+ day_number: z.ZodNumber;
45
+ date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ port_name: z.ZodString;
47
+ arrival_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ departure_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ is_at_sea: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
51
+ }, z.core.$strip>;
52
+ export declare const cruiseSailingSchema: z.ZodObject<{
53
+ id: z.ZodString;
54
+ source_ref: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
+ start_date: z.ZodString;
56
+ end_date: z.ZodString;
57
+ duration_nights: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
58
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ embarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
60
+ disembarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
61
+ itinerary_stops: z.ZodDefault<z.ZodArray<z.ZodObject<{
62
+ day_number: z.ZodNumber;
63
+ date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
64
+ port_name: z.ZodString;
65
+ arrival_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ departure_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ is_at_sea: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
69
+ }, z.core.$strip>>>;
70
+ lowest_price_cents: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
71
+ currency: z.ZodDefault<z.ZodNullable<z.ZodString>>;
72
+ }, z.core.$strip>;
73
+ export declare const cruiseCabinCategorySchema: z.ZodObject<{
74
+ id: z.ZodString;
75
+ code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
76
+ name: z.ZodString;
77
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
78
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
+ capacity_min: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
80
+ capacity_max: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
81
+ inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
82
+ feature_codes: z.ZodDefault<z.ZodArray<z.ZodString>>;
83
+ bed_configurations: z.ZodDefault<z.ZodArray<z.ZodEnum<{
84
+ single: "single";
85
+ twin: "twin";
86
+ double: "double";
87
+ queen: "queen";
88
+ king: "king";
89
+ convertible_twins: "convertible_twins";
90
+ sofa_bed: "sofa_bed";
91
+ pullman: "pullman";
92
+ bunk: "bunk";
93
+ murphy: "murphy";
94
+ }>>>;
95
+ accessibility_features: z.ZodDefault<z.ZodArray<z.ZodEnum<{
96
+ wheelchair_accessible: "wheelchair_accessible";
97
+ step_free_access: "step_free_access";
98
+ roll_in_shower: "roll_in_shower";
99
+ grab_bars: "grab_bars";
100
+ visual_alarm: "visual_alarm";
101
+ hearing_loop: "hearing_loop";
102
+ accessible_balcony: "accessible_balcony";
103
+ accessible_bathroom: "accessible_bathroom";
104
+ }>>>;
105
+ view_type: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
106
+ none: "none";
107
+ interior: "interior";
108
+ virtual: "virtual";
109
+ porthole: "porthole";
110
+ window: "window";
111
+ oceanview: "oceanview";
112
+ river_view: "river_view";
113
+ balcony: "balcony";
114
+ french_balcony: "french_balcony";
115
+ promenade: "promenade";
116
+ obstructed: "obstructed";
117
+ }>>>;
118
+ }, z.core.$strip>;
119
+ export declare const cruisePolicySchema: z.ZodObject<{
120
+ kind: z.ZodEnum<{
121
+ cancellation: "cancellation";
122
+ payment: "payment";
123
+ supplier_notes: "supplier_notes";
124
+ requirements: "requirements";
125
+ }>;
126
+ body: z.ZodString;
127
+ rules: z.ZodOptional<z.ZodUnknown>;
128
+ }, z.core.$strip>;
129
+ export declare const cruiseContentSchema: z.ZodObject<{
130
+ cruise: z.ZodObject<{
131
+ id: z.ZodString;
132
+ name: z.ZodString;
133
+ status: z.ZodOptional<z.ZodString>;
134
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
135
+ cruise_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
136
+ hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
137
+ highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ cruise_line: z.ZodOptional<z.ZodNullable<z.ZodString>>;
139
+ duration_nights: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
140
+ embarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
141
+ disembarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
142
+ }, z.core.$strip>;
143
+ ship: z.ZodOptional<z.ZodNullable<z.ZodObject<{
144
+ id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
145
+ name: z.ZodString;
146
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
147
+ capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
148
+ decks: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
149
+ year_built: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
150
+ }, z.core.$strip>>>;
151
+ sailings: z.ZodDefault<z.ZodArray<z.ZodObject<{
152
+ id: z.ZodString;
153
+ source_ref: z.ZodOptional<z.ZodNullable<z.ZodString>>;
154
+ start_date: z.ZodString;
155
+ end_date: z.ZodString;
156
+ duration_nights: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
157
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
158
+ embarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
159
+ disembarkation_port: z.ZodOptional<z.ZodNullable<z.ZodString>>;
160
+ itinerary_stops: z.ZodDefault<z.ZodArray<z.ZodObject<{
161
+ day_number: z.ZodNumber;
162
+ date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
163
+ port_name: z.ZodString;
164
+ arrival_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
165
+ departure_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
166
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
167
+ is_at_sea: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
168
+ }, z.core.$strip>>>;
169
+ lowest_price_cents: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
170
+ currency: z.ZodDefault<z.ZodNullable<z.ZodString>>;
171
+ }, z.core.$strip>>>;
172
+ cabin_categories: z.ZodDefault<z.ZodArray<z.ZodObject<{
173
+ id: z.ZodString;
174
+ code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
175
+ name: z.ZodString;
176
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
177
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
+ capacity_min: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
179
+ capacity_max: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
180
+ inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
181
+ feature_codes: z.ZodDefault<z.ZodArray<z.ZodString>>;
182
+ bed_configurations: z.ZodDefault<z.ZodArray<z.ZodEnum<{
183
+ single: "single";
184
+ twin: "twin";
185
+ double: "double";
186
+ queen: "queen";
187
+ king: "king";
188
+ convertible_twins: "convertible_twins";
189
+ sofa_bed: "sofa_bed";
190
+ pullman: "pullman";
191
+ bunk: "bunk";
192
+ murphy: "murphy";
193
+ }>>>;
194
+ accessibility_features: z.ZodDefault<z.ZodArray<z.ZodEnum<{
195
+ wheelchair_accessible: "wheelchair_accessible";
196
+ step_free_access: "step_free_access";
197
+ roll_in_shower: "roll_in_shower";
198
+ grab_bars: "grab_bars";
199
+ visual_alarm: "visual_alarm";
200
+ hearing_loop: "hearing_loop";
201
+ accessible_balcony: "accessible_balcony";
202
+ accessible_bathroom: "accessible_bathroom";
203
+ }>>>;
204
+ view_type: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
205
+ none: "none";
206
+ interior: "interior";
207
+ virtual: "virtual";
208
+ porthole: "porthole";
209
+ window: "window";
210
+ oceanview: "oceanview";
211
+ river_view: "river_view";
212
+ balcony: "balcony";
213
+ french_balcony: "french_balcony";
214
+ promenade: "promenade";
215
+ obstructed: "obstructed";
216
+ }>>>;
217
+ }, z.core.$strip>>>;
218
+ itinerary_stops: z.ZodDefault<z.ZodArray<z.ZodObject<{
219
+ day_number: z.ZodNumber;
220
+ date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
221
+ port_name: z.ZodString;
222
+ arrival_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
223
+ departure_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
224
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
225
+ is_at_sea: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
226
+ }, z.core.$strip>>>;
227
+ policies: z.ZodDefault<z.ZodArray<z.ZodObject<{
228
+ kind: z.ZodEnum<{
229
+ cancellation: "cancellation";
230
+ payment: "payment";
231
+ supplier_notes: "supplier_notes";
232
+ requirements: "requirements";
233
+ }>;
234
+ body: z.ZodString;
235
+ rules: z.ZodOptional<z.ZodUnknown>;
236
+ }, z.core.$strip>>>;
237
+ }, z.core.$strip>;
238
+ export type CruiseContent = z.infer<typeof cruiseContentSchema>;
239
+ export type CruiseSummary = z.infer<typeof cruiseSummarySchema>;
240
+ export type CruiseShip = z.infer<typeof cruiseShipSchema>;
241
+ export type CruiseSailing = z.infer<typeof cruiseSailingSchema>;
242
+ export type CruiseCabinCategory = z.infer<typeof cruiseCabinCategorySchema>;
243
+ export type CruiseItineraryStop = z.infer<typeof cruiseItineraryStopSchema>;
244
+ export type CruisePolicy = z.infer<typeof cruisePolicySchema>;
245
+ export declare function validateCruiseContent(payload: unknown): {
246
+ valid: true;
247
+ content: CruiseContent;
248
+ } | {
249
+ valid: false;
250
+ reason: string;
251
+ };
252
+ //# sourceMappingURL=content-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAQvB,eAAO,MAAM,8BAA8B,eAAe,CAAA;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAY9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;iBAO3B,CAAA;AAEF,eAAO,MAAM,yBAAyB;;;;;;;;iBAQpC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;iBAuB5B,CAAA;AAEJ,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAapC,CAAA;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;iBAI7B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAC3E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAC3E,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE7D,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAU5E"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Cruises content shape — the rich detail-page content shape returned
3
+ * by `getContent` for sourced cruises.
4
+ *
5
+ * The doc's "cruise content aggregate" (§E in the migration plan +
6
+ * §3.2): `{ cruise, ship, sailings[], cabinCategories[], itineraryStops[],
7
+ * policies[] }` is one content payload returned by a single getContent.
8
+ * The cruise adapter's existing internal multi-call composition
9
+ * (`fetchCruise / fetchSailing / fetchShip / fetchItinerary`) flattens
10
+ * to one `GetContentResult.content` blob; the public adapter contract
11
+ * gets one method, not five.
12
+ *
13
+ * Full pricing stays out — it's volatile and continues to flow through
14
+ * `liveResolve`. The content blob carries structural cabin categories and
15
+ * itinerary stops, plus optional per-sailing browse price summaries as
16
+ * integer minor units paired with currency.
17
+ *
18
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §E.
19
+ */
20
+ import { z } from "zod";
21
+ import { CABIN_ACCESSIBILITY_FEATURES, CABIN_BED_CONFIGURATIONS, CABIN_VIEW_TYPES, } from "./cabin-features.js";
22
+ export const CRUISES_CONTENT_SCHEMA_VERSION = "cruises/v1";
23
+ export const cruiseSummarySchema = z.object({
24
+ id: z.string(),
25
+ name: z.string(),
26
+ status: z.string().optional(),
27
+ description: z.string().nullable().optional(),
28
+ cruise_type: z.string().nullable().optional(),
29
+ hero_image_url: z.string().nullable().optional(),
30
+ highlights: z.array(z.string()).optional(),
31
+ cruise_line: z.string().nullable().optional(),
32
+ duration_nights: z.number().int().nonnegative().nullable().optional(),
33
+ embarkation_port: z.string().nullable().optional(),
34
+ disembarkation_port: z.string().nullable().optional(),
35
+ });
36
+ export const cruiseShipSchema = z.object({
37
+ id: z.string().nullable().optional(),
38
+ name: z.string(),
39
+ description: z.string().nullable().optional(),
40
+ capacity: z.number().int().nonnegative().nullable().optional(),
41
+ decks: z.number().int().nonnegative().nullable().optional(),
42
+ year_built: z.number().int().nonnegative().nullable().optional(),
43
+ });
44
+ export const cruiseItineraryStopSchema = z.object({
45
+ day_number: z.number().int().positive(),
46
+ date: z.string().nullable().optional(),
47
+ port_name: z.string(),
48
+ arrival_time: z.string().nullable().optional(),
49
+ departure_time: z.string().nullable().optional(),
50
+ description: z.string().nullable().optional(),
51
+ is_at_sea: z.boolean().optional().default(false),
52
+ });
53
+ export const cruiseSailingSchema = z
54
+ .object({
55
+ id: z.string(),
56
+ source_ref: z.string().nullable().optional(),
57
+ start_date: z.string(),
58
+ end_date: z.string(),
59
+ duration_nights: z.number().int().nonnegative().nullable().optional(),
60
+ status: z.string().nullable().optional(),
61
+ embarkation_port: z.string().nullable().optional(),
62
+ disembarkation_port: z.string().nullable().optional(),
63
+ itinerary_stops: z.array(cruiseItineraryStopSchema).default([]),
64
+ lowest_price_cents: z.number().int().nonnegative().nullable().default(null),
65
+ currency: z.string().min(1).nullable().default(null),
66
+ })
67
+ .superRefine((sailing, ctx) => {
68
+ const hasLowestPrice = sailing.lowest_price_cents !== null;
69
+ const hasCurrency = sailing.currency !== null;
70
+ if (hasLowestPrice === hasCurrency)
71
+ return;
72
+ ctx.addIssue({
73
+ code: "custom",
74
+ path: hasLowestPrice ? ["currency"] : ["lowest_price_cents"],
75
+ message: "lowest_price_cents and currency must both be present or both be null",
76
+ });
77
+ });
78
+ export const cruiseCabinCategorySchema = z.object({
79
+ id: z.string(),
80
+ code: z.string().nullable().optional(),
81
+ name: z.string(),
82
+ description: z.string().nullable().optional(),
83
+ type: z.string().nullable().optional(), // inside, outside, balcony, suite
84
+ capacity_min: z.number().int().nonnegative().nullable().optional(),
85
+ capacity_max: z.number().int().nonnegative().nullable().optional(),
86
+ inclusions: z.array(z.string()).optional().default([]),
87
+ feature_codes: z.array(z.string()).default([]),
88
+ bed_configurations: z.array(z.enum(CABIN_BED_CONFIGURATIONS)).default([]),
89
+ accessibility_features: z.array(z.enum(CABIN_ACCESSIBILITY_FEATURES)).default([]),
90
+ view_type: z.enum(CABIN_VIEW_TYPES).nullable().default(null),
91
+ });
92
+ export const cruisePolicySchema = z.object({
93
+ kind: z.enum(["cancellation", "payment", "supplier_notes", "requirements"]),
94
+ body: z.string(),
95
+ rules: z.unknown().optional(),
96
+ });
97
+ export const cruiseContentSchema = z.object({
98
+ cruise: cruiseSummarySchema,
99
+ ship: cruiseShipSchema.nullable().optional(),
100
+ sailings: z.array(cruiseSailingSchema).default([]),
101
+ cabin_categories: z.array(cruiseCabinCategorySchema).default([]),
102
+ itinerary_stops: z.array(cruiseItineraryStopSchema).default([]),
103
+ policies: z.array(cruisePolicySchema).default([]),
104
+ });
105
+ export function validateCruiseContent(payload) {
106
+ const result = cruiseContentSchema.safeParse(payload);
107
+ if (result.success) {
108
+ return { valid: true, content: result.data };
109
+ }
110
+ const issue = result.error.issues[0];
111
+ return {
112
+ valid: false,
113
+ reason: issue ? `${issue.path.join(".")}: ${issue.message}` : "validation failed",
114
+ };
115
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=content-shape.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-shape.test.d.ts","sourceRoot":"","sources":["../src/content-shape.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CRUISES_CONTENT_SCHEMA_VERSION, cruiseContentSchema, validateCruiseContent, } from "./index.js";
3
+ describe("@voyantjs/cruises-contracts content shape", () => {
4
+ it("validates the cruises/v1 rich content payload", () => {
5
+ const content = cruiseContentSchema.parse({
6
+ cruise: {
7
+ id: "cru_123",
8
+ name: "Danube Highlights",
9
+ },
10
+ sailings: [
11
+ {
12
+ id: "crsl_123",
13
+ start_date: "2026-07-01",
14
+ end_date: "2026-07-08",
15
+ lowest_price_cents: 120000,
16
+ currency: "EUR",
17
+ },
18
+ ],
19
+ });
20
+ expect(CRUISES_CONTENT_SCHEMA_VERSION).toBe("cruises/v1");
21
+ expect(validateCruiseContent(content)).toMatchObject({ valid: true });
22
+ expect(content.sailings[0]?.itinerary_stops).toEqual([]);
23
+ });
24
+ it("carries structured cabin feature facets", () => {
25
+ const content = cruiseContentSchema.parse({
26
+ cruise: { id: "cru_123", name: "Danube Highlights" },
27
+ cabin_categories: [
28
+ { id: "cab_inside", name: "Inside", type: "inside", view_type: "interior" },
29
+ {
30
+ id: "cab_balcony",
31
+ name: "Balcony",
32
+ type: "balcony",
33
+ feature_codes: ["minibar"],
34
+ bed_configurations: ["king", "convertible_twins"],
35
+ accessibility_features: ["step_free_access"],
36
+ view_type: "balcony",
37
+ },
38
+ ],
39
+ });
40
+ expect(content.cabin_categories[1]?.feature_codes).toEqual(["minibar"]);
41
+ expect(content.cabin_categories[1]?.bed_configurations).toEqual(["king", "convertible_twins"]);
42
+ expect(content.cabin_categories[1]?.accessibility_features).toEqual(["step_free_access"]);
43
+ expect(content.cabin_categories[1]?.view_type).toBe("balcony");
44
+ });
45
+ it("defaults cabin feature facets when omitted", () => {
46
+ const content = cruiseContentSchema.parse({
47
+ cruise: { id: "cru_123", name: "Danube Highlights" },
48
+ cabin_categories: [{ id: "cab_inside", name: "Inside" }],
49
+ });
50
+ expect(content.cabin_categories[0]?.feature_codes).toEqual([]);
51
+ expect(content.cabin_categories[0]?.bed_configurations).toEqual([]);
52
+ expect(content.cabin_categories[0]?.accessibility_features).toEqual([]);
53
+ expect(content.cabin_categories[0]?.view_type).toBeNull();
54
+ });
55
+ it("rejects unknown cabin facet enum values", () => {
56
+ expect(validateCruiseContent({
57
+ cruise: { id: "cru_123", name: "Danube Highlights" },
58
+ cabin_categories: [{ id: "cab_x", name: "X", view_type: "spaceship" }],
59
+ })).toMatchObject({ valid: false });
60
+ });
61
+ it("rejects partial browse price hints", () => {
62
+ const result = validateCruiseContent({
63
+ cruise: {
64
+ id: "cru_123",
65
+ name: "Danube Highlights",
66
+ },
67
+ sailings: [
68
+ {
69
+ id: "crsl_123",
70
+ start_date: "2026-07-01",
71
+ end_date: "2026-07-08",
72
+ lowest_price_cents: 120000,
73
+ },
74
+ ],
75
+ });
76
+ expect(result).toMatchObject({ valid: false });
77
+ });
78
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./cabin-features.js";
2
+ export * from "./content-shape.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,oBAAoB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./cabin-features.js";
2
+ export * from "./content-shape.js";
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@voyantjs/cruises-contracts",
3
+ "version": "0.90.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./cabin-features": "./src/cabin-features.ts",
9
+ "./content-shape": "./src/content-shape.ts"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "biome check src/",
14
+ "test": "vitest run --passWithNoTests",
15
+ "build": "tsc -p tsconfig.json",
16
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
17
+ "prepack": "pnpm run build"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./cabin-features": {
31
+ "types": "./dist/cabin-features.d.ts",
32
+ "import": "./dist/cabin-features.js",
33
+ "default": "./dist/cabin-features.js"
34
+ },
35
+ "./content-shape": {
36
+ "types": "./dist/content-shape.d.ts",
37
+ "import": "./dist/content-shape.js",
38
+ "default": "./dist/content-shape.js"
39
+ }
40
+ },
41
+ "main": "./dist/index.js",
42
+ "types": "./dist/index.d.ts"
43
+ },
44
+ "dependencies": {
45
+ "zod": "^4.3.6"
46
+ },
47
+ "devDependencies": {
48
+ "@voyantjs/voyant-typescript-config": "workspace:*",
49
+ "typescript": "^6.0.2",
50
+ "vitest": "^4.1.2"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/voyantjs/voyant.git",
55
+ "directory": "packages/cruises-contracts"
56
+ }
57
+ }