@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,142 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { encodeSourceRef } from "./lib/key.js";
|
|
3
|
+
export const createBookingPayloadSchema = z.object({
|
|
4
|
+
sailingId: z.string(),
|
|
5
|
+
cabinCategoryId: z.string(),
|
|
6
|
+
cabinCategoryRef: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
7
|
+
cabinId: z.string().optional().nullable(),
|
|
8
|
+
occupancy: z.number().int().min(1).max(8),
|
|
9
|
+
passengerComposition: passengerCompositionSchema().optional().nullable(),
|
|
10
|
+
fareCode: z.string().optional().nullable(),
|
|
11
|
+
fareVariant: z.enum(["cruise_only", "air_inclusive"]).optional().nullable(),
|
|
12
|
+
mode: z.enum(["inquiry", "reserve"]).optional(),
|
|
13
|
+
personId: z.string().optional().nullable(),
|
|
14
|
+
organizationId: z.string().optional().nullable(),
|
|
15
|
+
contact: z.object({
|
|
16
|
+
firstName: z.string().min(1),
|
|
17
|
+
lastName: z.string().min(1),
|
|
18
|
+
email: z.string().email().optional().nullable(),
|
|
19
|
+
phone: z.string().optional().nullable(),
|
|
20
|
+
language: z.string().optional().nullable(),
|
|
21
|
+
country: z.string().optional().nullable(),
|
|
22
|
+
region: z.string().optional().nullable(),
|
|
23
|
+
city: z.string().optional().nullable(),
|
|
24
|
+
address: z.string().optional().nullable(),
|
|
25
|
+
postalCode: z.string().optional().nullable(),
|
|
26
|
+
}),
|
|
27
|
+
passengers: z
|
|
28
|
+
.array(z.object({
|
|
29
|
+
firstName: z.string().min(1),
|
|
30
|
+
lastName: z.string().min(1),
|
|
31
|
+
email: z.string().email().optional().nullable(),
|
|
32
|
+
phone: z.string().optional().nullable(),
|
|
33
|
+
travelerCategory: z
|
|
34
|
+
.enum(["adult", "child", "infant", "senior", "other"])
|
|
35
|
+
.optional()
|
|
36
|
+
.nullable(),
|
|
37
|
+
preferredLanguage: z.string().optional().nullable(),
|
|
38
|
+
specialRequests: z.string().optional().nullable(),
|
|
39
|
+
personId: z.string().optional().nullable(),
|
|
40
|
+
isPrimary: z.boolean().optional(),
|
|
41
|
+
notes: z.string().optional().nullable(),
|
|
42
|
+
}))
|
|
43
|
+
.min(1),
|
|
44
|
+
notes: z.string().optional().nullable(),
|
|
45
|
+
});
|
|
46
|
+
export const createPartyBookingPayloadSchema = z.object({
|
|
47
|
+
sailingId: z.string(),
|
|
48
|
+
cabins: z
|
|
49
|
+
.array(z.object({
|
|
50
|
+
cabinCategoryId: z.string(),
|
|
51
|
+
cabinId: z.string().optional().nullable(),
|
|
52
|
+
occupancy: z.number().int().min(1).max(8),
|
|
53
|
+
fareCode: z.string().optional().nullable(),
|
|
54
|
+
fareVariant: z.enum(["cruise_only", "air_inclusive"]).optional().nullable(),
|
|
55
|
+
passengers: z
|
|
56
|
+
.array(z.object({
|
|
57
|
+
firstName: z.string().min(1),
|
|
58
|
+
lastName: z.string().min(1),
|
|
59
|
+
email: z.string().email().optional().nullable(),
|
|
60
|
+
phone: z.string().optional().nullable(),
|
|
61
|
+
travelerCategory: z
|
|
62
|
+
.enum(["adult", "child", "infant", "senior", "other"])
|
|
63
|
+
.optional()
|
|
64
|
+
.nullable(),
|
|
65
|
+
preferredLanguage: z.string().optional().nullable(),
|
|
66
|
+
specialRequests: z.string().optional().nullable(),
|
|
67
|
+
personId: z.string().optional().nullable(),
|
|
68
|
+
isPrimary: z.boolean().optional(),
|
|
69
|
+
notes: z.string().optional().nullable(),
|
|
70
|
+
}))
|
|
71
|
+
.min(1),
|
|
72
|
+
notes: z.string().optional().nullable(),
|
|
73
|
+
}))
|
|
74
|
+
.min(2)
|
|
75
|
+
.max(20),
|
|
76
|
+
leadPersonId: z.string().optional().nullable(),
|
|
77
|
+
organizationId: z.string().optional().nullable(),
|
|
78
|
+
contact: z.object({
|
|
79
|
+
firstName: z.string().min(1),
|
|
80
|
+
lastName: z.string().min(1),
|
|
81
|
+
email: z.string().email().optional().nullable(),
|
|
82
|
+
phone: z.string().optional().nullable(),
|
|
83
|
+
language: z.string().optional().nullable(),
|
|
84
|
+
country: z.string().optional().nullable(),
|
|
85
|
+
region: z.string().optional().nullable(),
|
|
86
|
+
city: z.string().optional().nullable(),
|
|
87
|
+
address: z.string().optional().nullable(),
|
|
88
|
+
postalCode: z.string().optional().nullable(),
|
|
89
|
+
}),
|
|
90
|
+
mode: z.enum(["inquiry", "reserve"]).optional(),
|
|
91
|
+
label: z.string().optional(),
|
|
92
|
+
notes: z.string().optional().nullable(),
|
|
93
|
+
});
|
|
94
|
+
export const quotePayloadSchema = z.object({
|
|
95
|
+
cabinCategoryId: z.string(),
|
|
96
|
+
cabinCategoryRef: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
97
|
+
occupancy: z.number().int().min(1).max(8),
|
|
98
|
+
guestCount: z.number().int().min(1).max(8).optional(),
|
|
99
|
+
passengerComposition: passengerCompositionSchema().optional().nullable(),
|
|
100
|
+
fareCode: z.string().optional().nullable(),
|
|
101
|
+
fareVariant: z.enum(["cruise_only", "air_inclusive"]).optional().nullable(),
|
|
102
|
+
});
|
|
103
|
+
function passengerCompositionSchema() {
|
|
104
|
+
return z
|
|
105
|
+
.object({
|
|
106
|
+
adults: z.number().int().min(0),
|
|
107
|
+
children: z.number().int().min(0).optional(),
|
|
108
|
+
childAges: z.array(z.number().int().min(0).max(17)).optional(),
|
|
109
|
+
infants: z.number().int().min(0).optional(),
|
|
110
|
+
seniors: z.number().int().min(0).optional(),
|
|
111
|
+
})
|
|
112
|
+
.catchall(z.unknown())
|
|
113
|
+
.refine((value) => value.adults + (value.children ?? 0) + (value.infants ?? 0) + (value.seniors ?? 0) > 0, "passengerComposition must include at least one passenger");
|
|
114
|
+
}
|
|
115
|
+
export function passengerCountFromComposition(composition) {
|
|
116
|
+
if (!composition)
|
|
117
|
+
return null;
|
|
118
|
+
return (composition.adults +
|
|
119
|
+
(composition.children ?? 0) +
|
|
120
|
+
(composition.infants ?? 0) +
|
|
121
|
+
(composition.seniors ?? 0));
|
|
122
|
+
}
|
|
123
|
+
export function sourceRefFromPayload(maybeRef, externalId) {
|
|
124
|
+
if (maybeRef && typeof maybeRef.externalId === "string")
|
|
125
|
+
return maybeRef;
|
|
126
|
+
return { externalId };
|
|
127
|
+
}
|
|
128
|
+
export function sourceRefMatches(candidate, requested) {
|
|
129
|
+
if (encodeSourceRef(candidate) === encodeSourceRef(requested))
|
|
130
|
+
return true;
|
|
131
|
+
const candidateIsLegacy = Object.keys(candidate).length === 1;
|
|
132
|
+
const requestedIsLegacy = Object.keys(requested).length === 1;
|
|
133
|
+
return (candidateIsLegacy || requestedIsLegacy) && candidate.externalId === requested.externalId;
|
|
134
|
+
}
|
|
135
|
+
export function passengerCompositionMatches(candidate, requested) {
|
|
136
|
+
if (!requested || !candidate)
|
|
137
|
+
return true;
|
|
138
|
+
return (encodeSourceRef({
|
|
139
|
+
externalId: "composition",
|
|
140
|
+
...candidate,
|
|
141
|
+
}) === encodeSourceRef({ externalId: "composition", ...requested }));
|
|
142
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise content routes — sourced-aware detail endpoint.
|
|
3
|
+
*
|
|
4
|
+
* GET /:key/content
|
|
5
|
+
*
|
|
6
|
+
* Returns the full `CruiseContent` payload for a sourced cruise:
|
|
7
|
+
* cache hit → cached row + overlay merge; cache miss with rich adapter
|
|
8
|
+
* → adapter fetch + write-through; cache miss with thin adapter →
|
|
9
|
+
* synthesizer fallback (sourced-content §3.3, §3.4, §3.6).
|
|
10
|
+
*
|
|
11
|
+
* The cruise vertical's unified key parser (`<provider>:<ref>` for
|
|
12
|
+
* external, plain TypeID for owned) is reused; this route accepts
|
|
13
|
+
* either form. Owned cruises return 404 — they have no sourced-entry
|
|
14
|
+
* row. Owned-cruise detail uses the existing `/:key` route.
|
|
15
|
+
*
|
|
16
|
+
* The catalog SourceAdapterRegistry is starter-owned and resolved via
|
|
17
|
+
* the `resolveRegistry` callback. For cruise adapters wrapped via
|
|
18
|
+
* `cruiseAdapterToSourceAdapter`, registry registration alongside the
|
|
19
|
+
* cruise vertical's per-vertical registry enables both detail surfaces
|
|
20
|
+
* (`/:key` for ad-hoc fetchCruise + `/:key/content` for cached
|
|
21
|
+
* CruiseContent) without code duplication.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.3.
|
|
24
|
+
*/
|
|
25
|
+
import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
|
|
26
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
27
|
+
import type { Context } from "hono";
|
|
28
|
+
import { Hono } from "hono";
|
|
29
|
+
export interface CruiseContentRoutesEnv {
|
|
30
|
+
Variables: {
|
|
31
|
+
db: AnyDrizzleDb;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface CreateCruiseContentRoutesOptions {
|
|
35
|
+
resolveRegistry: (c: Context) => SourceAdapterRegistry;
|
|
36
|
+
onOverlayError?: (event: {
|
|
37
|
+
field_path: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}) => void;
|
|
40
|
+
defaultAcceptMachineTranslated?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When the unified key resolves to a cruise typeid (`crus_*` —
|
|
43
|
+
* owned cruise), the route returns 404 by default. Set this to
|
|
44
|
+
* `true` to also dispatch through `getCruiseContent` for owned ids
|
|
45
|
+
* (useful when an owned cruise also has a sourced-entry row, e.g.
|
|
46
|
+
* after a detach + re-import workflow).
|
|
47
|
+
*/
|
|
48
|
+
allowOwnedKeys?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function createCruiseContentRoutes(options: CreateCruiseContentRoutesOptions): Hono<CruiseContentRoutesEnv>;
|
|
51
|
+
export declare function parseAcceptLanguage(header: string): string[];
|
|
52
|
+
export type CruiseContentRoutes = ReturnType<typeof createCruiseContentRoutes>;
|
|
53
|
+
//# 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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAc3B,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE;QACT,EAAE,EAAE,YAAY,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,gCAAgC;IAC/C,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;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,IAAI,CAAC,sBAAsB,CAAC,CA4H9B;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAqB5D;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAA"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cruise content routes — sourced-aware detail endpoint.
|
|
3
|
+
*
|
|
4
|
+
* GET /:key/content
|
|
5
|
+
*
|
|
6
|
+
* Returns the full `CruiseContent` payload for a sourced cruise:
|
|
7
|
+
* cache hit → cached row + overlay merge; cache miss with rich adapter
|
|
8
|
+
* → adapter fetch + write-through; cache miss with thin adapter →
|
|
9
|
+
* synthesizer fallback (sourced-content §3.3, §3.4, §3.6).
|
|
10
|
+
*
|
|
11
|
+
* The cruise vertical's unified key parser (`<provider>:<ref>` for
|
|
12
|
+
* external, plain TypeID for owned) is reused; this route accepts
|
|
13
|
+
* either form. Owned cruises return 404 — they have no sourced-entry
|
|
14
|
+
* row. Owned-cruise detail uses the existing `/:key` route.
|
|
15
|
+
*
|
|
16
|
+
* The catalog SourceAdapterRegistry is starter-owned and resolved via
|
|
17
|
+
* the `resolveRegistry` callback. For cruise adapters wrapped via
|
|
18
|
+
* `cruiseAdapterToSourceAdapter`, registry registration alongside the
|
|
19
|
+
* cruise vertical's per-vertical registry enables both detail surfaces
|
|
20
|
+
* (`/:key` for ad-hoc fetchCruise + `/:key/content` for cached
|
|
21
|
+
* CruiseContent) without code duplication.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.3.
|
|
24
|
+
*/
|
|
25
|
+
import { Hono } from "hono";
|
|
26
|
+
import { encodeSourceRef, isEncodedSourceEntityId, parseUnifiedKey, sourceRefFromExternalKeyRef, } from "./lib/key.js";
|
|
27
|
+
import { getCruiseContent, getCruiseSailingPricing, } from "./service-content.js";
|
|
28
|
+
export function createCruiseContentRoutes(options) {
|
|
29
|
+
return new Hono()
|
|
30
|
+
.get("/:key/content", async (c) => {
|
|
31
|
+
const rawKey = c.req.param("key");
|
|
32
|
+
const parsed = parseUnifiedKey(rawKey);
|
|
33
|
+
if (parsed.kind === "invalid") {
|
|
34
|
+
return c.json({
|
|
35
|
+
error: "invalid_key",
|
|
36
|
+
detail: `Unrecognized cruise key: ${parsed.raw}`,
|
|
37
|
+
}, 400);
|
|
38
|
+
}
|
|
39
|
+
// A catalog sourced entity id (`crus_sr_<base64>`) is inherently sourced,
|
|
40
|
+
// so it dispatches regardless of `allowOwnedKeys`. Only plain owned TypeIDs
|
|
41
|
+
// (`crus_<base32>`) need the opt-in.
|
|
42
|
+
const isSourcedEntityId = parsed.kind === "local" && isEncodedSourceEntityId(parsed.id);
|
|
43
|
+
if (parsed.kind === "local" && !isSourcedEntityId && !options.allowOwnedKeys) {
|
|
44
|
+
return c.json({
|
|
45
|
+
error: "owned_not_supported",
|
|
46
|
+
detail: "GET /:key/content serves sourced cruises only. Owned cruises use GET /:key. Set allowOwnedKeys: true on the route factory to opt into owned dispatch.",
|
|
47
|
+
}, 404);
|
|
48
|
+
}
|
|
49
|
+
// For external keys we resolve through the catalog sourced-entry
|
|
50
|
+
// store via service-content. The cruise adapter must be registered
|
|
51
|
+
// against the catalog SourceAdapterRegistry (typically via
|
|
52
|
+
// cruiseAdapterToSourceAdapter) for this to find a row.
|
|
53
|
+
const entityId = parsed.kind === "local" ? parsed.id : entityIdFromExternal(parsed);
|
|
54
|
+
const scope = parseScope(c);
|
|
55
|
+
const registry = options.resolveRegistry(c);
|
|
56
|
+
const result = await getCruiseContent(c.var.db, entityId, scope, {
|
|
57
|
+
registry,
|
|
58
|
+
onOverlayError: options.onOverlayError,
|
|
59
|
+
});
|
|
60
|
+
if (!result) {
|
|
61
|
+
return c.json({
|
|
62
|
+
error: "not_found",
|
|
63
|
+
detail: `Cruise ${rawKey} (entity ${entityId}) has no sourced-content row. Either no adapter is registered for this provider, or discovery hasn't run yet.`,
|
|
64
|
+
}, 404);
|
|
65
|
+
}
|
|
66
|
+
return c.json({
|
|
67
|
+
data: {
|
|
68
|
+
content: result.content,
|
|
69
|
+
served_locale: result.resolution.served_locale,
|
|
70
|
+
match_kind: result.resolution.match_kind,
|
|
71
|
+
source: result.source,
|
|
72
|
+
served_stale: result.served_stale,
|
|
73
|
+
synthesized: result.synthesized,
|
|
74
|
+
machine_translated: result.machine_translated,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
})
|
|
78
|
+
.get("/:key/sailings/:sailingExternalId/pricing", async (c) => {
|
|
79
|
+
// Live per-sailing cabin pricing for the detail sheet's Departures tab.
|
|
80
|
+
// Fetched fresh from the adapter (price is volatile-live), not cached.
|
|
81
|
+
const rawKey = c.req.param("key");
|
|
82
|
+
const parsed = parseUnifiedKey(rawKey);
|
|
83
|
+
if (parsed.kind === "invalid") {
|
|
84
|
+
return c.json({ error: "invalid_key", detail: `Unrecognized cruise key: ${parsed.raw}` }, 400);
|
|
85
|
+
}
|
|
86
|
+
const isSourcedEntityId = parsed.kind === "local" && isEncodedSourceEntityId(parsed.id);
|
|
87
|
+
if (parsed.kind === "local" && !isSourcedEntityId && !options.allowOwnedKeys) {
|
|
88
|
+
return c.json({ error: "owned_not_supported", detail: "Sourced cruises only." }, 404);
|
|
89
|
+
}
|
|
90
|
+
const entityId = parsed.kind === "local" ? parsed.id : entityIdFromExternal(parsed);
|
|
91
|
+
const sailingExternalId = c.req.param("sailingExternalId");
|
|
92
|
+
const pricing = await getCruiseSailingPricing(c.var.db, entityId, sailingExternalId, {
|
|
93
|
+
registry: options.resolveRegistry(c),
|
|
94
|
+
});
|
|
95
|
+
if (pricing == null) {
|
|
96
|
+
return c.json({
|
|
97
|
+
error: "not_found",
|
|
98
|
+
detail: `No pricing for sailing ${sailingExternalId} on cruise ${rawKey} (no sourced row or adapter can't price sailings).`,
|
|
99
|
+
}, 404);
|
|
100
|
+
}
|
|
101
|
+
return c.json({ data: { pricing } });
|
|
102
|
+
});
|
|
103
|
+
function parseScope(c) {
|
|
104
|
+
const localeParams = c.req.queries("locale") ?? c.req.queries("locales") ?? [];
|
|
105
|
+
const headerLocale = c.req.header("accept-language");
|
|
106
|
+
const acceptLanguageList = headerLocale ? parseAcceptLanguage(headerLocale) : [];
|
|
107
|
+
const preferredLocales = localeParams.length > 0
|
|
108
|
+
? localeParams
|
|
109
|
+
: acceptLanguageList.length > 0
|
|
110
|
+
? acceptLanguageList
|
|
111
|
+
: ["en-GB"];
|
|
112
|
+
const market = c.req.query("market") ?? undefined;
|
|
113
|
+
const currency = c.req.query("currency") ?? undefined;
|
|
114
|
+
const acceptMTQuery = c.req.query("accept_mt");
|
|
115
|
+
const acceptMachineTranslated = acceptMTQuery != null
|
|
116
|
+
? acceptMTQuery !== "false" && acceptMTQuery !== "0"
|
|
117
|
+
: (options.defaultAcceptMachineTranslated ?? true);
|
|
118
|
+
return {
|
|
119
|
+
preferredLocales,
|
|
120
|
+
market,
|
|
121
|
+
currency,
|
|
122
|
+
acceptMachineTranslated,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Translate a parsed external key (`<provider>:<ref>`) into the catalog-side
|
|
128
|
+
* entity_id. Mirrors the default `buildEntityId` from
|
|
129
|
+
* `cruiseAdapterToSourceAdapter`: `crus_<encoded SourceRef>`.
|
|
130
|
+
*/
|
|
131
|
+
function entityIdFromExternal(parsed) {
|
|
132
|
+
return `crus_${encodeSourceRef(sourceRefFromExternalKeyRef(parsed.ref))}`;
|
|
133
|
+
}
|
|
134
|
+
export function parseAcceptLanguage(header) {
|
|
135
|
+
const parts = header.split(",");
|
|
136
|
+
const ranked = [];
|
|
137
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
138
|
+
const part = parts[i].trim();
|
|
139
|
+
if (!part)
|
|
140
|
+
continue;
|
|
141
|
+
const [tagRaw, ...params] = part.split(";");
|
|
142
|
+
const tag = tagRaw.trim();
|
|
143
|
+
if (!tag || tag === "*")
|
|
144
|
+
continue;
|
|
145
|
+
let q = 1;
|
|
146
|
+
for (const p of params) {
|
|
147
|
+
const [k, v] = p.split("=").map((s) => s.trim());
|
|
148
|
+
if (k === "q" && v) {
|
|
149
|
+
const parsed = Number.parseFloat(v);
|
|
150
|
+
if (Number.isFinite(parsed))
|
|
151
|
+
q = parsed;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
ranked.push({ tag, q, idx: i });
|
|
155
|
+
}
|
|
156
|
+
ranked.sort((a, b) => b.q - a.q || a.idx - b.idx);
|
|
157
|
+
return ranked.map((r) => r.tag);
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-core.d.ts","sourceRoot":"","sources":["../src/routes-core.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAIhC,OAAO,KAAK,EAAE,eAAe,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAK7D,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAwEtD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseJsonBody, parseQuery } from "@voyant-travel/hono";
|
|
2
|
+
import { listCruiseAdapters } from "./adapters/registry.js";
|
|
3
|
+
import { makeExternalKey } from "./routes-keying.js";
|
|
4
|
+
import { cruisesService } from "./service.js";
|
|
5
|
+
import { cruiseListQuerySchema, insertCruiseSchema } from "./validation-core.js";
|
|
6
|
+
export function registerCruiseCoreRoutes(app) {
|
|
7
|
+
app
|
|
8
|
+
// --- list / unified detail ---
|
|
9
|
+
.get("/", async (c) => {
|
|
10
|
+
const query = parseQuery(c, cruiseListQuerySchema);
|
|
11
|
+
const local = await cruisesService.listCruises(c.get("db"), query);
|
|
12
|
+
const localItems = local.data.map((c) => ({
|
|
13
|
+
source: "local",
|
|
14
|
+
sourceProvider: null,
|
|
15
|
+
sourceRef: null,
|
|
16
|
+
key: c.id,
|
|
17
|
+
cruise: c,
|
|
18
|
+
}));
|
|
19
|
+
// Fan out to every registered adapter in parallel via Promise.allSettled —
|
|
20
|
+
// one slow or failing adapter doesn't block the rest. Each adapter's call
|
|
21
|
+
// is independent so there's no concurrency-control concern at this layer
|
|
22
|
+
// (adapters that need rate limiting handle it inside their own implementation).
|
|
23
|
+
const adapters = listCruiseAdapters();
|
|
24
|
+
const settled = await Promise.allSettled(adapters.map((adapter) => adapter
|
|
25
|
+
.listEntries({ limit: query.limit })
|
|
26
|
+
.then((result) => ({ adapter, result }))));
|
|
27
|
+
const adapterItems = [];
|
|
28
|
+
const adapterErrors = [];
|
|
29
|
+
for (let i = 0; i < settled.length; i++) {
|
|
30
|
+
const outcome = settled[i];
|
|
31
|
+
const adapter = adapters[i];
|
|
32
|
+
if (!outcome || !adapter)
|
|
33
|
+
continue;
|
|
34
|
+
if (outcome.status === "rejected") {
|
|
35
|
+
adapterErrors.push({
|
|
36
|
+
adapter: adapter.name,
|
|
37
|
+
error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of outcome.value.result.entries) {
|
|
42
|
+
adapterItems.push({
|
|
43
|
+
source: "external",
|
|
44
|
+
sourceProvider: adapter.name,
|
|
45
|
+
sourceRef: entry.sourceRef,
|
|
46
|
+
key: makeExternalKey(adapter, entry.sourceRef),
|
|
47
|
+
cruise: entry,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return c.json({
|
|
52
|
+
data: [...localItems, ...adapterItems],
|
|
53
|
+
total: local.total + adapterItems.length,
|
|
54
|
+
localTotal: local.total,
|
|
55
|
+
adapterCount: adapters.length,
|
|
56
|
+
adapterErrors,
|
|
57
|
+
limit: local.limit,
|
|
58
|
+
offset: local.offset,
|
|
59
|
+
});
|
|
60
|
+
})
|
|
61
|
+
.post("/", async (c) => {
|
|
62
|
+
const data = await parseJsonBody(c, insertCruiseSchema);
|
|
63
|
+
const row = await cruisesService.createCruise(c.get("db"), data, {
|
|
64
|
+
eventBus: c.get("eventBus"),
|
|
65
|
+
});
|
|
66
|
+
return c.json({ data: row }, 201);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-detail.d.ts","sourceRoot":"","sources":["../src/routes-detail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,eAAe,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAoB7D,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QA2PxD"}
|