@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.
Files changed (210) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +50 -0
  3. package/dist/adapters/connect-compat.d.ts +20 -0
  4. package/dist/adapters/connect-compat.d.ts.map +1 -0
  5. package/dist/adapters/connect-compat.js +71 -0
  6. package/dist/adapters/contract-fixture.d.ts +32 -0
  7. package/dist/adapters/contract-fixture.d.ts.map +1 -0
  8. package/dist/adapters/contract-fixture.js +152 -0
  9. package/dist/adapters/index.d.ts +331 -0
  10. package/dist/adapters/index.d.ts.map +1 -0
  11. package/dist/adapters/index.js +16 -0
  12. package/dist/adapters/memoize.d.ts +28 -0
  13. package/dist/adapters/memoize.d.ts.map +1 -0
  14. package/dist/adapters/memoize.js +131 -0
  15. package/dist/adapters/mock.d.ts +44 -0
  16. package/dist/adapters/mock.d.ts.map +1 -0
  17. package/dist/adapters/mock.js +192 -0
  18. package/dist/adapters/registry.d.ts +26 -0
  19. package/dist/adapters/registry.d.ts.map +1 -0
  20. package/dist/adapters/registry.js +42 -0
  21. package/dist/adapters/source-adapter-shim.d.ts +80 -0
  22. package/dist/adapters/source-adapter-shim.d.ts.map +1 -0
  23. package/dist/adapters/source-adapter-shim.js +390 -0
  24. package/dist/booking-engine/handler.d.ts +108 -0
  25. package/dist/booking-engine/handler.d.ts.map +1 -0
  26. package/dist/booking-engine/handler.js +225 -0
  27. package/dist/booking-engine/index.d.ts +9 -0
  28. package/dist/booking-engine/index.d.ts.map +1 -0
  29. package/dist/booking-engine/index.js +8 -0
  30. package/dist/booking-extension.d.ts +1179 -0
  31. package/dist/booking-extension.d.ts.map +1 -0
  32. package/dist/booking-extension.js +342 -0
  33. package/dist/cabin-features.d.ts +8 -0
  34. package/dist/cabin-features.d.ts.map +1 -0
  35. package/dist/cabin-features.js +7 -0
  36. package/dist/catalog-policy-cabins.d.ts +18 -0
  37. package/dist/catalog-policy-cabins.d.ts.map +1 -0
  38. package/dist/catalog-policy-cabins.js +96 -0
  39. package/dist/catalog-policy-core.d.ts +3 -0
  40. package/dist/catalog-policy-core.d.ts.map +1 -0
  41. package/dist/catalog-policy-core.js +247 -0
  42. package/dist/catalog-policy-structure.d.ts +3 -0
  43. package/dist/catalog-policy-structure.d.ts.map +1 -0
  44. package/dist/catalog-policy-structure.js +387 -0
  45. package/dist/catalog-policy.d.ts +15 -0
  46. package/dist/catalog-policy.d.ts.map +1 -0
  47. package/dist/catalog-policy.js +19 -0
  48. package/dist/content-shape.d.ts +5 -0
  49. package/dist/content-shape.d.ts.map +1 -0
  50. package/dist/content-shape.js +13 -0
  51. package/dist/draft-shape.d.ts +59 -0
  52. package/dist/draft-shape.d.ts.map +1 -0
  53. package/dist/draft-shape.js +98 -0
  54. package/dist/events.d.ts +21 -0
  55. package/dist/events.d.ts.map +1 -0
  56. package/dist/events.js +21 -0
  57. package/dist/index.d.ts +43 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +66 -0
  60. package/dist/lib/key.d.ts +41 -0
  61. package/dist/lib/key.d.ts.map +1 -0
  62. package/dist/lib/key.js +100 -0
  63. package/dist/routes-booking-payloads.d.ts +133 -0
  64. package/dist/routes-booking-payloads.d.ts.map +1 -0
  65. package/dist/routes-booking-payloads.js +142 -0
  66. package/dist/routes-content.d.ts +53 -0
  67. package/dist/routes-content.d.ts.map +1 -0
  68. package/dist/routes-content.js +158 -0
  69. package/dist/routes-core.d.ts +4 -0
  70. package/dist/routes-core.d.ts.map +1 -0
  71. package/dist/routes-core.js +68 -0
  72. package/dist/routes-detail.d.ts +4 -0
  73. package/dist/routes-detail.d.ts.map +1 -0
  74. package/dist/routes-detail.js +261 -0
  75. package/dist/routes-env.d.ts +13 -0
  76. package/dist/routes-env.d.ts.map +1 -0
  77. package/dist/routes-env.js +1 -0
  78. package/dist/routes-keying.d.ts +28 -0
  79. package/dist/routes-keying.d.ts.map +1 -0
  80. package/dist/routes-keying.js +70 -0
  81. package/dist/routes-public.d.ts +911 -0
  82. package/dist/routes-public.d.ts.map +1 -0
  83. package/dist/routes-public.js +252 -0
  84. package/dist/routes-sailings-prices.d.ts +4 -0
  85. package/dist/routes-sailings-prices.d.ts.map +1 -0
  86. package/dist/routes-sailings-prices.js +278 -0
  87. package/dist/routes-search-index.d.ts +4 -0
  88. package/dist/routes-search-index.d.ts.map +1 -0
  89. package/dist/routes-search-index.js +25 -0
  90. package/dist/routes-ships.d.ts +4 -0
  91. package/dist/routes-ships.d.ts.map +1 -0
  92. package/dist/routes-ships.js +147 -0
  93. package/dist/routes-voyage-groups.d.ts +4 -0
  94. package/dist/routes-voyage-groups.d.ts.map +1 -0
  95. package/dist/routes-voyage-groups.js +85 -0
  96. package/dist/routes.d.ts +5 -0
  97. package/dist/routes.d.ts.map +1 -0
  98. package/dist/routes.js +14 -0
  99. package/dist/schema-cabins.d.ts +1098 -0
  100. package/dist/schema-cabins.d.ts.map +1 -0
  101. package/dist/schema-cabins.js +105 -0
  102. package/dist/schema-content.d.ts +577 -0
  103. package/dist/schema-content.d.ts.map +1 -0
  104. package/dist/schema-content.js +63 -0
  105. package/dist/schema-core.d.ts +1790 -0
  106. package/dist/schema-core.d.ts.map +1 -0
  107. package/dist/schema-core.js +171 -0
  108. package/dist/schema-itinerary.d.ts +556 -0
  109. package/dist/schema-itinerary.d.ts.map +1 -0
  110. package/dist/schema-itinerary.js +50 -0
  111. package/dist/schema-pricing.d.ts +633 -0
  112. package/dist/schema-pricing.d.ts.map +1 -0
  113. package/dist/schema-pricing.js +73 -0
  114. package/dist/schema-search.d.ts +611 -0
  115. package/dist/schema-search.d.ts.map +1 -0
  116. package/dist/schema-search.js +64 -0
  117. package/dist/schema-shared.d.ts +23 -0
  118. package/dist/schema-shared.d.ts.map +1 -0
  119. package/dist/schema-shared.js +107 -0
  120. package/dist/schema-sourced-content.d.ts +247 -0
  121. package/dist/schema-sourced-content.d.ts.map +1 -0
  122. package/dist/schema-sourced-content.js +38 -0
  123. package/dist/schema.d.ts +10 -0
  124. package/dist/schema.d.ts.map +1 -0
  125. package/dist/schema.js +9 -0
  126. package/dist/service-booking-helpers.d.ts +12 -0
  127. package/dist/service-booking-helpers.d.ts.map +1 -0
  128. package/dist/service-booking-helpers.js +94 -0
  129. package/dist/service-booking-types.d.ts +101 -0
  130. package/dist/service-booking-types.d.ts.map +1 -0
  131. package/dist/service-booking-types.js +1 -0
  132. package/dist/service-bookings.d.ts +46 -0
  133. package/dist/service-bookings.d.ts.map +1 -0
  134. package/dist/service-bookings.js +420 -0
  135. package/dist/service-catalog-plane-cabins.d.ts +24 -0
  136. package/dist/service-catalog-plane-cabins.d.ts.map +1 -0
  137. package/dist/service-catalog-plane-cabins.js +90 -0
  138. package/dist/service-catalog-plane.d.ts +74 -0
  139. package/dist/service-catalog-plane.d.ts.map +1 -0
  140. package/dist/service-catalog-plane.js +194 -0
  141. package/dist/service-content-synthesizer.d.ts +42 -0
  142. package/dist/service-content-synthesizer.d.ts.map +1 -0
  143. package/dist/service-content-synthesizer.js +144 -0
  144. package/dist/service-content.d.ts +74 -0
  145. package/dist/service-content.d.ts.map +1 -0
  146. package/dist/service-content.js +315 -0
  147. package/dist/service-core.d.ts +134 -0
  148. package/dist/service-core.d.ts.map +1 -0
  149. package/dist/service-core.js +257 -0
  150. package/dist/service-detach.d.ts +18 -0
  151. package/dist/service-detach.d.ts.map +1 -0
  152. package/dist/service-detach.js +199 -0
  153. package/dist/service-enrichment.d.ts +11 -0
  154. package/dist/service-enrichment.d.ts.map +1 -0
  155. package/dist/service-enrichment.js +47 -0
  156. package/dist/service-external-refresh.d.ts +39 -0
  157. package/dist/service-external-refresh.d.ts.map +1 -0
  158. package/dist/service-external-refresh.js +47 -0
  159. package/dist/service-itinerary.d.ts +22 -0
  160. package/dist/service-itinerary.d.ts.map +1 -0
  161. package/dist/service-itinerary.js +34 -0
  162. package/dist/service-prices.d.ts +46 -0
  163. package/dist/service-prices.d.ts.map +1 -0
  164. package/dist/service-prices.js +89 -0
  165. package/dist/service-pricing.d.ts +97 -0
  166. package/dist/service-pricing.d.ts.map +1 -0
  167. package/dist/service-pricing.js +198 -0
  168. package/dist/service-sailings.d.ts +48 -0
  169. package/dist/service-sailings.d.ts.map +1 -0
  170. package/dist/service-sailings.js +145 -0
  171. package/dist/service-search-types.d.ts +54 -0
  172. package/dist/service-search-types.d.ts.map +1 -0
  173. package/dist/service-search-types.js +1 -0
  174. package/dist/service-search.d.ts +65 -0
  175. package/dist/service-search.d.ts.map +1 -0
  176. package/dist/service-search.js +467 -0
  177. package/dist/service-shared.d.ts +22 -0
  178. package/dist/service-shared.d.ts.map +1 -0
  179. package/dist/service-shared.js +22 -0
  180. package/dist/service-ships.d.ts +47 -0
  181. package/dist/service-ships.d.ts.map +1 -0
  182. package/dist/service-ships.js +156 -0
  183. package/dist/service.d.ts +255 -0
  184. package/dist/service.d.ts.map +1 -0
  185. package/dist/service.js +12 -0
  186. package/dist/validation-cabins.d.ts +267 -0
  187. package/dist/validation-cabins.d.ts.map +1 -0
  188. package/dist/validation-cabins.js +77 -0
  189. package/dist/validation-content.d.ts +123 -0
  190. package/dist/validation-content.d.ts.map +1 -0
  191. package/dist/validation-content.js +40 -0
  192. package/dist/validation-core.d.ts +393 -0
  193. package/dist/validation-core.d.ts.map +1 -0
  194. package/dist/validation-core.js +162 -0
  195. package/dist/validation-itinerary.d.ts +123 -0
  196. package/dist/validation-itinerary.d.ts.map +1 -0
  197. package/dist/validation-itinerary.js +47 -0
  198. package/dist/validation-pricing.d.ts +137 -0
  199. package/dist/validation-pricing.d.ts.map +1 -0
  200. package/dist/validation-pricing.js +49 -0
  201. package/dist/validation-search.d.ts +118 -0
  202. package/dist/validation-search.d.ts.map +1 -0
  203. package/dist/validation-search.js +60 -0
  204. package/dist/validation-shared.d.ts +123 -0
  205. package/dist/validation-shared.d.ts.map +1 -0
  206. package/dist/validation-shared.js +103 -0
  207. package/dist/validation.d.ts +8 -0
  208. package/dist/validation.d.ts.map +1 -0
  209. package/dist/validation.js +7 -0
  210. 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,4 @@
1
+ import type { Hono } from "hono";
2
+ import type { CruiseRoutesEnv as Env } from "./routes-env.js";
3
+ export declare function registerCruiseCoreRoutes(app: Hono<Env>): void;
4
+ //# sourceMappingURL=routes-core.d.ts.map
@@ -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,4 @@
1
+ import type { Hono } from "hono";
2
+ import type { CruiseRoutesEnv as Env } from "./routes-env.js";
3
+ export declare function registerCruiseDetailRoutes(app: Hono<Env>): void;
4
+ //# sourceMappingURL=routes-detail.d.ts.map
@@ -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"}