@voyantjs/bookings 0.1.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.
- package/LICENSE +109 -0
- package/README.md +42 -0
- package/dist/availability-ref.d.ts +418 -0
- package/dist/availability-ref.d.ts.map +1 -0
- package/dist/availability-ref.js +28 -0
- package/dist/extensions/suppliers.d.ts +3 -0
- package/dist/extensions/suppliers.d.ts.map +1 -0
- package/dist/extensions/suppliers.js +103 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/pii.d.ts +29 -0
- package/dist/pii.d.ts.map +1 -0
- package/dist/pii.js +131 -0
- package/dist/products-ref.d.ts +1043 -0
- package/dist/products-ref.d.ts.map +1 -0
- package/dist/products-ref.js +76 -0
- package/dist/routes.d.ts +2171 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +659 -0
- package/dist/schema/travel-details.d.ts +179 -0
- package/dist/schema/travel-details.d.ts.map +1 -0
- package/dist/schema/travel-details.js +46 -0
- package/dist/schema.d.ts +3180 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +509 -0
- package/dist/service.d.ts +5000 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +2016 -0
- package/dist/tasks/expire-stale-holds.d.ts +12 -0
- package/dist/tasks/expire-stale-holds.d.ts.map +1 -0
- package/dist/tasks/expire-stale-holds.js +7 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +1 -0
- package/dist/transactions-ref.d.ts +2223 -0
- package/dist/transactions-ref.d.ts.map +1 -0
- package/dist/transactions-ref.js +147 -0
- package/dist/validation.d.ts +643 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +355 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAi3BtB,CAAA;AAEJ,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAA"}
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { createKmsProviderFromEnv } from "@voyantjs/utils";
|
|
3
|
+
import { createBookingPiiService } from "./pii.js";
|
|
4
|
+
import { bookingsService } from "./service.js";
|
|
5
|
+
import { bookingPiiAccessLog } from "./schema.js";
|
|
6
|
+
import { bookingListQuerySchema, cancelBookingSchema, confirmBookingSchema, createBookingSchema, convertProductSchema, expireBookingSchema, expireStaleBookingsSchema, extendBookingHoldSchema, reserveBookingFromTransactionSchema, insertBookingFulfillmentSchema, insertBookingDocumentSchema, insertBookingItemParticipantSchema, insertBookingItemSchema, insertBookingNoteSchema, insertParticipantSchema, insertPassengerSchema, insertSupplierStatusSchema, recordBookingRedemptionSchema, reserveBookingSchema, upsertParticipantTravelDetailsSchema, updateBookingFulfillmentSchema, updateSupplierStatusSchema, updateBookingItemSchema, updateBookingSchema, updateBookingStatusSchema, updateParticipantSchema, updatePassengerSchema, } from "./validation.js";
|
|
7
|
+
function getRuntimeEnv(c) {
|
|
8
|
+
const processEnv = globalThis.process?.env ?? {};
|
|
9
|
+
return {
|
|
10
|
+
...processEnv,
|
|
11
|
+
...(c.env ?? {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function hasPiiScope(scopes, action) {
|
|
15
|
+
if (!scopes || scopes.length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return (scopes.includes("*") ||
|
|
19
|
+
scopes.includes("bookings-pii:*") ||
|
|
20
|
+
scopes.includes(`bookings-pii:${action}`));
|
|
21
|
+
}
|
|
22
|
+
async function logBookingPiiAccess(c, input) {
|
|
23
|
+
await c.get("db").insert(bookingPiiAccessLog).values({
|
|
24
|
+
bookingId: input.bookingId ?? null,
|
|
25
|
+
participantId: input.participantId ?? null,
|
|
26
|
+
actorId: c.get("userId") ?? null,
|
|
27
|
+
actorType: c.get("actor") ?? null,
|
|
28
|
+
callerType: c.get("callerType") ?? null,
|
|
29
|
+
action: input.action,
|
|
30
|
+
outcome: input.outcome,
|
|
31
|
+
reason: input.reason ?? null,
|
|
32
|
+
metadata: input.metadata ?? null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function authorizeBookingPiiAccess(c, input) {
|
|
36
|
+
if (c.get("isInternalRequest")) {
|
|
37
|
+
return { allowed: true };
|
|
38
|
+
}
|
|
39
|
+
const userId = c.get("userId");
|
|
40
|
+
if (!userId) {
|
|
41
|
+
await logBookingPiiAccess(c, {
|
|
42
|
+
...input,
|
|
43
|
+
outcome: "denied",
|
|
44
|
+
reason: "missing_user",
|
|
45
|
+
});
|
|
46
|
+
return { allowed: false, response: c.json({ error: "Unauthorized" }, 401) };
|
|
47
|
+
}
|
|
48
|
+
const customAuthorizer = c.get("authorizeBookingPii");
|
|
49
|
+
if (customAuthorizer) {
|
|
50
|
+
const allowed = await customAuthorizer({
|
|
51
|
+
db: c.get("db"),
|
|
52
|
+
userId,
|
|
53
|
+
actor: c.get("actor"),
|
|
54
|
+
callerType: c.get("callerType"),
|
|
55
|
+
scopes: c.get("scopes"),
|
|
56
|
+
isInternalRequest: c.get("isInternalRequest"),
|
|
57
|
+
...input,
|
|
58
|
+
});
|
|
59
|
+
if (!allowed) {
|
|
60
|
+
await logBookingPiiAccess(c, {
|
|
61
|
+
...input,
|
|
62
|
+
outcome: "denied",
|
|
63
|
+
reason: "custom_policy_denied",
|
|
64
|
+
});
|
|
65
|
+
return { allowed: false, response: c.json({ error: "Forbidden" }, 403) };
|
|
66
|
+
}
|
|
67
|
+
return { allowed: true };
|
|
68
|
+
}
|
|
69
|
+
const actor = c.get("actor");
|
|
70
|
+
const scopes = c.get("scopes");
|
|
71
|
+
const allowed = hasPiiScope(scopes, input.action) || actor === "staff";
|
|
72
|
+
if (!allowed) {
|
|
73
|
+
await logBookingPiiAccess(c, {
|
|
74
|
+
...input,
|
|
75
|
+
outcome: "denied",
|
|
76
|
+
reason: "insufficient_scope",
|
|
77
|
+
metadata: { actor: actor ?? null },
|
|
78
|
+
});
|
|
79
|
+
return { allowed: false, response: c.json({ error: "Forbidden" }, 403) };
|
|
80
|
+
}
|
|
81
|
+
return { allowed: true };
|
|
82
|
+
}
|
|
83
|
+
function handleKmsConfigError(c, error) {
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
return c.json({
|
|
86
|
+
error: "Booking PII encryption is not configured",
|
|
87
|
+
details: error.message,
|
|
88
|
+
}, 500);
|
|
89
|
+
}
|
|
90
|
+
return c.json({ error: "Booking PII encryption is not configured" }, 500);
|
|
91
|
+
}
|
|
92
|
+
// ==========================================================================
|
|
93
|
+
// Bookings — method-chained for Hono RPC type inference
|
|
94
|
+
// ==========================================================================
|
|
95
|
+
export const bookingRoutes = new Hono()
|
|
96
|
+
// ==========================================================================
|
|
97
|
+
// Bookings CRUD
|
|
98
|
+
// ==========================================================================
|
|
99
|
+
// 1. GET / — List bookings
|
|
100
|
+
.get("/", async (c) => {
|
|
101
|
+
const query = bookingListQuerySchema.parse(Object.fromEntries(new URL(c.req.url).searchParams));
|
|
102
|
+
return c.json(await bookingsService.listBookings(c.get("db"), query));
|
|
103
|
+
})
|
|
104
|
+
// 2. GET /:id — Get single booking
|
|
105
|
+
.get("/:id", async (c) => {
|
|
106
|
+
const row = await bookingsService.getBookingById(c.get("db"), c.req.param("id"));
|
|
107
|
+
if (!row) {
|
|
108
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
109
|
+
}
|
|
110
|
+
return c.json({ data: row });
|
|
111
|
+
})
|
|
112
|
+
// 3. POST /reserve — Reserve inventory and create on-hold booking
|
|
113
|
+
.post("/reserve", async (c) => {
|
|
114
|
+
const result = await bookingsService.reserveBooking(c.get("db"), reserveBookingSchema.parse(await c.req.json()), c.get("userId"));
|
|
115
|
+
if ("booking" in result) {
|
|
116
|
+
return c.json({ data: result.booking }, 201);
|
|
117
|
+
}
|
|
118
|
+
if (result.status === "slot_not_found") {
|
|
119
|
+
return c.json({ error: "Availability slot not found" }, 404);
|
|
120
|
+
}
|
|
121
|
+
if (result.status === "insufficient_capacity") {
|
|
122
|
+
return c.json({ error: "Insufficient slot capacity" }, 409);
|
|
123
|
+
}
|
|
124
|
+
if (result.status === "slot_unavailable") {
|
|
125
|
+
return c.json({ error: "Availability slot is not bookable" }, 409);
|
|
126
|
+
}
|
|
127
|
+
if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
|
|
128
|
+
return c.json({ error: "Reservation item does not match availability slot" }, 409);
|
|
129
|
+
}
|
|
130
|
+
return c.json({ error: "Unable to reserve booking" }, 400);
|
|
131
|
+
})
|
|
132
|
+
// 3a. POST /from-product — Create booking draft from product definition
|
|
133
|
+
.post("/from-product", async (c) => {
|
|
134
|
+
const row = await bookingsService.createBookingFromProduct(c.get("db"), convertProductSchema.parse(await c.req.json()), c.get("userId"));
|
|
135
|
+
if (!row) {
|
|
136
|
+
return c.json({ error: "Product or option not found" }, 404);
|
|
137
|
+
}
|
|
138
|
+
return c.json({ data: row }, 201);
|
|
139
|
+
})
|
|
140
|
+
// 3b. POST /from-offer/:offerId/reserve — Reserve booking from transaction offer
|
|
141
|
+
.post("/from-offer/:offerId/reserve", async (c) => {
|
|
142
|
+
const result = await bookingsService.reserveBookingFromOffer(c.get("db"), c.req.param("offerId"), reserveBookingFromTransactionSchema.parse(await c.req.json()), c.get("userId"));
|
|
143
|
+
if (result.status === "not_found") {
|
|
144
|
+
return c.json({ error: "Offer not found" }, 404);
|
|
145
|
+
}
|
|
146
|
+
if (result.status === "slot_not_found") {
|
|
147
|
+
return c.json({ error: "Availability slot not found" }, 404);
|
|
148
|
+
}
|
|
149
|
+
if (result.status === "insufficient_capacity") {
|
|
150
|
+
return c.json({ error: "Insufficient slot capacity" }, 409);
|
|
151
|
+
}
|
|
152
|
+
if (result.status === "slot_unavailable") {
|
|
153
|
+
return c.json({ error: "Availability slot is not bookable" }, 409);
|
|
154
|
+
}
|
|
155
|
+
if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
|
|
156
|
+
return c.json({ error: "Reservation item does not match availability slot" }, 409);
|
|
157
|
+
}
|
|
158
|
+
if ("booking" in result) {
|
|
159
|
+
return c.json({ data: result.booking }, 201);
|
|
160
|
+
}
|
|
161
|
+
return c.json({ error: "Unable to reserve booking from offer" }, 400);
|
|
162
|
+
})
|
|
163
|
+
// 3c. POST /from-order/:orderId/reserve — Reserve booking from transaction order
|
|
164
|
+
.post("/from-order/:orderId/reserve", async (c) => {
|
|
165
|
+
const result = await bookingsService.reserveBookingFromOrder(c.get("db"), c.req.param("orderId"), reserveBookingFromTransactionSchema.parse(await c.req.json()), c.get("userId"));
|
|
166
|
+
if (result.status === "not_found") {
|
|
167
|
+
return c.json({ error: "Order not found" }, 404);
|
|
168
|
+
}
|
|
169
|
+
if (result.status === "slot_not_found") {
|
|
170
|
+
return c.json({ error: "Availability slot not found" }, 404);
|
|
171
|
+
}
|
|
172
|
+
if (result.status === "insufficient_capacity") {
|
|
173
|
+
return c.json({ error: "Insufficient slot capacity" }, 409);
|
|
174
|
+
}
|
|
175
|
+
if (result.status === "slot_unavailable") {
|
|
176
|
+
return c.json({ error: "Availability slot is not bookable" }, 409);
|
|
177
|
+
}
|
|
178
|
+
if (result.status === "slot_product_mismatch" || result.status === "slot_option_mismatch") {
|
|
179
|
+
return c.json({ error: "Reservation item does not match availability slot" }, 409);
|
|
180
|
+
}
|
|
181
|
+
if ("booking" in result) {
|
|
182
|
+
return c.json({ data: result.booking }, 201);
|
|
183
|
+
}
|
|
184
|
+
return c.json({ error: "Unable to reserve booking from order" }, 400);
|
|
185
|
+
})
|
|
186
|
+
// 4. POST / — Create booking (manual/backoffice only)
|
|
187
|
+
.post("/", async (c) => {
|
|
188
|
+
const payload = await c.req.json();
|
|
189
|
+
const parsed = createBookingSchema.safeParse(payload);
|
|
190
|
+
if (!parsed.success) {
|
|
191
|
+
return c.json({
|
|
192
|
+
error: parsed.error.issues[0]?.message ?? "Invalid booking create payload",
|
|
193
|
+
details: parsed.error.flatten(),
|
|
194
|
+
}, 400);
|
|
195
|
+
}
|
|
196
|
+
return c.json({
|
|
197
|
+
data: await bookingsService.createBooking(c.get("db"), parsed.data, c.get("userId")),
|
|
198
|
+
}, 201);
|
|
199
|
+
})
|
|
200
|
+
// 5. PATCH /:id — Update booking
|
|
201
|
+
.patch("/:id", async (c) => {
|
|
202
|
+
const row = await bookingsService.updateBooking(c.get("db"), c.req.param("id"), updateBookingSchema.parse(await c.req.json()));
|
|
203
|
+
if (!row) {
|
|
204
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
205
|
+
}
|
|
206
|
+
return c.json({ data: row });
|
|
207
|
+
})
|
|
208
|
+
// 6. DELETE /:id — Delete booking
|
|
209
|
+
.delete("/:id", async (c) => {
|
|
210
|
+
const row = await bookingsService.deleteBooking(c.get("db"), c.req.param("id"));
|
|
211
|
+
if (!row) {
|
|
212
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
213
|
+
}
|
|
214
|
+
return c.json({ success: true }, 200);
|
|
215
|
+
})
|
|
216
|
+
// ==========================================================================
|
|
217
|
+
// Status
|
|
218
|
+
// ==========================================================================
|
|
219
|
+
// 7. PATCH /:id/status — Change booking status
|
|
220
|
+
.patch("/:id/status", async (c) => {
|
|
221
|
+
const result = await bookingsService.updateBookingStatus(c.get("db"), c.req.param("id"), updateBookingStatusSchema.parse(await c.req.json()), c.get("userId"));
|
|
222
|
+
if (result.status === "not_found") {
|
|
223
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
224
|
+
}
|
|
225
|
+
if (result.status === "invalid_transition") {
|
|
226
|
+
return c.json({ error: "Invalid booking status transition" }, 409);
|
|
227
|
+
}
|
|
228
|
+
if ("booking" in result) {
|
|
229
|
+
return c.json({ data: result.booking });
|
|
230
|
+
}
|
|
231
|
+
return c.json({ error: "Unable to update booking status" }, 400);
|
|
232
|
+
})
|
|
233
|
+
// 8. POST /:id/confirm — Confirm an on-hold booking
|
|
234
|
+
.post("/:id/confirm", async (c) => {
|
|
235
|
+
const result = await bookingsService.confirmBooking(c.get("db"), c.req.param("id"), confirmBookingSchema.parse(await c.req.json()), c.get("userId"));
|
|
236
|
+
if (result.status === "not_found") {
|
|
237
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
238
|
+
}
|
|
239
|
+
if (result.status === "hold_expired") {
|
|
240
|
+
return c.json({ error: "Booking hold has expired" }, 409);
|
|
241
|
+
}
|
|
242
|
+
if (result.status === "invalid_transition") {
|
|
243
|
+
return c.json({ error: "Booking is not in an on-hold state" }, 409);
|
|
244
|
+
}
|
|
245
|
+
if ("booking" in result) {
|
|
246
|
+
return c.json({ data: result.booking });
|
|
247
|
+
}
|
|
248
|
+
return c.json({ error: "Unable to confirm booking" }, 400);
|
|
249
|
+
})
|
|
250
|
+
// 9. POST /:id/extend-hold — Extend booking hold expiry
|
|
251
|
+
.post("/:id/extend-hold", async (c) => {
|
|
252
|
+
const result = await bookingsService.extendBookingHold(c.get("db"), c.req.param("id"), extendBookingHoldSchema.parse(await c.req.json()), c.get("userId"));
|
|
253
|
+
if (result.status === "not_found") {
|
|
254
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
255
|
+
}
|
|
256
|
+
if (result.status === "hold_expired") {
|
|
257
|
+
return c.json({ error: "Booking hold has expired" }, 409);
|
|
258
|
+
}
|
|
259
|
+
if (result.status === "invalid_transition") {
|
|
260
|
+
return c.json({ error: "Booking is not in an on-hold state" }, 409);
|
|
261
|
+
}
|
|
262
|
+
if ("booking" in result) {
|
|
263
|
+
return c.json({ data: result.booking });
|
|
264
|
+
}
|
|
265
|
+
return c.json({ error: "Unable to extend booking hold" }, 400);
|
|
266
|
+
})
|
|
267
|
+
// 10. POST /:id/expire — Expire an on-hold booking
|
|
268
|
+
.post("/:id/expire", async (c) => {
|
|
269
|
+
const result = await bookingsService.expireBooking(c.get("db"), c.req.param("id"), expireBookingSchema.parse(await c.req.json()), c.get("userId"));
|
|
270
|
+
if (result.status === "not_found") {
|
|
271
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
272
|
+
}
|
|
273
|
+
if (result.status === "invalid_transition") {
|
|
274
|
+
return c.json({ error: "Booking is not in an on-hold state" }, 409);
|
|
275
|
+
}
|
|
276
|
+
if ("booking" in result) {
|
|
277
|
+
return c.json({ data: result.booking });
|
|
278
|
+
}
|
|
279
|
+
return c.json({ error: "Unable to expire booking" }, 400);
|
|
280
|
+
})
|
|
281
|
+
// 10b. POST /expire-stale — Expire all stale on-hold bookings up to a cutoff
|
|
282
|
+
.post("/expire-stale", async (c) => {
|
|
283
|
+
return c.json(await bookingsService.expireStaleBookings(c.get("db"), expireStaleBookingsSchema.parse(await c.req.json()), c.get("userId")));
|
|
284
|
+
})
|
|
285
|
+
// 11. POST /:id/cancel — Cancel a booking and release allocations
|
|
286
|
+
.post("/:id/cancel", async (c) => {
|
|
287
|
+
const result = await bookingsService.cancelBooking(c.get("db"), c.req.param("id"), cancelBookingSchema.parse(await c.req.json()), c.get("userId"));
|
|
288
|
+
if (result.status === "not_found") {
|
|
289
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
290
|
+
}
|
|
291
|
+
if (result.status === "invalid_transition") {
|
|
292
|
+
return c.json({ error: "Booking cannot be cancelled from its current state" }, 409);
|
|
293
|
+
}
|
|
294
|
+
if ("booking" in result) {
|
|
295
|
+
return c.json({ data: result.booking });
|
|
296
|
+
}
|
|
297
|
+
return c.json({ error: "Unable to cancel booking" }, 400);
|
|
298
|
+
})
|
|
299
|
+
// ==========================================================================
|
|
300
|
+
// Participants
|
|
301
|
+
// ==========================================================================
|
|
302
|
+
// 12. GET /:id/allocations — List booking allocations
|
|
303
|
+
.get("/:id/allocations", async (c) => {
|
|
304
|
+
return c.json({ data: await bookingsService.listAllocations(c.get("db"), c.req.param("id")) });
|
|
305
|
+
})
|
|
306
|
+
// 13. GET /:id/participants — List participants
|
|
307
|
+
.get("/:id/participants", async (c) => {
|
|
308
|
+
return c.json({ data: await bookingsService.listParticipants(c.get("db"), c.req.param("id")) });
|
|
309
|
+
})
|
|
310
|
+
// 13a. GET /:id/participants/:participantId/travel-details — Read encrypted travel details
|
|
311
|
+
.get("/:id/participants/:participantId/travel-details", async (c) => {
|
|
312
|
+
const auth = await authorizeBookingPiiAccess(c, {
|
|
313
|
+
bookingId: c.req.param("id"),
|
|
314
|
+
participantId: c.req.param("participantId"),
|
|
315
|
+
action: "read",
|
|
316
|
+
});
|
|
317
|
+
if (!auth.allowed) {
|
|
318
|
+
return auth.response;
|
|
319
|
+
}
|
|
320
|
+
const participant = await bookingsService.getParticipantById(c.get("db"), c.req.param("id"), c.req.param("participantId"));
|
|
321
|
+
if (!participant) {
|
|
322
|
+
await logBookingPiiAccess(c, {
|
|
323
|
+
bookingId: c.req.param("id"),
|
|
324
|
+
participantId: c.req.param("participantId"),
|
|
325
|
+
action: "read",
|
|
326
|
+
outcome: "denied",
|
|
327
|
+
reason: "participant_not_found",
|
|
328
|
+
});
|
|
329
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const pii = createBookingPiiService({
|
|
333
|
+
kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
|
|
334
|
+
onAudit: async (event) => {
|
|
335
|
+
await logBookingPiiAccess(c, {
|
|
336
|
+
bookingId: participant.bookingId,
|
|
337
|
+
participantId: event.participantId,
|
|
338
|
+
action: event.action === "encrypt" ? "update" : event.action === "decrypt" ? "read" : event.action,
|
|
339
|
+
outcome: "allowed",
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const details = await pii.getParticipantTravelDetails(c.get("db"), participant.id, c.get("userId"));
|
|
344
|
+
if (!details) {
|
|
345
|
+
await logBookingPiiAccess(c, {
|
|
346
|
+
bookingId: participant.bookingId,
|
|
347
|
+
participantId: participant.id,
|
|
348
|
+
action: "read",
|
|
349
|
+
outcome: "denied",
|
|
350
|
+
reason: "travel_details_not_found",
|
|
351
|
+
});
|
|
352
|
+
return c.json({ error: "Participant travel details not found" }, 404);
|
|
353
|
+
}
|
|
354
|
+
return c.json({ data: details });
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
return handleKmsConfigError(c, error);
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
// 9. POST /:id/participants — Add participant
|
|
361
|
+
.post("/:id/participants", async (c) => {
|
|
362
|
+
const row = await bookingsService.createParticipant(c.get("db"), c.req.param("id"), insertParticipantSchema.parse(await c.req.json()), c.get("userId"));
|
|
363
|
+
if (!row) {
|
|
364
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
365
|
+
}
|
|
366
|
+
return c.json({ data: row }, 201);
|
|
367
|
+
})
|
|
368
|
+
// 9a. PATCH /:id/participants/:participantId/travel-details — Upsert encrypted travel details
|
|
369
|
+
.patch("/:id/participants/:participantId/travel-details", async (c) => {
|
|
370
|
+
const auth = await authorizeBookingPiiAccess(c, {
|
|
371
|
+
bookingId: c.req.param("id"),
|
|
372
|
+
participantId: c.req.param("participantId"),
|
|
373
|
+
action: "update",
|
|
374
|
+
});
|
|
375
|
+
if (!auth.allowed) {
|
|
376
|
+
return auth.response;
|
|
377
|
+
}
|
|
378
|
+
const participant = await bookingsService.getParticipantById(c.get("db"), c.req.param("id"), c.req.param("participantId"));
|
|
379
|
+
if (!participant) {
|
|
380
|
+
await logBookingPiiAccess(c, {
|
|
381
|
+
bookingId: c.req.param("id"),
|
|
382
|
+
participantId: c.req.param("participantId"),
|
|
383
|
+
action: "update",
|
|
384
|
+
outcome: "denied",
|
|
385
|
+
reason: "participant_not_found",
|
|
386
|
+
});
|
|
387
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const pii = createBookingPiiService({
|
|
391
|
+
kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
|
|
392
|
+
onAudit: async (event) => {
|
|
393
|
+
await logBookingPiiAccess(c, {
|
|
394
|
+
bookingId: participant.bookingId,
|
|
395
|
+
participantId: event.participantId,
|
|
396
|
+
action: event.action === "encrypt" ? "update" : event.action === "decrypt" ? "read" : event.action,
|
|
397
|
+
outcome: "allowed",
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
const row = await pii.upsertParticipantTravelDetails(c.get("db"), participant.id, upsertParticipantTravelDetailsSchema.parse(await c.req.json()), c.get("userId"));
|
|
402
|
+
if (!row) {
|
|
403
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
404
|
+
}
|
|
405
|
+
return c.json({ data: row });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
return handleKmsConfigError(c, error);
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
// 10. PATCH /:id/participants/:participantId — Update participant
|
|
412
|
+
.patch("/:id/participants/:participantId", async (c) => {
|
|
413
|
+
const row = await bookingsService.updateParticipant(c.get("db"), c.req.param("participantId"), updateParticipantSchema.parse(await c.req.json()));
|
|
414
|
+
if (!row) {
|
|
415
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
416
|
+
}
|
|
417
|
+
return c.json({ data: row });
|
|
418
|
+
})
|
|
419
|
+
// 10a. DELETE /:id/participants/:participantId/travel-details — Delete encrypted travel details
|
|
420
|
+
.delete("/:id/participants/:participantId/travel-details", async (c) => {
|
|
421
|
+
const auth = await authorizeBookingPiiAccess(c, {
|
|
422
|
+
bookingId: c.req.param("id"),
|
|
423
|
+
participantId: c.req.param("participantId"),
|
|
424
|
+
action: "delete",
|
|
425
|
+
});
|
|
426
|
+
if (!auth.allowed) {
|
|
427
|
+
return auth.response;
|
|
428
|
+
}
|
|
429
|
+
const participant = await bookingsService.getParticipantById(c.get("db"), c.req.param("id"), c.req.param("participantId"));
|
|
430
|
+
if (!participant) {
|
|
431
|
+
await logBookingPiiAccess(c, {
|
|
432
|
+
bookingId: c.req.param("id"),
|
|
433
|
+
participantId: c.req.param("participantId"),
|
|
434
|
+
action: "delete",
|
|
435
|
+
outcome: "denied",
|
|
436
|
+
reason: "participant_not_found",
|
|
437
|
+
});
|
|
438
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const pii = createBookingPiiService({
|
|
442
|
+
kms: createKmsProviderFromEnv(getRuntimeEnv(c)),
|
|
443
|
+
onAudit: async (event) => {
|
|
444
|
+
await logBookingPiiAccess(c, {
|
|
445
|
+
bookingId: participant.bookingId,
|
|
446
|
+
participantId: event.participantId,
|
|
447
|
+
action: event.action === "encrypt" ? "update" : event.action === "decrypt" ? "read" : event.action,
|
|
448
|
+
outcome: "allowed",
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
const row = await pii.deleteParticipantTravelDetails(c.get("db"), participant.id, c.get("userId"));
|
|
453
|
+
if (!row) {
|
|
454
|
+
return c.json({ error: "Participant travel details not found" }, 404);
|
|
455
|
+
}
|
|
456
|
+
return c.json({ success: true }, 200);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
return handleKmsConfigError(c, error);
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
// 11. DELETE /:id/participants/:participantId — Delete participant
|
|
463
|
+
.delete("/:id/participants/:participantId", async (c) => {
|
|
464
|
+
const row = await bookingsService.deleteParticipant(c.get("db"), c.req.param("participantId"));
|
|
465
|
+
if (!row) {
|
|
466
|
+
return c.json({ error: "Participant not found" }, 404);
|
|
467
|
+
}
|
|
468
|
+
return c.json({ success: true }, 200);
|
|
469
|
+
})
|
|
470
|
+
// ==========================================================================
|
|
471
|
+
// Passengers (legacy compatibility)
|
|
472
|
+
// ==========================================================================
|
|
473
|
+
// 12. GET /:id/passengers — List passengers
|
|
474
|
+
.get("/:id/passengers", async (c) => {
|
|
475
|
+
return c.json({ data: await bookingsService.listPassengers(c.get("db"), c.req.param("id")) });
|
|
476
|
+
})
|
|
477
|
+
// 13. POST /:id/passengers — Add passenger
|
|
478
|
+
.post("/:id/passengers", async (c) => {
|
|
479
|
+
const row = await bookingsService.createPassenger(c.get("db"), c.req.param("id"), insertPassengerSchema.parse(await c.req.json()), c.get("userId"));
|
|
480
|
+
if (!row) {
|
|
481
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
482
|
+
}
|
|
483
|
+
return c.json({ data: row }, 201);
|
|
484
|
+
})
|
|
485
|
+
// 14. PATCH /:id/passengers/:passengerId — Update passenger
|
|
486
|
+
.patch("/:id/passengers/:passengerId", async (c) => {
|
|
487
|
+
const row = await bookingsService.updatePassenger(c.get("db"), c.req.param("passengerId"), updatePassengerSchema.parse(await c.req.json()));
|
|
488
|
+
if (!row) {
|
|
489
|
+
return c.json({ error: "Passenger not found" }, 404);
|
|
490
|
+
}
|
|
491
|
+
return c.json({ data: row });
|
|
492
|
+
})
|
|
493
|
+
// 15. DELETE /:id/passengers/:passengerId — Delete passenger
|
|
494
|
+
.delete("/:id/passengers/:passengerId", async (c) => {
|
|
495
|
+
const row = await bookingsService.deletePassenger(c.get("db"), c.req.param("passengerId"));
|
|
496
|
+
if (!row) {
|
|
497
|
+
return c.json({ error: "Passenger not found" }, 404);
|
|
498
|
+
}
|
|
499
|
+
return c.json({ success: true }, 200);
|
|
500
|
+
})
|
|
501
|
+
// ==========================================================================
|
|
502
|
+
// Items
|
|
503
|
+
// ==========================================================================
|
|
504
|
+
// 16. GET /:id/items — List booking items
|
|
505
|
+
.get("/:id/items", async (c) => {
|
|
506
|
+
return c.json({ data: await bookingsService.listItems(c.get("db"), c.req.param("id")) });
|
|
507
|
+
})
|
|
508
|
+
// 17. POST /:id/items — Add booking item
|
|
509
|
+
.post("/:id/items", async (c) => {
|
|
510
|
+
const row = await bookingsService.createItem(c.get("db"), c.req.param("id"), insertBookingItemSchema.parse(await c.req.json()), c.get("userId"));
|
|
511
|
+
if (!row) {
|
|
512
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
513
|
+
}
|
|
514
|
+
return c.json({ data: row }, 201);
|
|
515
|
+
})
|
|
516
|
+
// 18. PATCH /:id/items/:itemId — Update booking item
|
|
517
|
+
.patch("/:id/items/:itemId", async (c) => {
|
|
518
|
+
const row = await bookingsService.updateItem(c.get("db"), c.req.param("itemId"), updateBookingItemSchema.parse(await c.req.json()));
|
|
519
|
+
if (!row) {
|
|
520
|
+
return c.json({ error: "Booking item not found" }, 404);
|
|
521
|
+
}
|
|
522
|
+
return c.json({ data: row });
|
|
523
|
+
})
|
|
524
|
+
// 19. DELETE /:id/items/:itemId — Delete booking item
|
|
525
|
+
.delete("/:id/items/:itemId", async (c) => {
|
|
526
|
+
const row = await bookingsService.deleteItem(c.get("db"), c.req.param("itemId"));
|
|
527
|
+
if (!row) {
|
|
528
|
+
return c.json({ error: "Booking item not found" }, 404);
|
|
529
|
+
}
|
|
530
|
+
return c.json({ success: true }, 200);
|
|
531
|
+
})
|
|
532
|
+
// 20. GET /:id/items/:itemId/participants — List item participants
|
|
533
|
+
.get("/:id/items/:itemId/participants", async (c) => {
|
|
534
|
+
return c.json({
|
|
535
|
+
data: await bookingsService.listItemParticipants(c.get("db"), c.req.param("itemId")),
|
|
536
|
+
});
|
|
537
|
+
})
|
|
538
|
+
// 21. POST /:id/items/:itemId/participants — Link participant to item
|
|
539
|
+
.post("/:id/items/:itemId/participants", async (c) => {
|
|
540
|
+
const row = await bookingsService.addItemParticipant(c.get("db"), c.req.param("itemId"), insertBookingItemParticipantSchema.parse(await c.req.json()));
|
|
541
|
+
if (!row) {
|
|
542
|
+
return c.json({ error: "Booking item or participant not found" }, 404);
|
|
543
|
+
}
|
|
544
|
+
return c.json({ data: row }, 201);
|
|
545
|
+
})
|
|
546
|
+
// 22. DELETE /:id/items/:itemId/participants/:linkId — Unlink participant from item
|
|
547
|
+
.delete("/:id/items/:itemId/participants/:linkId", async (c) => {
|
|
548
|
+
const row = await bookingsService.removeItemParticipant(c.get("db"), c.req.param("linkId"));
|
|
549
|
+
if (!row) {
|
|
550
|
+
return c.json({ error: "Booking item participant link not found" }, 404);
|
|
551
|
+
}
|
|
552
|
+
return c.json({ success: true }, 200);
|
|
553
|
+
})
|
|
554
|
+
// ==========================================================================
|
|
555
|
+
// Supplier Statuses
|
|
556
|
+
// ==========================================================================
|
|
557
|
+
.get("/:id/supplier-statuses", async (c) => {
|
|
558
|
+
return c.json({
|
|
559
|
+
data: await bookingsService.listSupplierStatuses(c.get("db"), c.req.param("id")),
|
|
560
|
+
});
|
|
561
|
+
})
|
|
562
|
+
.post("/:id/supplier-statuses", async (c) => {
|
|
563
|
+
const row = await bookingsService.createSupplierStatus(c.get("db"), c.req.param("id"), insertSupplierStatusSchema.parse(await c.req.json()), c.get("userId"));
|
|
564
|
+
if (!row) {
|
|
565
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
566
|
+
}
|
|
567
|
+
return c.json({ data: row }, 201);
|
|
568
|
+
})
|
|
569
|
+
.patch("/:id/supplier-statuses/:statusId", async (c) => {
|
|
570
|
+
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"));
|
|
571
|
+
if (!row) {
|
|
572
|
+
return c.json({ error: "Supplier status not found" }, 404);
|
|
573
|
+
}
|
|
574
|
+
return c.json({ data: row });
|
|
575
|
+
})
|
|
576
|
+
// ==========================================================================
|
|
577
|
+
// Fulfillment
|
|
578
|
+
// ==========================================================================
|
|
579
|
+
.get("/:id/fulfillments", async (c) => {
|
|
580
|
+
return c.json({ data: await bookingsService.listFulfillments(c.get("db"), c.req.param("id")) });
|
|
581
|
+
})
|
|
582
|
+
.post("/:id/fulfillments", async (c) => {
|
|
583
|
+
const row = await bookingsService.issueFulfillment(c.get("db"), c.req.param("id"), insertBookingFulfillmentSchema.parse(await c.req.json()), c.get("userId"));
|
|
584
|
+
if (!row) {
|
|
585
|
+
return c.json({ error: "Booking, item, or participant not found" }, 404);
|
|
586
|
+
}
|
|
587
|
+
return c.json({ data: row }, 201);
|
|
588
|
+
})
|
|
589
|
+
.patch("/:id/fulfillments/:fulfillmentId", async (c) => {
|
|
590
|
+
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"));
|
|
591
|
+
if (!row) {
|
|
592
|
+
return c.json({ error: "Fulfillment, item, or participant not found" }, 404);
|
|
593
|
+
}
|
|
594
|
+
return c.json({ data: row });
|
|
595
|
+
})
|
|
596
|
+
// ==========================================================================
|
|
597
|
+
// Redemption
|
|
598
|
+
// ==========================================================================
|
|
599
|
+
.get("/:id/redemptions", async (c) => {
|
|
600
|
+
return c.json({
|
|
601
|
+
data: await bookingsService.listRedemptionEvents(c.get("db"), c.req.param("id")),
|
|
602
|
+
});
|
|
603
|
+
})
|
|
604
|
+
.post("/:id/redemptions", async (c) => {
|
|
605
|
+
const row = await bookingsService.recordRedemption(c.get("db"), c.req.param("id"), recordBookingRedemptionSchema.parse(await c.req.json()), c.get("userId"));
|
|
606
|
+
if (!row) {
|
|
607
|
+
return c.json({ error: "Booking, item, or participant not found" }, 404);
|
|
608
|
+
}
|
|
609
|
+
return c.json({ data: row }, 201);
|
|
610
|
+
})
|
|
611
|
+
// ==========================================================================
|
|
612
|
+
// Activity Log
|
|
613
|
+
// ==========================================================================
|
|
614
|
+
// 26. GET /:id/activity — List activity log
|
|
615
|
+
.get("/:id/activity", async (c) => {
|
|
616
|
+
return c.json({ data: await bookingsService.listActivity(c.get("db"), c.req.param("id")) });
|
|
617
|
+
})
|
|
618
|
+
// ==========================================================================
|
|
619
|
+
// Notes
|
|
620
|
+
// ==========================================================================
|
|
621
|
+
// 27. GET /:id/notes — List notes
|
|
622
|
+
.get("/:id/notes", async (c) => {
|
|
623
|
+
return c.json({ data: await bookingsService.listNotes(c.get("db"), c.req.param("id")) });
|
|
624
|
+
})
|
|
625
|
+
// 28. POST /:id/notes — Add note
|
|
626
|
+
.post("/:id/notes", async (c) => {
|
|
627
|
+
const userId = c.get("userId");
|
|
628
|
+
if (!userId) {
|
|
629
|
+
return c.json({ error: "User ID required to create notes" }, 400);
|
|
630
|
+
}
|
|
631
|
+
const row = await bookingsService.createNote(c.get("db"), c.req.param("id"), userId, insertBookingNoteSchema.parse(await c.req.json()));
|
|
632
|
+
if (!row) {
|
|
633
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
634
|
+
}
|
|
635
|
+
return c.json({ data: row }, 201);
|
|
636
|
+
})
|
|
637
|
+
// ==========================================================================
|
|
638
|
+
// Documents
|
|
639
|
+
// ==========================================================================
|
|
640
|
+
// 29. GET /:id/documents — List documents for booking
|
|
641
|
+
.get("/:id/documents", async (c) => {
|
|
642
|
+
return c.json({ data: await bookingsService.listDocuments(c.get("db"), c.req.param("id")) });
|
|
643
|
+
})
|
|
644
|
+
// 30. POST /:id/documents — Add document to booking
|
|
645
|
+
.post("/:id/documents", async (c) => {
|
|
646
|
+
const row = await bookingsService.createDocument(c.get("db"), c.req.param("id"), insertBookingDocumentSchema.parse(await c.req.json()));
|
|
647
|
+
if (!row) {
|
|
648
|
+
return c.json({ error: "Booking not found" }, 404);
|
|
649
|
+
}
|
|
650
|
+
return c.json({ data: row }, 201);
|
|
651
|
+
})
|
|
652
|
+
// 31. DELETE /:id/documents/:documentId — Delete document
|
|
653
|
+
.delete("/:id/documents/:documentId", async (c) => {
|
|
654
|
+
const row = await bookingsService.deleteDocument(c.get("db"), c.req.param("documentId"));
|
|
655
|
+
if (!row) {
|
|
656
|
+
return c.json({ error: "Document not found" }, 404);
|
|
657
|
+
}
|
|
658
|
+
return c.json({ success: true }, 200);
|
|
659
|
+
});
|