@voyant-travel/mice 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/booking-extension.d.ts +2 -2
- package/dist/booking-extension.js +1 -1
- package/dist/routes.d.ts +1425 -557
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +863 -162
- package/dist/schema-delegates.d.ts +1 -1
- package/dist/schema-sessions.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/validation-delegates.d.ts +7 -7
- package/dist/validation-rfp.d.ts +2 -2
- package/dist/validation-sessions.d.ts +4 -4
- package/dist/validation.d.ts +8 -8
- package/package.json +11 -10
package/dist/routes.js
CHANGED
|
@@ -1,117 +1,546 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MICE program admin routes. Mounted by the deployment under `/v1/admin/mice`.
|
|
3
|
-
* Routes stay thin: validate, call
|
|
3
|
+
* Routes stay thin: validate, call the domain services, serialize. See RFC
|
|
4
|
+
* voyant#1489.
|
|
5
|
+
*
|
|
6
|
+
* Migrated to `@hono/zod-openapi` for the OpenAPI admin backfill (voyant#2114 —
|
|
7
|
+
* mice sub-batch). Request schemas reuse the exported `validation*.ts` schemas
|
|
8
|
+
* the handlers already parse; response row schemas are authored here from the
|
|
9
|
+
* Drizzle `$inferSelect` shapes (§17: `date`/timestamp columns serialize to
|
|
10
|
+
* ISO strings over the wire; integer fields stay numbers). The MICE list
|
|
11
|
+
* services return `{ data, limit, offset }` (no `total` count read), so the
|
|
12
|
+
* list envelope is declared locally rather than via `listResponseSchema`. The
|
|
13
|
+
* single-resource GET legs return the base row extended with the joined child
|
|
14
|
+
* collections (`inclusions`, `enrollments`, `delegates`, `invitations`/`bids`,
|
|
15
|
+
* `lines`/`evaluations`). Typed service-outcome → HTTP-status unions are
|
|
16
|
+
* declared inline per leg.
|
|
17
|
+
*
|
|
18
|
+
* Each resource is its own small `OpenAPIHono` sub-chain composed onto
|
|
19
|
+
* `miceAdminRoutes` via `.route("/", child)` so the `.openapi()` operations
|
|
20
|
+
* propagate up while keeping type-inference cost bounded (one flat chain has
|
|
21
|
+
* O(n²) inference cost). Within each child, static/collection paths are
|
|
22
|
+
* registered before dynamic `/{id}` legs.
|
|
4
23
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
24
|
+
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
|
25
|
+
import { openApiValidationHook } from "@voyant-travel/hono";
|
|
7
26
|
import { createProgram, getProgram, listPrograms, updateProgram } from "./service.js";
|
|
8
27
|
import { commercialsService } from "./service-commercials.js";
|
|
9
28
|
import { createDelegate, enrollDelegate, getDelegate, listDelegates, updateDelegate, } from "./service-delegates.js";
|
|
10
29
|
import { rfpService } from "./service-rfp.js";
|
|
11
30
|
import { createRoomingAssignment, getRoomingAssignment, listRoomingAssignments, setRoomingDelegates, updateRoomingAssignment, } from "./service-rooming.js";
|
|
12
31
|
import { createSession, deleteSession, getSession, listSessions, setSessionInclusions, updateSession, } from "./service-sessions.js";
|
|
13
|
-
import { createProgramSchema, programListQuerySchema, updateProgramSchema } from "./validation.js";
|
|
14
|
-
import { createDelegateSchema, delegateListQuerySchema, enrollDelegateSchema, updateDelegateSchema, } from "./validation-delegates.js";
|
|
15
|
-
import { addBidEvaluationSchema, awardRfpSchema, createBidSchema, createRfpSchema, inviteSupplierSchema, rfpListQuerySchema, setBidLinesSchema, updateBidSchema, updateRfpSchema, } from "./validation-rfp.js";
|
|
32
|
+
import { createProgramSchema, programListQuerySchema, programStatusSchema, programTypeSchema, updateProgramSchema, } from "./validation.js";
|
|
33
|
+
import { createDelegateSchema, delegateListQuerySchema, delegateRoleSchema, delegateStatusSchema, enrollDelegateSchema, enrollmentStatusSchema, updateDelegateSchema, } from "./validation-delegates.js";
|
|
34
|
+
import { addBidEvaluationSchema, awardRfpSchema, bidStatusSchema, createBidSchema, createRfpSchema, inviteSupplierSchema, rfpListQuerySchema, rfpStatusSchema, setBidLinesSchema, updateBidSchema, updateRfpSchema, } from "./validation-rfp.js";
|
|
16
35
|
import { createRoomingAssignmentSchema, roomingListQuerySchema, setRoomingDelegatesSchema, updateRoomingAssignmentSchema, } from "./validation-rooming.js";
|
|
17
|
-
import { createSessionSchema, sessionListQuerySchema, setSessionInclusionsSchema, updateSessionSchema, } from "./validation-sessions.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
import { createSessionSchema, sessionInclusionKindSchema, sessionListQuerySchema, sessionTypeSchema, setSessionInclusionsSchema, updateSessionSchema, } from "./validation-sessions.js";
|
|
37
|
+
// --- shared response building blocks ----------------------------------------
|
|
38
|
+
const errorResponseSchema = z.object({ error: z.string() });
|
|
39
|
+
const successResponseSchema = z.object({ success: z.literal(true) });
|
|
40
|
+
const idParamSchema = z.object({ id: z.string() });
|
|
41
|
+
// §17: timestamp/date columns serialize to ISO strings on the wire.
|
|
42
|
+
const isoTimestamp = z.string();
|
|
43
|
+
const isoDate = z.string();
|
|
44
|
+
const jsonRecord = z.record(z.string(), z.unknown());
|
|
45
|
+
/** One `application/json` "invalid request body" response entry. */
|
|
46
|
+
const invalidRequestResponse = {
|
|
47
|
+
description: "invalid_request: request body failed validation",
|
|
48
|
+
content: { "application/json": { schema: errorResponseSchema } },
|
|
49
|
+
};
|
|
50
|
+
const notFoundResponse = (description) => ({
|
|
51
|
+
description,
|
|
52
|
+
content: { "application/json": { schema: errorResponseSchema } },
|
|
53
|
+
});
|
|
54
|
+
/** Local list envelope — MICE lists return `{ data, limit, offset }` (no total). */
|
|
55
|
+
const listEnvelope = (row) => z.object({
|
|
56
|
+
data: z.array(row),
|
|
57
|
+
limit: z.number().int(),
|
|
58
|
+
offset: z.number().int(),
|
|
59
|
+
});
|
|
60
|
+
const dataEnvelope = (row) => z.object({ data: row });
|
|
61
|
+
// --- response row schemas (authored from the Drizzle `$inferSelect` shapes) --
|
|
62
|
+
const programSchema = z.object({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
organizationId: z.string().nullable(),
|
|
65
|
+
primaryContactPersonId: z.string().nullable(),
|
|
66
|
+
accountManagerId: z.string().nullable(),
|
|
67
|
+
name: z.string(),
|
|
68
|
+
code: z.string().nullable(),
|
|
69
|
+
type: programTypeSchema,
|
|
70
|
+
status: programStatusSchema,
|
|
71
|
+
destination: z.string().nullable(),
|
|
72
|
+
startDate: isoDate.nullable(),
|
|
73
|
+
endDate: isoDate.nullable(),
|
|
74
|
+
estimatedPax: z.number().int().nullable(),
|
|
75
|
+
confirmedPax: z.number().int().nullable(),
|
|
76
|
+
currency: z.string().nullable(),
|
|
77
|
+
budgetAmountCents: z.number().int().nullable(),
|
|
78
|
+
metadata: jsonRecord.nullable(),
|
|
79
|
+
createdAt: isoTimestamp,
|
|
80
|
+
updatedAt: isoTimestamp,
|
|
81
|
+
});
|
|
82
|
+
const sessionSchema = z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
programId: z.string(),
|
|
85
|
+
functionSpaceId: z.string().nullable(),
|
|
86
|
+
title: z.string(),
|
|
87
|
+
sessionType: sessionTypeSchema,
|
|
88
|
+
dayDate: isoDate.nullable(),
|
|
89
|
+
startsAt: isoTimestamp.nullable(),
|
|
90
|
+
endsAt: isoTimestamp.nullable(),
|
|
91
|
+
track: z.string().nullable(),
|
|
92
|
+
capacity: z.number().int().nullable(),
|
|
93
|
+
requiresRegistration: z.boolean(),
|
|
94
|
+
notes: z.string().nullable(),
|
|
95
|
+
metadata: jsonRecord.nullable(),
|
|
96
|
+
createdAt: isoTimestamp,
|
|
97
|
+
updatedAt: isoTimestamp,
|
|
98
|
+
});
|
|
99
|
+
const sessionInclusionSchema = z.object({
|
|
100
|
+
id: z.string(),
|
|
101
|
+
sessionId: z.string(),
|
|
102
|
+
kind: sessionInclusionKindSchema,
|
|
103
|
+
description: z.string().nullable(),
|
|
104
|
+
quantity: z.number().int(),
|
|
105
|
+
costAmountCents: z.number().int().nullable(),
|
|
106
|
+
currency: z.string().nullable(),
|
|
107
|
+
createdAt: isoTimestamp,
|
|
108
|
+
updatedAt: isoTimestamp,
|
|
109
|
+
});
|
|
110
|
+
const sessionWithInclusionsSchema = sessionSchema.extend({
|
|
111
|
+
inclusions: z.array(sessionInclusionSchema),
|
|
112
|
+
});
|
|
113
|
+
const delegateSchema = z.object({
|
|
114
|
+
id: z.string(),
|
|
115
|
+
programId: z.string(),
|
|
116
|
+
personId: z.string().nullable(),
|
|
117
|
+
bookingId: z.string().nullable(),
|
|
118
|
+
role: delegateRoleSchema,
|
|
119
|
+
status: delegateStatusSchema,
|
|
120
|
+
arrivalAt: isoTimestamp.nullable(),
|
|
121
|
+
departureAt: isoTimestamp.nullable(),
|
|
122
|
+
notes: z.string().nullable(),
|
|
123
|
+
metadata: jsonRecord.nullable(),
|
|
124
|
+
createdAt: isoTimestamp,
|
|
125
|
+
updatedAt: isoTimestamp,
|
|
126
|
+
});
|
|
127
|
+
const enrollmentSchema = z.object({
|
|
128
|
+
id: z.string(),
|
|
129
|
+
delegateId: z.string(),
|
|
130
|
+
sessionId: z.string(),
|
|
131
|
+
status: enrollmentStatusSchema,
|
|
132
|
+
createdAt: isoTimestamp,
|
|
133
|
+
updatedAt: isoTimestamp,
|
|
134
|
+
});
|
|
135
|
+
const delegateWithEnrollmentsSchema = delegateSchema.extend({
|
|
136
|
+
enrollments: z.array(enrollmentSchema),
|
|
137
|
+
});
|
|
138
|
+
const roomingAssignmentSchema = z.object({
|
|
139
|
+
id: z.string(),
|
|
140
|
+
programId: z.string(),
|
|
141
|
+
roomBlockId: z.string().nullable(),
|
|
142
|
+
roomTypeId: z.string().nullable(),
|
|
143
|
+
bedConfig: z.string().nullable(),
|
|
144
|
+
sharingGroupId: z.string().nullable(),
|
|
145
|
+
checkIn: isoDate.nullable(),
|
|
146
|
+
checkOut: isoDate.nullable(),
|
|
147
|
+
specialRequests: z.string().nullable(),
|
|
148
|
+
metadata: jsonRecord.nullable(),
|
|
149
|
+
createdAt: isoTimestamp,
|
|
150
|
+
updatedAt: isoTimestamp,
|
|
151
|
+
});
|
|
152
|
+
const roomingAssignmentDelegateSchema = z.object({
|
|
153
|
+
id: z.string(),
|
|
154
|
+
roomingAssignmentId: z.string(),
|
|
155
|
+
delegateId: z.string(),
|
|
156
|
+
isPrimary: z.boolean(),
|
|
157
|
+
bedLabel: z.string().nullable(),
|
|
158
|
+
createdAt: isoTimestamp,
|
|
159
|
+
});
|
|
160
|
+
const roomingWithDelegatesSchema = roomingAssignmentSchema.extend({
|
|
161
|
+
delegates: z.array(roomingAssignmentDelegateSchema),
|
|
162
|
+
});
|
|
163
|
+
const rfpInvitationStatusSchema = z.enum(["invited", "viewed", "declined", "responded"]);
|
|
164
|
+
const rfpSchema = z.object({
|
|
165
|
+
id: z.string(),
|
|
166
|
+
programId: z.string(),
|
|
167
|
+
title: z.string(),
|
|
168
|
+
requirements: jsonRecord.nullable(),
|
|
169
|
+
status: rfpStatusSchema,
|
|
170
|
+
issuedAt: isoTimestamp.nullable(),
|
|
171
|
+
dueAt: isoTimestamp.nullable(),
|
|
172
|
+
notes: z.string().nullable(),
|
|
173
|
+
createdAt: isoTimestamp,
|
|
174
|
+
updatedAt: isoTimestamp,
|
|
175
|
+
});
|
|
176
|
+
const rfpInvitationSchema = z.object({
|
|
177
|
+
id: z.string(),
|
|
178
|
+
rfpId: z.string(),
|
|
179
|
+
supplierId: z.string(),
|
|
180
|
+
status: rfpInvitationStatusSchema,
|
|
181
|
+
createdAt: isoTimestamp,
|
|
182
|
+
updatedAt: isoTimestamp,
|
|
183
|
+
});
|
|
184
|
+
const bidSchema = z.object({
|
|
185
|
+
id: z.string(),
|
|
186
|
+
rfpId: z.string(),
|
|
187
|
+
supplierId: z.string(),
|
|
188
|
+
status: bidStatusSchema,
|
|
189
|
+
totalCents: z.number().int().nullable(),
|
|
190
|
+
currency: z.string().nullable(),
|
|
191
|
+
proposalDoc: z.string().nullable(),
|
|
192
|
+
validUntil: isoTimestamp.nullable(),
|
|
193
|
+
notes: z.string().nullable(),
|
|
194
|
+
createdAt: isoTimestamp,
|
|
195
|
+
updatedAt: isoTimestamp,
|
|
196
|
+
});
|
|
197
|
+
const bidLineSchema = z.object({
|
|
198
|
+
id: z.string(),
|
|
199
|
+
bidId: z.string(),
|
|
200
|
+
requirementRef: z.string().nullable(),
|
|
201
|
+
description: z.string().nullable(),
|
|
202
|
+
quantity: z.number().int(),
|
|
203
|
+
unitCents: z.number().int().nullable(),
|
|
204
|
+
totalCents: z.number().int().nullable(),
|
|
205
|
+
createdAt: isoTimestamp,
|
|
206
|
+
});
|
|
207
|
+
const bidEvaluationSchema = z.object({
|
|
208
|
+
id: z.string(),
|
|
209
|
+
bidId: z.string(),
|
|
210
|
+
criterion: z.string(),
|
|
211
|
+
weight: z.number().int().nullable(),
|
|
212
|
+
score: z.number().int().nullable(),
|
|
213
|
+
notes: z.string().nullable(),
|
|
214
|
+
evaluatedBy: z.string().nullable(),
|
|
215
|
+
createdAt: isoTimestamp,
|
|
216
|
+
});
|
|
217
|
+
const rfpWithDetailsSchema = rfpSchema.extend({
|
|
218
|
+
invitations: z.array(rfpInvitationSchema),
|
|
219
|
+
bids: z.array(bidSchema),
|
|
220
|
+
});
|
|
221
|
+
const bidWithDetailsSchema = bidSchema.extend({
|
|
222
|
+
lines: z.array(bidLineSchema),
|
|
223
|
+
evaluations: z.array(bidEvaluationSchema),
|
|
224
|
+
});
|
|
225
|
+
// Program commercials — read-model cost sheet (no spine table).
|
|
226
|
+
const costSheetCategorySchema = z.object({
|
|
227
|
+
contractedCostCents: z.number().int(),
|
|
228
|
+
pickedCostCents: z.number().int(),
|
|
229
|
+
pickedSellCents: z.number().int(),
|
|
230
|
+
});
|
|
231
|
+
const costSheetCurrencyTotalsSchema = z.object({
|
|
232
|
+
currency: z.string(),
|
|
233
|
+
roomBlocks: costSheetCategorySchema,
|
|
234
|
+
spaceBlocks: costSheetCategorySchema,
|
|
235
|
+
sessionInclusionsCostCents: z.number().int(),
|
|
236
|
+
costCents: z.number().int(),
|
|
237
|
+
sellCents: z.number().int(),
|
|
238
|
+
marginCents: z.number().int(),
|
|
239
|
+
marginPct: z.number().nullable(),
|
|
240
|
+
});
|
|
241
|
+
const programCostSheetSchema = z.object({
|
|
242
|
+
programId: z.string(),
|
|
243
|
+
mixedCurrency: z.boolean(),
|
|
244
|
+
byCurrency: z.array(costSheetCurrencyTotalsSchema),
|
|
245
|
+
});
|
|
246
|
+
// === programs ===============================================================
|
|
247
|
+
const listProgramsRoute = createRoute({
|
|
248
|
+
method: "get",
|
|
249
|
+
path: "/programs",
|
|
250
|
+
request: { query: programListQuerySchema },
|
|
251
|
+
responses: {
|
|
252
|
+
200: {
|
|
253
|
+
description: "Paginated MICE programs",
|
|
254
|
+
content: { "application/json": { schema: listEnvelope(programSchema) } },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const createProgramRoute = createRoute({
|
|
259
|
+
method: "post",
|
|
260
|
+
path: "/programs",
|
|
261
|
+
request: {
|
|
262
|
+
body: { required: true, content: { "application/json": { schema: createProgramSchema } } },
|
|
263
|
+
},
|
|
264
|
+
responses: {
|
|
265
|
+
201: {
|
|
266
|
+
description: "The created program",
|
|
267
|
+
content: { "application/json": { schema: dataEnvelope(programSchema) } },
|
|
268
|
+
},
|
|
269
|
+
400: invalidRequestResponse,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const getProgramRoute = createRoute({
|
|
273
|
+
method: "get",
|
|
274
|
+
path: "/programs/{id}",
|
|
275
|
+
request: { params: idParamSchema },
|
|
276
|
+
responses: {
|
|
277
|
+
200: {
|
|
278
|
+
description: "A program by id",
|
|
279
|
+
content: { "application/json": { schema: dataEnvelope(programSchema) } },
|
|
280
|
+
},
|
|
281
|
+
404: notFoundResponse("Program not found"),
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const getProgramCostSheetRoute = createRoute({
|
|
285
|
+
method: "get",
|
|
286
|
+
path: "/programs/{id}/cost-sheet",
|
|
287
|
+
request: { params: idParamSchema },
|
|
288
|
+
responses: {
|
|
289
|
+
200: {
|
|
290
|
+
description: "The program cost sheet / P&L grouped by currency",
|
|
291
|
+
content: { "application/json": { schema: dataEnvelope(programCostSheetSchema) } },
|
|
292
|
+
},
|
|
293
|
+
404: notFoundResponse("Program not found"),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const updateProgramRoute = createRoute({
|
|
297
|
+
method: "patch",
|
|
298
|
+
path: "/programs/{id}",
|
|
299
|
+
request: {
|
|
300
|
+
params: idParamSchema,
|
|
301
|
+
body: { required: true, content: { "application/json": { schema: updateProgramSchema } } },
|
|
302
|
+
},
|
|
303
|
+
responses: {
|
|
304
|
+
200: {
|
|
305
|
+
description: "The updated program",
|
|
306
|
+
content: { "application/json": { schema: dataEnvelope(programSchema) } },
|
|
307
|
+
},
|
|
308
|
+
400: invalidRequestResponse,
|
|
309
|
+
404: notFoundResponse("Program not found"),
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const programRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
313
|
+
.openapi(listProgramsRoute, async (c) => c.json(await listPrograms(c.get("db"), c.req.valid("query")), 200))
|
|
314
|
+
.openapi(createProgramRoute, async (c) => c.json({ data: await createProgram(c.get("db"), c.req.valid("json")) }, 201))
|
|
315
|
+
// static sub-resource before the dynamic `/{id}` catch-all
|
|
316
|
+
.openapi(getProgramCostSheetRoute, async (c) => {
|
|
317
|
+
const id = c.req.valid("param").id;
|
|
36
318
|
const program = await getProgram(c.get("db"), id);
|
|
37
319
|
if (!program)
|
|
38
320
|
return c.json({ error: "Program not found" }, 404);
|
|
39
|
-
return c.json({ data: await commercialsService.getProgramCostSheet(c.get("db"), id) });
|
|
40
|
-
})
|
|
41
|
-
.patch("/programs/:id", async (c) => {
|
|
42
|
-
const body = await parseJsonBody(c, updateProgramSchema);
|
|
43
|
-
const program = await updateProgram(c.get("db"), c.req.param("id"), body);
|
|
44
|
-
if (!program)
|
|
45
|
-
return c.json({ error: "Program not found" }, 404);
|
|
46
|
-
return c.json({ data: program });
|
|
321
|
+
return c.json({ data: await commercialsService.getProgramCostSheet(c.get("db"), id) }, 200);
|
|
47
322
|
})
|
|
48
|
-
|
|
49
|
-
.get("
|
|
50
|
-
|
|
51
|
-
return c.json(await listSessions(c.get("db"), query));
|
|
323
|
+
.openapi(getProgramRoute, async (c) => {
|
|
324
|
+
const program = await getProgram(c.get("db"), c.req.valid("param").id);
|
|
325
|
+
return program ? c.json({ data: program }, 200) : c.json({ error: "Program not found" }, 404);
|
|
52
326
|
})
|
|
53
|
-
.
|
|
54
|
-
const
|
|
55
|
-
|
|
327
|
+
.openapi(updateProgramRoute, async (c) => {
|
|
328
|
+
const program = await updateProgram(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
329
|
+
return program ? c.json({ data: program }, 200) : c.json({ error: "Program not found" }, 404);
|
|
330
|
+
});
|
|
331
|
+
// === sessions ===============================================================
|
|
332
|
+
const listSessionsRoute = createRoute({
|
|
333
|
+
method: "get",
|
|
334
|
+
path: "/sessions",
|
|
335
|
+
request: { query: sessionListQuerySchema },
|
|
336
|
+
responses: {
|
|
337
|
+
200: {
|
|
338
|
+
description: "Paginated agenda sessions",
|
|
339
|
+
content: { "application/json": { schema: listEnvelope(sessionSchema) } },
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const createSessionRoute = createRoute({
|
|
344
|
+
method: "post",
|
|
345
|
+
path: "/sessions",
|
|
346
|
+
request: {
|
|
347
|
+
body: { required: true, content: { "application/json": { schema: createSessionSchema } } },
|
|
348
|
+
},
|
|
349
|
+
responses: {
|
|
350
|
+
201: {
|
|
351
|
+
description: "The created session",
|
|
352
|
+
content: { "application/json": { schema: dataEnvelope(sessionSchema) } },
|
|
353
|
+
},
|
|
354
|
+
400: invalidRequestResponse,
|
|
355
|
+
404: notFoundResponse("Program not found"),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const getSessionRoute = createRoute({
|
|
359
|
+
method: "get",
|
|
360
|
+
path: "/sessions/{id}",
|
|
361
|
+
request: { params: idParamSchema },
|
|
362
|
+
responses: {
|
|
363
|
+
200: {
|
|
364
|
+
description: "A session by id with its inclusions",
|
|
365
|
+
content: { "application/json": { schema: dataEnvelope(sessionWithInclusionsSchema) } },
|
|
366
|
+
},
|
|
367
|
+
404: notFoundResponse("Session not found"),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
const updateSessionRoute = createRoute({
|
|
371
|
+
method: "patch",
|
|
372
|
+
path: "/sessions/{id}",
|
|
373
|
+
request: {
|
|
374
|
+
params: idParamSchema,
|
|
375
|
+
body: { required: true, content: { "application/json": { schema: updateSessionSchema } } },
|
|
376
|
+
},
|
|
377
|
+
responses: {
|
|
378
|
+
200: {
|
|
379
|
+
description: "The updated session",
|
|
380
|
+
content: { "application/json": { schema: dataEnvelope(sessionSchema) } },
|
|
381
|
+
},
|
|
382
|
+
400: invalidRequestResponse,
|
|
383
|
+
404: notFoundResponse("Session not found"),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
const deleteSessionRoute = createRoute({
|
|
387
|
+
method: "delete",
|
|
388
|
+
path: "/sessions/{id}",
|
|
389
|
+
request: { params: idParamSchema },
|
|
390
|
+
responses: {
|
|
391
|
+
200: {
|
|
392
|
+
description: "Session deleted",
|
|
393
|
+
content: { "application/json": { schema: successResponseSchema } },
|
|
394
|
+
},
|
|
395
|
+
404: notFoundResponse("Session not found"),
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
const setSessionInclusionsRoute = createRoute({
|
|
399
|
+
method: "put",
|
|
400
|
+
path: "/sessions/{id}/inclusions",
|
|
401
|
+
request: {
|
|
402
|
+
params: idParamSchema,
|
|
403
|
+
body: {
|
|
404
|
+
required: true,
|
|
405
|
+
content: { "application/json": { schema: setSessionInclusionsSchema } },
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
responses: {
|
|
409
|
+
200: {
|
|
410
|
+
description: "The replaced session inclusions",
|
|
411
|
+
content: { "application/json": { schema: dataEnvelope(z.array(sessionInclusionSchema)) } },
|
|
412
|
+
},
|
|
413
|
+
400: invalidRequestResponse,
|
|
414
|
+
404: notFoundResponse("Session not found"),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const sessionRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
418
|
+
.openapi(listSessionsRoute, async (c) => c.json(await listSessions(c.get("db"), c.req.valid("query")), 200))
|
|
419
|
+
.openapi(createSessionRoute, async (c) => {
|
|
420
|
+
const outcome = await createSession(c.get("db"), c.req.valid("json"));
|
|
56
421
|
if (outcome.status === "program_not_found")
|
|
57
422
|
return c.json({ error: "Program not found" }, 404);
|
|
58
423
|
return c.json({ data: outcome.session }, 201);
|
|
59
424
|
})
|
|
60
|
-
.
|
|
61
|
-
const session = await getSession(c.get("db"), c.req.
|
|
62
|
-
|
|
63
|
-
return c.json({ error: "Session not found" }, 404);
|
|
64
|
-
return c.json({ data: session });
|
|
425
|
+
.openapi(getSessionRoute, async (c) => {
|
|
426
|
+
const session = await getSession(c.get("db"), c.req.valid("param").id);
|
|
427
|
+
return session ? c.json({ data: session }, 200) : c.json({ error: "Session not found" }, 404);
|
|
65
428
|
})
|
|
66
|
-
.
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (!session)
|
|
70
|
-
return c.json({ error: "Session not found" }, 404);
|
|
71
|
-
return c.json({ data: session });
|
|
429
|
+
.openapi(updateSessionRoute, async (c) => {
|
|
430
|
+
const session = await updateSession(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
431
|
+
return session ? c.json({ data: session }, 200) : c.json({ error: "Session not found" }, 404);
|
|
72
432
|
})
|
|
73
|
-
.
|
|
74
|
-
const ok = await deleteSession(c.get("db"), c.req.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
433
|
+
.openapi(deleteSessionRoute, async (c) => {
|
|
434
|
+
const ok = await deleteSession(c.get("db"), c.req.valid("param").id);
|
|
435
|
+
return ok
|
|
436
|
+
? c.json({ success: true }, 200)
|
|
437
|
+
: c.json({ error: "Session not found" }, 404);
|
|
78
438
|
})
|
|
79
|
-
.
|
|
80
|
-
const id = c.req.
|
|
439
|
+
.openapi(setSessionInclusionsRoute, async (c) => {
|
|
440
|
+
const id = c.req.valid("param").id;
|
|
81
441
|
const session = await getSession(c.get("db"), id);
|
|
82
442
|
if (!session)
|
|
83
443
|
return c.json({ error: "Session not found" }, 404);
|
|
84
|
-
const { inclusions } =
|
|
85
|
-
return c.json({ data: await setSessionInclusions(c.get("db"), id, inclusions) });
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
444
|
+
const { inclusions } = c.req.valid("json");
|
|
445
|
+
return c.json({ data: await setSessionInclusions(c.get("db"), id, inclusions) }, 200);
|
|
446
|
+
});
|
|
447
|
+
// === delegates ==============================================================
|
|
448
|
+
const listDelegatesRoute = createRoute({
|
|
449
|
+
method: "get",
|
|
450
|
+
path: "/delegates",
|
|
451
|
+
request: { query: delegateListQuerySchema },
|
|
452
|
+
responses: {
|
|
453
|
+
200: {
|
|
454
|
+
description: "Paginated program delegates",
|
|
455
|
+
content: { "application/json": { schema: listEnvelope(delegateSchema) } },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
const createDelegateRoute = createRoute({
|
|
460
|
+
method: "post",
|
|
461
|
+
path: "/delegates",
|
|
462
|
+
request: {
|
|
463
|
+
body: { required: true, content: { "application/json": { schema: createDelegateSchema } } },
|
|
464
|
+
},
|
|
465
|
+
responses: {
|
|
466
|
+
201: {
|
|
467
|
+
description: "The created delegate",
|
|
468
|
+
content: { "application/json": { schema: dataEnvelope(delegateSchema) } },
|
|
469
|
+
},
|
|
470
|
+
400: invalidRequestResponse,
|
|
471
|
+
404: notFoundResponse("Program not found"),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
const getDelegateRoute = createRoute({
|
|
475
|
+
method: "get",
|
|
476
|
+
path: "/delegates/{id}",
|
|
477
|
+
request: { params: idParamSchema },
|
|
478
|
+
responses: {
|
|
479
|
+
200: {
|
|
480
|
+
description: "A delegate by id with its session enrollments",
|
|
481
|
+
content: { "application/json": { schema: dataEnvelope(delegateWithEnrollmentsSchema) } },
|
|
482
|
+
},
|
|
483
|
+
404: notFoundResponse("Delegate not found"),
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const updateDelegateRoute = createRoute({
|
|
487
|
+
method: "patch",
|
|
488
|
+
path: "/delegates/{id}",
|
|
489
|
+
request: {
|
|
490
|
+
params: idParamSchema,
|
|
491
|
+
body: { required: true, content: { "application/json": { schema: updateDelegateSchema } } },
|
|
492
|
+
},
|
|
493
|
+
responses: {
|
|
494
|
+
200: {
|
|
495
|
+
description: "The updated delegate",
|
|
496
|
+
content: { "application/json": { schema: dataEnvelope(delegateSchema) } },
|
|
497
|
+
},
|
|
498
|
+
400: invalidRequestResponse,
|
|
499
|
+
404: notFoundResponse("Delegate not found"),
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
const enrollDelegateRoute = createRoute({
|
|
503
|
+
method: "post",
|
|
504
|
+
path: "/delegates/{id}/enrollments",
|
|
505
|
+
request: {
|
|
506
|
+
params: idParamSchema,
|
|
507
|
+
body: { required: true, content: { "application/json": { schema: enrollDelegateSchema } } },
|
|
508
|
+
},
|
|
509
|
+
responses: {
|
|
510
|
+
200: {
|
|
511
|
+
description: "The existing enrollment (idempotent)",
|
|
512
|
+
content: { "application/json": { schema: dataEnvelope(enrollmentSchema) } },
|
|
513
|
+
},
|
|
514
|
+
201: {
|
|
515
|
+
description: "The created enrollment",
|
|
516
|
+
content: { "application/json": { schema: dataEnvelope(enrollmentSchema) } },
|
|
517
|
+
},
|
|
518
|
+
400: invalidRequestResponse,
|
|
519
|
+
404: notFoundResponse("Delegate or session not found"),
|
|
520
|
+
409: {
|
|
521
|
+
description: "Session belongs to a different program",
|
|
522
|
+
content: { "application/json": { schema: errorResponseSchema } },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
const delegateRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
527
|
+
.openapi(listDelegatesRoute, async (c) => c.json(await listDelegates(c.get("db"), c.req.valid("query")), 200))
|
|
528
|
+
.openapi(createDelegateRoute, async (c) => {
|
|
529
|
+
const outcome = await createDelegate(c.get("db"), c.req.valid("json"));
|
|
95
530
|
if (outcome.status === "program_not_found")
|
|
96
531
|
return c.json({ error: "Program not found" }, 404);
|
|
97
532
|
return c.json({ data: outcome.delegate }, 201);
|
|
98
533
|
})
|
|
99
|
-
.
|
|
100
|
-
const delegate = await getDelegate(c.get("db"), c.req.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return c.json({ error: "Delegate not found" }, 404);
|
|
110
|
-
return c.json({ data: delegate });
|
|
111
|
-
})
|
|
112
|
-
.post("/delegates/:id/enrollments", async (c) => {
|
|
113
|
-
const body = await parseJsonBody(c, enrollDelegateSchema);
|
|
114
|
-
const outcome = await enrollDelegate(c.get("db"), c.req.param("id"), body);
|
|
534
|
+
.openapi(getDelegateRoute, async (c) => {
|
|
535
|
+
const delegate = await getDelegate(c.get("db"), c.req.valid("param").id);
|
|
536
|
+
return delegate ? c.json({ data: delegate }, 200) : c.json({ error: "Delegate not found" }, 404);
|
|
537
|
+
})
|
|
538
|
+
.openapi(updateDelegateRoute, async (c) => {
|
|
539
|
+
const delegate = await updateDelegate(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
540
|
+
return delegate ? c.json({ data: delegate }, 200) : c.json({ error: "Delegate not found" }, 404);
|
|
541
|
+
})
|
|
542
|
+
.openapi(enrollDelegateRoute, async (c) => {
|
|
543
|
+
const outcome = await enrollDelegate(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
115
544
|
switch (outcome.status) {
|
|
116
545
|
case "ok":
|
|
117
546
|
return c.json({ data: outcome.enrollment }, outcome.idempotent ? 200 : 201);
|
|
@@ -122,38 +551,136 @@ export const miceAdminRoutes = new Hono()
|
|
|
122
551
|
case "program_mismatch":
|
|
123
552
|
return c.json({ error: "Session belongs to a different program" }, 409);
|
|
124
553
|
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
554
|
+
});
|
|
555
|
+
// === rooming assignments ====================================================
|
|
556
|
+
const listRoomingAssignmentsRoute = createRoute({
|
|
557
|
+
method: "get",
|
|
558
|
+
path: "/rooming-assignments",
|
|
559
|
+
request: { query: roomingListQuerySchema },
|
|
560
|
+
responses: {
|
|
561
|
+
200: {
|
|
562
|
+
description: "Paginated rooming assignments",
|
|
563
|
+
content: { "application/json": { schema: listEnvelope(roomingAssignmentSchema) } },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
const createRoomingAssignmentRoute = createRoute({
|
|
568
|
+
method: "post",
|
|
569
|
+
path: "/rooming-assignments",
|
|
570
|
+
request: {
|
|
571
|
+
body: {
|
|
572
|
+
required: true,
|
|
573
|
+
content: { "application/json": { schema: createRoomingAssignmentSchema } },
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
responses: {
|
|
577
|
+
201: {
|
|
578
|
+
description: "The created rooming assignment",
|
|
579
|
+
content: { "application/json": { schema: dataEnvelope(roomingAssignmentSchema) } },
|
|
580
|
+
},
|
|
581
|
+
400: invalidRequestResponse,
|
|
582
|
+
404: notFoundResponse("Program not found"),
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
const getRoomingAssignmentRoute = createRoute({
|
|
586
|
+
method: "get",
|
|
587
|
+
path: "/rooming-assignments/{id}",
|
|
588
|
+
request: { params: idParamSchema },
|
|
589
|
+
responses: {
|
|
590
|
+
200: {
|
|
591
|
+
description: "A rooming assignment by id with its delegates",
|
|
592
|
+
content: { "application/json": { schema: dataEnvelope(roomingWithDelegatesSchema) } },
|
|
593
|
+
},
|
|
594
|
+
404: notFoundResponse("Rooming assignment not found"),
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
const updateRoomingAssignmentRoute = createRoute({
|
|
598
|
+
method: "patch",
|
|
599
|
+
path: "/rooming-assignments/{id}",
|
|
600
|
+
request: {
|
|
601
|
+
params: idParamSchema,
|
|
602
|
+
body: {
|
|
603
|
+
required: true,
|
|
604
|
+
content: { "application/json": { schema: updateRoomingAssignmentSchema } },
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
responses: {
|
|
608
|
+
200: {
|
|
609
|
+
description: "The updated rooming assignment",
|
|
610
|
+
content: { "application/json": { schema: dataEnvelope(roomingAssignmentSchema) } },
|
|
611
|
+
},
|
|
612
|
+
400: invalidRequestResponse,
|
|
613
|
+
404: notFoundResponse("Rooming assignment not found"),
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
const setRoomingDelegatesRoute = createRoute({
|
|
617
|
+
method: "put",
|
|
618
|
+
path: "/rooming-assignments/{id}/delegates",
|
|
619
|
+
request: {
|
|
620
|
+
params: idParamSchema,
|
|
621
|
+
body: {
|
|
622
|
+
required: true,
|
|
623
|
+
content: { "application/json": { schema: setRoomingDelegatesSchema } },
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
responses: {
|
|
627
|
+
200: {
|
|
628
|
+
description: "The replaced rooming-assignment occupants",
|
|
629
|
+
content: {
|
|
630
|
+
"application/json": { schema: dataEnvelope(z.array(roomingAssignmentDelegateSchema)) },
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
400: invalidRequestResponse,
|
|
634
|
+
404: {
|
|
635
|
+
description: "Rooming assignment not found, or one or more delegates not found",
|
|
636
|
+
content: {
|
|
637
|
+
"application/json": {
|
|
638
|
+
schema: z.object({
|
|
639
|
+
error: z.string(),
|
|
640
|
+
detail: z.object({ missing: z.array(z.string()) }).optional(),
|
|
641
|
+
}),
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
409: {
|
|
646
|
+
description: "One or more delegates belong to a different program",
|
|
647
|
+
content: {
|
|
648
|
+
"application/json": {
|
|
649
|
+
schema: z.object({
|
|
650
|
+
error: z.string(),
|
|
651
|
+
detail: z.object({ offending: z.array(z.string()) }).optional(),
|
|
652
|
+
}),
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
const roomingRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
659
|
+
.openapi(listRoomingAssignmentsRoute, async (c) => c.json(await listRoomingAssignments(c.get("db"), c.req.valid("query")), 200))
|
|
660
|
+
.openapi(createRoomingAssignmentRoute, async (c) => {
|
|
661
|
+
const outcome = await createRoomingAssignment(c.get("db"), c.req.valid("json"));
|
|
134
662
|
if (outcome.status === "program_not_found")
|
|
135
663
|
return c.json({ error: "Program not found" }, 404);
|
|
136
664
|
return c.json({ data: outcome.assignment }, 201);
|
|
137
665
|
})
|
|
138
|
-
.
|
|
139
|
-
const assignment = await getRoomingAssignment(c.get("db"), c.req.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})
|
|
144
|
-
.
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
const
|
|
153
|
-
const outcome = await setRoomingDelegates(c.get("db"), c.req.param("id"), delegates);
|
|
666
|
+
.openapi(getRoomingAssignmentRoute, async (c) => {
|
|
667
|
+
const assignment = await getRoomingAssignment(c.get("db"), c.req.valid("param").id);
|
|
668
|
+
return assignment
|
|
669
|
+
? c.json({ data: assignment }, 200)
|
|
670
|
+
: c.json({ error: "Rooming assignment not found" }, 404);
|
|
671
|
+
})
|
|
672
|
+
.openapi(updateRoomingAssignmentRoute, async (c) => {
|
|
673
|
+
const assignment = await updateRoomingAssignment(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
674
|
+
return assignment
|
|
675
|
+
? c.json({ data: assignment }, 200)
|
|
676
|
+
: c.json({ error: "Rooming assignment not found" }, 404);
|
|
677
|
+
})
|
|
678
|
+
.openapi(setRoomingDelegatesRoute, async (c) => {
|
|
679
|
+
const { delegates } = c.req.valid("json");
|
|
680
|
+
const outcome = await setRoomingDelegates(c.get("db"), c.req.valid("param").id, delegates);
|
|
154
681
|
switch (outcome.status) {
|
|
155
682
|
case "ok":
|
|
156
|
-
return c.json({ data: outcome.delegates });
|
|
683
|
+
return c.json({ data: outcome.delegates }, 200);
|
|
157
684
|
case "assignment_not_found":
|
|
158
685
|
return c.json({ error: "Rooming assignment not found" }, 404);
|
|
159
686
|
case "delegate_not_found":
|
|
@@ -164,52 +691,149 @@ export const miceAdminRoutes = new Hono()
|
|
|
164
691
|
detail: { offending: outcome.offending },
|
|
165
692
|
}, 409);
|
|
166
693
|
}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
694
|
+
});
|
|
695
|
+
// === RFPs ===================================================================
|
|
696
|
+
const listRfpsRoute = createRoute({
|
|
697
|
+
method: "get",
|
|
698
|
+
path: "/rfps",
|
|
699
|
+
request: { query: rfpListQuerySchema },
|
|
700
|
+
responses: {
|
|
701
|
+
200: {
|
|
702
|
+
description: "Paginated RFPs",
|
|
703
|
+
content: { "application/json": { schema: listEnvelope(rfpSchema) } },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
const createRfpRoute = createRoute({
|
|
708
|
+
method: "post",
|
|
709
|
+
path: "/rfps",
|
|
710
|
+
request: {
|
|
711
|
+
body: { required: true, content: { "application/json": { schema: createRfpSchema } } },
|
|
712
|
+
},
|
|
713
|
+
responses: {
|
|
714
|
+
201: {
|
|
715
|
+
description: "The created RFP",
|
|
716
|
+
content: { "application/json": { schema: dataEnvelope(rfpSchema) } },
|
|
717
|
+
},
|
|
718
|
+
400: invalidRequestResponse,
|
|
719
|
+
404: notFoundResponse("Program not found"),
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
const getRfpRoute = createRoute({
|
|
723
|
+
method: "get",
|
|
724
|
+
path: "/rfps/{id}",
|
|
725
|
+
request: { params: idParamSchema },
|
|
726
|
+
responses: {
|
|
727
|
+
200: {
|
|
728
|
+
description: "An RFP by id with its invitations and bids",
|
|
729
|
+
content: { "application/json": { schema: dataEnvelope(rfpWithDetailsSchema) } },
|
|
730
|
+
},
|
|
731
|
+
404: notFoundResponse("RFP not found"),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
const updateRfpRoute = createRoute({
|
|
735
|
+
method: "patch",
|
|
736
|
+
path: "/rfps/{id}",
|
|
737
|
+
request: {
|
|
738
|
+
params: idParamSchema,
|
|
739
|
+
body: { required: true, content: { "application/json": { schema: updateRfpSchema } } },
|
|
740
|
+
},
|
|
741
|
+
responses: {
|
|
742
|
+
200: {
|
|
743
|
+
description: "The updated RFP",
|
|
744
|
+
content: { "application/json": { schema: dataEnvelope(rfpSchema) } },
|
|
745
|
+
},
|
|
746
|
+
400: invalidRequestResponse,
|
|
747
|
+
404: notFoundResponse("RFP not found"),
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
const inviteSupplierRoute = createRoute({
|
|
751
|
+
method: "post",
|
|
752
|
+
path: "/rfps/{id}/invitations",
|
|
753
|
+
request: {
|
|
754
|
+
params: idParamSchema,
|
|
755
|
+
body: { required: true, content: { "application/json": { schema: inviteSupplierSchema } } },
|
|
756
|
+
},
|
|
757
|
+
responses: {
|
|
758
|
+
200: {
|
|
759
|
+
description: "The existing invitation (idempotent)",
|
|
760
|
+
content: { "application/json": { schema: dataEnvelope(rfpInvitationSchema) } },
|
|
761
|
+
},
|
|
762
|
+
201: {
|
|
763
|
+
description: "The created invitation",
|
|
764
|
+
content: { "application/json": { schema: dataEnvelope(rfpInvitationSchema) } },
|
|
765
|
+
},
|
|
766
|
+
400: invalidRequestResponse,
|
|
767
|
+
404: notFoundResponse("RFP not found"),
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
const createBidRoute = createRoute({
|
|
771
|
+
method: "post",
|
|
772
|
+
path: "/rfps/{id}/bids",
|
|
773
|
+
request: {
|
|
774
|
+
params: idParamSchema,
|
|
775
|
+
body: { required: true, content: { "application/json": { schema: createBidSchema } } },
|
|
776
|
+
},
|
|
777
|
+
responses: {
|
|
778
|
+
201: {
|
|
779
|
+
description: "The created bid",
|
|
780
|
+
content: { "application/json": { schema: dataEnvelope(bidSchema) } },
|
|
781
|
+
},
|
|
782
|
+
400: invalidRequestResponse,
|
|
783
|
+
404: notFoundResponse("RFP not found"),
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
const awardRfpRoute = createRoute({
|
|
787
|
+
method: "post",
|
|
788
|
+
path: "/rfps/{id}/award",
|
|
789
|
+
request: {
|
|
790
|
+
params: idParamSchema,
|
|
791
|
+
body: { required: true, content: { "application/json": { schema: awardRfpSchema } } },
|
|
792
|
+
},
|
|
793
|
+
responses: {
|
|
794
|
+
200: {
|
|
795
|
+
description: "The awarded RFP and accepted bid",
|
|
796
|
+
content: {
|
|
797
|
+
"application/json": {
|
|
798
|
+
schema: z.object({ data: z.object({ rfp: rfpSchema, bid: bidSchema }) }),
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
400: invalidRequestResponse,
|
|
803
|
+
404: notFoundResponse("RFP or bid not found"),
|
|
804
|
+
409: {
|
|
805
|
+
description: "RFP is already awarded",
|
|
806
|
+
content: { "application/json": { schema: errorResponseSchema } },
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
const rfpRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
811
|
+
.openapi(listRfpsRoute, async (c) => c.json(await rfpService.listRfps(c.get("db"), c.req.valid("query")), 200))
|
|
812
|
+
.openapi(createRfpRoute, async (c) => {
|
|
813
|
+
const outcome = await rfpService.createRfp(c.get("db"), c.req.valid("json"));
|
|
176
814
|
if (outcome.status === "program_not_found")
|
|
177
815
|
return c.json({ error: "Program not found" }, 404);
|
|
178
816
|
return c.json({ data: outcome.rfp }, 201);
|
|
179
817
|
})
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return c.json({ error: "RFP not found" }, 404);
|
|
184
|
-
return c.json({ data: rfp });
|
|
185
|
-
})
|
|
186
|
-
.patch("/rfps/:id", async (c) => {
|
|
187
|
-
const body = await parseJsonBody(c, updateRfpSchema);
|
|
188
|
-
const rfp = await rfpService.updateRfp(c.get("db"), c.req.param("id"), body);
|
|
189
|
-
if (!rfp)
|
|
190
|
-
return c.json({ error: "RFP not found" }, 404);
|
|
191
|
-
return c.json({ data: rfp });
|
|
192
|
-
})
|
|
193
|
-
.post("/rfps/:id/invitations", async (c) => {
|
|
194
|
-
const body = await parseJsonBody(c, inviteSupplierSchema);
|
|
195
|
-
const outcome = await rfpService.inviteSupplier(c.get("db"), c.req.param("id"), body);
|
|
818
|
+
// static sub-resources before the dynamic `/{id}` catch-all
|
|
819
|
+
.openapi(inviteSupplierRoute, async (c) => {
|
|
820
|
+
const outcome = await rfpService.inviteSupplier(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
196
821
|
if (outcome.status === "rfp_not_found")
|
|
197
822
|
return c.json({ error: "RFP not found" }, 404);
|
|
198
823
|
return c.json({ data: outcome.invitation }, outcome.idempotent ? 200 : 201);
|
|
199
824
|
})
|
|
200
|
-
.
|
|
201
|
-
const
|
|
202
|
-
const outcome = await rfpService.createBid(c.get("db"), c.req.param("id"), body);
|
|
825
|
+
.openapi(createBidRoute, async (c) => {
|
|
826
|
+
const outcome = await rfpService.createBid(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
203
827
|
if (outcome.status === "rfp_not_found")
|
|
204
828
|
return c.json({ error: "RFP not found" }, 404);
|
|
205
829
|
return c.json({ data: outcome.bid }, 201);
|
|
206
830
|
})
|
|
207
|
-
.
|
|
208
|
-
const { bidId } =
|
|
209
|
-
const outcome = await rfpService.awardRfp(c.get("db"), c.req.
|
|
831
|
+
.openapi(awardRfpRoute, async (c) => {
|
|
832
|
+
const { bidId } = c.req.valid("json");
|
|
833
|
+
const outcome = await rfpService.awardRfp(c.get("db"), c.req.valid("param").id, bidId);
|
|
210
834
|
switch (outcome.status) {
|
|
211
835
|
case "ok":
|
|
212
|
-
return c.json({ data: { rfp: outcome.rfp, bid: outcome.bid } });
|
|
836
|
+
return c.json({ data: { rfp: outcome.rfp, bid: outcome.bid } }, 200);
|
|
213
837
|
case "rfp_not_found":
|
|
214
838
|
return c.json({ error: "RFP not found" }, 404);
|
|
215
839
|
case "bid_not_found":
|
|
@@ -218,30 +842,107 @@ export const miceAdminRoutes = new Hono()
|
|
|
218
842
|
return c.json({ error: "RFP is already awarded" }, 409);
|
|
219
843
|
}
|
|
220
844
|
})
|
|
221
|
-
.
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
return c.json({ error: "Bid not found" }, 404);
|
|
225
|
-
return c.json({ data: bid });
|
|
226
|
-
})
|
|
227
|
-
.patch("/bids/:id", async (c) => {
|
|
228
|
-
const body = await parseJsonBody(c, updateBidSchema);
|
|
229
|
-
const bid = await rfpService.updateBid(c.get("db"), c.req.param("id"), body);
|
|
230
|
-
if (!bid)
|
|
231
|
-
return c.json({ error: "Bid not found" }, 404);
|
|
232
|
-
return c.json({ data: bid });
|
|
845
|
+
.openapi(getRfpRoute, async (c) => {
|
|
846
|
+
const rfp = await rfpService.getRfp(c.get("db"), c.req.valid("param").id);
|
|
847
|
+
return rfp ? c.json({ data: rfp }, 200) : c.json({ error: "RFP not found" }, 404);
|
|
233
848
|
})
|
|
234
|
-
.
|
|
235
|
-
const
|
|
236
|
-
|
|
849
|
+
.openapi(updateRfpRoute, async (c) => {
|
|
850
|
+
const rfp = await rfpService.updateRfp(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
851
|
+
return rfp ? c.json({ data: rfp }, 200) : c.json({ error: "RFP not found" }, 404);
|
|
852
|
+
});
|
|
853
|
+
// === bids ===================================================================
|
|
854
|
+
const getBidRoute = createRoute({
|
|
855
|
+
method: "get",
|
|
856
|
+
path: "/bids/{id}",
|
|
857
|
+
request: { params: idParamSchema },
|
|
858
|
+
responses: {
|
|
859
|
+
200: {
|
|
860
|
+
description: "A bid by id with its lines and evaluations",
|
|
861
|
+
content: { "application/json": { schema: dataEnvelope(bidWithDetailsSchema) } },
|
|
862
|
+
},
|
|
863
|
+
404: notFoundResponse("Bid not found"),
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
const updateBidRoute = createRoute({
|
|
867
|
+
method: "patch",
|
|
868
|
+
path: "/bids/{id}",
|
|
869
|
+
request: {
|
|
870
|
+
params: idParamSchema,
|
|
871
|
+
body: { required: true, content: { "application/json": { schema: updateBidSchema } } },
|
|
872
|
+
},
|
|
873
|
+
responses: {
|
|
874
|
+
200: {
|
|
875
|
+
description: "The updated bid",
|
|
876
|
+
content: { "application/json": { schema: dataEnvelope(bidSchema) } },
|
|
877
|
+
},
|
|
878
|
+
400: invalidRequestResponse,
|
|
879
|
+
404: notFoundResponse("Bid not found"),
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
const setBidLinesRoute = createRoute({
|
|
883
|
+
method: "put",
|
|
884
|
+
path: "/bids/{id}/lines",
|
|
885
|
+
request: {
|
|
886
|
+
params: idParamSchema,
|
|
887
|
+
body: { required: true, content: { "application/json": { schema: setBidLinesSchema } } },
|
|
888
|
+
},
|
|
889
|
+
responses: {
|
|
890
|
+
200: {
|
|
891
|
+
description: "The replaced bid line items",
|
|
892
|
+
content: { "application/json": { schema: dataEnvelope(z.array(bidLineSchema)) } },
|
|
893
|
+
},
|
|
894
|
+
400: invalidRequestResponse,
|
|
895
|
+
404: notFoundResponse("Bid not found"),
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
const addBidEvaluationRoute = createRoute({
|
|
899
|
+
method: "post",
|
|
900
|
+
path: "/bids/{id}/evaluations",
|
|
901
|
+
request: {
|
|
902
|
+
params: idParamSchema,
|
|
903
|
+
body: { required: true, content: { "application/json": { schema: addBidEvaluationSchema } } },
|
|
904
|
+
},
|
|
905
|
+
responses: {
|
|
906
|
+
201: {
|
|
907
|
+
description: "The created bid evaluation",
|
|
908
|
+
content: { "application/json": { schema: dataEnvelope(bidEvaluationSchema) } },
|
|
909
|
+
},
|
|
910
|
+
400: invalidRequestResponse,
|
|
911
|
+
404: notFoundResponse("Bid not found"),
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
const bidRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
915
|
+
// static sub-resources before the dynamic `/{id}` catch-all
|
|
916
|
+
.openapi(setBidLinesRoute, async (c) => {
|
|
917
|
+
const { lines } = c.req.valid("json");
|
|
918
|
+
const outcome = await rfpService.setBidLines(c.get("db"), c.req.valid("param").id, lines);
|
|
237
919
|
if (outcome.status === "bid_not_found")
|
|
238
920
|
return c.json({ error: "Bid not found" }, 404);
|
|
239
|
-
return c.json({ data: outcome.lines });
|
|
921
|
+
return c.json({ data: outcome.lines }, 200);
|
|
240
922
|
})
|
|
241
|
-
.
|
|
242
|
-
const
|
|
243
|
-
const outcome = await rfpService.addBidEvaluation(c.get("db"), c.req.param("id"), body);
|
|
923
|
+
.openapi(addBidEvaluationRoute, async (c) => {
|
|
924
|
+
const outcome = await rfpService.addBidEvaluation(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
244
925
|
if (outcome.status === "bid_not_found")
|
|
245
926
|
return c.json({ error: "Bid not found" }, 404);
|
|
246
927
|
return c.json({ data: outcome.evaluation }, 201);
|
|
928
|
+
})
|
|
929
|
+
.openapi(getBidRoute, async (c) => {
|
|
930
|
+
const bid = await rfpService.getBid(c.get("db"), c.req.valid("param").id);
|
|
931
|
+
return bid ? c.json({ data: bid }, 200) : c.json({ error: "Bid not found" }, 404);
|
|
932
|
+
})
|
|
933
|
+
.openapi(updateBidRoute, async (c) => {
|
|
934
|
+
const bid = await rfpService.updateBid(c.get("db"), c.req.valid("param").id, c.req.valid("json"));
|
|
935
|
+
return bid ? c.json({ data: bid }, 200) : c.json({ error: "Bid not found" }, 404);
|
|
247
936
|
});
|
|
937
|
+
/**
|
|
938
|
+
* Compose the per-resource sub-chains onto a single `OpenAPIHono` so the
|
|
939
|
+
* `.openapi()` operations propagate up through any parent registry while keeping
|
|
940
|
+
* type-inference cost bounded (one flat chain has O(n²) inference cost).
|
|
941
|
+
*/
|
|
942
|
+
export const miceAdminRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
|
|
943
|
+
.route("/", programRoutes)
|
|
944
|
+
.route("/", sessionRoutes)
|
|
945
|
+
.route("/", delegateRoutes)
|
|
946
|
+
.route("/", roomingRoutes)
|
|
947
|
+
.route("/", rfpRoutes)
|
|
948
|
+
.route("/", bidRoutes);
|