@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 @@
1
+ {"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAYjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;KACvB,CAAA;CACF,CAAA;AAkFD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBA6K3B,CAAA;AAEJ,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAA"}
@@ -0,0 +1,252 @@
1
+ import { parseJsonBody, parseQuery } from "@voyant-travel/hono";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import { resolveCruiseAdapter } from "./adapters/registry.js";
5
+ import { encodeSourceRef, parseUnifiedKey, sourceRefFromExternalKeyRef } from "./lib/key.js";
6
+ import { cruisesService } from "./service.js";
7
+ import { composeQuote, pricingService } from "./service-pricing.js";
8
+ import { cruisesSearchService } from "./service-search.js";
9
+ import { searchIndexQuerySchema } from "./validation-search.js";
10
+ const TYPEID_RE = /^[a-z]+_[0-9a-zA-Z]+$/;
11
+ function isTypeId(s) {
12
+ return TYPEID_RE.test(s);
13
+ }
14
+ const quotePayloadSchema = z.object({
15
+ cabinCategoryId: z.string(),
16
+ cabinCategoryRef: z.record(z.string(), z.unknown()).optional().nullable(),
17
+ occupancy: z.number().int().min(1).max(8),
18
+ guestCount: z.number().int().min(1).max(8).optional(),
19
+ passengerComposition: passengerCompositionSchema().optional().nullable(),
20
+ fareCode: z.string().optional().nullable(),
21
+ fareVariant: z.enum(["cruise_only", "air_inclusive"]).optional().nullable(),
22
+ bookingTerms: z.record(z.string(), z.unknown()).optional().nullable(),
23
+ });
24
+ function passengerCompositionSchema() {
25
+ return z
26
+ .object({
27
+ adults: z.number().int().min(0),
28
+ children: z.number().int().min(0).optional(),
29
+ childAges: z.array(z.number().int().min(0).max(17)).optional(),
30
+ infants: z.number().int().min(0).optional(),
31
+ seniors: z.number().int().min(0).optional(),
32
+ })
33
+ .catchall(z.unknown())
34
+ .refine((value) => value.adults + (value.children ?? 0) + (value.infants ?? 0) + (value.seniors ?? 0) > 0, "passengerComposition must include at least one passenger");
35
+ }
36
+ function passengerCountFromComposition(composition) {
37
+ if (!composition)
38
+ return null;
39
+ return (composition.adults +
40
+ (composition.children ?? 0) +
41
+ (composition.infants ?? 0) +
42
+ (composition.seniors ?? 0));
43
+ }
44
+ function resolveExternalKey(key) {
45
+ const parsed = parseUnifiedKey(key);
46
+ if (parsed.kind !== "external")
47
+ return null;
48
+ return { provider: parsed.provider, sourceRef: sourceRefFromExternalKeyRef(parsed.ref) };
49
+ }
50
+ function sourceRefFromPayload(maybeRef, externalId) {
51
+ if (maybeRef && typeof maybeRef.externalId === "string")
52
+ return maybeRef;
53
+ return { externalId };
54
+ }
55
+ function sourceRefMatches(candidate, requested) {
56
+ if (encodeSourceRef(candidate) === encodeSourceRef(requested))
57
+ return true;
58
+ const candidateIsLegacy = Object.keys(candidate).length === 1;
59
+ const requestedIsLegacy = Object.keys(requested).length === 1;
60
+ return (candidateIsLegacy || requestedIsLegacy) && candidate.externalId === requested.externalId;
61
+ }
62
+ function passengerCompositionMatches(candidate, requested) {
63
+ if (!requested || !candidate)
64
+ return true;
65
+ return (encodeSourceRef({
66
+ externalId: "composition",
67
+ ...candidate,
68
+ }) === encodeSourceRef({ externalId: "composition", ...requested }));
69
+ }
70
+ /**
71
+ * Public/storefront routes. Reads exclusively from `cruise_search_index` for
72
+ * list and slug lookups; detail endpoints (sailing, ship, quote) resolve
73
+ * through the appropriate source — local DB for source='local' rows, the
74
+ * registered adapter for source='external'.
75
+ *
76
+ * Operators that don't run a Voyant-powered storefront leave the search index
77
+ * empty; the list endpoint returns no rows but detail endpoints still work
78
+ * for direct sailing/ship key lookups.
79
+ */
80
+ export const cruisePublicRoutes = new Hono()
81
+ .get("/", async (c) => {
82
+ const query = parseQuery(c, searchIndexQuerySchema);
83
+ const result = await cruisesSearchService.query(c.get("db"), query);
84
+ return c.json(result);
85
+ })
86
+ .get("/:slug", async (c) => {
87
+ const slug = c.req.param("slug");
88
+ const indexEntry = await cruisesSearchService.getBySlug(c.get("db"), slug);
89
+ if (!indexEntry)
90
+ return c.json({ error: "not_found" }, 404);
91
+ if (indexEntry.source === "local" && indexEntry.localCruiseId) {
92
+ const detail = await cruisesService.getCruiseById(c.get("db"), indexEntry.localCruiseId, {
93
+ withSailings: true,
94
+ withDays: true,
95
+ });
96
+ if (!detail)
97
+ return c.json({ error: "not_found" }, 404);
98
+ return c.json({
99
+ data: {
100
+ source: "local",
101
+ sourceProvider: null,
102
+ sourceRef: null,
103
+ summary: indexEntry,
104
+ cruise: detail,
105
+ },
106
+ });
107
+ }
108
+ if (indexEntry.source === "external" && indexEntry.sourceProvider && indexEntry.sourceRef) {
109
+ const externalId = indexEntry.sourceRef.externalId;
110
+ if (typeof externalId !== "string" || externalId.length === 0) {
111
+ return c.json({ error: "invalid_index_entry", detail: "sourceRef.externalId missing" }, 500);
112
+ }
113
+ const adapter = resolveCruiseAdapter(indexEntry.sourceProvider);
114
+ if (!adapter) {
115
+ return c.json({
116
+ error: "adapter_not_registered",
117
+ detail: `Search-index entry references provider '${indexEntry.sourceProvider}' but no adapter is registered.`,
118
+ }, 501);
119
+ }
120
+ const adapterRef = { ...indexEntry.sourceRef, externalId };
121
+ const cruise = await adapter.fetchCruise(adapterRef);
122
+ if (!cruise)
123
+ return c.json({ error: "not_found" }, 404);
124
+ const sailings = await adapter.listSailingsForCruise(adapterRef);
125
+ return c.json({
126
+ data: {
127
+ source: "external",
128
+ sourceProvider: adapter.name,
129
+ sourceRef: adapterRef,
130
+ summary: indexEntry,
131
+ cruise,
132
+ sailings,
133
+ },
134
+ });
135
+ }
136
+ return c.json({ error: "invalid_index_entry" }, 500);
137
+ })
138
+ .get("/sailings/:key", async (c) => {
139
+ const key = c.req.param("key");
140
+ if (isTypeId(key)) {
141
+ const sailing = await cruisesService.getSailingById(c.get("db"), key, {
142
+ withPricing: true,
143
+ withItinerary: true,
144
+ });
145
+ if (!sailing)
146
+ return c.json({ error: "not_found" }, 404);
147
+ return c.json({ data: { source: "local", sailing } });
148
+ }
149
+ const parsed = resolveExternalKey(key);
150
+ if (!parsed)
151
+ return c.json({ error: "invalid_key" }, 400);
152
+ const adapter = resolveCruiseAdapter(parsed.provider);
153
+ if (!adapter)
154
+ return c.json({ error: "adapter_not_registered" }, 501);
155
+ const sailing = await adapter.fetchSailing(parsed.sourceRef);
156
+ if (!sailing)
157
+ return c.json({ error: "not_found" }, 404);
158
+ const [pricing, itinerary] = await Promise.all([
159
+ adapter.fetchSailingPricing(parsed.sourceRef),
160
+ adapter.fetchSailingItinerary(parsed.sourceRef),
161
+ ]);
162
+ return c.json({
163
+ data: { source: "external", sourceProvider: adapter.name, sailing, pricing, itinerary },
164
+ });
165
+ })
166
+ .post("/sailings/:key/quote", async (c) => {
167
+ const key = c.req.param("key");
168
+ const payload = await parseJsonBody(c, quotePayloadSchema);
169
+ const guestCount = payload.guestCount ?? passengerCountFromComposition(payload.passengerComposition);
170
+ if (!guestCount) {
171
+ return c.json({
172
+ error: "guest_count_required",
173
+ detail: "Provide guestCount or passengerComposition for cruise quote requests.",
174
+ }, 400);
175
+ }
176
+ if (isTypeId(key)) {
177
+ const quote = await pricingService.assembleQuote(c.get("db"), {
178
+ sailingId: key,
179
+ cabinCategoryId: payload.cabinCategoryId,
180
+ occupancy: payload.occupancy,
181
+ guestCount,
182
+ fareCode: payload.fareCode ?? null,
183
+ fareVariant: payload.fareVariant ?? null,
184
+ });
185
+ return c.json({ data: quote });
186
+ }
187
+ const parsed = resolveExternalKey(key);
188
+ if (!parsed)
189
+ return c.json({ error: "invalid_key" }, 400);
190
+ const adapter = resolveCruiseAdapter(parsed.provider);
191
+ if (!adapter)
192
+ return c.json({ error: "adapter_not_registered" }, 501);
193
+ const prices = await adapter.fetchSailingPricing(parsed.sourceRef);
194
+ const cabinCategoryRef = sourceRefFromPayload(payload.cabinCategoryRef, payload.cabinCategoryId);
195
+ const matching = prices.find((p) => sourceRefMatches(p.cabinCategoryRef, cabinCategoryRef) &&
196
+ p.occupancy === payload.occupancy &&
197
+ passengerCompositionMatches(p.passengerComposition, payload.passengerComposition) &&
198
+ (!payload.fareCode || p.fareCode === payload.fareCode) &&
199
+ (!payload.fareVariant || p.fareVariant === payload.fareVariant));
200
+ if (!matching)
201
+ return c.json({ error: "no_matching_price" }, 404);
202
+ const quote = composeQuote({
203
+ price: {
204
+ pricePerPerson: matching.pricePerPerson,
205
+ originalPricePerPerson: matching.originalPricePerPerson ?? null,
206
+ secondGuestPricePerPerson: matching.secondGuestPricePerPerson ?? null,
207
+ singlePricePerPerson: matching.singlePricePerPerson ?? null,
208
+ singleSupplementPercent: matching.singleSupplementPercent ?? null,
209
+ currency: matching.currency,
210
+ fareCode: matching.fareCode ?? null,
211
+ fareCodeName: matching.fareCodeName ?? null,
212
+ fareVariant: matching.fareVariant ?? "cruise_only",
213
+ earlyBookingDeadline: matching.earlyBookingDeadline ?? null,
214
+ earlyBookingBonusDescription: matching.earlyBookingBonusDescription ?? null,
215
+ },
216
+ components: (matching.components ?? []).map((c) => ({
217
+ kind: c.kind,
218
+ label: c.label ?? null,
219
+ amount: c.amount,
220
+ currency: c.currency,
221
+ direction: c.direction,
222
+ perPerson: c.perPerson,
223
+ })),
224
+ occupancy: payload.occupancy,
225
+ guestCount,
226
+ bookingTerms: payload.bookingTerms ?? matching.bookingTerms ?? null,
227
+ });
228
+ return c.json({ data: quote });
229
+ })
230
+ .get("/ships/:key", async (c) => {
231
+ const key = c.req.param("key");
232
+ if (isTypeId(key)) {
233
+ const ship = await cruisesService.getShipById(c.get("db"), key);
234
+ if (!ship)
235
+ return c.json({ error: "not_found" }, 404);
236
+ const [decks, categories] = await Promise.all([
237
+ cruisesService.listShipDecks(c.get("db"), key),
238
+ cruisesService.listShipCabinCategories(c.get("db"), key),
239
+ ]);
240
+ return c.json({ data: { ...ship, decks, categories } });
241
+ }
242
+ const parsed = resolveExternalKey(key);
243
+ if (!parsed)
244
+ return c.json({ error: "invalid_key" }, 400);
245
+ const adapter = resolveCruiseAdapter(parsed.provider);
246
+ if (!adapter)
247
+ return c.json({ error: "adapter_not_registered" }, 501);
248
+ const ship = await adapter.fetchShip(parsed.sourceRef);
249
+ if (!ship)
250
+ return c.json({ error: "not_found" }, 404);
251
+ return c.json({ data: ship });
252
+ });
@@ -0,0 +1,4 @@
1
+ import type { Hono } from "hono";
2
+ import type { CruiseRoutesEnv as Env } from "./routes-env.js";
3
+ export declare function registerCruiseSailingAndPriceRoutes(app: Hono<Env>): void;
4
+ //# sourceMappingURL=routes-sailings-prices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-sailings-prices.d.ts","sourceRoot":"","sources":["../src/routes-sailings-prices.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAahC,OAAO,KAAK,EAAE,eAAe,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAkB7D,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAgSjE"}
@@ -0,0 +1,278 @@
1
+ import { parseJsonBody, parseQuery } from "@voyant-travel/hono";
2
+ import { z } from "zod";
3
+ import { parseUnifiedKey } from "./lib/key.js";
4
+ import { createBookingPayloadSchema, createPartyBookingPayloadSchema, passengerCompositionMatches, passengerCountFromComposition, quotePayloadSchema, sourceRefFromPayload, sourceRefMatches, } from "./routes-booking-payloads.js";
5
+ import { adapterNotRegistered, invalidKey, resolveExternal } from "./routes-keying.js";
6
+ import { cruisesService } from "./service.js";
7
+ import { cruisesBookingService } from "./service-bookings.js";
8
+ import { pricingService } from "./service-pricing.js";
9
+ import { insertSailingSchema, sailingListQuerySchema, updateSailingSchema, } from "./validation-core.js";
10
+ import { replaceSailingDaysSchema } from "./validation-itinerary.js";
11
+ import { insertPriceComponentSchema, insertPriceSchema, priceListQuerySchema, updatePriceSchema, } from "./validation-pricing.js";
12
+ export function registerCruiseSailingAndPriceRoutes(app) {
13
+ app
14
+ // --- sailings ---
15
+ .get("/sailings", async (c) => {
16
+ const query = parseQuery(c, sailingListQuerySchema);
17
+ const result = await cruisesService.listSailings(c.get("db"), query);
18
+ return c.json(result);
19
+ })
20
+ .get("/sailings/:key", async (c) => {
21
+ const parsed = parseUnifiedKey(c.req.param("key"));
22
+ if (parsed.kind === "invalid")
23
+ return c.json(invalidKey(parsed.raw), 400);
24
+ if (parsed.kind === "external") {
25
+ const ext = resolveExternal(parsed);
26
+ if (!ext)
27
+ return c.json(adapterNotRegistered(parsed.provider), 501);
28
+ const sailing = await ext.adapter.fetchSailing(ext.sourceRef);
29
+ if (!sailing)
30
+ return c.json({ error: "not_found" }, 404);
31
+ const includeRaw = c.req.query("include") ?? "";
32
+ const includes = new Set(includeRaw
33
+ .split(",")
34
+ .map((s) => s.trim())
35
+ .filter(Boolean));
36
+ const enriched = {
37
+ source: "external",
38
+ sourceProvider: ext.adapter.name,
39
+ sourceRef: sailing.sourceRef,
40
+ sailing,
41
+ };
42
+ if (includes.has("pricing")) {
43
+ enriched.pricing = await ext.adapter.fetchSailingPricing(ext.sourceRef);
44
+ }
45
+ if (includes.has("itinerary")) {
46
+ enriched.itinerary = await ext.adapter.fetchSailingItinerary(ext.sourceRef);
47
+ }
48
+ return c.json({ data: enriched });
49
+ }
50
+ const includeRaw = c.req.query("include") ?? "";
51
+ const includes = new Set(includeRaw
52
+ .split(",")
53
+ .map((s) => s.trim())
54
+ .filter(Boolean));
55
+ const row = await cruisesService.getSailingById(c.get("db"), parsed.id, {
56
+ withPricing: includes.has("pricing"),
57
+ withItinerary: includes.has("itinerary"),
58
+ });
59
+ if (!row)
60
+ return c.json({ error: "not_found" }, 404);
61
+ return c.json({ data: row });
62
+ })
63
+ .post("/sailings", async (c) => {
64
+ const data = await parseJsonBody(c, insertSailingSchema);
65
+ const row = await cruisesService.upsertSailing(c.get("db"), data);
66
+ return c.json({ data: row }, 201);
67
+ })
68
+ .put("/sailings/:key", async (c) => {
69
+ const parsed = parseUnifiedKey(c.req.param("key"));
70
+ if (parsed.kind === "external")
71
+ return c.json({ error: "external_cruise_read_only" }, 409);
72
+ if (parsed.kind === "invalid")
73
+ return c.json(invalidKey(parsed.raw), 400);
74
+ const data = await parseJsonBody(c, updateSailingSchema);
75
+ const row = await cruisesService.updateSailing(c.get("db"), parsed.id, data);
76
+ if (!row)
77
+ return c.json({ error: "not_found" }, 404);
78
+ return c.json({ data: row });
79
+ })
80
+ .get("/sailings/:key/itinerary", async (c) => {
81
+ const parsed = parseUnifiedKey(c.req.param("key"));
82
+ if (parsed.kind === "invalid")
83
+ return c.json(invalidKey(parsed.raw), 400);
84
+ if (parsed.kind === "external") {
85
+ const ext = resolveExternal(parsed);
86
+ if (!ext)
87
+ return c.json(adapterNotRegistered(parsed.provider), 501);
88
+ const days = await ext.adapter.fetchSailingItinerary(ext.sourceRef);
89
+ return c.json({ data: days });
90
+ }
91
+ const days = await cruisesService.getEffectiveItinerary(c.get("db"), parsed.id);
92
+ return c.json({ data: days });
93
+ })
94
+ .put("/sailings/:key/days/bulk", async (c) => {
95
+ const parsed = parseUnifiedKey(c.req.param("key"));
96
+ if (parsed.kind === "external")
97
+ return c.json({ error: "external_cruise_read_only" }, 409);
98
+ if (parsed.kind === "invalid")
99
+ return c.json(invalidKey(parsed.raw), 400);
100
+ const payload = await parseJsonBody(c, replaceSailingDaysSchema.omit({ sailingId: true }));
101
+ const days = await cruisesService.replaceSailingDays(c.get("db"), {
102
+ sailingId: parsed.id,
103
+ days: payload.days,
104
+ });
105
+ return c.json({ data: days });
106
+ })
107
+ .get("/sailings/:key/pricing", async (c) => {
108
+ const parsed = parseUnifiedKey(c.req.param("key"));
109
+ if (parsed.kind === "invalid")
110
+ return c.json(invalidKey(parsed.raw), 400);
111
+ if (parsed.kind === "external") {
112
+ const ext = resolveExternal(parsed);
113
+ if (!ext)
114
+ return c.json(adapterNotRegistered(parsed.provider), 501);
115
+ const prices = await ext.adapter.fetchSailingPricing(ext.sourceRef);
116
+ return c.json({ data: prices });
117
+ }
118
+ const result = await cruisesService.listPrices(c.get("db"), {
119
+ sailingId: parsed.id,
120
+ limit: 100,
121
+ offset: 0,
122
+ });
123
+ return c.json(result);
124
+ })
125
+ .put("/sailings/:key/pricing/bulk", async (c) => {
126
+ const parsed = parseUnifiedKey(c.req.param("key"));
127
+ if (parsed.kind === "external")
128
+ return c.json({ error: "external_cruise_read_only" }, 409);
129
+ if (parsed.kind === "invalid")
130
+ return c.json(invalidKey(parsed.raw), 400);
131
+ const payload = await parseJsonBody(c, z.object({
132
+ prices: z.array(insertPriceSchema.extend({
133
+ components: z.array(insertPriceComponentSchema.omit({ priceId: true })).optional(),
134
+ })),
135
+ }));
136
+ const data = await cruisesService.replaceSailingPricing(c.get("db"), parsed.id, payload);
137
+ return c.json({ data });
138
+ })
139
+ .post("/sailings/:key/quote", async (c) => {
140
+ const parsed = parseUnifiedKey(c.req.param("key"));
141
+ if (parsed.kind === "invalid")
142
+ return c.json(invalidKey(parsed.raw), 400);
143
+ const payload = await parseJsonBody(c, quotePayloadSchema);
144
+ const guestCount = payload.guestCount ?? passengerCountFromComposition(payload.passengerComposition);
145
+ if (!guestCount) {
146
+ return c.json({
147
+ error: "guest_count_required",
148
+ detail: "Provide guestCount or passengerComposition for cruise quote requests.",
149
+ }, 400);
150
+ }
151
+ if (parsed.kind === "external") {
152
+ const ext = resolveExternal(parsed);
153
+ if (!ext)
154
+ return c.json(adapterNotRegistered(parsed.provider), 501);
155
+ // Fetch upstream pricing then compose locally — the cabinCategoryId in
156
+ // the payload is interpreted as the upstream cabin category externalId.
157
+ const prices = await ext.adapter.fetchSailingPricing(ext.sourceRef);
158
+ const cabinCategoryRef = sourceRefFromPayload(payload.cabinCategoryRef, payload.cabinCategoryId);
159
+ const matching = prices.find((p) => sourceRefMatches(p.cabinCategoryRef, cabinCategoryRef) &&
160
+ p.occupancy === payload.occupancy &&
161
+ passengerCompositionMatches(p.passengerComposition, payload.passengerComposition) &&
162
+ (!payload.fareCode || p.fareCode === payload.fareCode) &&
163
+ (!payload.fareVariant || p.fareVariant === payload.fareVariant));
164
+ if (!matching)
165
+ return c.json({ error: "no_matching_price" }, 404);
166
+ const { composeQuote } = await import("./service-pricing.js");
167
+ const quote = composeQuote({
168
+ price: {
169
+ pricePerPerson: matching.pricePerPerson,
170
+ originalPricePerPerson: matching.originalPricePerPerson ?? null,
171
+ secondGuestPricePerPerson: matching.secondGuestPricePerPerson ?? null,
172
+ singlePricePerPerson: matching.singlePricePerPerson ?? null,
173
+ singleSupplementPercent: matching.singleSupplementPercent ?? null,
174
+ currency: matching.currency,
175
+ fareCode: matching.fareCode ?? null,
176
+ fareCodeName: matching.fareCodeName ?? null,
177
+ fareVariant: matching.fareVariant ?? "cruise_only",
178
+ earlyBookingDeadline: matching.earlyBookingDeadline ?? null,
179
+ earlyBookingBonusDescription: matching.earlyBookingBonusDescription ?? null,
180
+ },
181
+ components: (matching.components ?? []).map((c) => ({
182
+ kind: c.kind,
183
+ label: c.label ?? null,
184
+ amount: c.amount,
185
+ currency: c.currency,
186
+ direction: c.direction,
187
+ perPerson: c.perPerson,
188
+ })),
189
+ occupancy: payload.occupancy,
190
+ guestCount,
191
+ bookingTerms: matching.bookingTerms ?? null,
192
+ });
193
+ return c.json({ data: quote });
194
+ }
195
+ const quote = await pricingService.assembleQuote(c.get("db"), {
196
+ sailingId: parsed.id,
197
+ cabinCategoryId: payload.cabinCategoryId,
198
+ occupancy: payload.occupancy,
199
+ guestCount,
200
+ fareCode: payload.fareCode ?? null,
201
+ fareVariant: payload.fareVariant ?? null,
202
+ });
203
+ return c.json({ data: quote });
204
+ })
205
+ // --- bookings (single + party) ---
206
+ .post("/sailings/:key/bookings", async (c) => {
207
+ const parsed = parseUnifiedKey(c.req.param("key"));
208
+ if (parsed.kind === "invalid")
209
+ return c.json(invalidKey(parsed.raw), 400);
210
+ if (parsed.kind === "external") {
211
+ const ext = resolveExternal(parsed);
212
+ if (!ext)
213
+ return c.json(adapterNotRegistered(parsed.provider), 501);
214
+ const payload = await parseJsonBody(c, createBookingPayloadSchema);
215
+ const result = await cruisesBookingService.createExternalCruiseBooking(c.get("db"), {
216
+ adapter: ext.adapter,
217
+ sailingRef: ext.sourceRef,
218
+ cabinCategoryRef: sourceRefFromPayload(payload.cabinCategoryRef, payload.cabinCategoryId),
219
+ cabinId: payload.cabinId ?? null,
220
+ occupancy: payload.occupancy,
221
+ passengerComposition: payload.passengerComposition ?? null,
222
+ fareCode: payload.fareCode ?? null,
223
+ fareVariant: payload.fareVariant ?? null,
224
+ mode: payload.mode,
225
+ personId: payload.personId ?? null,
226
+ organizationId: payload.organizationId ?? null,
227
+ contact: payload.contact,
228
+ passengers: payload.passengers,
229
+ notes: payload.notes ?? null,
230
+ }, c.get("userId"));
231
+ return c.json({ data: result }, 201);
232
+ }
233
+ const payload = await parseJsonBody(c, createBookingPayloadSchema);
234
+ if (payload.sailingId !== parsed.id) {
235
+ return c.json({ error: "sailing_id_mismatch", detail: "URL key and payload sailingId must match" }, 400);
236
+ }
237
+ const result = await cruisesBookingService.createCruiseBooking(c.get("db"), payload, c.get("userId"));
238
+ return c.json({ data: result }, 201);
239
+ })
240
+ .post("/sailings/:key/party-bookings", async (c) => {
241
+ const parsed = parseUnifiedKey(c.req.param("key"));
242
+ if (parsed.kind === "invalid")
243
+ return c.json(invalidKey(parsed.raw), 400);
244
+ if (parsed.kind === "external") {
245
+ // External party bookings deferred — most cruise lines don't expose a
246
+ // multi-cabin atomic upstream commit; we'd have to implement the group
247
+ // semantics by serial bookings + rollback. Out of v1 scope.
248
+ return c.json({
249
+ error: "external_party_booking_not_supported",
250
+ detail: "Multi-cabin party bookings against external adapters are not yet supported. Submit each cabin individually via POST /sailings/:key/bookings.",
251
+ }, 501);
252
+ }
253
+ const payload = await parseJsonBody(c, createPartyBookingPayloadSchema);
254
+ if (payload.sailingId !== parsed.id) {
255
+ return c.json({ error: "sailing_id_mismatch", detail: "URL key and payload sailingId must match" }, 400);
256
+ }
257
+ const result = await cruisesBookingService.createCruisePartyBooking(c.get("db"), payload, c.get("userId"));
258
+ return c.json({ data: result }, 201);
259
+ })
260
+ // --- prices (read endpoints; mutations go through bulk replace on the sailing) ---
261
+ .get("/prices", async (c) => {
262
+ const query = parseQuery(c, priceListQuerySchema);
263
+ const result = await cruisesService.listPrices(c.get("db"), query);
264
+ return c.json(result);
265
+ })
266
+ .post("/prices", async (c) => {
267
+ const data = await parseJsonBody(c, insertPriceSchema);
268
+ const row = await cruisesService.createPrice(c.get("db"), data);
269
+ return c.json({ data: row }, 201);
270
+ })
271
+ .put("/prices/:priceId", async (c) => {
272
+ const data = await parseJsonBody(c, updatePriceSchema);
273
+ const row = await cruisesService.updatePrice(c.get("db"), c.req.param("priceId"), data);
274
+ if (!row)
275
+ return c.json({ error: "not_found" }, 404);
276
+ return c.json({ data: row });
277
+ });
278
+ }
@@ -0,0 +1,4 @@
1
+ import type { Hono } from "hono";
2
+ import type { CruiseRoutesEnv as Env } from "./routes-env.js";
3
+ export declare function registerCruiseSearchIndexRoutes(app: Hono<Env>): void;
4
+ //# sourceMappingURL=routes-search-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-search-index.d.ts","sourceRoot":"","sources":["../src/routes-search-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,eAAe,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAI7D,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAsB7D"}
@@ -0,0 +1,25 @@
1
+ import { parseJsonBody } from "@voyant-travel/hono";
2
+ import { z } from "zod";
3
+ import { cruisesSearchService } from "./service-search.js";
4
+ import { insertSearchIndexSchema } from "./validation-search.js";
5
+ export function registerCruiseSearchIndexRoutes(app) {
6
+ app
7
+ // --- search-index management ---
8
+ .put("/search-index/bulk", async (c) => {
9
+ const payload = await parseJsonBody(c, z.object({
10
+ entries: z.array(insertSearchIndexSchema),
11
+ }));
12
+ const result = await cruisesSearchService.bulkUpsert(c.get("db"), payload.entries);
13
+ return c.json({ data: result });
14
+ })
15
+ .delete("/search-index/:crsiId", async (c) => {
16
+ const ok = await cruisesSearchService.removeEntry(c.get("db"), c.req.param("crsiId"));
17
+ if (!ok)
18
+ return c.json({ error: "not_found" }, 404);
19
+ return c.body(null, 204);
20
+ })
21
+ .post("/search-index/rebuild", async (c) => {
22
+ const result = await cruisesSearchService.rebuildAll(c.get("db"));
23
+ return c.json({ data: result });
24
+ });
25
+ }
@@ -0,0 +1,4 @@
1
+ import type { Hono } from "hono";
2
+ import type { CruiseRoutesEnv as Env } from "./routes-env.js";
3
+ export declare function registerCruiseShipRoutes(app: Hono<Env>): void;
4
+ //# sourceMappingURL=routes-ships.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes-ships.d.ts","sourceRoot":"","sources":["../src/routes-ships.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAIhC,OAAO,KAAK,EAAE,eAAe,IAAI,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAe7D,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,QAuItD"}