@voyant-travel/charters 0.117.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 (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +16 -0
  3. package/dist/adapters/index.d.ts +254 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/memoize.d.ts +28 -0
  7. package/dist/adapters/memoize.d.ts.map +1 -0
  8. package/dist/adapters/memoize.js +121 -0
  9. package/dist/adapters/mock.d.ts +50 -0
  10. package/dist/adapters/mock.d.ts.map +1 -0
  11. package/dist/adapters/mock.js +194 -0
  12. package/dist/adapters/registry.d.ts +24 -0
  13. package/dist/adapters/registry.d.ts.map +1 -0
  14. package/dist/adapters/registry.js +40 -0
  15. package/dist/booking-extension.d.ts +895 -0
  16. package/dist/booking-extension.d.ts.map +1 -0
  17. package/dist/booking-extension.js +339 -0
  18. package/dist/catalog-policy.d.ts +23 -0
  19. package/dist/catalog-policy.d.ts.map +1 -0
  20. package/dist/catalog-policy.js +400 -0
  21. package/dist/content-shape.d.ts +5 -0
  22. package/dist/content-shape.d.ts.map +1 -0
  23. package/dist/content-shape.js +13 -0
  24. package/dist/draft-shape.d.ts +29 -0
  25. package/dist/draft-shape.d.ts.map +1 -0
  26. package/dist/draft-shape.js +63 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +55 -0
  30. package/dist/lib/key.d.ts +22 -0
  31. package/dist/lib/key.d.ts.map +1 -0
  32. package/dist/lib/key.js +24 -0
  33. package/dist/routes-public.d.ts +785 -0
  34. package/dist/routes-public.d.ts.map +1 -0
  35. package/dist/routes-public.js +234 -0
  36. package/dist/routes.d.ts +1744 -0
  37. package/dist/routes.d.ts.map +1 -0
  38. package/dist/routes.js +543 -0
  39. package/dist/schema-core.d.ts +815 -0
  40. package/dist/schema-core.d.ts.map +1 -0
  41. package/dist/schema-core.js +98 -0
  42. package/dist/schema-itinerary.d.ts +239 -0
  43. package/dist/schema-itinerary.d.ts.map +1 -0
  44. package/dist/schema-itinerary.js +30 -0
  45. package/dist/schema-pricing.d.ts +385 -0
  46. package/dist/schema-pricing.d.ts.map +1 -0
  47. package/dist/schema-pricing.js +62 -0
  48. package/dist/schema-shared.d.ts +8 -0
  49. package/dist/schema-shared.d.ts.map +1 -0
  50. package/dist/schema-shared.js +37 -0
  51. package/dist/schema-sourced-content.d.ts +253 -0
  52. package/dist/schema-sourced-content.d.ts.map +1 -0
  53. package/dist/schema-sourced-content.js +44 -0
  54. package/dist/schema-yachts.d.ts +367 -0
  55. package/dist/schema-yachts.d.ts.map +1 -0
  56. package/dist/schema-yachts.js +30 -0
  57. package/dist/schema.d.ts +8 -0
  58. package/dist/schema.d.ts.map +1 -0
  59. package/dist/schema.js +7 -0
  60. package/dist/service-bookings-helpers.d.ts +20 -0
  61. package/dist/service-bookings-helpers.d.ts.map +1 -0
  62. package/dist/service-bookings-helpers.js +67 -0
  63. package/dist/service-bookings-local.d.ts +5 -0
  64. package/dist/service-bookings-local.d.ts.map +1 -0
  65. package/dist/service-bookings-local.js +177 -0
  66. package/dist/service-bookings-types.d.ts +88 -0
  67. package/dist/service-bookings-types.d.ts.map +1 -0
  68. package/dist/service-bookings-types.js +1 -0
  69. package/dist/service-bookings.d.ts +36 -0
  70. package/dist/service-bookings.d.ts.map +1 -0
  71. package/dist/service-bookings.js +267 -0
  72. package/dist/service-catalog-plane.d.ts +58 -0
  73. package/dist/service-catalog-plane.d.ts.map +1 -0
  74. package/dist/service-catalog-plane.js +145 -0
  75. package/dist/service-content-synthesizer.d.ts +42 -0
  76. package/dist/service-content-synthesizer.d.ts.map +1 -0
  77. package/dist/service-content-synthesizer.js +122 -0
  78. package/dist/service-content.d.ts +43 -0
  79. package/dist/service-content.d.ts.map +1 -0
  80. package/dist/service-content.js +248 -0
  81. package/dist/service-myba.d.ts +85 -0
  82. package/dist/service-myba.d.ts.map +1 -0
  83. package/dist/service-myba.js +88 -0
  84. package/dist/service-pricing.d.ts +64 -0
  85. package/dist/service-pricing.d.ts.map +1 -0
  86. package/dist/service-pricing.js +167 -0
  87. package/dist/service.d.ts +131 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +279 -0
  90. package/dist/validation-core.d.ts +152 -0
  91. package/dist/validation-core.d.ts.map +1 -0
  92. package/dist/validation-core.js +66 -0
  93. package/dist/validation-itinerary.d.ts +43 -0
  94. package/dist/validation-itinerary.d.ts.map +1 -0
  95. package/dist/validation-itinerary.js +19 -0
  96. package/dist/validation-pricing.d.ts +103 -0
  97. package/dist/validation-pricing.d.ts.map +1 -0
  98. package/dist/validation-pricing.js +28 -0
  99. package/dist/validation-shared.d.ts +61 -0
  100. package/dist/validation-shared.d.ts.map +1 -0
  101. package/dist/validation-shared.js +60 -0
  102. package/dist/validation-yachts.d.ts +76 -0
  103. package/dist/validation-yachts.d.ts.map +1 -0
  104. package/dist/validation-yachts.js +36 -0
  105. package/dist/validation.d.ts +6 -0
  106. package/dist/validation.d.ts.map +1 -0
  107. package/dist/validation.js +5 -0
  108. package/package.json +116 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAajE,OAAO,EAAE,KAAK,uBAAuB,EAAe,MAAM,mBAAmB,CAAA;AAiB7E,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf;;;;;WAKG;QACH,wBAAwB,CAAC,EAAE,uBAAuB,CAAA;KACnD,CAAA;CACF,CAAA;AAkGD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAuBlB,UAAU;oCACF,MAAM;;;;;;yBAEjB,MAAM;;;;;;;6BAGyB,MAAM;2BAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAga3D,CAAA;AAEJ,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
package/dist/routes.js ADDED
@@ -0,0 +1,543 @@
1
+ // agent-quality: file-size exception -- owner: charters; existing route module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { parseJsonBody, parseQuery } from "@voyant-travel/hono";
3
+ import { Hono } from "hono";
4
+ import { z } from "zod";
5
+ import { listCharterAdapters, resolveCharterAdapter } from "./adapters/registry.js";
6
+ import { parseUnifiedKey } from "./lib/key.js";
7
+ import { chartersService } from "./service.js";
8
+ import { chartersBookingService, } from "./service-bookings.js";
9
+ import { mybaService } from "./service-myba.js";
10
+ import { composePerSuiteQuote, composeWholeYachtQuote, pricingService } from "./service-pricing.js";
11
+ import { insertProductSchema, insertVoyageSchema, productListQuerySchema, updateProductSchema, updateVoyageSchema, voyageListQuerySchema, } from "./validation-core.js";
12
+ import { replaceVoyageScheduleSchema } from "./validation-itinerary.js";
13
+ import { replaceVoyageSuitesSchema } from "./validation-pricing.js";
14
+ import { currencyCodeSchema } from "./validation-shared.js";
15
+ import { insertYachtSchema, updateYachtSchema, yachtListQuerySchema } from "./validation-yachts.js";
16
+ // ---------- shared helpers ----------
17
+ const adapterNotRegistered = (provider) => ({
18
+ error: "adapter_not_registered",
19
+ detail: `No CharterAdapter registered for source provider '${provider}'. Register one at app startup via registerCharterAdapter().`,
20
+ });
21
+ const externalReadOnly = {
22
+ error: "external_charter_read_only",
23
+ detail: "External charter rows can't be edited locally. Edit at the upstream system or use a local TypeID for new content.",
24
+ };
25
+ const invalidKey = (raw) => ({ error: "invalid_key", detail: `Unrecognized key: ${raw}` });
26
+ function resolveExternal(parsed) {
27
+ const adapter = resolveCharterAdapter(parsed.provider);
28
+ if (!adapter)
29
+ return null;
30
+ return { adapter, sourceRef: { externalId: parsed.ref } };
31
+ }
32
+ function makeExternalKey(adapter, ref) {
33
+ return `${adapter.name}:${ref.externalId}`;
34
+ }
35
+ // ---------- payload schemas ----------
36
+ const guestSchema = z.object({
37
+ firstName: z.string().min(1),
38
+ lastName: z.string().min(1),
39
+ email: z.string().email().optional().nullable(),
40
+ phone: z.string().optional().nullable(),
41
+ travelerCategory: z.enum(["adult", "child", "infant", "senior", "other"]).optional().nullable(),
42
+ preferredLanguage: z.string().optional().nullable(),
43
+ specialRequests: z.string().optional().nullable(),
44
+ personId: z.string().optional().nullable(),
45
+ isPrimary: z.boolean().optional(),
46
+ notes: z.string().optional().nullable(),
47
+ });
48
+ const contactSchema = z.object({
49
+ firstName: z.string().min(1),
50
+ lastName: z.string().min(1),
51
+ email: z.string().email().optional().nullable(),
52
+ phone: z.string().optional().nullable(),
53
+ language: z.string().optional().nullable(),
54
+ country: z.string().optional().nullable(),
55
+ region: z.string().optional().nullable(),
56
+ city: z.string().optional().nullable(),
57
+ address: z.string().optional().nullable(),
58
+ postalCode: z.string().optional().nullable(),
59
+ });
60
+ const createPerSuiteBookingPayload = z.object({
61
+ voyageId: z.string(),
62
+ suiteId: z.string(),
63
+ currency: currencyCodeSchema,
64
+ personId: z.string().optional().nullable(),
65
+ organizationId: z.string().optional().nullable(),
66
+ contact: contactSchema,
67
+ guests: z.array(guestSchema).min(1),
68
+ notes: z.string().optional().nullable(),
69
+ });
70
+ const createWholeYachtBookingPayload = z.object({
71
+ voyageId: z.string(),
72
+ currency: currencyCodeSchema,
73
+ personId: z.string().optional().nullable(),
74
+ organizationId: z.string().optional().nullable(),
75
+ contact: contactSchema,
76
+ guests: z.array(guestSchema).optional(),
77
+ notes: z.string().optional().nullable(),
78
+ });
79
+ const generateMybaPayload = z.object({
80
+ templateIdOverride: z.string().optional().nullable(),
81
+ language: z.string().optional(),
82
+ extraVariables: z.record(z.string(), z.unknown()).optional(),
83
+ title: z.string().optional(),
84
+ });
85
+ const perSuiteQuotePayload = z.object({
86
+ /** For local voyages, a `chst_…` TypeID. For external voyages, the upstream suite externalId. */
87
+ suiteId: z.string(),
88
+ currency: currencyCodeSchema,
89
+ });
90
+ const wholeYachtQuotePayload = z.object({
91
+ currency: currencyCodeSchema,
92
+ });
93
+ // ---------- routes ----------
94
+ export const chartersAdminRoutes = new Hono()
95
+ // --- products ---
96
+ .get("/products", async (c) => {
97
+ const query = parseQuery(c, productListQuerySchema);
98
+ const local = await chartersService.listProducts(c.get("db"), query);
99
+ const localItems = local.data.map((p) => ({
100
+ source: "local",
101
+ sourceProvider: null,
102
+ sourceRef: null,
103
+ key: p.id,
104
+ product: p,
105
+ }));
106
+ // Fan out to every registered adapter in parallel via Promise.allSettled —
107
+ // one slow or failing adapter doesn't block the rest.
108
+ const adapters = listCharterAdapters();
109
+ const settled = await Promise.allSettled(adapters.map((adapter) => adapter
110
+ .listEntries({ limit: query.limit })
111
+ .then((result) => ({ adapter, result }))));
112
+ const adapterItems = [];
113
+ const adapterErrors = [];
114
+ for (let i = 0; i < settled.length; i++) {
115
+ const outcome = settled[i];
116
+ const adapter = adapters[i];
117
+ if (!outcome || !adapter)
118
+ continue;
119
+ if (outcome.status === "rejected") {
120
+ adapterErrors.push({
121
+ adapter: adapter.name,
122
+ error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),
123
+ });
124
+ continue;
125
+ }
126
+ for (const entry of outcome.value.result.entries) {
127
+ adapterItems.push({
128
+ source: "external",
129
+ sourceProvider: adapter.name,
130
+ sourceRef: entry.sourceRef,
131
+ key: makeExternalKey(adapter, entry.sourceRef),
132
+ product: entry,
133
+ });
134
+ }
135
+ }
136
+ return c.json({
137
+ data: [...localItems, ...adapterItems],
138
+ total: local.total + adapterItems.length,
139
+ localTotal: local.total,
140
+ adapterCount: adapters.length,
141
+ adapterErrors,
142
+ limit: local.limit,
143
+ offset: local.offset,
144
+ });
145
+ })
146
+ .post("/products", async (c) => {
147
+ const data = await parseJsonBody(c, insertProductSchema);
148
+ const row = await chartersService.createProduct(c.get("db"), data);
149
+ return c.json({ data: row }, 201);
150
+ })
151
+ .get("/products/:key", async (c) => {
152
+ const parsed = parseUnifiedKey(c.req.param("key"));
153
+ if (parsed.kind === "invalid")
154
+ return c.json(invalidKey(parsed.raw), 400);
155
+ if (parsed.kind === "external") {
156
+ const ext = resolveExternal(parsed);
157
+ if (!ext)
158
+ return c.json(adapterNotRegistered(parsed.provider), 501);
159
+ const product = await ext.adapter.fetchProduct(ext.sourceRef);
160
+ if (!product)
161
+ return c.json({ error: "not_found" }, 404);
162
+ const includeRaw = c.req.query("include") ?? "";
163
+ const includes = new Set(includeRaw
164
+ .split(",")
165
+ .map((s) => s.trim())
166
+ .filter(Boolean));
167
+ const enriched = {
168
+ source: "external",
169
+ sourceProvider: ext.adapter.name,
170
+ sourceRef: product.sourceRef,
171
+ product,
172
+ };
173
+ if (includes.has("voyages")) {
174
+ enriched.voyages = await ext.adapter.listVoyagesForProduct(ext.sourceRef);
175
+ }
176
+ if (includes.has("yacht") && product.defaultYachtRef) {
177
+ enriched.yacht = await ext.adapter.fetchYacht(product.defaultYachtRef);
178
+ }
179
+ return c.json({ data: enriched });
180
+ }
181
+ const includeRaw = c.req.query("include") ?? "";
182
+ const includes = new Set(includeRaw
183
+ .split(",")
184
+ .map((s) => s.trim())
185
+ .filter(Boolean));
186
+ const row = await chartersService.getProductById(c.get("db"), parsed.id, {
187
+ withVoyages: includes.has("voyages"),
188
+ withYacht: includes.has("yacht"),
189
+ });
190
+ if (!row)
191
+ return c.json({ error: "not_found" }, 404);
192
+ return c.json({ data: row });
193
+ })
194
+ .put("/products/:key", async (c) => {
195
+ const parsed = parseUnifiedKey(c.req.param("key"));
196
+ if (parsed.kind === "invalid")
197
+ return c.json(invalidKey(parsed.raw), 400);
198
+ if (parsed.kind === "external")
199
+ return c.json(externalReadOnly, 409);
200
+ const data = await parseJsonBody(c, updateProductSchema);
201
+ const row = await chartersService.updateProduct(c.get("db"), parsed.id, data);
202
+ if (!row)
203
+ return c.json({ error: "not_found" }, 404);
204
+ return c.json({ data: row });
205
+ })
206
+ .delete("/products/:key", 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
+ return c.json(externalReadOnly, 409);
212
+ const row = await chartersService.archiveProduct(c.get("db"), parsed.id);
213
+ if (!row)
214
+ return c.json({ error: "not_found" }, 404);
215
+ return c.json({ data: row });
216
+ })
217
+ .post("/products/:key/aggregates/recompute", async (c) => {
218
+ const parsed = parseUnifiedKey(c.req.param("key"));
219
+ if (parsed.kind === "invalid")
220
+ return c.json(invalidKey(parsed.raw), 400);
221
+ if (parsed.kind === "external")
222
+ return c.json(externalReadOnly, 409);
223
+ const row = await chartersService.recomputeProductAggregates(c.get("db"), parsed.id);
224
+ if (!row)
225
+ return c.json({ error: "not_found" }, 404);
226
+ return c.json({ data: row });
227
+ })
228
+ .get("/products/:key/voyages", async (c) => {
229
+ const parsed = parseUnifiedKey(c.req.param("key"));
230
+ if (parsed.kind === "invalid")
231
+ return c.json(invalidKey(parsed.raw), 400);
232
+ if (parsed.kind === "external") {
233
+ const ext = resolveExternal(parsed);
234
+ if (!ext)
235
+ return c.json(adapterNotRegistered(parsed.provider), 501);
236
+ const voyages = await ext.adapter.listVoyagesForProduct(ext.sourceRef);
237
+ return c.json({
238
+ data: voyages.map((v) => ({
239
+ source: "external",
240
+ sourceProvider: ext.adapter.name,
241
+ key: makeExternalKey(ext.adapter, v.sourceRef),
242
+ voyage: v,
243
+ })),
244
+ total: voyages.length,
245
+ });
246
+ }
247
+ const result = await chartersService.listVoyages(c.get("db"), {
248
+ productId: parsed.id,
249
+ limit: 100,
250
+ offset: 0,
251
+ });
252
+ return c.json(result);
253
+ })
254
+ // --- voyages ---
255
+ .get("/voyages", async (c) => {
256
+ const query = parseQuery(c, voyageListQuerySchema);
257
+ const result = await chartersService.listVoyages(c.get("db"), query);
258
+ return c.json(result);
259
+ })
260
+ .post("/voyages", async (c) => {
261
+ const data = await parseJsonBody(c, insertVoyageSchema);
262
+ const row = await chartersService.upsertVoyage(c.get("db"), data);
263
+ return c.json({ data: row }, 201);
264
+ })
265
+ .get("/voyages/:key", async (c) => {
266
+ const parsed = parseUnifiedKey(c.req.param("key"));
267
+ if (parsed.kind === "invalid")
268
+ return c.json(invalidKey(parsed.raw), 400);
269
+ if (parsed.kind === "external") {
270
+ const ext = resolveExternal(parsed);
271
+ if (!ext)
272
+ return c.json(adapterNotRegistered(parsed.provider), 501);
273
+ const voyage = await ext.adapter.fetchVoyage(ext.sourceRef);
274
+ if (!voyage)
275
+ return c.json({ error: "not_found" }, 404);
276
+ const includeRaw = c.req.query("include") ?? "";
277
+ const includes = new Set(includeRaw
278
+ .split(",")
279
+ .map((s) => s.trim())
280
+ .filter(Boolean));
281
+ const enriched = {
282
+ source: "external",
283
+ sourceProvider: ext.adapter.name,
284
+ sourceRef: voyage.sourceRef,
285
+ voyage,
286
+ };
287
+ if (includes.has("suites")) {
288
+ enriched.suites = await ext.adapter.fetchVoyageSuites(ext.sourceRef);
289
+ }
290
+ if (includes.has("schedule")) {
291
+ enriched.schedule = await ext.adapter.fetchVoyageSchedule(ext.sourceRef);
292
+ }
293
+ return c.json({ data: enriched });
294
+ }
295
+ const includeRaw = c.req.query("include") ?? "";
296
+ const includes = new Set(includeRaw
297
+ .split(",")
298
+ .map((s) => s.trim())
299
+ .filter(Boolean));
300
+ const row = await chartersService.getVoyageById(c.get("db"), parsed.id, {
301
+ withSuites: includes.has("suites"),
302
+ withSchedule: includes.has("schedule"),
303
+ });
304
+ if (!row)
305
+ return c.json({ error: "not_found" }, 404);
306
+ return c.json({ data: row });
307
+ })
308
+ .put("/voyages/:key", async (c) => {
309
+ const parsed = parseUnifiedKey(c.req.param("key"));
310
+ if (parsed.kind === "invalid")
311
+ return c.json(invalidKey(parsed.raw), 400);
312
+ if (parsed.kind === "external")
313
+ return c.json(externalReadOnly, 409);
314
+ const data = await parseJsonBody(c, updateVoyageSchema);
315
+ const row = await chartersService.updateVoyage(c.get("db"), parsed.id, data);
316
+ if (!row)
317
+ return c.json({ error: "not_found" }, 404);
318
+ return c.json({ data: row });
319
+ })
320
+ .put("/voyages/:key/suites/bulk", async (c) => {
321
+ const parsed = parseUnifiedKey(c.req.param("key"));
322
+ if (parsed.kind === "invalid")
323
+ return c.json(invalidKey(parsed.raw), 400);
324
+ if (parsed.kind === "external")
325
+ return c.json(externalReadOnly, 409);
326
+ const payload = await parseJsonBody(c, replaceVoyageSuitesSchema.omit({ voyageId: true }));
327
+ const rows = await chartersService.replaceVoyageSuites(c.get("db"), {
328
+ voyageId: parsed.id,
329
+ suites: payload.suites,
330
+ });
331
+ return c.json({ data: rows });
332
+ })
333
+ .put("/voyages/:key/schedule/bulk", async (c) => {
334
+ const parsed = parseUnifiedKey(c.req.param("key"));
335
+ if (parsed.kind === "invalid")
336
+ return c.json(invalidKey(parsed.raw), 400);
337
+ if (parsed.kind === "external")
338
+ return c.json(externalReadOnly, 409);
339
+ const payload = await parseJsonBody(c, replaceVoyageScheduleSchema.omit({ voyageId: true }));
340
+ const rows = await chartersService.replaceVoyageSchedule(c.get("db"), {
341
+ voyageId: parsed.id,
342
+ days: payload.days,
343
+ });
344
+ return c.json({ data: rows });
345
+ })
346
+ // --- per-suite quote + bookings ---
347
+ .post("/voyages/:key/quote/per-suite", async (c) => {
348
+ const parsed = parseUnifiedKey(c.req.param("key"));
349
+ if (parsed.kind === "invalid")
350
+ return c.json(invalidKey(parsed.raw), 400);
351
+ const payload = await parseJsonBody(c, perSuiteQuotePayload);
352
+ if (parsed.kind === "external") {
353
+ const ext = resolveExternal(parsed);
354
+ if (!ext)
355
+ return c.json(adapterNotRegistered(parsed.provider), 501);
356
+ const suites = await ext.adapter.fetchVoyageSuites(ext.sourceRef);
357
+ const matching = suites.find((s) => s.sourceRef.externalId === payload.suiteId);
358
+ if (!matching)
359
+ return c.json({ error: "no_matching_suite" }, 404);
360
+ const quote = composePerSuiteQuote({
361
+ voyageId: ext.sourceRef.externalId,
362
+ suite: {
363
+ id: matching.sourceRef.externalId,
364
+ suiteName: matching.suiteName,
365
+ pricesByCurrency: matching.pricesByCurrency ?? {},
366
+ portFeesByCurrency: matching.portFeesByCurrency ?? {},
367
+ },
368
+ currency: payload.currency,
369
+ });
370
+ return c.json({ data: quote });
371
+ }
372
+ const quote = await pricingService.quotePerSuite(c.get("db"), {
373
+ suiteId: payload.suiteId,
374
+ currency: payload.currency,
375
+ });
376
+ return c.json({ data: quote });
377
+ })
378
+ .post("/voyages/:key/bookings/per-suite", async (c) => {
379
+ const parsed = parseUnifiedKey(c.req.param("key"));
380
+ if (parsed.kind === "invalid")
381
+ return c.json(invalidKey(parsed.raw), 400);
382
+ if (parsed.kind === "external") {
383
+ const ext = resolveExternal(parsed);
384
+ if (!ext)
385
+ return c.json(adapterNotRegistered(parsed.provider), 501);
386
+ const payload = await parseJsonBody(c, createPerSuiteBookingPayload);
387
+ const result = await chartersBookingService.createExternalPerSuiteBooking(c.get("db"), {
388
+ adapter: ext.adapter,
389
+ voyageRef: ext.sourceRef,
390
+ suiteRef: { externalId: payload.suiteId },
391
+ currency: payload.currency,
392
+ personId: payload.personId ?? null,
393
+ organizationId: payload.organizationId ?? null,
394
+ contact: payload.contact,
395
+ guests: payload.guests,
396
+ notes: payload.notes ?? null,
397
+ }, c.get("userId"));
398
+ return c.json({ data: result }, 201);
399
+ }
400
+ const payload = await parseJsonBody(c, createPerSuiteBookingPayload);
401
+ if (payload.voyageId !== parsed.id) {
402
+ return c.json({ error: "voyage_id_mismatch", detail: "URL key and payload voyageId must match" }, 400);
403
+ }
404
+ const result = await chartersBookingService.createPerSuiteBooking(c.get("db"), payload, c.get("userId"));
405
+ return c.json({ data: result }, 201);
406
+ })
407
+ // --- whole-yacht quote + bookings ---
408
+ .post("/voyages/:key/quote/whole-yacht", async (c) => {
409
+ const parsed = parseUnifiedKey(c.req.param("key"));
410
+ if (parsed.kind === "invalid")
411
+ return c.json(invalidKey(parsed.raw), 400);
412
+ const payload = await parseJsonBody(c, wholeYachtQuotePayload);
413
+ if (parsed.kind === "external") {
414
+ const ext = resolveExternal(parsed);
415
+ if (!ext)
416
+ return c.json(adapterNotRegistered(parsed.provider), 501);
417
+ const voyage = await ext.adapter.fetchVoyage(ext.sourceRef);
418
+ if (!voyage)
419
+ return c.json({ error: "not_found" }, 404);
420
+ const product = await ext.adapter.fetchProduct(voyage.productRef);
421
+ const quote = composeWholeYachtQuote({
422
+ voyage: {
423
+ id: voyage.sourceRef.externalId,
424
+ wholeYachtPricesByCurrency: voyage.wholeYachtPricesByCurrency ?? {},
425
+ apaPercentOverride: voyage.apaPercentOverride ?? null,
426
+ },
427
+ productDefaultApaPercent: product?.defaultApaPercent ?? null,
428
+ currency: payload.currency,
429
+ });
430
+ return c.json({ data: quote });
431
+ }
432
+ const quote = await pricingService.quoteWholeYacht(c.get("db"), {
433
+ voyageId: parsed.id,
434
+ currency: payload.currency,
435
+ });
436
+ return c.json({ data: quote });
437
+ })
438
+ .post("/voyages/:key/bookings/whole-yacht", async (c) => {
439
+ const parsed = parseUnifiedKey(c.req.param("key"));
440
+ if (parsed.kind === "invalid")
441
+ return c.json(invalidKey(parsed.raw), 400);
442
+ if (parsed.kind === "external") {
443
+ const ext = resolveExternal(parsed);
444
+ if (!ext)
445
+ return c.json(adapterNotRegistered(parsed.provider), 501);
446
+ const payload = await parseJsonBody(c, createWholeYachtBookingPayload);
447
+ const result = await chartersBookingService.createExternalWholeYachtBooking(c.get("db"), {
448
+ adapter: ext.adapter,
449
+ voyageRef: ext.sourceRef,
450
+ currency: payload.currency,
451
+ personId: payload.personId ?? null,
452
+ organizationId: payload.organizationId ?? null,
453
+ contact: payload.contact,
454
+ guests: payload.guests,
455
+ notes: payload.notes ?? null,
456
+ }, c.get("userId"));
457
+ return c.json({ data: result }, 201);
458
+ }
459
+ const payload = await parseJsonBody(c, createWholeYachtBookingPayload);
460
+ if (payload.voyageId !== parsed.id) {
461
+ return c.json({ error: "voyage_id_mismatch", detail: "URL key and payload voyageId must match" }, 400);
462
+ }
463
+ const result = await chartersBookingService.createWholeYachtBooking(c.get("db"), payload, c.get("userId"));
464
+ return c.json({ data: result }, 201);
465
+ })
466
+ .post("/bookings/:bookingId/myba", async (c) => {
467
+ const contractsService = c.get("chartersContractsService");
468
+ if (!contractsService) {
469
+ return c.json({
470
+ error: "contracts_service_unavailable",
471
+ detail: "MYBA generation requires the legal/contracts service to be wired into Hono context as `chartersContractsService` at app boot.",
472
+ }, 501);
473
+ }
474
+ const payload = await parseJsonBody(c, generateMybaPayload);
475
+ const result = await mybaService.generateContract(c.get("db"), contractsService, {
476
+ bookingId: c.req.param("bookingId"),
477
+ templateIdOverride: payload.templateIdOverride ?? null,
478
+ language: payload.language,
479
+ extraVariables: payload.extraVariables,
480
+ title: payload.title,
481
+ });
482
+ if (result.status === "not_found")
483
+ return c.json({ error: result.status }, 404);
484
+ if (result.status === "wrong_mode")
485
+ return c.json({ error: result.status, ...result }, 409);
486
+ if (result.status === "no_template")
487
+ return c.json({ error: result.status }, 412);
488
+ if (result.status === "template_not_found")
489
+ return c.json({ error: result.status, ...result }, 404);
490
+ if (result.status === "contract_create_failed")
491
+ return c.json({ error: result.status }, 500);
492
+ return c.json({
493
+ data: { contractId: result.contractId, charterDetails: result.detail },
494
+ });
495
+ })
496
+ // --- yachts ---
497
+ .get("/yachts", async (c) => {
498
+ const query = parseQuery(c, yachtListQuerySchema);
499
+ const result = await chartersService.listYachts(c.get("db"), query);
500
+ return c.json(result);
501
+ })
502
+ .post("/yachts", async (c) => {
503
+ const data = await parseJsonBody(c, insertYachtSchema);
504
+ const row = await chartersService.createYacht(c.get("db"), data);
505
+ return c.json({ data: row }, 201);
506
+ })
507
+ .get("/yachts/:key", async (c) => {
508
+ const parsed = parseUnifiedKey(c.req.param("key"));
509
+ if (parsed.kind === "invalid")
510
+ return c.json(invalidKey(parsed.raw), 400);
511
+ if (parsed.kind === "external") {
512
+ const ext = resolveExternal(parsed);
513
+ if (!ext)
514
+ return c.json(adapterNotRegistered(parsed.provider), 501);
515
+ const yacht = await ext.adapter.fetchYacht(ext.sourceRef);
516
+ if (!yacht)
517
+ return c.json({ error: "not_found" }, 404);
518
+ return c.json({
519
+ data: {
520
+ source: "external",
521
+ sourceProvider: ext.adapter.name,
522
+ sourceRef: yacht.sourceRef,
523
+ yacht,
524
+ },
525
+ });
526
+ }
527
+ const row = await chartersService.getYachtById(c.get("db"), parsed.id);
528
+ if (!row)
529
+ return c.json({ error: "not_found" }, 404);
530
+ return c.json({ data: row });
531
+ })
532
+ .put("/yachts/:key", async (c) => {
533
+ const parsed = parseUnifiedKey(c.req.param("key"));
534
+ if (parsed.kind === "invalid")
535
+ return c.json(invalidKey(parsed.raw), 400);
536
+ if (parsed.kind === "external")
537
+ return c.json(externalReadOnly, 409);
538
+ const data = await parseJsonBody(c, updateYachtSchema);
539
+ const row = await chartersService.updateYacht(c.get("db"), parsed.id, data);
540
+ if (!row)
541
+ return c.json({ error: "not_found" }, 404);
542
+ return c.json({ data: row });
543
+ });