@voyantjs/accommodations 0.55.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.
Files changed (47) hide show
  1. package/README.md +11 -0
  2. package/dist/booking-engine/handler.d.ts +103 -0
  3. package/dist/booking-engine/handler.d.ts.map +1 -0
  4. package/dist/booking-engine/handler.js +254 -0
  5. package/dist/booking-engine/index.d.ts +8 -0
  6. package/dist/booking-engine/index.d.ts.map +1 -0
  7. package/dist/booking-engine/index.js +7 -0
  8. package/dist/catalog-policy.d.ts +23 -0
  9. package/dist/catalog-policy.d.ts.map +1 -0
  10. package/dist/catalog-policy.js +422 -0
  11. package/dist/content-shape.d.ts +185 -0
  12. package/dist/content-shape.d.ts.map +1 -0
  13. package/dist/content-shape.js +122 -0
  14. package/dist/draft-shape.d.ts +35 -0
  15. package/dist/draft-shape.d.ts.map +1 -0
  16. package/dist/draft-shape.js +84 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +7 -0
  20. package/dist/routes-content.d.ts +31 -0
  21. package/dist/routes-content.d.ts.map +1 -0
  22. package/dist/routes-content.js +87 -0
  23. package/dist/schema-bookings.d.ts +582 -0
  24. package/dist/schema-bookings.d.ts.map +1 -0
  25. package/dist/schema-bookings.js +65 -0
  26. package/dist/schema-inventory.d.ts +1361 -0
  27. package/dist/schema-inventory.d.ts.map +1 -0
  28. package/dist/schema-inventory.js +132 -0
  29. package/dist/schema-shared.d.ts +5 -0
  30. package/dist/schema-shared.d.ts.map +1 -0
  31. package/dist/schema-shared.js +24 -0
  32. package/dist/schema-sourced-content.d.ts +254 -0
  33. package/dist/schema-sourced-content.d.ts.map +1 -0
  34. package/dist/schema-sourced-content.js +48 -0
  35. package/dist/schema.d.ts +5 -0
  36. package/dist/schema.d.ts.map +1 -0
  37. package/dist/schema.js +4 -0
  38. package/dist/service-catalog-plane.d.ts +55 -0
  39. package/dist/service-catalog-plane.d.ts.map +1 -0
  40. package/dist/service-catalog-plane.js +202 -0
  41. package/dist/service-content-synthesizer.d.ts +43 -0
  42. package/dist/service-content-synthesizer.d.ts.map +1 -0
  43. package/dist/service-content-synthesizer.js +149 -0
  44. package/dist/service-content.d.ts +54 -0
  45. package/dist/service-content.d.ts.map +1 -0
  46. package/dist/service-content.js +480 -0
  47. package/package.json +113 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Accommodation content shape — the rich detail-page content shape
3
+ * returned by `getContent` for sourced room types (bedbanks like
4
+ * Hotelbeds / Expedia, direct-property feeds, hotel groups via Voyant
5
+ * Connect).
6
+ *
7
+ * Per sourced-content §3.6, the accommodation content aggregate is
8
+ * `{ hotel, room_types[], rate_plans[], meal_plans[], amenities[],
9
+ * policies[] }` — one payload returned by a single `getContent`.
10
+ * Pricing stays out (volatile-live, flows through `liveResolve`).
11
+ *
12
+ * The aggregate is **per property** — one row per property × locale ×
13
+ * market — even though the sellable catalog entry is a room type.
14
+ * That's because bedbanks return whole-property payloads and splitting
15
+ * by room type would multiply cache writes and refresh work without
16
+ * benefit. The vertical's read service projects the cached property
17
+ * payload to the requested room-type detail page.
18
+ *
19
+ * See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
20
+ */
21
+ import { mergeOverlaysIntoContent, } from "@voyantjs/catalog";
22
+ import { z } from "zod";
23
+ export const ACCOMMODATION_CONTENT_SCHEMA_VERSION = "accommodations/v1";
24
+ export const hotelSummarySchema = z.object({
25
+ id: z.string(),
26
+ name: z.string(),
27
+ description: z.string().nullable().optional(),
28
+ star_rating: z.number().min(0).max(5).nullable().optional(),
29
+ hero_image_url: z.string().nullable().optional(),
30
+ highlights: z.array(z.string()).optional(),
31
+ brand: z.string().nullable().optional(),
32
+ // Geo / location
33
+ country: z.string().nullable().optional(),
34
+ city: z.string().nullable().optional(),
35
+ address: z.string().nullable().optional(),
36
+ postal_code: z.string().nullable().optional(),
37
+ latitude: z.number().nullable().optional(),
38
+ longitude: z.number().nullable().optional(),
39
+ // Operational
40
+ check_in_time: z.string().nullable().optional(),
41
+ check_out_time: z.string().nullable().optional(),
42
+ });
43
+ export const accommodationRoomTypeSchema = z.object({
44
+ id: z.string(),
45
+ code: z.string().nullable().optional(),
46
+ name: z.string(),
47
+ description: z.string().nullable().optional(),
48
+ room_class: z.string().nullable().optional(), // e.g. "deluxe", "suite"
49
+ /** Inside / outside / balcony / suite — bedbank conventions vary. */
50
+ view: z.string().nullable().optional(),
51
+ bedrooms: z.number().int().nonnegative().nullable().optional(),
52
+ beds: z.array(z.string()).optional().default([]),
53
+ size_sqm: z.number().int().nonnegative().nullable().optional(),
54
+ max_adults: z.number().int().nonnegative().nullable().optional(),
55
+ max_children: z.number().int().nonnegative().nullable().optional(),
56
+ max_occupancy: z.number().int().nonnegative().nullable().optional(),
57
+ amenities: z.array(z.string()).optional().default([]),
58
+ images: z.array(z.string()).optional().default([]),
59
+ });
60
+ export const accommodationRatePlanSchema = z.object({
61
+ id: z.string(),
62
+ code: z.string().nullable().optional(),
63
+ name: z.string(),
64
+ description: z.string().nullable().optional(),
65
+ /** "per_night" | "per_stay" — the plan's charge frequency. */
66
+ charge_frequency: z.enum(["per_night", "per_stay"]).optional().default("per_night"),
67
+ /** Room types this plan is bookable on; empty = all. */
68
+ applies_to_room_type_ids: z.array(z.string()).optional().default([]),
69
+ cancellation_policy: z.string().nullable().optional(),
70
+ inclusions: z.array(z.string()).optional().default([]),
71
+ });
72
+ export const accommodationMealPlanSchema = z.object({
73
+ id: z.string(),
74
+ code: z.string().nullable().optional(),
75
+ name: z.string(),
76
+ description: z.string().nullable().optional(),
77
+ /** "room_only" | "bed_breakfast" | "half_board" | "full_board" | "all_inclusive". */
78
+ basis: z.string(),
79
+ inclusions: z.array(z.string()).optional().default([]),
80
+ });
81
+ export const accommodationAmenitySchema = z.object({
82
+ id: z.string(),
83
+ /** "pool" | "spa" | "wifi" | … */
84
+ category: z.string().nullable().optional(),
85
+ name: z.string(),
86
+ description: z.string().nullable().optional(),
87
+ is_free: z.boolean().optional(),
88
+ });
89
+ export const accommodationPolicySchema = z.object({
90
+ kind: z.enum(["cancellation", "payment", "supplier_notes", "requirements", "check_in"]),
91
+ body: z.string(),
92
+ rules: z.unknown().optional(),
93
+ });
94
+ export const accommodationContentSchema = z.object({
95
+ hotel: hotelSummarySchema,
96
+ room_types: z.array(accommodationRoomTypeSchema).default([]),
97
+ rate_plans: z.array(accommodationRatePlanSchema).default([]),
98
+ meal_plans: z.array(accommodationMealPlanSchema).default([]),
99
+ amenities: z.array(accommodationAmenitySchema).default([]),
100
+ policies: z.array(accommodationPolicySchema).default([]),
101
+ });
102
+ export function validateAccommodationContent(payload) {
103
+ const result = accommodationContentSchema.safeParse(payload);
104
+ if (result.success) {
105
+ return { valid: true, content: result.data };
106
+ }
107
+ const issue = result.error.issues[0];
108
+ return {
109
+ valid: false,
110
+ reason: issue ? `${issue.path.join(".")}: ${issue.message}` : "validation failed",
111
+ };
112
+ }
113
+ export function mergeOverlaysIntoAccommodationContent(payload, overlays, options = {}) {
114
+ const merged = mergeOverlaysIntoContent(payload, overlays, {
115
+ validate(p) {
116
+ const r = validateAccommodationContent(p);
117
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
118
+ },
119
+ onOverlayError: options.onOverlayError,
120
+ });
121
+ return accommodationContentSchema.parse(merged);
122
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Project a `AccommodationContent` payload into a `BookingDraftShape`.
3
+ *
4
+ * Hotel / room-type bookings need:
5
+ * - Configure: date-range (check-in → check-out) + occupancy.
6
+ * - Accommodation: room selection — the journey's
7
+ * `accommodation.subSteps` rooms variant, populated from
8
+ * `content.room_types[]`.
9
+ * - Add-ons: meal plans + amenities surfaced as upsells (when the
10
+ * supplier prices them as add-ons; otherwise they're informational).
11
+ *
12
+ * Pricing flows through liveResolve at quote time per night /
13
+ * occupancy / rate-plan.
14
+ */
15
+ import { type BookingDraftShape, type PaxBandSpec } from "@voyantjs/catalog/booking-engine";
16
+ import type { AccommodationContent } from "./content-shape.js";
17
+ export declare const DEFAULT_ACCOMMODATION_PAX_BANDS: ReadonlyArray<PaxBandSpec>;
18
+ export interface BuildAccommodationDraftShapeOptions {
19
+ locale?: string;
20
+ paxBands?: ReadonlyArray<PaxBandSpec>;
21
+ paxBandsAllowedTotal?: {
22
+ min: number;
23
+ max: number;
24
+ };
25
+ /**
26
+ * Default minimum-nights window. Most bedbanks accept 1–30 nights;
27
+ * templates override per supplier when known.
28
+ */
29
+ minNights?: number;
30
+ maxNights?: number;
31
+ /** When true, the wizard allows multiple guests sharing one room. */
32
+ sharedRoomAllowed?: boolean;
33
+ }
34
+ export declare function buildAccommodationDraftShape(content: AccommodationContent, options?: BuildAccommodationDraftShapeOptions): BookingDraftShape;
35
+ //# sourceMappingURL=draft-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-shape.d.ts","sourceRoot":"","sources":["../src/draft-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,KAAK,iBAAiB,EAItB,KAAK,WAAW,EAIjB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAE9D,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC,WAAW,CAGtE,CAAA;AAED,MAAM,WAAW,mCAAmC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC,oBAAoB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,oBAAoB,EAC7B,OAAO,GAAE,mCAAwC,GAChD,iBAAiB,CAoEnB"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Project a `AccommodationContent` payload into a `BookingDraftShape`.
3
+ *
4
+ * Hotel / room-type bookings need:
5
+ * - Configure: date-range (check-in → check-out) + occupancy.
6
+ * - Accommodation: room selection — the journey's
7
+ * `accommodation.subSteps` rooms variant, populated from
8
+ * `content.room_types[]`.
9
+ * - Add-ons: meal plans + amenities surfaced as upsells (when the
10
+ * supplier prices them as add-ons; otherwise they're informational).
11
+ *
12
+ * Pricing flows through liveResolve at quote time per night /
13
+ * occupancy / rate-plan.
14
+ */
15
+ import { defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
16
+ export const DEFAULT_ACCOMMODATION_PAX_BANDS = [
17
+ { code: "adult", label: "Adult", minCount: 1, maxCount: 6 },
18
+ { code: "child", label: "Child", minAge: 0, maxAge: 17, minCount: 0, maxCount: 4 },
19
+ ];
20
+ export function buildAccommodationDraftShape(content, options = {}) {
21
+ const paxBands = options.paxBands ?? DEFAULT_ACCOMMODATION_PAX_BANDS;
22
+ const total = options.paxBandsAllowedTotal ?? paxBandsAllowedTotalFrom(paxBands);
23
+ const minNights = options.minNights ?? 1;
24
+ const maxNights = options.maxNights ?? 30;
25
+ const sharedRoomAllowed = options.sharedRoomAllowed ?? true;
26
+ // Project each rate plan once. The journey filters per-room based
27
+ // on `applies_to_room_type_ids` (empty = applies to all rooms).
28
+ const planByRoom = new Map();
29
+ for (const rt of content.room_types) {
30
+ planByRoom.set(rt.id, []);
31
+ }
32
+ for (const plan of content.rate_plans) {
33
+ const rooms = plan.applies_to_room_type_ids.length === 0
34
+ ? content.room_types.map((r) => r.id)
35
+ : plan.applies_to_room_type_ids;
36
+ const planOption = {
37
+ id: plan.id,
38
+ name: plan.name,
39
+ description: plan.description ?? null,
40
+ chargeFrequency: plan.charge_frequency,
41
+ cancellationPolicy: plan.cancellation_policy ?? null,
42
+ inclusions: plan.inclusions,
43
+ };
44
+ for (const roomId of rooms) {
45
+ const list = planByRoom.get(roomId);
46
+ if (list)
47
+ list.push(planOption);
48
+ }
49
+ }
50
+ const roomOptions = content.room_types.map((rt) => ({
51
+ id: rt.id,
52
+ name: rt.name,
53
+ description: rt.description ?? null,
54
+ capacity: rt.max_occupancy ?? rt.max_adults ?? null,
55
+ baseRateHint: null,
56
+ ratePlans: planByRoom.get(rt.id) ?? [],
57
+ }));
58
+ return {
59
+ ...defaultDraftShapeFlags(),
60
+ showsAccommodation: roomOptions.length > 0,
61
+ paxBands,
62
+ paxBandsAllowedTotal: total,
63
+ travelerFields: defaultTravelerFields(),
64
+ bookingFields: defaultBookingFields(),
65
+ configureSubSteps: [
66
+ { kind: "date-range", minNights, maxNights },
67
+ { kind: "occupancy", bands: paxBands },
68
+ ],
69
+ accommodation: roomOptions.length > 0
70
+ ? {
71
+ roomOptions,
72
+ sharedRoomAllowed,
73
+ subSteps: [
74
+ {
75
+ kind: "rooms",
76
+ options: roomOptions,
77
+ sharedRoomAllowed,
78
+ },
79
+ ],
80
+ }
81
+ : undefined,
82
+ paymentIntents: ["hold", "card"],
83
+ };
84
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./booking-engine/index.js";
2
+ export * from "./catalog-policy.js";
3
+ export * from "./content-shape.js";
4
+ export * from "./draft-shape.js";
5
+ export * from "./service-catalog-plane.js";
6
+ export * from "./service-content.js";
7
+ export * from "./service-content-synthesizer.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,sBAAsB,CAAA;AACpC,cAAc,kCAAkC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./booking-engine/index.js";
2
+ export * from "./catalog-policy.js";
3
+ export * from "./content-shape.js";
4
+ export * from "./draft-shape.js";
5
+ export * from "./service-catalog-plane.js";
6
+ export * from "./service-content.js";
7
+ export * from "./service-content-synthesizer.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Accommodation content routes — sourced-aware detail endpoint.
3
+ *
4
+ * GET /:id/content
5
+ *
6
+ * Returns lodging detail content for sourced accommodation inventory:
7
+ * cache hit -> cached row + overlay merge; cache miss with rich adapter ->
8
+ * adapter fetch + write-through; cache miss with thin adapter -> synthesizer
9
+ * fallback.
10
+ */
11
+ import type { SourceAdapterRegistry } from "@voyantjs/catalog/booking-engine";
12
+ import type { AnyDrizzleDb } from "@voyantjs/db";
13
+ import type { Context } from "hono";
14
+ import { Hono } from "hono";
15
+ export interface AccommodationContentRoutesEnv {
16
+ Variables: {
17
+ db: AnyDrizzleDb;
18
+ };
19
+ }
20
+ export interface CreateAccommodationContentRoutesOptions {
21
+ resolveRegistry: (c: Context) => SourceAdapterRegistry;
22
+ onOverlayError?: (event: {
23
+ field_path: string;
24
+ reason: string;
25
+ }) => void;
26
+ defaultAcceptMachineTranslated?: boolean;
27
+ }
28
+ export declare function createAccommodationContentRoutes(options: CreateAccommodationContentRoutesOptions): Hono<AccommodationContentRoutesEnv>;
29
+ export declare function parseAcceptLanguage(header: string): string[];
30
+ export type AccommodationContentRoutes = ReturnType<typeof createAccommodationContentRoutes>;
31
+ //# sourceMappingURL=routes-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-content.d.ts","sourceRoot":"","sources":["../src/routes-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE;QACT,EAAE,EAAE,YAAY,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,uCAAuC;IACtD,eAAe,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,qBAAqB,CAAA;IACtD,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACxE,8BAA8B,CAAC,EAAE,OAAO,CAAA;CACzC;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,uCAAuC,GAC/C,IAAI,CAAC,6BAA6B,CAAC,CA4DrC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB5D;AAED,MAAM,MAAM,0BAA0B,GAAG,UAAU,CAAC,OAAO,gCAAgC,CAAC,CAAA"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Accommodation content routes — sourced-aware detail endpoint.
3
+ *
4
+ * GET /:id/content
5
+ *
6
+ * Returns lodging detail content for sourced accommodation inventory:
7
+ * cache hit -> cached row + overlay merge; cache miss with rich adapter ->
8
+ * adapter fetch + write-through; cache miss with thin adapter -> synthesizer
9
+ * fallback.
10
+ */
11
+ import { Hono } from "hono";
12
+ import { getAccommodationContent } from "./service-content.js";
13
+ export function createAccommodationContentRoutes(options) {
14
+ return new Hono().get("/:id/content", async (c) => {
15
+ const entityId = c.req.param("id");
16
+ const scope = parseScope(c);
17
+ const registry = options.resolveRegistry(c);
18
+ const result = await getAccommodationContent(c.var.db, entityId, scope, {
19
+ registry,
20
+ onOverlayError: options.onOverlayError,
21
+ });
22
+ if (!result) {
23
+ return c.json({
24
+ error: "not_found",
25
+ detail: `Accommodation ${entityId} has no sourced-content row.`,
26
+ }, 404);
27
+ }
28
+ return c.json({
29
+ data: {
30
+ content: result.content,
31
+ served_locale: result.resolution.served_locale,
32
+ match_kind: result.resolution.match_kind,
33
+ source: result.source,
34
+ served_stale: result.served_stale,
35
+ synthesized: result.synthesized,
36
+ machine_translated: result.machine_translated,
37
+ },
38
+ });
39
+ });
40
+ function parseScope(c) {
41
+ const localeParams = c.req.queries("locale") ?? c.req.queries("locales") ?? [];
42
+ const headerLocale = c.req.header("accept-language");
43
+ const acceptLanguageList = headerLocale ? parseAcceptLanguage(headerLocale) : [];
44
+ const preferredLocales = localeParams.length > 0
45
+ ? localeParams
46
+ : acceptLanguageList.length > 0
47
+ ? acceptLanguageList
48
+ : ["en-GB"];
49
+ const market = c.req.query("market") ?? undefined;
50
+ const currency = c.req.query("currency") ?? undefined;
51
+ const acceptMTQuery = c.req.query("accept_mt");
52
+ const acceptMachineTranslated = acceptMTQuery != null
53
+ ? acceptMTQuery !== "false" && acceptMTQuery !== "0"
54
+ : (options.defaultAcceptMachineTranslated ?? true);
55
+ return {
56
+ preferredLocales,
57
+ market,
58
+ currency,
59
+ acceptMachineTranslated,
60
+ };
61
+ }
62
+ }
63
+ export function parseAcceptLanguage(header) {
64
+ const parts = header.split(",");
65
+ const ranked = [];
66
+ for (let i = 0; i < parts.length; i += 1) {
67
+ const part = parts[i].trim();
68
+ if (!part)
69
+ continue;
70
+ const [tagRaw, ...params] = part.split(";");
71
+ const tag = tagRaw.trim();
72
+ if (!tag || tag === "*")
73
+ continue;
74
+ let q = 1;
75
+ for (const p of params) {
76
+ const [k, v] = p.split("=").map((s) => s.trim());
77
+ if (k === "q" && v) {
78
+ const parsed = Number.parseFloat(v);
79
+ if (Number.isFinite(parsed))
80
+ q = parsed;
81
+ }
82
+ }
83
+ ranked.push({ tag, q, idx: i });
84
+ }
85
+ ranked.sort((a, b) => b.q - a.q || a.idx - b.idx);
86
+ return ranked.map((r) => r.tag);
87
+ }