@voyantjs/bookings 0.119.2 → 0.119.3

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 (37) hide show
  1. package/dist/pricing-assignment/age.d.ts +33 -0
  2. package/dist/pricing-assignment/age.d.ts.map +1 -0
  3. package/dist/pricing-assignment/age.js +109 -0
  4. package/dist/pricing-assignment/draft.d.ts +69 -0
  5. package/dist/pricing-assignment/draft.d.ts.map +1 -0
  6. package/dist/pricing-assignment/draft.js +284 -0
  7. package/dist/pricing-assignment/types.d.ts +113 -0
  8. package/dist/pricing-assignment/types.d.ts.map +1 -0
  9. package/dist/pricing-assignment/types.js +30 -0
  10. package/dist/pricing-assignment/unit-helpers.d.ts +11 -0
  11. package/dist/pricing-assignment/unit-helpers.d.ts.map +1 -0
  12. package/dist/pricing-assignment/unit-helpers.js +19 -0
  13. package/dist/pricing-assignment/verify.d.ts +67 -0
  14. package/dist/pricing-assignment/verify.d.ts.map +1 -0
  15. package/dist/pricing-assignment/verify.js +121 -0
  16. package/dist/pricing-assignment.d.ts +4 -275
  17. package/dist/pricing-assignment.d.ts.map +1 -1
  18. package/dist/pricing-assignment.js +4 -559
  19. package/dist/routes-admin.d.ts +2894 -0
  20. package/dist/routes-admin.d.ts.map +1 -0
  21. package/dist/routes-admin.js +2111 -0
  22. package/dist/routes.d.ts +1 -2893
  23. package/dist/routes.d.ts.map +1 -1
  24. package/dist/routes.js +1 -2111
  25. package/dist/service-core.d.ts +6240 -0
  26. package/dist/service-core.d.ts.map +1 -0
  27. package/dist/service-core.js +4350 -0
  28. package/dist/service-public-core.d.ts +907 -0
  29. package/dist/service-public-core.d.ts.map +1 -0
  30. package/dist/service-public-core.js +1176 -0
  31. package/dist/service-public.d.ts +1 -906
  32. package/dist/service-public.d.ts.map +1 -1
  33. package/dist/service-public.js +1 -1176
  34. package/dist/service.d.ts +1 -6239
  35. package/dist/service.d.ts.map +1 -1
  36. package/dist/service.js +1 -4350
  37. package/package.json +5 -5
package/dist/routes.js CHANGED
@@ -1,2111 +1 @@
1
- // agent-quality: file-size exception -- owner: bookings; existing route module stays co-located until a dedicated split preserves behavior and tests.
2
- import { ACTION_LEDGER_APPROVAL_ID_HEADER, ActionApprovalDecisionConflictError, ActionLedgerIdempotencyConflictError, actionLedgerService, appendActionLedgerMutation, appendActionLedgerSensitiveRead, buildActionApprovalCommandFingerprint, buildActionLedgerApprovedExecutionFields, decideActionLedgerApproval, evaluateActionLedgerApprovalRequirement, evaluateActionLedgerCapabilityAccess, ledgerSensitiveRead, mapActionLedgerRequestContext, requestActionLedgerApproval, } from "@voyantjs/action-ledger";
3
- import { aggregateSnapshotKey, readThroughAggregateSnapshot, } from "@voyantjs/db/aggregate-snapshots";
4
- import { ForbiddenApiError, handleApiError, idempotencyKey, normalizeValidationError, parseJsonBody, parseQuery, RequestValidationError, requireUserId, UnauthorizedApiError, } from "@voyantjs/hono";
5
- import { Hono } from "hono";
6
- import { z } from "zod";
7
- import { BOOKING_PII_READ_CAPABILITY, BOOKING_STATUS_CAPABILITIES, } from "./action-ledger-capabilities.js";
8
- import { createBookingPiiService } from "./pii.js";
9
- import { redactBookingContact, redactTravelerIdentity, shouldRevealBookingPii, } from "./pii-redaction.js";
10
- import { BOOKING_ROUTE_RUNTIME_CONTAINER_KEY, buildBookingRouteRuntime, } from "./route-runtime.js";
11
- import { bookingGroupRoutes } from "./routes-groups.js";
12
- import { bookingPiiAccessLog } from "./schema.js";
13
- import { bookingsService } from "./service.js";
14
- import { bookingGroupsService } from "./service-groups.js";
15
- import { publicBookingsService, resolveSessionPricingSnapshot } from "./service-public.js";
16
- import { bookingAggregatesQuerySchema, bookingListQuerySchema, cancelBookingSchema, completeBookingSchema, confirmBookingSchema, convertProductSchema, createBookingSchema, createTravelerWithTravelDetailsSchema, expireBookingSchema, expireStaleBookingsSchema, extendBookingHoldSchema, insertBookingDocumentSchema, insertBookingFulfillmentSchema, insertBookingItemSchema, insertBookingItemTravelerSchema, insertBookingNoteSchema, insertSupplierStatusSchema, insertTravelerSchema, internalBookingOverviewLookupQuerySchema, overrideBookingStatusSchema, pricingPreviewSchema, recordBookingRedemptionSchema, reserveBookingFromTransactionSchema, reserveBookingSchema, sharingGroupsForSlotQuerySchema, startBookingSchema, updateBookingFulfillmentSchema, updateBookingItemSchema, updateBookingNoteSchema, updateBookingSchema, updateSupplierStatusSchema, updateTravelerSchema, updateTravelerWithTravelDetailsSchema, upsertTravelerTravelDetailsSchema, } from "./validation.js";
17
- function hasPiiScope(scopes, action) {
18
- if (!scopes || scopes.length === 0) {
19
- return false;
20
- }
21
- return (scopes.includes("*") ||
22
- scopes.includes("bookings-pii:*") ||
23
- scopes.includes(`bookings-pii:${action}`));
24
- }
25
- const BOOKING_PII_READ_ACTION_NAME = "booking.pii.read";
26
- const BOOKING_PII_READ_ACTION_VERSION = "v1";
27
- const BOOKING_PII_DECISION_POLICY = "bookings-pii-scope-or-staff-v1";
28
- const BOOKING_PII_AUTHORIZATION_SOURCE = "bookings.pii.route";
29
- const BOOKING_STATUS_APPROVAL_POLICY = "bookings-status-approval-v1";
30
- const BOOKING_TRAVELER_LEDGER_ACTION_VERSION = "v1";
31
- const BOOKING_ITEM_LEDGER_ACTION_VERSION = "v1";
32
- const BOOKING_NOTE_LEDGER_ACTION_VERSION = "v1";
33
- const TRAVELER_IDENTITY_DISCLOSED_FIELDS = [
34
- "firstName",
35
- "lastName",
36
- "email",
37
- "phone",
38
- "specialRequests",
39
- "notes",
40
- ];
41
- const TRAVELER_TRAVEL_DETAIL_DISCLOSED_FIELDS = [
42
- "nationality",
43
- "documentType",
44
- "documentNumber",
45
- "documentExpiry",
46
- "documentIssuingCountry",
47
- "documentIssuingAuthority",
48
- "documentPersonDocumentId",
49
- "dateOfBirth",
50
- "dietaryRequirements",
51
- "accessibilityNeeds",
52
- "isLeadTraveler",
53
- "sharingGroupId",
54
- "roomTypeId",
55
- "bedPreference",
56
- "allocations",
57
- ];
58
- const TRAVELER_MUTATION_FIELDS = [
59
- "personId",
60
- "participantType",
61
- "travelerCategory",
62
- "firstName",
63
- "lastName",
64
- "email",
65
- "phone",
66
- "preferredLanguage",
67
- "specialRequests",
68
- "isPrimary",
69
- "notes",
70
- ];
71
- const BOOKING_ITEM_MUTATION_FIELDS = [
72
- "title",
73
- "description",
74
- "itemType",
75
- "status",
76
- "serviceDate",
77
- "startsAt",
78
- "endsAt",
79
- "quantity",
80
- "sellCurrency",
81
- "unitSellAmountCents",
82
- "totalSellAmountCents",
83
- "costCurrency",
84
- "unitCostAmountCents",
85
- "totalCostAmountCents",
86
- "notes",
87
- "productId",
88
- "optionId",
89
- "optionUnitId",
90
- "pricingCategoryId",
91
- "availabilitySlotId",
92
- "productNameSnapshot",
93
- "optionNameSnapshot",
94
- "unitNameSnapshot",
95
- "departureLabelSnapshot",
96
- "sourceSnapshotId",
97
- "sourceOfferId",
98
- "metadata",
99
- ];
100
- const bookingActionLedgerQuerySchema = z
101
- .object({
102
- cursorOccurredAt: z.string().datetime().optional(),
103
- cursorId: z.string().trim().min(1).optional(),
104
- limit: z.coerce.number().int().min(1).max(199).optional(),
105
- })
106
- .superRefine((value, ctx) => {
107
- if (Boolean(value.cursorOccurredAt) === Boolean(value.cursorId))
108
- return;
109
- ctx.addIssue({
110
- code: z.ZodIssueCode.custom,
111
- path: value.cursorOccurredAt ? ["cursorId"] : ["cursorOccurredAt"],
112
- message: "cursorOccurredAt and cursorId must be provided together",
113
- });
114
- })
115
- .transform(({ cursorOccurredAt, cursorId, ...query }) => ({
116
- ...query,
117
- cursor: cursorOccurredAt && cursorId
118
- ? {
119
- occurredAt: cursorOccurredAt,
120
- id: cursorId,
121
- }
122
- : undefined,
123
- }));
124
- const decideBookingActionApprovalBodySchema = z.object({
125
- status: z.enum(["approved", "denied"]),
126
- });
127
- const DASHBOARD_AGGREGATES_CACHE_CONTROL = "private, max-age=30";
128
- /** Server-side snapshot TTL — see readThroughAggregateSnapshot (#1629). */
129
- const DASHBOARD_AGGREGATES_TTL_SECONDS = 60;
130
- function cacheDashboardAggregates(c) {
131
- c.header("Cache-Control", DASHBOARD_AGGREGATES_CACHE_CONTROL);
132
- c.header("Vary", "Authorization", { append: true });
133
- c.header("Vary", "Cookie", { append: true });
134
- }
135
- function getActionLedgerRequestContext(c) {
136
- return {
137
- userId: c.get("userId") ?? null,
138
- agentId: c.get("agentId") ?? null,
139
- workflowPrincipalId: c.get("workflowPrincipalId") ?? null,
140
- principalSubtype: c.get("principalSubtype") ?? null,
141
- sessionId: c.get("sessionId") ?? null,
142
- apiTokenId: c.get("apiTokenId") ?? c.get("apiKeyId") ?? null,
143
- callerType: c.get("callerType") ?? null,
144
- actor: c.get("actor") ?? null,
145
- isInternalRequest: c.get("isInternalRequest") ?? false,
146
- organizationId: c.get("organizationId") ?? null,
147
- workflowRunId: c.get("workflowRunId") ?? null,
148
- workflowStepId: c.get("workflowStepId") ?? null,
149
- correlationId: c.req.header("x-correlation-id") ?? c.req.header("x-request-id") ?? null,
150
- };
151
- }
152
- async function validateBookingBillingPartyReferences(c, data) {
153
- const runtime = getRouteRuntime(c);
154
- const db = c.get("db");
155
- if (data.personId && runtime.resolveBillingPersonById) {
156
- const exists = await runtime.resolveBillingPersonById(db, data.personId);
157
- if (!exists) {
158
- throw new RequestValidationError("Booking personId does not reference an existing person", {
159
- fields: {
160
- fieldErrors: { personId: ["Person not found"] },
161
- formErrors: [],
162
- },
163
- });
164
- }
165
- }
166
- if (data.organizationId && runtime.resolveBillingOrganizationById) {
167
- const exists = await runtime.resolveBillingOrganizationById(db, data.organizationId);
168
- if (!exists) {
169
- throw new RequestValidationError("Booking organizationId does not reference an existing organization", {
170
- fields: {
171
- fieldErrors: { organizationId: ["Organization not found"] },
172
- formErrors: [],
173
- },
174
- });
175
- }
176
- }
177
- }
178
- function changedBookingMutationFields(input, before, after) {
179
- const fields = Object.keys(input).filter((field) => !ignoredBookingMutationFields.has(field));
180
- if (!before || !after)
181
- return fields.sort();
182
- return fields.filter((field) => !bookingValuesEqual(before[field], after[field])).sort();
183
- }
184
- function changedBookingTravelerFields(input, before, after) {
185
- const travelerFields = new Set(TRAVELER_MUTATION_FIELDS);
186
- return changedBookingMutationFields(input, before, after).filter((field) => travelerFields.has(field));
187
- }
188
- function changedBookingTravelDetailFields(input) {
189
- const travelDetailFields = new Set(TRAVELER_TRAVEL_DETAIL_DISCLOSED_FIELDS);
190
- return Object.keys(input)
191
- .filter((field) => travelDetailFields.has(field))
192
- .sort();
193
- }
194
- function changedBookingItemFields(input, before, after) {
195
- const itemFields = new Set(BOOKING_ITEM_MUTATION_FIELDS);
196
- return changedBookingMutationFields(input, before, after).filter((field) => itemFields.has(field));
197
- }
198
- function bookingMutationSummary(action, fields, subject) {
199
- if (action === "delete")
200
- return `Deleted ${subject}`;
201
- if (fields.length === 0)
202
- return action === "create" ? `Created ${subject}` : `Updated ${subject}`;
203
- const verb = action === "create" ? "Created" : "Updated";
204
- return `${verb} ${subject} fields: ${fields.join(", ")}`;
205
- }
206
- async function appendBookingMutationLedgerEntry(c, input) {
207
- return appendBookingMutationLedgerEntryToDb(c.get("db"), getActionLedgerRequestContext(c), input);
208
- }
209
- async function appendBookingMutationLedgerEntryToDb(db, context, input) {
210
- return appendActionLedgerMutation(db, {
211
- context,
212
- actionName: input.actionName,
213
- actionVersion: input.actionVersion,
214
- actionKind: input.action,
215
- evaluatedRisk: input.evaluatedRisk ?? "medium",
216
- targetType: input.targetType,
217
- targetId: input.targetId,
218
- routeOrToolName: input.routeOrToolName,
219
- authorizationSource: input.authorizationSource ?? "bookings.route",
220
- mutationDetail: {
221
- summary: input.summary ?? bookingMutationSummary(input.action, input.changedFields, input.subject),
222
- reversalKind: "none",
223
- },
224
- });
225
- }
226
- async function appendBookingTravelerMutationLedgerEntryToDb(db, context, input) {
227
- return appendBookingMutationLedgerEntryToDb(db, context, {
228
- ...input,
229
- actionName: input.actionName,
230
- actionVersion: BOOKING_TRAVELER_LEDGER_ACTION_VERSION,
231
- targetType: "booking_traveler",
232
- targetId: input.travelerId,
233
- authorizationSource: BOOKING_PII_AUTHORIZATION_SOURCE,
234
- evaluatedRisk: input.evaluatedRisk ?? "high",
235
- });
236
- }
237
- function bookingValuesEqual(left, right) {
238
- if (left instanceof Date || right instanceof Date) {
239
- const leftTime = left instanceof Date ? left.getTime() : new Date(String(left)).getTime();
240
- const rightTime = right instanceof Date ? right.getTime() : new Date(String(right)).getTime();
241
- return leftTime === rightTime;
242
- }
243
- return JSON.stringify(left) === JSON.stringify(right);
244
- }
245
- const ignoredBookingMutationFields = new Set(["updatedAt", "createdAt"]);
246
- async function authorizeBookingStatusMutation(c, input) {
247
- const capability = BOOKING_STATUS_CAPABILITIES[input.key];
248
- const access = evaluateActionLedgerCapabilityAccess({
249
- definition: capability,
250
- actor: c.get("actor"),
251
- callerType: c.get("callerType"),
252
- scopes: c.get("scopes"),
253
- isInternalRequest: c.get("isInternalRequest"),
254
- });
255
- if (access.allowed) {
256
- const approvalRequirement = evaluateActionLedgerApprovalRequirement({
257
- access,
258
- conditionalApprovalRequired: requiresBookingStatusApproval(c, input.key),
259
- reasonCode: bookingStatusApprovalReason(c, input.key),
260
- });
261
- if (approvalRequirement.required) {
262
- const approvedAction = await resolveApprovedBookingStatusAction(c, input, access, approvalRequirement);
263
- if (approvedAction) {
264
- if (!approvedAction.allowed)
265
- return approvedAction;
266
- return { allowed: true, access, approvedAction: approvedAction.action };
267
- }
268
- const idempotencyKey = c.req.header("idempotency-key") ?? null;
269
- if (!idempotencyKey) {
270
- return {
271
- allowed: false,
272
- response: c.json({
273
- error: "Approval-required booking status actions require an Idempotency-Key",
274
- }, 400),
275
- };
276
- }
277
- const idempotencyScope = `${input.routeOrToolName}:${input.bookingId}`;
278
- const idempotencyFingerprint = await buildBookingStatusApprovalFingerprint(input, await loadBookingStatusApprovalTargetState(c, input.bookingId), access, approvalRequirement);
279
- const requestInput = {
280
- context: getActionLedgerRequestContext(c),
281
- actionName: input.actionName,
282
- actionVersion: capability.version,
283
- actionKind: "update",
284
- evaluatedRisk: approvalRequirement.evaluatedRisk,
285
- targetType: "booking",
286
- targetId: input.bookingId,
287
- routeOrToolName: input.routeOrToolName,
288
- capabilityId: access.capabilityId,
289
- capabilityVersion: access.capabilityVersion,
290
- authorizationSource: access.authorizationSource,
291
- idempotencyScope,
292
- idempotencyKey,
293
- idempotencyFingerprint,
294
- mutationDetail: {
295
- summary: `Booking status ${capability.action} awaiting approval: ${approvalRequirement.reasonCode}`,
296
- reversalKind: "none",
297
- },
298
- approval: {
299
- policyName: BOOKING_STATUS_APPROVAL_POLICY,
300
- policyVersion: capability.version,
301
- riskSnapshot: approvalRequirement.evaluatedRisk,
302
- reasonCode: approvalRequirement.reasonCode,
303
- },
304
- };
305
- let result;
306
- try {
307
- result = await requestActionLedgerApproval(c.get("db"), requestInput);
308
- }
309
- catch (error) {
310
- if (error instanceof ActionLedgerIdempotencyConflictError) {
311
- return {
312
- allowed: false,
313
- response: c.json({
314
- error: error.message,
315
- existingActionId: error.existingActionId,
316
- }, 409),
317
- };
318
- }
319
- throw error;
320
- }
321
- return {
322
- allowed: false,
323
- response: c.json({
324
- data: {
325
- approvalRequired: true,
326
- requestedAction: {
327
- id: result.requestedAction.id,
328
- status: result.requestedAction.status,
329
- actionName: result.requestedAction.actionName,
330
- targetType: result.requestedAction.targetType,
331
- targetId: result.requestedAction.targetId,
332
- },
333
- approval: {
334
- id: result.approval.id,
335
- status: result.approval.status,
336
- requestedActionId: result.approval.requestedActionId,
337
- requestedByPrincipalId: result.approval.requestedByPrincipalId,
338
- assignedToPrincipalId: result.approval.assignedToPrincipalId,
339
- policyName: result.approval.policyName,
340
- policyVersion: result.approval.policyVersion,
341
- riskSnapshot: result.approval.riskSnapshot,
342
- reasonCode: result.approval.reasonCode,
343
- expiresAt: result.approval.expiresAt,
344
- createdAt: result.approval.createdAt,
345
- },
346
- replayed: result.replayed,
347
- },
348
- }, 202),
349
- };
350
- }
351
- return { allowed: true, access };
352
- }
353
- await appendActionLedgerMutation(c.get("db"), {
354
- context: getActionLedgerRequestContext(c),
355
- actionName: input.actionName,
356
- actionVersion: capability.version,
357
- actionKind: "update",
358
- status: "denied",
359
- evaluatedRisk: access.evaluatedRisk,
360
- targetType: "booking",
361
- targetId: input.bookingId,
362
- routeOrToolName: input.routeOrToolName,
363
- capabilityId: access.capabilityId,
364
- capabilityVersion: access.capabilityVersion,
365
- authorizationSource: access.authorizationSource,
366
- mutationDetail: {
367
- summary: `Booking status ${capability.action} denied: ${access.reason}`,
368
- reversalKind: "none",
369
- },
370
- });
371
- return {
372
- allowed: false,
373
- response: handleApiError(new ForbiddenApiError(), c),
374
- };
375
- }
376
- async function resolveApprovedBookingStatusAction(c, input, access, approvalRequirement) {
377
- const approvalId = c.req.header(ACTION_LEDGER_APPROVAL_ID_HEADER);
378
- if (!approvalId)
379
- return null;
380
- const executionFingerprint = await buildBookingStatusApprovalFingerprint(input, await loadBookingStatusApprovalTargetState(c, input.bookingId), access, approvalRequirement);
381
- const actorFields = mapActionLedgerRequestContext(getActionLedgerRequestContext(c));
382
- const validation = await actionLedgerService.validateApprovedAction(c.get("db"), {
383
- approvalId,
384
- actionName: input.actionName,
385
- actionVersion: BOOKING_STATUS_CAPABILITIES[input.key].version,
386
- requestedActionKind: "update",
387
- requestedActionStatus: "awaiting_approval",
388
- targetType: "booking",
389
- targetId: input.bookingId,
390
- routeOrToolName: input.routeOrToolName,
391
- principalType: actorFields.principalType,
392
- principalId: actorFields.principalId,
393
- idempotencyFingerprint: executionFingerprint,
394
- executionActionKind: "update",
395
- executionStatus: "succeeded",
396
- });
397
- if (!validation.ok) {
398
- return {
399
- allowed: false,
400
- response: actionApprovalValidationResponse(c, validation),
401
- };
402
- }
403
- return {
404
- allowed: true,
405
- action: {
406
- requestedActionId: validation.requestedAction.id,
407
- approvalId: validation.approval.id,
408
- idempotencyFingerprint: validation.idempotencyFingerprint,
409
- },
410
- };
411
- }
412
- function buildBookingStatusApprovalFingerprint(input, targetState, access, approvalRequirement) {
413
- return buildActionApprovalCommandFingerprint({
414
- actionName: input.actionName,
415
- actionVersion: BOOKING_STATUS_CAPABILITIES[input.key].version,
416
- targetType: "booking",
417
- targetId: input.bookingId,
418
- commandInput: {
419
- command: input.commandInput ?? null,
420
- targetState,
421
- },
422
- approvalPolicy: approvalRequirement.approvalPolicy,
423
- capabilityId: access.capabilityId,
424
- capabilityVersion: access.capabilityVersion,
425
- evaluatedRisk: approvalRequirement.evaluatedRisk,
426
- reasonCode: approvalRequirement.reasonCode,
427
- });
428
- }
429
- async function loadBookingStatusApprovalTargetState(c, bookingId) {
430
- const booking = await bookingsService.getBookingById(c.get("db"), bookingId);
431
- if (!booking)
432
- return { exists: false };
433
- return {
434
- exists: true,
435
- status: booking.status,
436
- sellCurrency: booking.sellCurrency,
437
- sellAmountCents: booking.sellAmountCents,
438
- costAmountCents: booking.costAmountCents,
439
- customerPaymentPolicy: booking.customerPaymentPolicy,
440
- holdExpiresAt: serializeBookingApprovalDate(booking.holdExpiresAt),
441
- confirmedAt: serializeBookingApprovalDate(booking.confirmedAt),
442
- awaitingPaymentAt: serializeBookingApprovalDate(booking.awaitingPaymentAt),
443
- paidAt: serializeBookingApprovalDate(booking.paidAt),
444
- cancelledAt: serializeBookingApprovalDate(booking.cancelledAt),
445
- completedAt: serializeBookingApprovalDate(booking.completedAt),
446
- expiredAt: serializeBookingApprovalDate(booking.expiredAt),
447
- };
448
- }
449
- function serializeBookingApprovalDate(value) {
450
- if (!value)
451
- return null;
452
- return value instanceof Date ? value.toISOString() : value;
453
- }
454
- function actionApprovalValidationResponse(c, validation) {
455
- switch (validation.reason) {
456
- case "not_found":
457
- return c.json({ error: "Action approval not found" }, 404);
458
- case "not_approved":
459
- return c.json({
460
- error: "Action approval is not approved",
461
- approvalId: validation.approval?.id,
462
- status: validation.status,
463
- }, 409);
464
- case "expired":
465
- return c.json({
466
- error: "Action approval has expired",
467
- approvalId: validation.approval?.id,
468
- }, 409);
469
- case "mismatched_action":
470
- return c.json({
471
- error: "Action approval does not match this booking status action",
472
- approvalId: validation.approval?.id,
473
- }, 409);
474
- case "already_executed":
475
- return c.json({
476
- error: "Action approval has already been executed",
477
- approvalId: validation.approval?.id,
478
- existingActionId: validation.existingActionId,
479
- }, 409);
480
- case "missing_fingerprint":
481
- return c.json({
482
- error: "Action approval is missing an approved command fingerprint",
483
- approvalId: validation.approval?.id,
484
- }, 409);
485
- case "fingerprint_mismatch":
486
- return c.json({
487
- error: "Action approval command input does not match the approved request",
488
- approvalId: validation.approval?.id,
489
- }, 409);
490
- case "principal_mismatch":
491
- return c.json({ error: "Action approval belongs to a different principal" }, 403);
492
- }
493
- const exhaustiveReason = validation.reason;
494
- return c.json({ error: `Unhandled action approval validation failure: ${exhaustiveReason}` }, 500);
495
- }
496
- function bookingStatusMutationRuntime(c, auth) {
497
- const approvedExecution = auth.approvedAction
498
- ? buildActionLedgerApprovedExecutionFields(auth.approvedAction)
499
- : null;
500
- return {
501
- eventBus: c.get("eventBus"),
502
- closePaymentSchedulesForBooking: getRouteRuntime(c).closePaymentSchedulesForBooking,
503
- actionLedgerContext: getActionLedgerRequestContext(c),
504
- actionLedgerAuthorizationSource: auth.access.authorizationSource,
505
- actionLedgerCausationActionId: approvedExecution?.causationActionId ?? null,
506
- actionLedgerApprovalId: approvedExecution?.approvalId ?? null,
507
- actionLedgerIdempotencyScope: approvedExecution?.idempotencyScope ?? null,
508
- actionLedgerIdempotencyKey: approvedExecution?.idempotencyKey ?? null,
509
- actionLedgerIdempotencyFingerprint: approvedExecution?.idempotencyFingerprint ?? null,
510
- };
511
- }
512
- function requiresBookingStatusApproval(c, key) {
513
- const capability = BOOKING_STATUS_CAPABILITIES[key];
514
- if (capability.approvalPolicy !== "conditional")
515
- return false;
516
- return (c.get("callerType") === "agent" ||
517
- c.get("callerType") === "workflow" ||
518
- Boolean(c.get("agentId")) ||
519
- Boolean(c.get("workflowPrincipalId")));
520
- }
521
- function bookingStatusApprovalReason(c, key) {
522
- if (c.get("callerType") === "agent" || c.get("agentId")) {
523
- return `${key}_requested_by_agent`;
524
- }
525
- if (c.get("callerType") === "workflow" || c.get("workflowPrincipalId")) {
526
- return `${key}_requested_by_workflow`;
527
- }
528
- return null;
529
- }
530
- async function logBookingPiiReadActionLedger(c, input) {
531
- await appendActionLedgerSensitiveRead(c.get("db"), {
532
- context: getActionLedgerRequestContext(c),
533
- actionName: BOOKING_PII_READ_ACTION_NAME,
534
- actionVersion: BOOKING_PII_READ_ACTION_VERSION,
535
- status: input.status,
536
- evaluatedRisk: input.evaluatedRisk ?? "high",
537
- targetType: "booking_traveler",
538
- targetId: input.travelerId,
539
- routeOrToolName: input.routeOrToolName,
540
- capabilityId: BOOKING_PII_READ_CAPABILITY.id,
541
- capabilityVersion: BOOKING_PII_READ_CAPABILITY.version,
542
- authorizationSource: input.authorizationSource ?? BOOKING_PII_AUTHORIZATION_SOURCE,
543
- reasonCode: input.reason,
544
- disclosedFieldSet: input.status === "succeeded" ? (input.disclosedFieldSet ?? []) : [],
545
- disclosureSummary: input.disclosureSummary ?? null,
546
- decisionPolicy: input.decisionPolicy ?? BOOKING_PII_DECISION_POLICY,
547
- });
548
- }
549
- async function logBookingPiiAccess(c, input) {
550
- await c
551
- .get("db")
552
- .insert(bookingPiiAccessLog)
553
- .values({
554
- bookingId: input.bookingId ?? null,
555
- travelerId: input.travelerId ?? null,
556
- actorId: c.get("userId") ?? null,
557
- actorType: c.get("actor") ?? null,
558
- callerType: c.get("callerType") ?? null,
559
- action: input.action,
560
- outcome: input.outcome,
561
- reason: input.reason ?? null,
562
- metadata: input.metadata ?? null,
563
- });
564
- }
565
- async function authorizeBookingPiiAccess(c, input) {
566
- if (c.get("isInternalRequest")) {
567
- return { allowed: true, access: undefined };
568
- }
569
- const userId = c.get("userId");
570
- if (!userId) {
571
- await logBookingPiiAccess(c, {
572
- ...input,
573
- outcome: "denied",
574
- reason: "missing_user",
575
- });
576
- if (input.action === "read") {
577
- await logBookingPiiReadActionLedger(c, {
578
- travelerId: input.travelerId,
579
- status: "denied",
580
- reason: "missing_user",
581
- routeOrToolName: "bookings.pii.authorize",
582
- disclosureSummary: "Booking PII read denied before reveal",
583
- });
584
- }
585
- return {
586
- allowed: false,
587
- response: handleApiError(new UnauthorizedApiError(), c),
588
- };
589
- }
590
- const customAuthorizer = c.get("authorizeBookingPii");
591
- if (customAuthorizer) {
592
- const allowed = await customAuthorizer({
593
- db: c.get("db"),
594
- userId,
595
- actor: c.get("actor"),
596
- callerType: c.get("callerType"),
597
- scopes: c.get("scopes"),
598
- isInternalRequest: c.get("isInternalRequest"),
599
- ...input,
600
- });
601
- if (!allowed) {
602
- await logBookingPiiAccess(c, {
603
- ...input,
604
- outcome: "denied",
605
- reason: "custom_policy_denied",
606
- });
607
- if (input.action === "read") {
608
- await logBookingPiiReadActionLedger(c, {
609
- travelerId: input.travelerId,
610
- status: "denied",
611
- reason: "custom_policy_denied",
612
- routeOrToolName: "bookings.pii.authorize",
613
- disclosureSummary: "Booking PII read denied before reveal",
614
- decisionPolicy: "custom",
615
- });
616
- }
617
- return {
618
- allowed: false,
619
- response: handleApiError(new ForbiddenApiError(), c),
620
- };
621
- }
622
- return { allowed: true, access: undefined };
623
- }
624
- const actor = c.get("actor");
625
- const scopes = c.get("scopes");
626
- if (input.action === "read") {
627
- const access = evaluateActionLedgerCapabilityAccess({
628
- definition: BOOKING_PII_READ_CAPABILITY,
629
- actor,
630
- callerType: c.get("callerType"),
631
- scopes,
632
- isInternalRequest: c.get("isInternalRequest"),
633
- });
634
- if (!access.allowed) {
635
- await logBookingPiiAccess(c, {
636
- ...input,
637
- outcome: "denied",
638
- reason: access.reason,
639
- metadata: {
640
- actor: actor ?? null,
641
- authorizationSource: access.authorizationSource,
642
- capabilityId: access.capabilityId,
643
- capabilityVersion: access.capabilityVersion,
644
- },
645
- });
646
- await logBookingPiiReadActionLedger(c, {
647
- travelerId: input.travelerId,
648
- status: "denied",
649
- reason: access.reason,
650
- routeOrToolName: "bookings.pii.authorize",
651
- disclosureSummary: "Booking PII read denied before reveal",
652
- authorizationSource: access.authorizationSource,
653
- evaluatedRisk: access.evaluatedRisk,
654
- });
655
- return {
656
- allowed: false,
657
- response: handleApiError(new ForbiddenApiError(), c),
658
- };
659
- }
660
- return { allowed: true, access };
661
- }
662
- const allowed = hasPiiScope(scopes, input.action) || actor === "staff";
663
- if (!allowed) {
664
- await logBookingPiiAccess(c, {
665
- ...input,
666
- outcome: "denied",
667
- reason: "insufficient_scope",
668
- metadata: { actor: actor ?? null },
669
- });
670
- return {
671
- allowed: false,
672
- response: handleApiError(new ForbiddenApiError(), c),
673
- };
674
- }
675
- return { allowed: true, access: undefined };
676
- }
677
- function handleKmsConfigError(c, error) {
678
- if (error instanceof Error) {
679
- return c.json({
680
- error: "Booking PII encryption is not configured",
681
- details: error.message,
682
- }, 500);
683
- }
684
- return c.json({ error: "Booking PII encryption is not configured" }, 500);
685
- }
686
- function getRouteRuntime(c) {
687
- try {
688
- const runtime = c.var.container?.resolve(BOOKING_ROUTE_RUNTIME_CONTAINER_KEY);
689
- return runtime ?? buildBookingRouteRuntime(c.env);
690
- }
691
- catch {
692
- return buildBookingRouteRuntime(c.env);
693
- }
694
- }
695
- async function createAuditedBookingPiiService(c, bookingId) {
696
- const runtime = getRouteRuntime(c);
697
- const kms = await runtime.getKmsProvider();
698
- return createBookingPiiService({
699
- kms,
700
- onAudit: async (event) => {
701
- await logBookingPiiAccess(c, {
702
- bookingId,
703
- travelerId: event.travelerId,
704
- action: event.action === "encrypt"
705
- ? "update"
706
- : event.action === "decrypt"
707
- ? "read"
708
- : event.action,
709
- outcome: "allowed",
710
- });
711
- },
712
- });
713
- }
714
- function serializeBookingActionLedgerDate(value) {
715
- const date = value instanceof Date ? value : new Date(value);
716
- if (Number.isNaN(date.getTime())) {
717
- throw new Error("Booking action ledger timestamp must be a valid date");
718
- }
719
- return date.toISOString();
720
- }
721
- function serializeBookingActionLedgerEntry(entry) {
722
- return {
723
- ...entry,
724
- occurredAt: serializeBookingActionLedgerDate(entry.occurredAt),
725
- createdAt: serializeBookingActionLedgerDate(entry.createdAt),
726
- };
727
- }
728
- function toBookingActionLedgerCursor(entry) {
729
- return {
730
- occurredAt: serializeBookingActionLedgerDate(entry.occurredAt),
731
- id: entry.id,
732
- };
733
- }
734
- function sortBookingActionLedgerEntries(entries) {
735
- return [...entries].sort((a, b) => {
736
- const occurredAtDelta = new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime();
737
- if (occurredAtDelta !== 0)
738
- return occurredAtDelta;
739
- return b.id.localeCompare(a.id);
740
- });
741
- }
742
- function buildBookingActionLedgerPage({ bookingEntries, travelerEntries, itemEntries = [], limit, }) {
743
- const entriesById = new Map();
744
- for (const entry of bookingEntries) {
745
- entriesById.set(entry.id, entry);
746
- }
747
- for (const entry of travelerEntries) {
748
- entriesById.set(entry.id, entry);
749
- }
750
- for (const entry of itemEntries) {
751
- entriesById.set(entry.id, entry);
752
- }
753
- const sortedEntries = sortBookingActionLedgerEntries([...entriesById.values()]);
754
- const entries = sortedEntries.slice(0, limit);
755
- const lastEntry = entries.at(-1);
756
- const nextCursor = sortedEntries.length > limit && lastEntry ? toBookingActionLedgerCursor(lastEntry) : null;
757
- return { entries, nextCursor };
758
- }
759
- async function listBookingActionLedger(c) {
760
- const bookingId = c.req.param("id");
761
- if (!bookingId) {
762
- return c.json({ error: "Booking not found" }, 404);
763
- }
764
- const query = parseQuery(c, bookingActionLedgerQuerySchema);
765
- const limit = query.limit ?? 50;
766
- const queryLimit = limit + 1;
767
- const booking = await bookingsService.getBookingById(c.get("db"), bookingId);
768
- if (!booking) {
769
- return c.json({ error: "Booking not found" }, 404);
770
- }
771
- const travelers = await bookingsService.listTravelers(c.get("db"), bookingId);
772
- const reveal = shouldRevealBookingPii({
773
- actor: c.get("actor"),
774
- scopes: c.get("scopes"),
775
- callerType: c.get("callerType"),
776
- isInternalRequest: c.get("isInternalRequest"),
777
- });
778
- const visibleTravelers = reveal ? travelers : travelers.map((row) => redactTravelerIdentity(row));
779
- const travelerIds = travelers.map((traveler) => traveler.id);
780
- const items = await bookingsService.listItems(c.get("db"), bookingId);
781
- const itemIds = items.map((item) => item.id);
782
- const [bookingEntriesResult, travelerEntriesResult, itemEntriesResult] = await Promise.all([
783
- actionLedgerService.listEntries(c.get("db"), {
784
- targetType: "booking",
785
- targetId: bookingId,
786
- cursor: query.cursor,
787
- limit: queryLimit,
788
- }),
789
- travelerIds.length > 0
790
- ? actionLedgerService.listEntries(c.get("db"), {
791
- targetType: "booking_traveler",
792
- targetIds: travelerIds,
793
- cursor: query.cursor,
794
- limit: queryLimit,
795
- })
796
- : Promise.resolve({ entries: [], nextCursor: null }),
797
- itemIds.length > 0
798
- ? actionLedgerService.listEntries(c.get("db"), {
799
- targetType: "booking_item",
800
- targetIds: itemIds,
801
- cursor: query.cursor,
802
- limit: queryLimit,
803
- })
804
- : Promise.resolve({ entries: [], nextCursor: null }),
805
- ]);
806
- const page = buildBookingActionLedgerPage({
807
- bookingEntries: bookingEntriesResult.entries,
808
- travelerEntries: travelerEntriesResult.entries,
809
- itemEntries: itemEntriesResult.entries,
810
- limit,
811
- });
812
- return c.json({
813
- data: page.entries.map(serializeBookingActionLedgerEntry),
814
- travelers: visibleTravelers.map((traveler) => ({
815
- id: traveler.id,
816
- firstName: traveler.firstName,
817
- lastName: traveler.lastName,
818
- })),
819
- pageInfo: {
820
- nextCursor: page.nextCursor,
821
- },
822
- });
823
- }
824
- function serializeBookingActionApproval(approval) {
825
- return {
826
- ...approval,
827
- expiresAt: approval.expiresAt ? serializeBookingActionLedgerDate(approval.expiresAt) : null,
828
- decidedAt: approval.decidedAt ? serializeBookingActionLedgerDate(approval.decidedAt) : null,
829
- createdAt: serializeBookingActionLedgerDate(approval.createdAt),
830
- };
831
- }
832
- function findBookingStatusCapability(capabilityId) {
833
- return Object.values(BOOKING_STATUS_CAPABILITIES).find((capability) => capability.id === capabilityId);
834
- }
835
- async function decideBookingActionApproval(c) {
836
- const bookingId = c.req.param("id");
837
- const approvalId = c.req.param("approvalId");
838
- if (!bookingId || !approvalId) {
839
- return c.json({ error: "Action approval not found" }, 404);
840
- }
841
- if (!c.get("isInternalRequest") && !c.get("userId")) {
842
- return handleApiError(new UnauthorizedApiError(), c);
843
- }
844
- const body = await parseJsonBody(c, decideBookingActionApprovalBodySchema);
845
- const existing = await actionLedgerService.getApproval(c.get("db"), approvalId);
846
- if (!existing?.requestedAction?.entry) {
847
- return c.json({ error: "Action approval not found" }, 404);
848
- }
849
- const requestedAction = existing.requestedAction.entry;
850
- if (requestedAction.targetType !== "booking" || requestedAction.targetId !== bookingId) {
851
- return c.json({ error: "Action approval not found" }, 404);
852
- }
853
- const capability = findBookingStatusCapability(requestedAction.capabilityId);
854
- if (!capability) {
855
- return c.json({ error: "Action approval is not a booking status approval" }, 409);
856
- }
857
- const access = evaluateActionLedgerCapabilityAccess({
858
- definition: capability,
859
- actor: c.get("actor"),
860
- callerType: c.get("callerType"),
861
- scopes: c.get("scopes"),
862
- isInternalRequest: c.get("isInternalRequest"),
863
- });
864
- if (!access.allowed) {
865
- return handleApiError(new ForbiddenApiError(), c);
866
- }
867
- try {
868
- const result = await decideActionLedgerApproval(c.get("db"), {
869
- context: getActionLedgerRequestContext(c),
870
- id: approvalId,
871
- status: body.status,
872
- actionName: "booking.status.approval.decide",
873
- actionVersion: capability.version,
874
- evaluatedRisk: requestedAction.evaluatedRisk,
875
- targetType: "booking",
876
- targetId: bookingId,
877
- routeOrToolName: "bookings.approvals.decide",
878
- capabilityId: capability.id,
879
- capabilityVersion: capability.version,
880
- authorizationSource: access.authorizationSource,
881
- });
882
- if (!result) {
883
- return c.json({ error: "Action approval not found" }, 404);
884
- }
885
- return c.json({
886
- data: {
887
- approval: serializeBookingActionApproval(result.approval),
888
- decisionAction: serializeBookingActionLedgerEntry(result.decisionAction),
889
- },
890
- });
891
- }
892
- catch (error) {
893
- if (error instanceof ActionApprovalDecisionConflictError) {
894
- return c.json({
895
- error: "Action approval has already been decided",
896
- approvalId: error.approvalId,
897
- status: error.currentStatus,
898
- }, 409);
899
- }
900
- throw error;
901
- }
902
- }
903
- // ==========================================================================
904
- // Bookings — method-chained for Hono RPC type inference
905
- // ==========================================================================
906
- export const bookingRoutes = new Hono()
907
- // ==========================================================================
908
- // Bookings CRUD
909
- // ==========================================================================
910
- // 1. GET / — List bookings
911
- .get("/", async (c) => {
912
- const query = parseQuery(c, bookingListQuerySchema);
913
- const result = await bookingsService.listBookings(c.get("db"), query);
914
- const reveal = shouldRevealBookingPii({
915
- actor: c.get("actor"),
916
- scopes: c.get("scopes"),
917
- callerType: c.get("callerType"),
918
- isInternalRequest: c.get("isInternalRequest"),
919
- });
920
- await logBookingPiiAccess(c, {
921
- action: "read",
922
- outcome: "allowed",
923
- reason: reveal ? "list_reveal" : "list_redacted",
924
- metadata: { rowCount: result.data.length, reveal },
925
- });
926
- if (reveal)
927
- return c.json(result);
928
- return c.json({
929
- ...result,
930
- data: result.data.map((row) => redactBookingContact(row)),
931
- });
932
- })
933
- // 1a. POST /pricing-preview — Resolve a pricing snapshot without creating a session.
934
- .post("/pricing-preview", async (c) => {
935
- const body = await parseJsonBody(c, pricingPreviewSchema);
936
- const snapshot = await resolveSessionPricingSnapshot(c.get("db"), body.productId, {
937
- optionId: body.optionId ?? undefined,
938
- catalogId: body.catalogId ?? undefined,
939
- requirePublicProduct: false,
940
- });
941
- if (!snapshot) {
942
- return c.json({ error: "Pricing unavailable for this selection" }, 404);
943
- }
944
- return c.json({ data: snapshot });
945
- })
946
- // 1b. GET /aggregates — Pre-aggregated dashboard metrics. Served from a
947
- // read-through TTL snapshot so a warm dashboard load is one indexed read
948
- // instead of the full aggregate fan-out (#1629).
949
- .get("/aggregates", async (c) => {
950
- const query = parseQuery(c, bookingAggregatesQuerySchema);
951
- cacheDashboardAggregates(c);
952
- const snapshot = await readThroughAggregateSnapshot(c.get("db"), {
953
- key: aggregateSnapshotKey("bookings", "aggregates", query),
954
- ttlSeconds: DASHBOARD_AGGREGATES_TTL_SECONDS,
955
- compute: () => bookingsService.getBookingAggregates(c.get("db"), query),
956
- });
957
- return c.json({ data: snapshot.data });
958
- })
959
- // 1b. GET /overview — Internal/admin booking overview lookup
960
- .get("/overview", async (c) => {
961
- const overview = await publicBookingsService.getOverviewByLookup(c.get("db"), parseQuery(c, internalBookingOverviewLookupQuerySchema));
962
- if (!overview) {
963
- return c.json({ error: "Booking overview not found" }, 404);
964
- }
965
- return c.json({ data: overview });
966
- })
967
- .get("/sharing-groups", async (c) => {
968
- const query = parseQuery(c, sharingGroupsForSlotQuerySchema);
969
- const data = await bookingsService.listSharingGroupsForSlot(c.get("db"), query.slotId);
970
- return c.json({ data });
971
- })
972
- .get("/sharing-groups/:groupId/travelers", async (c) => {
973
- const query = parseQuery(c, sharingGroupsForSlotQuerySchema);
974
- const data = await bookingsService.listTravelersBySharingGroup(c.get("db"), query.slotId, c.req.param("groupId"));
975
- const reveal = shouldRevealBookingPii({
976
- actor: c.get("actor"),
977
- scopes: c.get("scopes"),
978
- callerType: c.get("callerType"),
979
- isInternalRequest: c.get("isInternalRequest"),
980
- });
981
- await logBookingPiiAccess(c, {
982
- action: "read",
983
- outcome: "allowed",
984
- reason: reveal ? "sharing_group_travelers_reveal" : "sharing_group_travelers_redacted",
985
- metadata: { rowCount: data.length, reveal },
986
- });
987
- return c.json({ data: reveal ? data : data.map((row) => redactTravelerIdentity(row)) });
988
- })
989
- // 2. GET /:id — Get single booking
990
- .get("/:id", async (c) => {
991
- const row = await bookingsService.getBookingById(c.get("db"), c.req.param("id"));
992
- if (!row) {
993
- return c.json({ error: "Booking not found" }, 404);
994
- }
995
- const reveal = shouldRevealBookingPii({
996
- actor: c.get("actor"),
997
- scopes: c.get("scopes"),
998
- callerType: c.get("callerType"),
999
- isInternalRequest: c.get("isInternalRequest"),
1000
- });
1001
- await logBookingPiiAccess(c, {
1002
- bookingId: row.id,
1003
- action: "read",
1004
- outcome: "allowed",
1005
- reason: reveal ? "detail_reveal" : "detail_redacted",
1006
- metadata: { reveal },
1007
- });
1008
- return c.json({ data: reveal ? row : redactBookingContact(row) });
1009
- })
1010
- // 2b. GET /:id/action-ledger — Booking-scoped action timeline
1011
- .get("/:id/action-ledger", listBookingActionLedger)
1012
- // 2c. POST /:id/action-approvals/:approvalId/decide — Booking-scoped approval decision
1013
- .post("/:id/action-approvals/:approvalId/decide", decideBookingActionApproval)
1014
- // 3. POST /reserve — Reserve inventory and create on-hold booking
1015
- .post("/reserve", idempotencyKey({ scope: "POST /v1/admin/bookings/reserve" }), async (c) => {
1016
- const data = await parseJsonBody(c, reserveBookingSchema);
1017
- await validateBookingBillingPartyReferences(c, data);
1018
- const result = await bookingsService.reserveBooking(c.get("db"), data, c.get("userId"));
1019
- if ("booking" in result) {
1020
- return c.json({ data: result.booking }, 201);
1021
- }
1022
- if (result.status === "slot_not_found") {
1023
- return c.json({ error: "Availability slot not found" }, 404);
1024
- }
1025
- if (result.status === "insufficient_capacity") {
1026
- return c.json({ error: "Insufficient slot capacity" }, 409);
1027
- }
1028
- if (result.status === "slot_unavailable") {
1029
- return c.json({ error: "Availability slot is not bookable" }, 409);
1030
- }
1031
- if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
1032
- return c.json({ error: "Reservation item does not match availability slot" }, 409);
1033
- }
1034
- return c.json({ error: "Unable to reserve booking" }, 400);
1035
- })
1036
- // 3a. POST /from-product — Create booking draft from product definition
1037
- .post("/from-product", idempotencyKey({ scope: "POST /v1/admin/bookings/from-product" }), async (c) => {
1038
- const data = await parseJsonBody(c, convertProductSchema);
1039
- await validateBookingBillingPartyReferences(c, data);
1040
- const row = await bookingsService.createBookingFromProduct(c.get("db"), data, c.get("userId"));
1041
- if (!row) {
1042
- return c.json({ error: "Product or option not found" }, 404);
1043
- }
1044
- return c.json({ data: row }, 201);
1045
- })
1046
- // 3b. POST /from-offer/:offerId/reserve — Reserve booking from transaction offer
1047
- .post("/from-offer/:offerId/reserve", idempotencyKey({ scope: "POST /v1/admin/bookings/from-offer" }), async (c) => {
1048
- const result = await bookingsService.reserveBookingFromOffer(c.get("db"), c.req.param("offerId"), await parseJsonBody(c, reserveBookingFromTransactionSchema), c.get("userId"));
1049
- if (result.status === "not_found") {
1050
- return c.json({ error: "Offer not found" }, 404);
1051
- }
1052
- if (result.status === "slot_not_found") {
1053
- return c.json({ error: "Availability slot not found" }, 404);
1054
- }
1055
- if (result.status === "insufficient_capacity") {
1056
- return c.json({ error: "Insufficient slot capacity" }, 409);
1057
- }
1058
- if (result.status === "slot_unavailable") {
1059
- return c.json({ error: "Availability slot is not bookable" }, 409);
1060
- }
1061
- if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
1062
- return c.json({ error: "Reservation item does not match availability slot" }, 409);
1063
- }
1064
- if ("booking" in result) {
1065
- return c.json({ data: result.booking }, 201);
1066
- }
1067
- return c.json({ error: "Unable to reserve booking from offer" }, 400);
1068
- })
1069
- // 3c. POST /from-order/:orderId/reserve — Reserve booking from transaction order
1070
- .post("/from-order/:orderId/reserve", idempotencyKey({ scope: "POST /v1/admin/bookings/from-order" }), async (c) => {
1071
- const result = await bookingsService.reserveBookingFromOrder(c.get("db"), c.req.param("orderId"), await parseJsonBody(c, reserveBookingFromTransactionSchema), c.get("userId"));
1072
- if (result.status === "not_found") {
1073
- return c.json({ error: "Order not found" }, 404);
1074
- }
1075
- if (result.status === "slot_not_found") {
1076
- return c.json({ error: "Availability slot not found" }, 404);
1077
- }
1078
- if (result.status === "insufficient_capacity") {
1079
- return c.json({ error: "Insufficient slot capacity" }, 409);
1080
- }
1081
- if (result.status === "slot_unavailable") {
1082
- return c.json({ error: "Availability slot is not bookable" }, 409);
1083
- }
1084
- if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
1085
- return c.json({ error: "Reservation item does not match availability slot" }, 409);
1086
- }
1087
- if ("booking" in result) {
1088
- return c.json({ data: result.booking }, 201);
1089
- }
1090
- return c.json({ error: "Unable to reserve booking from order" }, 400);
1091
- })
1092
- // 4. POST / — Create booking (manual/backoffice only)
1093
- .post("/", idempotencyKey({ scope: "POST /v1/admin/bookings" }), async (c) => {
1094
- try {
1095
- const data = await parseJsonBody(c, createBookingSchema, {
1096
- invalidBodyMessage: "Invalid booking create payload",
1097
- });
1098
- await validateBookingBillingPartyReferences(c, data);
1099
- return c.json({
1100
- data: await bookingsService.createBooking(c.get("db"), data, c.get("userId")),
1101
- }, 201);
1102
- }
1103
- catch (error) {
1104
- const validationError = normalizeValidationError(error);
1105
- if (validationError?.status === 400) {
1106
- return c.json({
1107
- error: validationError.message,
1108
- details: validationError.details?.fields ?? validationError.details,
1109
- }, 400);
1110
- }
1111
- throw error;
1112
- }
1113
- })
1114
- // 5. PATCH /:id — Update booking
1115
- .patch("/:id", async (c) => {
1116
- const data = await parseJsonBody(c, updateBookingSchema);
1117
- await validateBookingBillingPartyReferences(c, data);
1118
- const row = await bookingsService.updateBooking(c.get("db"), c.req.param("id"), data);
1119
- if (!row) {
1120
- return c.json({ error: "Booking not found" }, 404);
1121
- }
1122
- return c.json({ data: row });
1123
- })
1124
- // 6. DELETE /:id — Delete booking
1125
- .delete("/:id", async (c) => {
1126
- const row = await bookingsService.deleteBooking(c.get("db"), c.req.param("id"));
1127
- if (!row) {
1128
- return c.json({ error: "Booking not found" }, 404);
1129
- }
1130
- return c.json({ success: true }, 200);
1131
- })
1132
- // ==========================================================================
1133
- // Status
1134
- // ==========================================================================
1135
- // 8. POST /:id/confirm — Confirm an on-hold booking
1136
- .post("/:id/confirm", async (c) => {
1137
- const bookingId = c.req.param("id");
1138
- const data = await parseJsonBody(c, confirmBookingSchema);
1139
- const auth = await authorizeBookingStatusMutation(c, {
1140
- key: "confirm",
1141
- actionName: "booking.status.confirm",
1142
- routeOrToolName: "bookings.confirm",
1143
- bookingId,
1144
- });
1145
- if (!auth.allowed)
1146
- return auth.response;
1147
- const result = await bookingsService.confirmBooking(c.get("db"), bookingId, data, c.get("userId"), bookingStatusMutationRuntime(c, auth));
1148
- if (result.status === "not_found") {
1149
- return c.json({ error: "Booking not found" }, 404);
1150
- }
1151
- if (result.status === "hold_expired") {
1152
- return c.json({ error: "Booking hold has expired" }, 409);
1153
- }
1154
- if (result.status === "invalid_transition") {
1155
- return c.json({ error: "Booking is not in an on-hold state" }, 409);
1156
- }
1157
- if ("booking" in result) {
1158
- return c.json({ data: result.booking });
1159
- }
1160
- return c.json({ error: "Unable to confirm booking" }, 400);
1161
- })
1162
- // 9. POST /:id/extend-hold — Extend booking hold expiry
1163
- .post("/:id/extend-hold", async (c) => {
1164
- const result = await bookingsService.extendBookingHold(c.get("db"), c.req.param("id"), await parseJsonBody(c, extendBookingHoldSchema), c.get("userId"));
1165
- if (result.status === "not_found") {
1166
- return c.json({ error: "Booking not found" }, 404);
1167
- }
1168
- if (result.status === "hold_expired") {
1169
- return c.json({ error: "Booking hold has expired" }, 409);
1170
- }
1171
- if (result.status === "invalid_transition") {
1172
- return c.json({ error: "Booking is not in an on-hold state" }, 409);
1173
- }
1174
- if ("booking" in result) {
1175
- return c.json({ data: result.booking });
1176
- }
1177
- return c.json({ error: "Unable to extend booking hold" }, 400);
1178
- })
1179
- // 10. POST /:id/expire — Expire an on-hold booking
1180
- .post("/:id/expire", async (c) => {
1181
- const bookingId = c.req.param("id");
1182
- const data = await parseJsonBody(c, expireBookingSchema);
1183
- const auth = await authorizeBookingStatusMutation(c, {
1184
- key: "expire",
1185
- actionName: "booking.status.expire",
1186
- routeOrToolName: "bookings.expire",
1187
- bookingId,
1188
- });
1189
- if (!auth.allowed)
1190
- return auth.response;
1191
- const result = await bookingsService.expireBooking(c.get("db"), bookingId, data, c.get("userId"), {
1192
- ...bookingStatusMutationRuntime(c, auth),
1193
- cause: "route",
1194
- });
1195
- if (result.status === "not_found") {
1196
- return c.json({ error: "Booking not found" }, 404);
1197
- }
1198
- if (result.status === "invalid_transition") {
1199
- return c.json({ error: "Booking is not in an on-hold state" }, 409);
1200
- }
1201
- if ("booking" in result) {
1202
- return c.json({ data: result.booking });
1203
- }
1204
- return c.json({ error: "Unable to expire booking" }, 400);
1205
- })
1206
- // 10b. POST /expire-stale — Expire all stale on-hold bookings up to a cutoff
1207
- .post("/expire-stale", async (c) => {
1208
- return c.json(await bookingsService.expireStaleBookings(c.get("db"), await parseJsonBody(c, expireStaleBookingsSchema), c.get("userId"), {
1209
- eventBus: c.get("eventBus"),
1210
- closePaymentSchedulesForBooking: getRouteRuntime(c).closePaymentSchedulesForBooking,
1211
- }));
1212
- })
1213
- // 11. POST /:id/cancel — Cancel a booking and release allocations
1214
- .post("/:id/cancel", async (c) => {
1215
- const bookingId = c.req.param("id");
1216
- const data = await parseJsonBody(c, cancelBookingSchema);
1217
- const auth = await authorizeBookingStatusMutation(c, {
1218
- key: "cancel",
1219
- actionName: "booking.status.cancel",
1220
- routeOrToolName: "bookings.cancel",
1221
- bookingId,
1222
- commandInput: data,
1223
- });
1224
- if (!auth.allowed)
1225
- return auth.response;
1226
- const result = await bookingsService.cancelBooking(c.get("db"), bookingId, data, c.get("userId"), bookingStatusMutationRuntime(c, auth));
1227
- if (result.status === "not_found") {
1228
- return c.json({ error: "Booking not found" }, 404);
1229
- }
1230
- if (result.status === "invalid_transition") {
1231
- return c.json({ error: "Booking cannot be cancelled from its current state" }, 409);
1232
- }
1233
- if ("booking" in result) {
1234
- return c.json({ data: result.booking });
1235
- }
1236
- return c.json({ error: "Unable to cancel booking" }, 400);
1237
- })
1238
- // 11a. POST /:id/start — Mark a confirmed booking as in-progress
1239
- .post("/:id/start", async (c) => {
1240
- const bookingId = c.req.param("id");
1241
- const data = await parseJsonBody(c, startBookingSchema);
1242
- const auth = await authorizeBookingStatusMutation(c, {
1243
- key: "start",
1244
- actionName: "booking.status.start",
1245
- routeOrToolName: "bookings.start",
1246
- bookingId,
1247
- });
1248
- if (!auth.allowed)
1249
- return auth.response;
1250
- const result = await bookingsService.startBooking(c.get("db"), bookingId, data, c.get("userId"), bookingStatusMutationRuntime(c, auth));
1251
- if (result.status === "not_found") {
1252
- return c.json({ error: "Booking not found" }, 404);
1253
- }
1254
- if (result.status === "invalid_transition") {
1255
- return c.json({ error: "Booking is not in a confirmed state" }, 409);
1256
- }
1257
- if ("booking" in result) {
1258
- return c.json({ data: result.booking });
1259
- }
1260
- return c.json({ error: "Unable to start booking" }, 400);
1261
- })
1262
- // 11b. POST /:id/complete — Mark an in-progress booking as completed
1263
- .post("/:id/complete", async (c) => {
1264
- const bookingId = c.req.param("id");
1265
- const data = await parseJsonBody(c, completeBookingSchema);
1266
- const auth = await authorizeBookingStatusMutation(c, {
1267
- key: "complete",
1268
- actionName: "booking.status.complete",
1269
- routeOrToolName: "bookings.complete",
1270
- bookingId,
1271
- });
1272
- if (!auth.allowed)
1273
- return auth.response;
1274
- const result = await bookingsService.completeBooking(c.get("db"), bookingId, data, c.get("userId"), bookingStatusMutationRuntime(c, auth));
1275
- if (result.status === "not_found") {
1276
- return c.json({ error: "Booking not found" }, 404);
1277
- }
1278
- if (result.status === "invalid_transition") {
1279
- return c.json({ error: "Booking is not in an in-progress state" }, 409);
1280
- }
1281
- if ("booking" in result) {
1282
- return c.json({ data: result.booking });
1283
- }
1284
- return c.json({ error: "Unable to complete booking" }, 400);
1285
- })
1286
- // 11c. POST /:id/override-status — Admin override that bypasses the
1287
- // transition graph. Terminal overrides cascade to items and allocations;
1288
- // non-terminal overrides remain booking-row data correction. Always emits
1289
- // booking.status_overridden for audit. Confirmed overrides also emit
1290
- // booking.confirmed unless suppressLifecycleEvents is true. Requires a
1291
- // non-empty `reason`.
1292
- .post("/:id/override-status", async (c) => {
1293
- const bookingId = c.req.param("id");
1294
- const data = await parseJsonBody(c, overrideBookingStatusSchema);
1295
- const auth = await authorizeBookingStatusMutation(c, {
1296
- key: "override",
1297
- actionName: "booking.status.override",
1298
- routeOrToolName: "bookings.override-status",
1299
- bookingId,
1300
- commandInput: data,
1301
- });
1302
- if (!auth.allowed)
1303
- return auth.response;
1304
- const result = await bookingsService.overrideBookingStatus(c.get("db"), bookingId, data, c.get("userId"), bookingStatusMutationRuntime(c, auth));
1305
- if (result.status === "not_found") {
1306
- return c.json({ error: "Booking not found" }, 404);
1307
- }
1308
- if ("booking" in result) {
1309
- return c.json({ data: result.booking });
1310
- }
1311
- return c.json({ error: "Unable to override booking status" }, 400);
1312
- })
1313
- // 12. GET /:id/allocations — List booking allocations
1314
- .get("/:id/allocations", async (c) => {
1315
- return c.json({ data: await bookingsService.listAllocations(c.get("db"), c.req.param("id")) });
1316
- })
1317
- // ==========================================================================
1318
- // Travelers
1319
- // ==========================================================================
1320
- .get("/:id/travelers", async (c) => {
1321
- const travelers = await bookingsService.listTravelers(c.get("db"), c.req.param("id"));
1322
- const reveal = shouldRevealBookingPii({
1323
- actor: c.get("actor"),
1324
- scopes: c.get("scopes"),
1325
- callerType: c.get("callerType"),
1326
- isInternalRequest: c.get("isInternalRequest"),
1327
- });
1328
- await logBookingPiiAccess(c, {
1329
- bookingId: c.req.param("id"),
1330
- action: "read",
1331
- outcome: "allowed",
1332
- reason: reveal ? "travelers_reveal" : "travelers_redacted",
1333
- metadata: { rowCount: travelers.length, reveal },
1334
- });
1335
- if (reveal)
1336
- return c.json({ data: travelers });
1337
- return c.json({ data: travelers.map((row) => redactTravelerIdentity(row)) });
1338
- })
1339
- /**
1340
- * GET /:id/travelers/:travelerId/reveal
1341
- *
1342
- * Per-traveler unmasked read. The list endpoint masks PII for
1343
- * non-superuser staff; this endpoint reveals on a single click so
1344
- * the operator can confirm a phone / email when actually needed,
1345
- * with the access logged as `traveler_reveal`. Authorization uses
1346
- * the same policy as `/travel-details` (staff or `bookings-pii:read`
1347
- * scope), so it Just Works for the dashboard's normal staff session.
1348
- *
1349
- * Returns 404 when the traveler isn't found, 403 when the caller
1350
- * isn't authorized to see PII.
1351
- */
1352
- .get("/:id/travelers/:travelerId/reveal", async (c) => {
1353
- const bookingId = c.req.param("id");
1354
- const travelerId = c.req.param("travelerId");
1355
- const auth = await authorizeBookingPiiAccess(c, {
1356
- bookingId,
1357
- travelerId,
1358
- action: "read",
1359
- });
1360
- if (!auth.allowed)
1361
- return auth.response;
1362
- const traveler = await bookingsService.getTravelerRecordById(c.get("db"), bookingId, travelerId);
1363
- if (!traveler) {
1364
- await logBookingPiiAccess(c, {
1365
- bookingId,
1366
- travelerId,
1367
- action: "read",
1368
- outcome: "denied",
1369
- reason: "traveler_not_found",
1370
- });
1371
- await logBookingPiiReadActionLedger(c, {
1372
- travelerId,
1373
- status: "denied",
1374
- reason: "traveler_not_found",
1375
- routeOrToolName: "bookings.travelers.reveal",
1376
- disclosureSummary: "Booking PII read denied before reveal",
1377
- authorizationSource: auth.access?.authorizationSource,
1378
- evaluatedRisk: auth.access?.evaluatedRisk,
1379
- });
1380
- return c.json({ error: "Traveler not found" }, 404);
1381
- }
1382
- let travelDetails;
1383
- try {
1384
- const pii = await createAuditedBookingPiiService(c, traveler.bookingId);
1385
- travelDetails = await ledgerSensitiveRead(c.get("db"), {
1386
- context: getActionLedgerRequestContext(c),
1387
- actionName: BOOKING_PII_READ_ACTION_NAME,
1388
- actionVersion: BOOKING_PII_READ_ACTION_VERSION,
1389
- status: "succeeded",
1390
- evaluatedRisk: auth.access?.evaluatedRisk ?? "high",
1391
- targetType: "booking_traveler",
1392
- targetId: traveler.id,
1393
- routeOrToolName: "bookings.travelers.reveal",
1394
- capabilityId: BOOKING_PII_READ_CAPABILITY.id,
1395
- capabilityVersion: BOOKING_PII_READ_CAPABILITY.version,
1396
- authorizationSource: auth.access?.authorizationSource ?? BOOKING_PII_AUTHORIZATION_SOURCE,
1397
- reasonCode: "traveler_reveal",
1398
- disclosedFieldSet: [
1399
- ...TRAVELER_IDENTITY_DISCLOSED_FIELDS,
1400
- ...TRAVELER_TRAVEL_DETAIL_DISCLOSED_FIELDS,
1401
- ],
1402
- disclosureSummary: "Traveler identity reveal",
1403
- decisionPolicy: BOOKING_PII_DECISION_POLICY,
1404
- }, () => pii.getTravelerTravelDetails(c.get("db"), traveler.id, c.get("userId")));
1405
- }
1406
- catch (error) {
1407
- return handleKmsConfigError(c, error);
1408
- }
1409
- await logBookingPiiAccess(c, {
1410
- bookingId,
1411
- travelerId,
1412
- action: "read",
1413
- outcome: "allowed",
1414
- reason: "traveler_reveal",
1415
- });
1416
- return c.json({ data: { ...traveler, travelDetails } });
1417
- })
1418
- .get("/:id/travelers/:travelerId/travel-details", async (c) => {
1419
- const auth = await authorizeBookingPiiAccess(c, {
1420
- bookingId: c.req.param("id"),
1421
- travelerId: c.req.param("travelerId"),
1422
- action: "read",
1423
- });
1424
- if (!auth.allowed) {
1425
- return auth.response;
1426
- }
1427
- const traveler = await bookingsService.getTravelerRecordById(c.get("db"), c.req.param("id"), c.req.param("travelerId"));
1428
- if (!traveler) {
1429
- await logBookingPiiAccess(c, {
1430
- bookingId: c.req.param("id"),
1431
- travelerId: c.req.param("travelerId"),
1432
- action: "read",
1433
- outcome: "denied",
1434
- reason: "participant_not_found",
1435
- });
1436
- await logBookingPiiReadActionLedger(c, {
1437
- travelerId: c.req.param("travelerId"),
1438
- status: "denied",
1439
- reason: "participant_not_found",
1440
- routeOrToolName: "bookings.travelers.travel-details",
1441
- disclosureSummary: "Booking PII read denied before reveal",
1442
- authorizationSource: auth.access?.authorizationSource,
1443
- evaluatedRisk: auth.access?.evaluatedRisk,
1444
- });
1445
- return c.json({ error: "Traveler not found" }, 404);
1446
- }
1447
- let details;
1448
- try {
1449
- const pii = await createAuditedBookingPiiService(c, traveler.bookingId);
1450
- details = await ledgerSensitiveRead(c.get("db"), {
1451
- context: getActionLedgerRequestContext(c),
1452
- actionName: BOOKING_PII_READ_ACTION_NAME,
1453
- actionVersion: BOOKING_PII_READ_ACTION_VERSION,
1454
- status: "succeeded",
1455
- evaluatedRisk: auth.access?.evaluatedRisk ?? "high",
1456
- targetType: "booking_traveler",
1457
- targetId: traveler.id,
1458
- routeOrToolName: "bookings.travelers.travel-details",
1459
- capabilityId: BOOKING_PII_READ_CAPABILITY.id,
1460
- capabilityVersion: BOOKING_PII_READ_CAPABILITY.version,
1461
- authorizationSource: auth.access?.authorizationSource ?? BOOKING_PII_AUTHORIZATION_SOURCE,
1462
- reasonCode: "travel_details_reveal",
1463
- disclosedFieldSet: TRAVELER_TRAVEL_DETAIL_DISCLOSED_FIELDS,
1464
- disclosureSummary: "Traveler travel details reveal",
1465
- decisionPolicy: BOOKING_PII_DECISION_POLICY,
1466
- }, () => pii.getTravelerTravelDetails(c.get("db"), traveler.id, c.get("userId")));
1467
- }
1468
- catch (error) {
1469
- return handleKmsConfigError(c, error);
1470
- }
1471
- if (!details) {
1472
- await logBookingPiiAccess(c, {
1473
- bookingId: traveler.bookingId,
1474
- travelerId: traveler.id,
1475
- action: "read",
1476
- outcome: "denied",
1477
- reason: "travel_details_not_found",
1478
- });
1479
- return c.json({ error: "Traveler travel details not found" }, 404);
1480
- }
1481
- return c.json({ data: details });
1482
- })
1483
- .post("/:id/travelers", async (c) => {
1484
- const body = await parseJsonBody(c, insertTravelerSchema);
1485
- const ledgerContext = getActionLedgerRequestContext(c);
1486
- const row = await c.get("db").transaction(async (tx) => {
1487
- const txDb = tx;
1488
- const row = await bookingsService.createTraveler(txDb, c.req.param("id"), body, c.get("userId"));
1489
- if (!row)
1490
- return null;
1491
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1492
- action: "create",
1493
- travelerId: row.id,
1494
- changedFields: changedBookingTravelerFields(body, null, row),
1495
- subject: "booking traveler",
1496
- actionName: "booking.traveler.create",
1497
- routeOrToolName: "bookings.travelers.create",
1498
- evaluatedRisk: "high",
1499
- });
1500
- return row;
1501
- });
1502
- if (!row) {
1503
- return c.json({ error: "Booking not found" }, 404);
1504
- }
1505
- return c.json({ data: row }, 201);
1506
- })
1507
- /**
1508
- * Combined "create traveler + write encrypted travel details" path.
1509
- * When `data.personId` is provided AND a `resolveTravelSnapshot`
1510
- * resolver is wired on the runtime, the route auto-snapshots
1511
- * dietary / accessibility / primary-passport from the linked
1512
- * person record. Explicit input always wins over snapshot.
1513
- */
1514
- .post("/:id/travelers/with-travel-details", async (c) => {
1515
- try {
1516
- const data = await parseJsonBody(c, createTravelerWithTravelDetailsSchema);
1517
- const runtime = getRouteRuntime(c);
1518
- const kms = await runtime.getKmsProvider();
1519
- const pii = await createAuditedBookingPiiService(c, c.req.param("id"));
1520
- const ledgerContext = getActionLedgerRequestContext(c);
1521
- const result = await c.get("db").transaction(async (tx) => {
1522
- const txDb = tx;
1523
- const result = await bookingsService.createTravelerWithTravelDetails(txDb, c.req.param("id"), data, {
1524
- pii,
1525
- userId: c.get("userId"),
1526
- actorId: c.get("userId"),
1527
- resolveTravelSnapshot: runtime.resolveTravelSnapshot
1528
- ? (personId) => runtime.resolveTravelSnapshot(txDb, personId, { kms })
1529
- : undefined,
1530
- });
1531
- if (!result)
1532
- return null;
1533
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1534
- action: "create",
1535
- travelerId: result.traveler.id,
1536
- changedFields: [
1537
- ...new Set([
1538
- ...changedBookingTravelerFields(data, null, result.traveler),
1539
- ...changedBookingTravelDetailFields(data),
1540
- ]),
1541
- ].sort(),
1542
- subject: "booking traveler with travel details",
1543
- actionName: "booking.traveler_with_travel_details.create",
1544
- routeOrToolName: "bookings.travelers.with-travel-details.create",
1545
- evaluatedRisk: "high",
1546
- });
1547
- return result;
1548
- });
1549
- if (!result) {
1550
- return c.json({ error: "Booking not found" }, 404);
1551
- }
1552
- return c.json({ data: result }, 201);
1553
- }
1554
- catch (error) {
1555
- return handleKmsConfigError(c, error);
1556
- }
1557
- })
1558
- /**
1559
- * Combined "update traveler + (re-)write encrypted travel details"
1560
- * path. Snapshot-on-update is intentionally NOT wired here — once a
1561
- * booking is open, the traveler row is its own source of truth and
1562
- * person-record edits should not retroactively rewrite trip data.
1563
- */
1564
- .patch("/:id/travelers/:travelerId/with-travel-details", async (c) => {
1565
- try {
1566
- const bookingId = c.req.param("id");
1567
- const travelerId = c.req.param("travelerId");
1568
- // Enforce booking↔traveler pairing before delegating to the
1569
- // service, which writes by traveler id only. Without this guard
1570
- // a caller could pass /bookings/A/travelers/<traveler-from-B>
1571
- // and write to B while the audit/PII context is built from A.
1572
- const traveler = await bookingsService.getTravelerRecordById(c.get("db"), bookingId, travelerId);
1573
- if (!traveler) {
1574
- return c.json({ error: "Traveler not found" }, 404);
1575
- }
1576
- const data = await parseJsonBody(c, updateTravelerWithTravelDetailsSchema);
1577
- const pii = await createAuditedBookingPiiService(c, bookingId);
1578
- const ledgerContext = getActionLedgerRequestContext(c);
1579
- const result = await c.get("db").transaction(async (tx) => {
1580
- const result = await bookingsService.updateTravelerWithTravelDetails(tx, travelerId, data, { pii, actorId: c.get("userId") });
1581
- if (!result)
1582
- return null;
1583
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1584
- action: "update",
1585
- travelerId: result.traveler.id,
1586
- changedFields: [
1587
- ...new Set([
1588
- ...changedBookingTravelerFields(data, traveler, result.traveler),
1589
- ...changedBookingTravelDetailFields(data),
1590
- ]),
1591
- ].sort(),
1592
- subject: "booking traveler with travel details",
1593
- actionName: "booking.traveler_with_travel_details.update",
1594
- routeOrToolName: "bookings.travelers.with-travel-details.update",
1595
- evaluatedRisk: "high",
1596
- });
1597
- return result;
1598
- });
1599
- if (!result) {
1600
- return c.json({ error: "Traveler not found" }, 404);
1601
- }
1602
- return c.json({ data: result });
1603
- }
1604
- catch (error) {
1605
- return handleKmsConfigError(c, error);
1606
- }
1607
- })
1608
- .patch("/:id/travelers/:travelerId/travel-details", async (c) => {
1609
- const auth = await authorizeBookingPiiAccess(c, {
1610
- bookingId: c.req.param("id"),
1611
- travelerId: c.req.param("travelerId"),
1612
- action: "update",
1613
- });
1614
- if (!auth.allowed) {
1615
- return auth.response;
1616
- }
1617
- const traveler = await bookingsService.getTravelerRecordById(c.get("db"), c.req.param("id"), c.req.param("travelerId"));
1618
- if (!traveler) {
1619
- await logBookingPiiAccess(c, {
1620
- bookingId: c.req.param("id"),
1621
- travelerId: c.req.param("travelerId"),
1622
- action: "update",
1623
- outcome: "denied",
1624
- reason: "participant_not_found",
1625
- });
1626
- return c.json({ error: "Traveler not found" }, 404);
1627
- }
1628
- try {
1629
- const pii = await createAuditedBookingPiiService(c, traveler.bookingId);
1630
- const body = await parseJsonBody(c, upsertTravelerTravelDetailsSchema);
1631
- const ledgerContext = getActionLedgerRequestContext(c);
1632
- const row = await c.get("db").transaction(async (tx) => {
1633
- const row = await pii.upsertTravelerTravelDetails(tx, traveler.id, body, c.get("userId"));
1634
- if (!row)
1635
- return null;
1636
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1637
- action: "update",
1638
- travelerId: traveler.id,
1639
- changedFields: changedBookingTravelDetailFields(body),
1640
- subject: "booking traveler travel details",
1641
- actionName: "booking.traveler_travel_details.update",
1642
- routeOrToolName: "bookings.travelers.travel-details.update",
1643
- evaluatedRisk: "high",
1644
- });
1645
- return row;
1646
- });
1647
- if (!row) {
1648
- return c.json({ error: "Traveler not found" }, 404);
1649
- }
1650
- return c.json({ data: row });
1651
- }
1652
- catch (error) {
1653
- return handleKmsConfigError(c, error);
1654
- }
1655
- })
1656
- .patch("/:id/travelers/:travelerId", async (c) => {
1657
- const bookingId = c.req.param("id");
1658
- const travelerId = c.req.param("travelerId");
1659
- const before = await bookingsService.getTravelerRecordById(c.get("db"), bookingId, travelerId);
1660
- if (!before) {
1661
- return c.json({ error: "Traveler not found" }, 404);
1662
- }
1663
- const body = await parseJsonBody(c, updateTravelerSchema);
1664
- const ledgerContext = getActionLedgerRequestContext(c);
1665
- const row = await c.get("db").transaction(async (tx) => {
1666
- const row = await bookingsService.updateTraveler(tx, travelerId, body);
1667
- if (!row)
1668
- return null;
1669
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1670
- action: "update",
1671
- travelerId: row.id,
1672
- changedFields: changedBookingTravelerFields(body, before, row),
1673
- subject: "booking traveler",
1674
- actionName: "booking.traveler.update",
1675
- routeOrToolName: "bookings.travelers.update",
1676
- evaluatedRisk: "high",
1677
- });
1678
- return row;
1679
- });
1680
- if (!row) {
1681
- return c.json({ error: "Traveler not found" }, 404);
1682
- }
1683
- return c.json({ data: row });
1684
- })
1685
- .delete("/:id/travelers/:travelerId/travel-details", async (c) => {
1686
- const auth = await authorizeBookingPiiAccess(c, {
1687
- bookingId: c.req.param("id"),
1688
- travelerId: c.req.param("travelerId"),
1689
- action: "delete",
1690
- });
1691
- if (!auth.allowed) {
1692
- return auth.response;
1693
- }
1694
- const traveler = await bookingsService.getTravelerRecordById(c.get("db"), c.req.param("id"), c.req.param("travelerId"));
1695
- if (!traveler) {
1696
- await logBookingPiiAccess(c, {
1697
- bookingId: c.req.param("id"),
1698
- travelerId: c.req.param("travelerId"),
1699
- action: "delete",
1700
- outcome: "denied",
1701
- reason: "participant_not_found",
1702
- });
1703
- return c.json({ error: "Traveler not found" }, 404);
1704
- }
1705
- try {
1706
- const pii = await createAuditedBookingPiiService(c, traveler.bookingId);
1707
- const ledgerContext = getActionLedgerRequestContext(c);
1708
- const row = await c.get("db").transaction(async (tx) => {
1709
- const row = await pii.deleteTravelerTravelDetails(tx, traveler.id, c.get("userId"));
1710
- if (!row)
1711
- return null;
1712
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1713
- action: "delete",
1714
- travelerId: traveler.id,
1715
- changedFields: [],
1716
- subject: "booking traveler travel details",
1717
- actionName: "booking.traveler_travel_details.delete",
1718
- routeOrToolName: "bookings.travelers.travel-details.delete",
1719
- evaluatedRisk: "high",
1720
- });
1721
- return row;
1722
- });
1723
- if (!row) {
1724
- return c.json({ error: "Traveler travel details not found" }, 404);
1725
- }
1726
- return c.json({ success: true }, 200);
1727
- }
1728
- catch (error) {
1729
- return handleKmsConfigError(c, error);
1730
- }
1731
- })
1732
- .delete("/:id/travelers/:travelerId", async (c) => {
1733
- const bookingId = c.req.param("id");
1734
- const travelerId = c.req.param("travelerId");
1735
- const before = await bookingsService.getTravelerRecordById(c.get("db"), bookingId, travelerId);
1736
- if (!before) {
1737
- return c.json({ error: "Traveler not found" }, 404);
1738
- }
1739
- const ledgerContext = getActionLedgerRequestContext(c);
1740
- const row = await c.get("db").transaction(async (tx) => {
1741
- const row = await bookingsService.deleteTraveler(tx, travelerId);
1742
- if (!row)
1743
- return null;
1744
- await appendBookingTravelerMutationLedgerEntryToDb(tx, ledgerContext, {
1745
- action: "delete",
1746
- travelerId,
1747
- changedFields: [],
1748
- subject: "booking traveler",
1749
- actionName: "booking.traveler.delete",
1750
- routeOrToolName: "bookings.travelers.delete",
1751
- evaluatedRisk: "high",
1752
- });
1753
- return row;
1754
- });
1755
- if (!row) {
1756
- return c.json({ error: "Traveler not found" }, 404);
1757
- }
1758
- return c.json({ success: true }, 200);
1759
- })
1760
- // ==========================================================================
1761
- // Items
1762
- // ==========================================================================
1763
- // 16. GET /:id/items — List booking items
1764
- .get("/:id/items", async (c) => {
1765
- return c.json({ data: await bookingsService.listItems(c.get("db"), c.req.param("id")) });
1766
- })
1767
- // 17. POST /:id/items — Add booking item
1768
- .post("/:id/items", async (c) => {
1769
- const body = await parseJsonBody(c, insertBookingItemSchema);
1770
- const ledgerContext = getActionLedgerRequestContext(c);
1771
- const row = await c.get("db").transaction(async (tx) => {
1772
- const row = await bookingsService.createItem(tx, c.req.param("id"), body, c.get("userId"));
1773
- if (!row)
1774
- return null;
1775
- await appendBookingMutationLedgerEntryToDb(tx, ledgerContext, {
1776
- action: "create",
1777
- actionName: "booking.item.create",
1778
- actionVersion: BOOKING_ITEM_LEDGER_ACTION_VERSION,
1779
- targetType: "booking_item",
1780
- targetId: row.id,
1781
- changedFields: changedBookingItemFields(body, null, row),
1782
- subject: "booking item",
1783
- routeOrToolName: "bookings.items.create",
1784
- evaluatedRisk: "high",
1785
- });
1786
- return row;
1787
- });
1788
- if (!row) {
1789
- return c.json({ error: "Booking not found" }, 404);
1790
- }
1791
- return c.json({ data: row }, 201);
1792
- })
1793
- // 18. PATCH /:id/items/:itemId — Update booking item
1794
- .patch("/:id/items/:itemId", async (c) => {
1795
- const bookingId = c.req.param("id");
1796
- const itemId = c.req.param("itemId");
1797
- const before = (await bookingsService.listItems(c.get("db"), bookingId)).find((item) => item.id === itemId) ?? null;
1798
- if (!before) {
1799
- return c.json({ error: "Booking item not found" }, 404);
1800
- }
1801
- const body = await parseJsonBody(c, updateBookingItemSchema);
1802
- const ledgerContext = getActionLedgerRequestContext(c);
1803
- const row = await c.get("db").transaction(async (tx) => {
1804
- const row = await bookingsService.updateItem(tx, itemId, body);
1805
- if (!row)
1806
- return null;
1807
- await appendBookingMutationLedgerEntryToDb(tx, ledgerContext, {
1808
- action: "update",
1809
- actionName: "booking.item.update",
1810
- actionVersion: BOOKING_ITEM_LEDGER_ACTION_VERSION,
1811
- targetType: "booking_item",
1812
- targetId: row.id,
1813
- changedFields: changedBookingItemFields(body, before, row),
1814
- subject: "booking item",
1815
- routeOrToolName: "bookings.items.update",
1816
- evaluatedRisk: "high",
1817
- });
1818
- return row;
1819
- });
1820
- if (!row) {
1821
- return c.json({ error: "Booking item not found" }, 404);
1822
- }
1823
- return c.json({ data: row });
1824
- })
1825
- // 19. DELETE /:id/items/:itemId — Delete booking item
1826
- .delete("/:id/items/:itemId", async (c) => {
1827
- const bookingId = c.req.param("id");
1828
- const itemId = c.req.param("itemId");
1829
- const before = (await bookingsService.listItems(c.get("db"), bookingId)).find((item) => item.id === itemId) ?? null;
1830
- if (!before) {
1831
- return c.json({ error: "Booking item not found" }, 404);
1832
- }
1833
- const ledgerContext = getActionLedgerRequestContext(c);
1834
- const row = await c.get("db").transaction(async (tx) => {
1835
- const row = await bookingsService.deleteItem(tx, itemId);
1836
- if (!row)
1837
- return null;
1838
- await appendBookingMutationLedgerEntryToDb(tx, ledgerContext, {
1839
- action: "delete",
1840
- actionName: "booking.item.delete",
1841
- actionVersion: BOOKING_ITEM_LEDGER_ACTION_VERSION,
1842
- targetType: "booking_item",
1843
- targetId: itemId,
1844
- changedFields: [],
1845
- subject: "booking item",
1846
- routeOrToolName: "bookings.items.delete",
1847
- evaluatedRisk: "high",
1848
- summary: "Deleted booking item",
1849
- });
1850
- return row;
1851
- });
1852
- if (!row) {
1853
- return c.json({ error: "Booking item not found" }, 404);
1854
- }
1855
- return c.json({ success: true }, 200);
1856
- })
1857
- // 20. GET /:id/items/:itemId/travelers — List item travelers
1858
- .get("/:id/items/:itemId/travelers", async (c) => {
1859
- return c.json({
1860
- data: await bookingsService.listItemParticipants(c.get("db"), c.req.param("itemId")),
1861
- });
1862
- })
1863
- // 21. POST /:id/items/:itemId/travelers — Link traveler to item
1864
- .post("/:id/items/:itemId/travelers", async (c) => {
1865
- const bookingId = c.req.param("id");
1866
- const itemId = c.req.param("itemId");
1867
- const item = (await bookingsService.listItems(c.get("db"), bookingId)).find((row) => row.id === itemId) ??
1868
- null;
1869
- if (!item) {
1870
- return c.json({ error: "Booking item not found" }, 404);
1871
- }
1872
- const body = await parseJsonBody(c, insertBookingItemTravelerSchema);
1873
- const ledgerContext = getActionLedgerRequestContext(c);
1874
- const row = await c.get("db").transaction(async (tx) => {
1875
- const row = await bookingsService.addItemParticipant(tx, itemId, body);
1876
- if (!row)
1877
- return null;
1878
- await appendBookingMutationLedgerEntryToDb(tx, ledgerContext, {
1879
- action: "create",
1880
- actionName: "booking.item_traveler.create",
1881
- actionVersion: BOOKING_ITEM_LEDGER_ACTION_VERSION,
1882
- targetType: "booking_item",
1883
- targetId: itemId,
1884
- changedFields: changedBookingItemFields(body, null, row),
1885
- subject: "booking item traveler link",
1886
- routeOrToolName: "bookings.items.travelers.create",
1887
- evaluatedRisk: "high",
1888
- summary: "Linked traveler to booking item",
1889
- });
1890
- return row;
1891
- });
1892
- if (!row) {
1893
- return c.json({ error: "Booking item or traveler not found" }, 404);
1894
- }
1895
- return c.json({ data: row }, 201);
1896
- })
1897
- // 22. DELETE /:id/items/:itemId/travelers/:linkId — Unlink traveler from item
1898
- .delete("/:id/items/:itemId/travelers/:linkId", async (c) => {
1899
- const bookingId = c.req.param("id");
1900
- const itemId = c.req.param("itemId");
1901
- const linkId = c.req.param("linkId");
1902
- const item = (await bookingsService.listItems(c.get("db"), bookingId)).find((row) => row.id === itemId) ??
1903
- null;
1904
- if (!item) {
1905
- return c.json({ error: "Booking item not found" }, 404);
1906
- }
1907
- const before = (await bookingsService.listItemParticipants(c.get("db"), itemId)).find((link) => link.id === linkId) ?? null;
1908
- if (!before) {
1909
- return c.json({ error: "Booking item traveler link not found" }, 404);
1910
- }
1911
- const ledgerContext = getActionLedgerRequestContext(c);
1912
- const row = await c.get("db").transaction(async (tx) => {
1913
- const row = await bookingsService.removeItemParticipant(tx, linkId);
1914
- if (!row)
1915
- return null;
1916
- await appendBookingMutationLedgerEntryToDb(tx, ledgerContext, {
1917
- action: "delete",
1918
- actionName: "booking.item_traveler.delete",
1919
- actionVersion: BOOKING_ITEM_LEDGER_ACTION_VERSION,
1920
- targetType: "booking_item",
1921
- targetId: itemId,
1922
- changedFields: [],
1923
- subject: "booking item traveler link",
1924
- routeOrToolName: "bookings.items.travelers.delete",
1925
- evaluatedRisk: "high",
1926
- summary: "Unlinked traveler from booking item",
1927
- });
1928
- return row;
1929
- });
1930
- if (!row) {
1931
- return c.json({ error: "Booking item traveler link not found" }, 404);
1932
- }
1933
- return c.json({ success: true }, 200);
1934
- })
1935
- // ==========================================================================
1936
- // Supplier Statuses
1937
- // ==========================================================================
1938
- .get("/:id/supplier-statuses", async (c) => {
1939
- return c.json({
1940
- data: await bookingsService.listSupplierStatuses(c.get("db"), c.req.param("id")),
1941
- });
1942
- })
1943
- .post("/:id/supplier-statuses", async (c) => {
1944
- const row = await bookingsService.createSupplierStatus(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertSupplierStatusSchema), c.get("userId"));
1945
- if (!row) {
1946
- return c.json({ error: "Booking not found" }, 404);
1947
- }
1948
- return c.json({ data: row }, 201);
1949
- })
1950
- .patch("/:id/supplier-statuses/:statusId", async (c) => {
1951
- const row = await bookingsService.updateSupplierStatus(c.get("db"), c.req.param("id"), c.req.param("statusId"), await parseJsonBody(c, updateSupplierStatusSchema), c.get("userId"));
1952
- if (!row) {
1953
- return c.json({ error: "Supplier status not found" }, 404);
1954
- }
1955
- return c.json({ data: row });
1956
- })
1957
- // ==========================================================================
1958
- // Fulfillment
1959
- // ==========================================================================
1960
- .get("/:id/fulfillments", async (c) => {
1961
- return c.json({ data: await bookingsService.listFulfillments(c.get("db"), c.req.param("id")) });
1962
- })
1963
- .post("/:id/fulfillments", async (c) => {
1964
- const row = await bookingsService.issueFulfillment(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertBookingFulfillmentSchema), c.get("userId"));
1965
- if (!row) {
1966
- return c.json({ error: "Booking, item, or traveler not found" }, 404);
1967
- }
1968
- return c.json({ data: row }, 201);
1969
- })
1970
- .patch("/:id/fulfillments/:fulfillmentId", async (c) => {
1971
- const row = await bookingsService.updateFulfillment(c.get("db"), c.req.param("id"), c.req.param("fulfillmentId"), await parseJsonBody(c, updateBookingFulfillmentSchema), c.get("userId"));
1972
- if (!row) {
1973
- return c.json({ error: "Fulfillment, item, or traveler not found" }, 404);
1974
- }
1975
- return c.json({ data: row });
1976
- })
1977
- // ==========================================================================
1978
- // Redemption
1979
- // ==========================================================================
1980
- .get("/:id/redemptions", async (c) => {
1981
- return c.json({
1982
- data: await bookingsService.listRedemptionEvents(c.get("db"), c.req.param("id")),
1983
- });
1984
- })
1985
- .post("/:id/redemptions", async (c) => {
1986
- const row = await bookingsService.recordRedemption(c.get("db"), c.req.param("id"), await parseJsonBody(c, recordBookingRedemptionSchema), c.get("userId"));
1987
- if (!row) {
1988
- return c.json({ error: "Booking, item, or traveler not found" }, 404);
1989
- }
1990
- return c.json({ data: row }, 201);
1991
- })
1992
- // ==========================================================================
1993
- // Activity Log
1994
- // ==========================================================================
1995
- // 26. GET /:id/activity — List activity log
1996
- .get("/:id/activity", async (c) => {
1997
- return c.json({ data: await bookingsService.listActivity(c.get("db"), c.req.param("id")) });
1998
- })
1999
- // 26a. GET /:id/group — Shared-room group membership for this booking (or null)
2000
- .get("/:id/group", async (c) => {
2001
- const result = await bookingGroupsService.getBookingGroupForBooking(c.get("db"), c.req.param("id"));
2002
- return c.json({ data: result ?? null });
2003
- })
2004
- // ==========================================================================
2005
- // Notes
2006
- // ==========================================================================
2007
- // 27. GET /:id/notes — List notes
2008
- .get("/:id/notes", async (c) => {
2009
- return c.json({ data: await bookingsService.listNotes(c.get("db"), c.req.param("id")) });
2010
- })
2011
- // 28. POST /:id/notes — Add note
2012
- .post("/:id/notes", async (c) => {
2013
- const userId = requireUserId(c);
2014
- const bookingId = c.req.param("id");
2015
- const row = await bookingsService.createNote(c.get("db"), bookingId, userId, await parseJsonBody(c, insertBookingNoteSchema));
2016
- if (!row) {
2017
- return c.json({ error: "Booking not found" }, 404);
2018
- }
2019
- await appendBookingMutationLedgerEntry(c, {
2020
- action: "create",
2021
- actionName: "booking.note.create",
2022
- actionVersion: BOOKING_NOTE_LEDGER_ACTION_VERSION,
2023
- targetType: "booking",
2024
- targetId: bookingId,
2025
- changedFields: ["content"],
2026
- subject: "booking note",
2027
- routeOrToolName: "bookings.notes.create",
2028
- evaluatedRisk: "medium",
2029
- summary: "Created booking note",
2030
- });
2031
- return c.json({ data: row }, 201);
2032
- })
2033
- // 28b. PATCH /:id/notes/:noteId — Edit note
2034
- .patch("/:id/notes/:noteId", async (c) => {
2035
- const bookingId = c.req.param("id");
2036
- const noteId = c.req.param("noteId");
2037
- const row = await bookingsService.updateNote(c.get("db"), bookingId, noteId, await parseJsonBody(c, updateBookingNoteSchema));
2038
- if (!row) {
2039
- return c.json({ error: "Note not found" }, 404);
2040
- }
2041
- await appendBookingMutationLedgerEntry(c, {
2042
- action: "update",
2043
- actionName: "booking.note.update",
2044
- actionVersion: BOOKING_NOTE_LEDGER_ACTION_VERSION,
2045
- targetType: "booking",
2046
- targetId: bookingId,
2047
- changedFields: ["content"],
2048
- subject: "booking note",
2049
- routeOrToolName: "bookings.notes.update",
2050
- evaluatedRisk: "medium",
2051
- summary: "Updated booking note",
2052
- });
2053
- return c.json({ data: row });
2054
- })
2055
- // 28c. DELETE /:id/notes/:noteId — Delete note
2056
- .delete("/:id/notes/:noteId", async (c) => {
2057
- const bookingId = c.req.param("id");
2058
- const row = await bookingsService.deleteNote(c.get("db"), bookingId, c.req.param("noteId"));
2059
- if (!row) {
2060
- return c.json({ error: "Note not found" }, 404);
2061
- }
2062
- await appendBookingMutationLedgerEntry(c, {
2063
- action: "delete",
2064
- actionName: "booking.note.delete",
2065
- actionVersion: BOOKING_NOTE_LEDGER_ACTION_VERSION,
2066
- targetType: "booking",
2067
- targetId: bookingId,
2068
- changedFields: [],
2069
- subject: "booking note",
2070
- routeOrToolName: "bookings.notes.delete",
2071
- evaluatedRisk: "medium",
2072
- summary: "Deleted booking note",
2073
- });
2074
- return c.json({ success: true }, 200);
2075
- })
2076
- // ==========================================================================
2077
- // Documents
2078
- // ==========================================================================
2079
- // 29. GET /:id/documents — List documents for booking
2080
- .get("/:id/documents", async (c) => {
2081
- return c.json({ data: await bookingsService.listDocuments(c.get("db"), c.req.param("id")) });
2082
- })
2083
- // 30. POST /:id/documents — Add document to booking
2084
- .post("/:id/documents", async (c) => {
2085
- const row = await bookingsService.createDocument(c.get("db"), c.req.param("id"), await parseJsonBody(c, insertBookingDocumentSchema));
2086
- if (!row) {
2087
- return c.json({ error: "Booking not found" }, 404);
2088
- }
2089
- return c.json({ data: row }, 201);
2090
- })
2091
- // 31. DELETE /:id/documents/:documentId — Delete document
2092
- .delete("/:id/documents/:documentId", async (c) => {
2093
- const row = await bookingsService.deleteDocument(c.get("db"), c.req.param("documentId"));
2094
- if (!row) {
2095
- return c.json({ error: "Document not found" }, 404);
2096
- }
2097
- return c.json({ success: true }, 200);
2098
- })
2099
- // ==========================================================================
2100
- // Booking Groups (shared-room / split-booking model)
2101
- // ==========================================================================
2102
- .route("/groups", bookingGroupRoutes);
2103
- export const __test__ = {
2104
- bookingActionLedgerQuerySchema,
2105
- bookingMutationSummary,
2106
- buildBookingActionLedgerPage,
2107
- changedBookingItemFields,
2108
- changedBookingMutationFields,
2109
- changedBookingTravelDetailFields,
2110
- changedBookingTravelerFields,
2111
- };
1
+ export * from "./routes-admin.js";