@voyantjs/bookings 0.4.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/extensions/suppliers.d.ts.map +1 -1
  2. package/dist/extensions/suppliers.js +3 -2
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +20 -8
  6. package/dist/route-runtime.d.ts +8 -0
  7. package/dist/route-runtime.d.ts.map +1 -0
  8. package/dist/route-runtime.js +17 -0
  9. package/dist/routes-groups.d.ts +354 -0
  10. package/dist/routes-groups.d.ts.map +1 -0
  11. package/dist/routes-groups.js +62 -0
  12. package/dist/routes-public.d.ts +5 -5
  13. package/dist/routes-public.d.ts.map +1 -1
  14. package/dist/routes-public.js +8 -7
  15. package/dist/routes-shared.d.ts +2 -0
  16. package/dist/routes-shared.d.ts.map +1 -1
  17. package/dist/routes.d.ts +413 -257
  18. package/dist/routes.d.ts.map +1 -1
  19. package/dist/routes.js +100 -92
  20. package/dist/schema-core.d.ts +1 -1
  21. package/dist/schema-groups.d.ts +261 -0
  22. package/dist/schema-groups.d.ts.map +1 -0
  23. package/dist/schema-groups.js +34 -0
  24. package/dist/schema-items.d.ts +2 -2
  25. package/dist/schema-relations.d.ts +8 -0
  26. package/dist/schema-relations.d.ts.map +1 -1
  27. package/dist/schema-relations.js +15 -0
  28. package/dist/schema.d.ts +1 -0
  29. package/dist/schema.d.ts.map +1 -1
  30. package/dist/schema.js +1 -0
  31. package/dist/service-groups.d.ts +69 -0
  32. package/dist/service-groups.d.ts.map +1 -0
  33. package/dist/service-groups.js +199 -0
  34. package/dist/service-public.d.ts +14 -14
  35. package/dist/service.d.ts +22 -22
  36. package/dist/service.d.ts.map +1 -1
  37. package/dist/service.js +3 -0
  38. package/dist/validation-public.d.ts +11 -11
  39. package/dist/validation-shared.d.ts +3 -3
  40. package/dist/validation.d.ts +59 -12
  41. package/dist/validation.d.ts.map +1 -1
  42. package/dist/validation.js +24 -0
  43. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,KAAK,GAAG,EAAiB,MAAM,oBAAoB,CAAA;AA2J5D,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCA44BtB,CAAA;AAEJ,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA;AAChD,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AAqM7C,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iDAg3Ba,CAAA;AAEvC,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA;AAChD,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAA"}
package/dist/routes.js CHANGED
@@ -1,9 +1,11 @@
1
- import { createKmsProviderFromEnv } from "@voyantjs/utils";
1
+ import { ForbiddenApiError, handleApiError, normalizeValidationError, parseJsonBody, parseQuery, requireUserId, UnauthorizedApiError, } from "@voyantjs/hono";
2
2
  import { Hono } from "hono";
3
3
  import { createBookingPiiService } from "./pii.js";
4
- import { getRuntimeEnv } from "./routes-shared.js";
4
+ import { BOOKING_ROUTE_RUNTIME_CONTAINER_KEY, buildBookingRouteRuntime, } from "./route-runtime.js";
5
+ import { bookingGroupRoutes } from "./routes-groups.js";
5
6
  import { bookingPiiAccessLog } from "./schema.js";
6
7
  import { bookingsService } from "./service.js";
8
+ import { bookingGroupsService } from "./service-groups.js";
7
9
  import { publicBookingsService } from "./service-public.js";
8
10
  import { bookingListQuerySchema, cancelBookingSchema, confirmBookingSchema, convertProductSchema, createBookingSchema, expireBookingSchema, expireStaleBookingsSchema, extendBookingHoldSchema, insertBookingDocumentSchema, insertBookingFulfillmentSchema, insertBookingItemParticipantSchema, insertBookingItemSchema, insertBookingNoteSchema, insertParticipantSchema, insertPassengerSchema, insertSupplierStatusSchema, internalBookingOverviewLookupQuerySchema, recordBookingRedemptionSchema, reserveBookingFromTransactionSchema, reserveBookingSchema, updateBookingFulfillmentSchema, updateBookingItemSchema, updateBookingSchema, updateBookingStatusSchema, updateParticipantSchema, updatePassengerSchema, updateSupplierStatusSchema, upsertParticipantTravelDetailsSchema, } from "./validation.js";
9
11
  function hasPiiScope(scopes, action) {
@@ -41,7 +43,10 @@ async function authorizeBookingPiiAccess(c, input) {
41
43
  outcome: "denied",
42
44
  reason: "missing_user",
43
45
  });
44
- return { allowed: false, response: c.json({ error: "Unauthorized" }, 401) };
46
+ return {
47
+ allowed: false,
48
+ response: handleApiError(new UnauthorizedApiError(), c),
49
+ };
45
50
  }
46
51
  const customAuthorizer = c.get("authorizeBookingPii");
47
52
  if (customAuthorizer) {
@@ -60,7 +65,10 @@ async function authorizeBookingPiiAccess(c, input) {
60
65
  outcome: "denied",
61
66
  reason: "custom_policy_denied",
62
67
  });
63
- return { allowed: false, response: c.json({ error: "Forbidden" }, 403) };
68
+ return {
69
+ allowed: false,
70
+ response: handleApiError(new ForbiddenApiError(), c),
71
+ };
64
72
  }
65
73
  return { allowed: true };
66
74
  }
@@ -74,7 +82,10 @@ async function authorizeBookingPiiAccess(c, input) {
74
82
  reason: "insufficient_scope",
75
83
  metadata: { actor: actor ?? null },
76
84
  });
77
- return { allowed: false, response: c.json({ error: "Forbidden" }, 403) };
85
+ return {
86
+ allowed: false,
87
+ response: handleApiError(new ForbiddenApiError(), c),
88
+ };
78
89
  }
79
90
  return { allowed: true };
80
91
  }
@@ -87,6 +98,33 @@ function handleKmsConfigError(c, error) {
87
98
  }
88
99
  return c.json({ error: "Booking PII encryption is not configured" }, 500);
89
100
  }
101
+ function getRouteRuntime(c) {
102
+ try {
103
+ return (c.var.container?.resolve(BOOKING_ROUTE_RUNTIME_CONTAINER_KEY) ??
104
+ buildBookingRouteRuntime(c.env));
105
+ }
106
+ catch {
107
+ return buildBookingRouteRuntime(c.env);
108
+ }
109
+ }
110
+ function createAuditedBookingPiiService(c, bookingId) {
111
+ const runtime = getRouteRuntime(c);
112
+ return createBookingPiiService({
113
+ kms: runtime.getKmsProvider(),
114
+ onAudit: async (event) => {
115
+ await logBookingPiiAccess(c, {
116
+ bookingId,
117
+ participantId: event.participantId,
118
+ action: event.action === "encrypt"
119
+ ? "update"
120
+ : event.action === "decrypt"
121
+ ? "read"
122
+ : event.action,
123
+ outcome: "allowed",
124
+ });
125
+ },
126
+ });
127
+ }
90
128
  // ==========================================================================
91
129
  // Bookings — method-chained for Hono RPC type inference
92
130
  // ==========================================================================
@@ -96,12 +134,12 @@ export const bookingRoutes = new Hono()
96
134
  // ==========================================================================
97
135
  // 1. GET / — List bookings
98
136
  .get("/", async (c) => {
99
- const query = bookingListQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams));
137
+ const query = parseQuery(c, bookingListQuerySchema);
100
138
  return c.json(await bookingsService.listBookings(c.get("db"), query));
101
139
  })
102
140
  // 1a. GET /overview — Internal/admin booking overview lookup
103
141
  .get("/overview", async (c) => {
104
- const overview = await publicBookingsService.getOverviewByLookup(c.get("db"), internalBookingOverviewLookupQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams)));
142
+ const overview = await publicBookingsService.getOverviewByLookup(c.get("db"), parseQuery(c, internalBookingOverviewLookupQuerySchema));
105
143
  if (!overview) {
106
144
  return c.json({ error: "Booking overview not found" }, 404);
107
145
  }
@@ -117,7 +155,7 @@ export const bookingRoutes = new Hono()
117
155
  })
118
156
  // 3. POST /reserve — Reserve inventory and create on-hold booking
119
157
  .post("/reserve", async (c) => {
120
- const result = await bookingsService.reserveBooking(c.get("db"), reserveBookingSchema.parse(await c.req.json()), c.get("userId"));
158
+ const result = await bookingsService.reserveBooking(c.get("db"), await parseJsonBody(c, reserveBookingSchema), c.get("userId"));
121
159
  if ("booking" in result) {
122
160
  return c.json({ data: result.booking }, 201);
123
161
  }
@@ -137,7 +175,7 @@ export const bookingRoutes = new Hono()
137
175
  })
138
176
  // 3a. POST /from-product — Create booking draft from product definition
139
177
  .post("/from-product", async (c) => {
140
- const row = await bookingsService.createBookingFromProduct(c.get("db"), convertProductSchema.parse(await c.req.json()), c.get("userId"));
178
+ const row = await bookingsService.createBookingFromProduct(c.get("db"), await parseJsonBody(c, convertProductSchema), c.get("userId"));
141
179
  if (!row) {
142
180
  return c.json({ error: "Product or option not found" }, 404);
143
181
  }
@@ -145,7 +183,7 @@ export const bookingRoutes = new Hono()
145
183
  })
146
184
  // 3b. POST /from-offer/:offerId/reserve — Reserve booking from transaction offer
147
185
  .post("/from-offer/:offerId/reserve", async (c) => {
148
- const result = await bookingsService.reserveBookingFromOffer(c.get("db"), c.req.param("offerId"), reserveBookingFromTransactionSchema.parse(await c.req.json()), c.get("userId"));
186
+ const result = await bookingsService.reserveBookingFromOffer(c.get("db"), c.req.param("offerId"), await parseJsonBody(c, reserveBookingFromTransactionSchema), c.get("userId"));
149
187
  if (result.status === "not_found") {
150
188
  return c.json({ error: "Offer not found" }, 404);
151
189
  }
@@ -168,7 +206,7 @@ export const bookingRoutes = new Hono()
168
206
  })
169
207
  // 3c. POST /from-order/:orderId/reserve — Reserve booking from transaction order
170
208
  .post("/from-order/:orderId/reserve", async (c) => {
171
- const result = await bookingsService.reserveBookingFromOrder(c.get("db"), c.req.param("orderId"), reserveBookingFromTransactionSchema.parse(await c.req.json()), c.get("userId"));
209
+ const result = await bookingsService.reserveBookingFromOrder(c.get("db"), c.req.param("orderId"), await parseJsonBody(c, reserveBookingFromTransactionSchema), c.get("userId"));
172
210
  if (result.status === "not_found") {
173
211
  return c.json({ error: "Order not found" }, 404);
174
212
  }
@@ -191,21 +229,27 @@ export const bookingRoutes = new Hono()
191
229
  })
192
230
  // 4. POST / — Create booking (manual/backoffice only)
193
231
  .post("/", async (c) => {
194
- const payload = await c.req.json();
195
- const parsed = createBookingSchema.safeParse(payload);
196
- if (!parsed.success) {
232
+ try {
197
233
  return c.json({
198
- error: parsed.error.issues[0]?.message ?? "Invalid booking create payload",
199
- details: parsed.error.flatten(),
200
- }, 400);
234
+ data: await bookingsService.createBooking(c.get("db"), await parseJsonBody(c, createBookingSchema, {
235
+ invalidBodyMessage: "Invalid booking create payload",
236
+ }), c.get("userId")),
237
+ }, 201);
238
+ }
239
+ catch (error) {
240
+ const validationError = normalizeValidationError(error);
241
+ if (validationError?.status === 400) {
242
+ return c.json({
243
+ error: validationError.message,
244
+ details: validationError.details?.fields ?? validationError.details,
245
+ }, 400);
246
+ }
247
+ throw error;
201
248
  }
202
- return c.json({
203
- data: await bookingsService.createBooking(c.get("db"), parsed.data, c.get("userId")),
204
- }, 201);
205
249
  })
206
250
  // 5. PATCH /:id — Update booking
207
251
  .patch("/:id", async (c) => {
208
- const row = await bookingsService.updateBooking(c.get("db"), c.req.param("id"), updateBookingSchema.parse(await c.req.json()));
252
+ const row = await bookingsService.updateBooking(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateBookingSchema));
209
253
  if (!row) {
210
254
  return c.json({ error: "Booking not found" }, 404);
211
255
  }
@@ -224,7 +268,7 @@ export const bookingRoutes = new Hono()
224
268
  // ==========================================================================
225
269
  // 7. PATCH /:id/status — Change booking status
226
270
  .patch("/:id/status", async (c) => {
227
- const result = await bookingsService.updateBookingStatus(c.get("db"), c.req.param("id"), updateBookingStatusSchema.parse(await c.req.json()), c.get("userId"));
271
+ const result = await bookingsService.updateBookingStatus(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateBookingStatusSchema), c.get("userId"));
228
272
  if (result.status === "not_found") {
229
273
  return c.json({ error: "Booking not found" }, 404);
230
274
  }
@@ -238,7 +282,7 @@ export const bookingRoutes = new Hono()
238
282
  })
239
283
  // 8. POST /:id/confirm — Confirm an on-hold booking
240
284
  .post("/:id/confirm", async (c) => {
241
- const result = await bookingsService.confirmBooking(c.get("db"), c.req.param("id"), confirmBookingSchema.parse(await c.req.json()), c.get("userId"));
285
+ const result = await bookingsService.confirmBooking(c.get("db"), c.req.param("id"), await parseJsonBody(c, confirmBookingSchema), c.get("userId"));
242
286
  if (result.status === "not_found") {
243
287
  return c.json({ error: "Booking not found" }, 404);
244
288
  }
@@ -255,7 +299,7 @@ export const bookingRoutes = new Hono()
255
299
  })
256
300
  // 9. POST /:id/extend-hold — Extend booking hold expiry
257
301
  .post("/:id/extend-hold", async (c) => {
258
- const result = await bookingsService.extendBookingHold(c.get("db"), c.req.param("id"), extendBookingHoldSchema.parse(await c.req.json()), c.get("userId"));
302
+ const result = await bookingsService.extendBookingHold(c.get("db"), c.req.param("id"), await parseJsonBody(c, extendBookingHoldSchema), c.get("userId"));
259
303
  if (result.status === "not_found") {
260
304
  return c.json({ error: "Booking not found" }, 404);
261
305
  }
@@ -272,7 +316,7 @@ export const bookingRoutes = new Hono()
272
316
  })
273
317
  // 10. POST /:id/expire — Expire an on-hold booking
274
318
  .post("/:id/expire", async (c) => {
275
- const result = await bookingsService.expireBooking(c.get("db"), c.req.param("id"), expireBookingSchema.parse(await c.req.json()), c.get("userId"));
319
+ const result = await bookingsService.expireBooking(c.get("db"), c.req.param("id"), await parseJsonBody(c, expireBookingSchema), c.get("userId"));
276
320
  if (result.status === "not_found") {
277
321
  return c.json({ error: "Booking not found" }, 404);
278
322
  }
@@ -286,11 +330,11 @@ export const bookingRoutes = new Hono()
286
330
  })
287
331
  // 10b. POST /expire-stale — Expire all stale on-hold bookings up to a cutoff
288
332
  .post("/expire-stale", async (c) => {
289
- return c.json(await bookingsService.expireStaleBookings(c.get("db"), expireStaleBookingsSchema.parse(await c.req.json()), c.get("userId")));
333
+ return c.json(await bookingsService.expireStaleBookings(c.get("db"), await parseJsonBody(c, expireStaleBookingsSchema), c.get("userId")));
290
334
  })
291
335
  // 11. POST /:id/cancel — Cancel a booking and release allocations
292
336
  .post("/:id/cancel", async (c) => {
293
- const result = await bookingsService.cancelBooking(c.get("db"), c.req.param("id"), cancelBookingSchema.parse(await c.req.json()), c.get("userId"));
337
+ const result = await bookingsService.cancelBooking(c.get("db"), c.req.param("id"), await parseJsonBody(c, cancelBookingSchema), c.get("userId"));
294
338
  if (result.status === "not_found") {
295
339
  return c.json({ error: "Booking not found" }, 404);
296
340
  }
@@ -335,21 +379,7 @@ export const bookingRoutes = new Hono()
335
379
  return c.json({ error: "Participant not found" }, 404);
336
380
  }
337
381
  try {
338
- const pii = createBookingPiiService({
339
- kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
340
- onAudit: async (event) => {
341
- await logBookingPiiAccess(c, {
342
- bookingId: participant.bookingId,
343
- participantId: event.participantId,
344
- action: event.action === "encrypt"
345
- ? "update"
346
- : event.action === "decrypt"
347
- ? "read"
348
- : event.action,
349
- outcome: "allowed",
350
- });
351
- },
352
- });
382
+ const pii = createAuditedBookingPiiService(c, participant.bookingId);
353
383
  const details = await pii.getParticipantTravelDetails(c.get("db"), participant.id, c.get("userId"));
354
384
  if (!details) {
355
385
  await logBookingPiiAccess(c, {
@@ -369,7 +399,7 @@ export const bookingRoutes = new Hono()
369
399
  })
370
400
  // 9. POST /:id/participants — Add participant
371
401
  .post("/:id/participants", async (c) => {
372
- const row = await bookingsService.createParticipant(c.get("db"), c.req.param("id"), insertParticipantSchema.parse(await c.req.json()), c.get("userId"));
402
+ const row = await bookingsService.createParticipant(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertParticipantSchema), c.get("userId"));
373
403
  if (!row) {
374
404
  return c.json({ error: "Booking not found" }, 404);
375
405
  }
@@ -397,22 +427,8 @@ export const bookingRoutes = new Hono()
397
427
  return c.json({ error: "Participant not found" }, 404);
398
428
  }
399
429
  try {
400
- const pii = createBookingPiiService({
401
- kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
402
- onAudit: async (event) => {
403
- await logBookingPiiAccess(c, {
404
- bookingId: participant.bookingId,
405
- participantId: event.participantId,
406
- action: event.action === "encrypt"
407
- ? "update"
408
- : event.action === "decrypt"
409
- ? "read"
410
- : event.action,
411
- outcome: "allowed",
412
- });
413
- },
414
- });
415
- const row = await pii.upsertParticipantTravelDetails(c.get("db"), participant.id, upsertParticipantTravelDetailsSchema.parse(await c.req.json()), c.get("userId"));
430
+ const pii = createAuditedBookingPiiService(c, participant.bookingId);
431
+ const row = await pii.upsertParticipantTravelDetails(c.get("db"), participant.id, await parseJsonBody(c, upsertParticipantTravelDetailsSchema), c.get("userId"));
416
432
  if (!row) {
417
433
  return c.json({ error: "Participant not found" }, 404);
418
434
  }
@@ -424,7 +440,7 @@ export const bookingRoutes = new Hono()
424
440
  })
425
441
  // 10. PATCH /:id/participants/:participantId — Update participant
426
442
  .patch("/:id/participants/:participantId", async (c) => {
427
- const row = await bookingsService.updateParticipant(c.get("db"), c.req.param("participantId"), updateParticipantSchema.parse(await c.req.json()));
443
+ const row = await bookingsService.updateParticipant(c.get("db"), c.req.param("participantId"), await parseJsonBody(c, updateParticipantSchema));
428
444
  if (!row) {
429
445
  return c.json({ error: "Participant not found" }, 404);
430
446
  }
@@ -452,21 +468,7 @@ export const bookingRoutes = new Hono()
452
468
  return c.json({ error: "Participant not found" }, 404);
453
469
  }
454
470
  try {
455
- const pii = createBookingPiiService({
456
- kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
457
- onAudit: async (event) => {
458
- await logBookingPiiAccess(c, {
459
- bookingId: participant.bookingId,
460
- participantId: event.participantId,
461
- action: event.action === "encrypt"
462
- ? "update"
463
- : event.action === "decrypt"
464
- ? "read"
465
- : event.action,
466
- outcome: "allowed",
467
- });
468
- },
469
- });
471
+ const pii = createAuditedBookingPiiService(c, participant.bookingId);
470
472
  const row = await pii.deleteParticipantTravelDetails(c.get("db"), participant.id, c.get("userId"));
471
473
  if (!row) {
472
474
  return c.json({ error: "Participant travel details not found" }, 404);
@@ -494,7 +496,7 @@ export const bookingRoutes = new Hono()
494
496
  })
495
497
  // 13. POST /:id/passengers — Add passenger
496
498
  .post("/:id/passengers", async (c) => {
497
- const row = await bookingsService.createPassenger(c.get("db"), c.req.param("id"), insertPassengerSchema.parse(await c.req.json()), c.get("userId"));
499
+ const row = await bookingsService.createPassenger(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertPassengerSchema), c.get("userId"));
498
500
  if (!row) {
499
501
  return c.json({ error: "Booking not found" }, 404);
500
502
  }
@@ -502,7 +504,7 @@ export const bookingRoutes = new Hono()
502
504
  })
503
505
  // 14. PATCH /:id/passengers/:passengerId — Update passenger
504
506
  .patch("/:id/passengers/:passengerId", async (c) => {
505
- const row = await bookingsService.updatePassenger(c.get("db"), c.req.param("passengerId"), updatePassengerSchema.parse(await c.req.json()));
507
+ const row = await bookingsService.updatePassenger(c.get("db"), c.req.param("passengerId"), await parseJsonBody(c, updatePassengerSchema));
506
508
  if (!row) {
507
509
  return c.json({ error: "Passenger not found" }, 404);
508
510
  }
@@ -525,7 +527,7 @@ export const bookingRoutes = new Hono()
525
527
  })
526
528
  // 17. POST /:id/items — Add booking item
527
529
  .post("/:id/items", async (c) => {
528
- const row = await bookingsService.createItem(c.get("db"), c.req.param("id"), insertBookingItemSchema.parse(await c.req.json()), c.get("userId"));
530
+ const row = await bookingsService.createItem(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertBookingItemSchema), c.get("userId"));
529
531
  if (!row) {
530
532
  return c.json({ error: "Booking not found" }, 404);
531
533
  }
@@ -533,7 +535,7 @@ export const bookingRoutes = new Hono()
533
535
  })
534
536
  // 18. PATCH /:id/items/:itemId — Update booking item
535
537
  .patch("/:id/items/:itemId", async (c) => {
536
- const row = await bookingsService.updateItem(c.get("db"), c.req.param("itemId"), updateBookingItemSchema.parse(await c.req.json()));
538
+ const row = await bookingsService.updateItem(c.get("db"), c.req.param("itemId"), await parseJsonBody(c, updateBookingItemSchema));
537
539
  if (!row) {
538
540
  return c.json({ error: "Booking item not found" }, 404);
539
541
  }
@@ -555,7 +557,7 @@ export const bookingRoutes = new Hono()
555
557
  })
556
558
  // 21. POST /:id/items/:itemId/participants — Link participant to item
557
559
  .post("/:id/items/:itemId/participants", async (c) => {
558
- const row = await bookingsService.addItemParticipant(c.get("db"), c.req.param("itemId"), insertBookingItemParticipantSchema.parse(await c.req.json()));
560
+ const row = await bookingsService.addItemParticipant(c.get("db"), c.req.param("itemId"), await parseJsonBody(c, insertBookingItemParticipantSchema));
559
561
  if (!row) {
560
562
  return c.json({ error: "Booking item or participant not found" }, 404);
561
563
  }
@@ -578,14 +580,14 @@ export const bookingRoutes = new Hono()
578
580
  });
579
581
  })
580
582
  .post("/:id/supplier-statuses", async (c) => {
581
- const row = await bookingsService.createSupplierStatus(c.get("db"), c.req.param("id"), insertSupplierStatusSchema.parse(await c.req.json()), c.get("userId"));
583
+ const row = await bookingsService.createSupplierStatus(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertSupplierStatusSchema), c.get("userId"));
582
584
  if (!row) {
583
585
  return c.json({ error: "Booking not found" }, 404);
584
586
  }
585
587
  return c.json({ data: row }, 201);
586
588
  })
587
589
  .patch("/:id/supplier-statuses/:statusId", async (c) => {
588
- const row = await bookingsService.updateSupplierStatus(c.get("db"), c.req.param("id"), c.req.param("statusId"), updateSupplierStatusSchema.parse(await c.req.json()), c.get("userId"));
590
+ const row = await bookingsService.updateSupplierStatus(c.get("db"), c.req.param("id"), c.req.param("statusId"), await parseJsonBody(c, updateSupplierStatusSchema), c.get("userId"));
589
591
  if (!row) {
590
592
  return c.json({ error: "Supplier status not found" }, 404);
591
593
  }
@@ -598,14 +600,14 @@ export const bookingRoutes = new Hono()
598
600
  return c.json({ data: await bookingsService.listFulfillments(c.get("db"), c.req.param("id")) });
599
601
  })
600
602
  .post("/:id/fulfillments", async (c) => {
601
- const row = await bookingsService.issueFulfillment(c.get("db"), c.req.param("id"), insertBookingFulfillmentSchema.parse(await c.req.json()), c.get("userId"));
603
+ const row = await bookingsService.issueFulfillment(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertBookingFulfillmentSchema), c.get("userId"));
602
604
  if (!row) {
603
605
  return c.json({ error: "Booking, item, or participant not found" }, 404);
604
606
  }
605
607
  return c.json({ data: row }, 201);
606
608
  })
607
609
  .patch("/:id/fulfillments/:fulfillmentId", async (c) => {
608
- const row = await bookingsService.updateFulfillment(c.get("db"), c.req.param("id"), c.req.param("fulfillmentId"), updateBookingFulfillmentSchema.parse(await c.req.json()), c.get("userId"));
610
+ const row = await bookingsService.updateFulfillment(c.get("db"), c.req.param("id"), c.req.param("fulfillmentId"), await parseJsonBody(c, updateBookingFulfillmentSchema), c.get("userId"));
609
611
  if (!row) {
610
612
  return c.json({ error: "Fulfillment, item, or participant not found" }, 404);
611
613
  }
@@ -620,7 +622,7 @@ export const bookingRoutes = new Hono()
620
622
  });
621
623
  })
622
624
  .post("/:id/redemptions", async (c) => {
623
- const row = await bookingsService.recordRedemption(c.get("db"), c.req.param("id"), recordBookingRedemptionSchema.parse(await c.req.json()), c.get("userId"));
625
+ const row = await bookingsService.recordRedemption(c.get("db"), c.req.param("id"), await parseJsonBody(c, recordBookingRedemptionSchema), c.get("userId"));
624
626
  if (!row) {
625
627
  return c.json({ error: "Booking, item, or participant not found" }, 404);
626
628
  }
@@ -632,6 +634,11 @@ export const bookingRoutes = new Hono()
632
634
  // 26. GET /:id/activity — List activity log
633
635
  .get("/:id/activity", async (c) => {
634
636
  return c.json({ data: await bookingsService.listActivity(c.get("db"), c.req.param("id")) });
637
+ })
638
+ // 26a. GET /:id/group — Shared-room group membership for this booking (or null)
639
+ .get("/:id/group", async (c) => {
640
+ const result = await bookingGroupsService.getBookingGroupForBooking(c.get("db"), c.req.param("id"));
641
+ return c.json({ data: result ?? null });
635
642
  })
636
643
  // ==========================================================================
637
644
  // Notes
@@ -642,11 +649,8 @@ export const bookingRoutes = new Hono()
642
649
  })
643
650
  // 28. POST /:id/notes — Add note
644
651
  .post("/:id/notes", async (c) => {
645
- const userId = c.get("userId");
646
- if (!userId) {
647
- return c.json({ error: "User ID required to create notes" }, 400);
648
- }
649
- const row = await bookingsService.createNote(c.get("db"), c.req.param("id"), userId, insertBookingNoteSchema.parse(await c.req.json()));
652
+ const userId = requireUserId(c);
653
+ const row = await bookingsService.createNote(c.get("db"), c.req.param("id"), userId, await parseJsonBody(c, insertBookingNoteSchema));
650
654
  if (!row) {
651
655
  return c.json({ error: "Booking not found" }, 404);
652
656
  }
@@ -661,7 +665,7 @@ export const bookingRoutes = new Hono()
661
665
  })
662
666
  // 30. POST /:id/documents — Add document to booking
663
667
  .post("/:id/documents", async (c) => {
664
- const row = await bookingsService.createDocument(c.get("db"), c.req.param("id"), insertBookingDocumentSchema.parse(await c.req.json()));
668
+ const row = await bookingsService.createDocument(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertBookingDocumentSchema));
665
669
  if (!row) {
666
670
  return c.json({ error: "Booking not found" }, 404);
667
671
  }
@@ -674,4 +678,8 @@ export const bookingRoutes = new Hono()
674
678
  return c.json({ error: "Document not found" }, 404);
675
679
  }
676
680
  return c.json({ success: true }, 200);
677
- });
681
+ })
682
+ // ==========================================================================
683
+ // Booking Groups (shared-room / split-booking model)
684
+ // ==========================================================================
685
+ .route("/groups", bookingGroupRoutes);
@@ -92,7 +92,7 @@ export declare const bookings: import("drizzle-orm/pg-core").PgTableWithColumns<
92
92
  tableName: "bookings";
93
93
  dataType: "string";
94
94
  columnType: "PgEnumColumn";
95
- data: "internal" | "direct" | "manual" | "affiliate" | "ota" | "reseller" | "api_partner";
95
+ data: "internal" | "reseller" | "direct" | "manual" | "affiliate" | "ota" | "api_partner";
96
96
  driverParam: string;
97
97
  notNull: true;
98
98
  hasDefault: true;