@voyant-travel/accommodations 0.108.2 → 0.108.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.
@@ -6,7 +6,14 @@
6
6
  * the transactional DB. See RFC voyant#1489 §4.2/§8.
7
7
  *
8
8
  * Routes stay thin: validate input, call `roomBlockService`, serialize.
9
+ *
10
+ * Contract: authored as `@hono/zod-openapi` routes (voyant#2114). The request
11
+ * bodies reuse the exported `validation-room-blocks` schemas; the response
12
+ * schemas are authored here against the service return types (room-block /
13
+ * pickup rows + the derived summary), with `Date`-origin columns serialized as
14
+ * strings per api-route-authoring §17 — the wire shape, not the Drizzle row.
9
15
  */
16
+ import { OpenAPIHono, z } from "@hono/zod-openapi";
10
17
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
11
18
  type Env = {
12
19
  Variables: {
@@ -14,26 +21,107 @@ type Env = {
14
21
  userId?: string;
15
22
  };
16
23
  };
17
- export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env, {
24
+ /**
25
+ * Wire shape of a `room_blocks` row. `optionDate`/`cutoffDate` are SQL `date`
26
+ * columns and `createdAt`/`updatedAt` are timestamps — both serialize to
27
+ * strings over the wire (§17), never `Date`.
28
+ */
29
+ export declare const roomBlockSchema: z.ZodObject<{
30
+ id: z.ZodString;
31
+ programId: z.ZodNullable<z.ZodString>;
32
+ supplierId: z.ZodNullable<z.ZodString>;
33
+ propertyId: z.ZodNullable<z.ZodString>;
34
+ roomTypeId: z.ZodString;
35
+ name: z.ZodString;
36
+ status: z.ZodEnum<{
37
+ inquiry: "inquiry";
38
+ cancelled: "cancelled";
39
+ held: "held";
40
+ confirmed: "confirmed";
41
+ released: "released";
42
+ expired: "expired";
43
+ }>;
44
+ currency: z.ZodString;
45
+ netRateCents: z.ZodNullable<z.ZodNumber>;
46
+ sellRateCents: z.ZodNullable<z.ZodNumber>;
47
+ optionDate: z.ZodNullable<z.ZodString>;
48
+ cutoffDate: z.ZodNullable<z.ZodString>;
49
+ attritionTerms: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
50
+ depositTerms: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
51
+ notes: z.ZodNullable<z.ZodString>;
52
+ metadata: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
53
+ createdAt: z.ZodString;
54
+ updatedAt: z.ZodString;
55
+ }, z.core.$strip>;
56
+ /**
57
+ * Wire shape of a `room_block_pickups` ledger row. `checkIn`/`checkOut` are
58
+ * `date` columns and `pickedUpAt`/`reversedAt` are timestamps — strings over
59
+ * the wire (§17).
60
+ */
61
+ export declare const roomBlockPickupRowSchema: z.ZodObject<{
62
+ id: z.ZodString;
63
+ blockId: z.ZodString;
64
+ bookingId: z.ZodNullable<z.ZodString>;
65
+ stayBookingItemId: z.ZodNullable<z.ZodString>;
66
+ checkIn: z.ZodString;
67
+ checkOut: z.ZodString;
68
+ rooms: z.ZodNumber;
69
+ status: z.ZodEnum<{
70
+ active: "active";
71
+ reversed: "reversed";
72
+ }>;
73
+ pickedUpAt: z.ZodString;
74
+ reversedAt: z.ZodNullable<z.ZodString>;
75
+ }, z.core.$strip>;
76
+ /** Derived ops view — counters + pickup progress for a block. */
77
+ export declare const roomBlockSummarySchema: z.ZodObject<{
78
+ blockId: z.ZodString;
79
+ status: z.ZodEnum<{
80
+ inquiry: "inquiry";
81
+ cancelled: "cancelled";
82
+ held: "held";
83
+ confirmed: "confirmed";
84
+ released: "released";
85
+ expired: "expired";
86
+ }>;
87
+ totalHeld: z.ZodNumber;
88
+ totalPickedUp: z.ZodNumber;
89
+ totalReleased: z.ZodNumber;
90
+ totalRemaining: z.ZodNumber;
91
+ pickupProgress: z.ZodEnum<{
92
+ none: "none";
93
+ partial: "partial";
94
+ full: "full";
95
+ }>;
96
+ }, z.core.$strip>;
97
+ export declare const roomBlockAdminRoutes: OpenAPIHono<Env, {
18
98
  "/room-blocks": {
19
99
  $post: {
20
- input: {};
100
+ input: {
101
+ json: {
102
+ roomTypeId: string;
103
+ name: string;
104
+ currency: string;
105
+ programId?: string | undefined;
106
+ supplierId?: string | undefined;
107
+ propertyId?: string | undefined;
108
+ netRateCents?: number | undefined;
109
+ sellRateCents?: number | undefined;
110
+ optionDate?: string | undefined;
111
+ cutoffDate?: string | undefined;
112
+ notes?: string | undefined;
113
+ };
114
+ };
21
115
  output: {
22
116
  data: {
23
117
  id: string;
24
- createdAt: string;
25
- updatedAt: string;
26
- propertyId: string | null;
118
+ programId: string | null;
27
119
  supplierId: string | null;
120
+ propertyId: string | null;
121
+ roomTypeId: string;
28
122
  name: string;
123
+ status: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
29
124
  currency: string;
30
- status: "held" | "confirmed" | "cancelled" | "expired" | "inquiry" | "released";
31
- metadata: {
32
- [x: string]: import("hono/utils/types").JSONValue;
33
- } | null;
34
- notes: string | null;
35
- roomTypeId: string;
36
- programId: string | null;
37
125
  netRateCents: number | null;
38
126
  sellRateCents: number | null;
39
127
  optionDate: string | null;
@@ -44,26 +132,42 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
44
132
  depositTerms: {
45
133
  [x: string]: import("hono/utils/types").JSONValue;
46
134
  } | null;
135
+ notes: string | null;
136
+ metadata: {
137
+ [x: string]: import("hono/utils/types").JSONValue;
138
+ } | null;
139
+ createdAt: string;
140
+ updatedAt: string;
47
141
  };
48
142
  };
49
143
  outputFormat: "json";
50
144
  status: 201;
51
- };
52
- };
53
- } & {
54
- "/room-blocks/:id": {
55
- $get: {
145
+ } | {
56
146
  input: {
57
- param: {
58
- id: string;
147
+ json: {
148
+ roomTypeId: string;
149
+ name: string;
150
+ currency: string;
151
+ programId?: string | undefined;
152
+ supplierId?: string | undefined;
153
+ propertyId?: string | undefined;
154
+ netRateCents?: number | undefined;
155
+ sellRateCents?: number | undefined;
156
+ optionDate?: string | undefined;
157
+ cutoffDate?: string | undefined;
158
+ notes?: string | undefined;
59
159
  };
60
160
  };
61
161
  output: {
62
162
  error: string;
63
163
  };
64
164
  outputFormat: "json";
65
- status: 404;
66
- } | {
165
+ status: 400;
166
+ };
167
+ };
168
+ } & {
169
+ "/room-blocks/:id": {
170
+ $get: {
67
171
  input: {
68
172
  param: {
69
173
  id: string;
@@ -73,19 +177,13 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
73
177
  data: {
74
178
  block: {
75
179
  id: string;
76
- createdAt: string;
77
- updatedAt: string;
78
- propertyId: string | null;
180
+ programId: string | null;
79
181
  supplierId: string | null;
182
+ propertyId: string | null;
183
+ roomTypeId: string;
80
184
  name: string;
185
+ status: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
81
186
  currency: string;
82
- status: "held" | "confirmed" | "cancelled" | "expired" | "inquiry" | "released";
83
- metadata: {
84
- [x: string]: import("hono/utils/types").JSONValue;
85
- } | null;
86
- notes: string | null;
87
- roomTypeId: string;
88
- programId: string | null;
89
187
  netRateCents: number | null;
90
188
  sellRateCents: number | null;
91
189
  optionDate: string | null;
@@ -96,20 +194,37 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
96
194
  depositTerms: {
97
195
  [x: string]: import("hono/utils/types").JSONValue;
98
196
  } | null;
197
+ notes: string | null;
198
+ metadata: {
199
+ [x: string]: import("hono/utils/types").JSONValue;
200
+ } | null;
201
+ createdAt: string;
202
+ updatedAt: string;
99
203
  };
100
204
  summary: {
101
205
  blockId: string;
102
- status: import("./schema-room-blocks.js").RoomBlock["status"];
206
+ status: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
103
207
  totalHeld: number;
104
208
  totalPickedUp: number;
105
209
  totalReleased: number;
106
210
  totalRemaining: number;
107
- pickupProgress: import("@voyant-travel/allotments").PickupProgress;
211
+ pickupProgress: "none" | "partial" | "full";
108
212
  } | null;
109
213
  };
110
214
  };
111
215
  outputFormat: "json";
112
- status: import("hono/utils/http-status").ContentfulStatusCode;
216
+ status: 200;
217
+ } | {
218
+ input: {
219
+ param: {
220
+ id: string;
221
+ };
222
+ };
223
+ output: {
224
+ error: string;
225
+ };
226
+ outputFormat: "json";
227
+ status: 404;
113
228
  };
114
229
  };
115
230
  } & {
@@ -119,6 +234,35 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
119
234
  param: {
120
235
  id: string;
121
236
  };
237
+ } & {
238
+ json: {
239
+ nights: {
240
+ date: string;
241
+ roomsHeld: number;
242
+ netRateCentsOverride?: number | undefined;
243
+ sellRateCentsOverride?: number | undefined;
244
+ }[];
245
+ };
246
+ };
247
+ output: {
248
+ error: string;
249
+ };
250
+ outputFormat: "json";
251
+ status: 400;
252
+ } | {
253
+ input: {
254
+ param: {
255
+ id: string;
256
+ };
257
+ } & {
258
+ json: {
259
+ nights: {
260
+ date: string;
261
+ roomsHeld: number;
262
+ netRateCentsOverride?: number | undefined;
263
+ sellRateCentsOverride?: number | undefined;
264
+ }[];
265
+ };
122
266
  };
123
267
  output: {
124
268
  error: string;
@@ -130,20 +274,29 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
130
274
  param: {
131
275
  id: string;
132
276
  };
277
+ } & {
278
+ json: {
279
+ nights: {
280
+ date: string;
281
+ roomsHeld: number;
282
+ netRateCentsOverride?: number | undefined;
283
+ sellRateCentsOverride?: number | undefined;
284
+ }[];
285
+ };
133
286
  };
134
287
  output: {
135
288
  data: {
136
289
  blockId: string;
137
- status: import("./schema-room-blocks.js").RoomBlock["status"];
290
+ status: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
138
291
  totalHeld: number;
139
292
  totalPickedUp: number;
140
293
  totalReleased: number;
141
294
  totalRemaining: number;
142
- pickupProgress: import("@voyant-travel/allotments").PickupProgress;
295
+ pickupProgress: "none" | "partial" | "full";
143
296
  } | null;
144
297
  };
145
298
  outputFormat: "json";
146
- status: import("hono/utils/http-status").ContentfulStatusCode;
299
+ status: 200;
147
300
  };
148
301
  };
149
302
  } & {
@@ -153,53 +306,122 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
153
306
  param: {
154
307
  id: string;
155
308
  };
309
+ } & {
310
+ json: {
311
+ checkIn: string;
312
+ checkOut: string;
313
+ bookingId?: string | undefined;
314
+ stayBookingItemId?: string | undefined;
315
+ rooms?: number | undefined;
316
+ };
156
317
  };
157
318
  output: {
158
- data: {
319
+ error: string;
320
+ };
321
+ outputFormat: "json";
322
+ status: 400;
323
+ } | {
324
+ input: {
325
+ param: {
159
326
  id: string;
160
- status: "active" | "reversed";
161
- bookingId: string | null;
162
- rooms: number;
327
+ };
328
+ } & {
329
+ json: {
163
330
  checkIn: string;
164
331
  checkOut: string;
165
- stayBookingItemId: string | null;
166
- blockId: string;
167
- pickedUpAt: string;
168
- reversedAt: string | null;
332
+ bookingId?: string | undefined;
333
+ stayBookingItemId?: string | undefined;
334
+ rooms?: number | undefined;
169
335
  };
170
336
  };
337
+ output: {
338
+ error: string;
339
+ };
171
340
  outputFormat: "json";
172
- status: 200 | 201;
341
+ status: 404;
173
342
  } | {
174
343
  input: {
175
344
  param: {
176
345
  id: string;
177
346
  };
347
+ } & {
348
+ json: {
349
+ checkIn: string;
350
+ checkOut: string;
351
+ bookingId?: string | undefined;
352
+ stayBookingItemId?: string | undefined;
353
+ rooms?: number | undefined;
354
+ };
178
355
  };
179
356
  output: {
180
- error: string;
357
+ data: {
358
+ id: string;
359
+ blockId: string;
360
+ bookingId: string | null;
361
+ stayBookingItemId: string | null;
362
+ checkIn: string;
363
+ checkOut: string;
364
+ rooms: number;
365
+ status: "active" | "reversed";
366
+ pickedUpAt: string;
367
+ reversedAt: string | null;
368
+ };
181
369
  };
182
370
  outputFormat: "json";
183
- status: 404;
371
+ status: 201;
184
372
  } | {
185
373
  input: {
186
374
  param: {
187
375
  id: string;
188
376
  };
377
+ } & {
378
+ json: {
379
+ checkIn: string;
380
+ checkOut: string;
381
+ bookingId?: string | undefined;
382
+ stayBookingItemId?: string | undefined;
383
+ rooms?: number | undefined;
384
+ };
189
385
  };
190
386
  output: {
191
- error: string;
387
+ data: {
388
+ id: string;
389
+ blockId: string;
390
+ bookingId: string | null;
391
+ stayBookingItemId: string | null;
392
+ checkIn: string;
393
+ checkOut: string;
394
+ rooms: number;
395
+ status: "active" | "reversed";
396
+ pickedUpAt: string;
397
+ reversedAt: string | null;
398
+ };
192
399
  };
193
400
  outputFormat: "json";
194
- status: 400;
401
+ status: 200;
195
402
  } | {
196
403
  input: {
197
404
  param: {
198
405
  id: string;
199
406
  };
407
+ } & {
408
+ json: {
409
+ checkIn: string;
410
+ checkOut: string;
411
+ bookingId?: string | undefined;
412
+ stayBookingItemId?: string | undefined;
413
+ rooms?: number | undefined;
414
+ };
200
415
  };
201
416
  output: {
202
417
  error: string;
418
+ } | {
419
+ error: string;
420
+ detail: {
421
+ date: string;
422
+ remaining: number;
423
+ needed: number;
424
+ };
203
425
  };
204
426
  outputFormat: "json";
205
427
  status: 409;
@@ -212,6 +434,27 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
212
434
  param: {
213
435
  id: string;
214
436
  };
437
+ } & {
438
+ json: {
439
+ pickupId?: string | undefined;
440
+ stayBookingItemId?: string | undefined;
441
+ };
442
+ };
443
+ output: {
444
+ error: string;
445
+ };
446
+ outputFormat: "json";
447
+ status: 400;
448
+ } | {
449
+ input: {
450
+ param: {
451
+ id: string;
452
+ };
453
+ } & {
454
+ json: {
455
+ pickupId?: string | undefined;
456
+ stayBookingItemId?: string | undefined;
457
+ };
215
458
  };
216
459
  output: {
217
460
  error: string;
@@ -223,23 +466,28 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
223
466
  param: {
224
467
  id: string;
225
468
  };
469
+ } & {
470
+ json: {
471
+ pickupId?: string | undefined;
472
+ stayBookingItemId?: string | undefined;
473
+ };
226
474
  };
227
475
  output: {
228
476
  data: {
229
477
  id: string;
230
- status: "active" | "reversed";
478
+ blockId: string;
231
479
  bookingId: string | null;
232
- rooms: number;
480
+ stayBookingItemId: string | null;
233
481
  checkIn: string;
234
482
  checkOut: string;
235
- stayBookingItemId: string | null;
236
- blockId: string;
483
+ rooms: number;
484
+ status: "active" | "reversed";
237
485
  pickedUpAt: string;
238
486
  reversedAt: string | null;
239
487
  };
240
488
  };
241
489
  outputFormat: "json";
242
- status: import("hono/utils/http-status").ContentfulStatusCode;
490
+ status: 200;
243
491
  };
244
492
  };
245
493
  } & {
@@ -266,19 +514,13 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
266
514
  releasedRooms: number;
267
515
  block: {
268
516
  id: string;
269
- createdAt: string;
270
- updatedAt: string;
271
- propertyId: string | null;
517
+ programId: string | null;
272
518
  supplierId: string | null;
519
+ propertyId: string | null;
520
+ roomTypeId: string;
273
521
  name: string;
522
+ status: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
274
523
  currency: string;
275
- status: "held" | "confirmed" | "cancelled" | "expired" | "inquiry" | "released";
276
- metadata: {
277
- [x: string]: import("hono/utils/types").JSONValue;
278
- } | null;
279
- notes: string | null;
280
- roomTypeId: string;
281
- programId: string | null;
282
524
  netRateCents: number | null;
283
525
  sellRateCents: number | null;
284
526
  optionDate: string | null;
@@ -289,14 +531,20 @@ export declare const roomBlockAdminRoutes: import("hono/hono-base").HonoBase<Env
289
531
  depositTerms: {
290
532
  [x: string]: import("hono/utils/types").JSONValue;
291
533
  } | null;
534
+ notes: string | null;
535
+ metadata: {
536
+ [x: string]: import("hono/utils/types").JSONValue;
537
+ } | null;
538
+ createdAt: string;
539
+ updatedAt: string;
292
540
  };
293
541
  };
294
542
  };
295
543
  outputFormat: "json";
296
- status: import("hono/utils/http-status").ContentfulStatusCode;
544
+ status: 200;
297
545
  };
298
546
  };
299
- }, "/", "/room-blocks/:id/release">;
547
+ }, "/">;
300
548
  export type RoomBlockAdminRoutes = typeof roomBlockAdminRoutes;
301
549
  export {};
302
550
  //# sourceMappingURL=routes-room-blocks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes-room-blocks.d.ts","sourceRoot":"","sources":["../src/routes-room-blocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAmBjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCA2D7B,CAAA;AAEJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAA"}
1
+ {"version":3,"file":"routes-room-blocks.d.ts","sourceRoot":"","sources":["../src/routes-room-blocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAe,WAAW,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAA;AAG/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkBjE,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAUD;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmB1B,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;iBAWnC,CAAA;AAEF,iEAAiE;AACjE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;iBAQjC,CAAA;AAmLF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+D7B,CAAA;AAEJ,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAA"}
@@ -6,36 +6,266 @@
6
6
  * the transactional DB. See RFC voyant#1489 §4.2/§8.
7
7
  *
8
8
  * Routes stay thin: validate input, call `roomBlockService`, serialize.
9
+ *
10
+ * Contract: authored as `@hono/zod-openapi` routes (voyant#2114). The request
11
+ * bodies reuse the exported `validation-room-blocks` schemas; the response
12
+ * schemas are authored here against the service return types (room-block /
13
+ * pickup rows + the derived summary), with `Date`-origin columns serialized as
14
+ * strings per api-route-authoring §17 — the wire shape, not the Drizzle row.
9
15
  */
10
- import { parseJsonBody } from "@voyant-travel/hono";
11
- import { Hono } from "hono";
16
+ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
17
+ import { ALLOTMENT_PICKUP_STATUSES, ALLOTMENT_STATUSES } from "@voyant-travel/allotments";
18
+ import { openApiValidationHook } from "@voyant-travel/hono";
12
19
  import { createRoomBlock, getRoomBlock, pickupRoomBlock, releaseRoomBlockAtCutoff, reverseRoomBlockPickup, setRoomBlockNights, summarizeRoomBlock, } from "./service-room-blocks.js";
13
20
  import { createRoomBlockSchema, reverseRoomBlockPickupSchema, roomBlockPickupSchema, setRoomBlockNightsSchema, } from "./validation-room-blocks.js";
14
- export const roomBlockAdminRoutes = new Hono()
15
- .post("/room-blocks", async (c) => {
16
- const body = await parseJsonBody(c, createRoomBlockSchema);
17
- return c.json({ data: await createRoomBlock(c.get("db"), body) }, 201);
18
- })
19
- .get("/room-blocks/:id", async (c) => {
20
- const id = c.req.param("id");
21
+ const errorResponseSchema = z.object({ error: z.string() });
22
+ const idParamSchema = z.object({ id: z.string() });
23
+ const jsonRecordSchema = z.record(z.string(), z.unknown());
24
+ /** DERIVED at read time from per-night counters — never a stored header value. */
25
+ const PICKUP_PROGRESS_VALUES = ["none", "partial", "full"];
26
+ /**
27
+ * Wire shape of a `room_blocks` row. `optionDate`/`cutoffDate` are SQL `date`
28
+ * columns and `createdAt`/`updatedAt` are timestamps — both serialize to
29
+ * strings over the wire (§17), never `Date`.
30
+ */
31
+ export const roomBlockSchema = z.object({
32
+ id: z.string(),
33
+ programId: z.string().nullable(),
34
+ supplierId: z.string().nullable(),
35
+ propertyId: z.string().nullable(),
36
+ roomTypeId: z.string(),
37
+ name: z.string(),
38
+ status: z.enum(ALLOTMENT_STATUSES),
39
+ currency: z.string(),
40
+ netRateCents: z.number().int().nullable(),
41
+ sellRateCents: z.number().int().nullable(),
42
+ optionDate: z.string().nullable(),
43
+ cutoffDate: z.string().nullable(),
44
+ attritionTerms: jsonRecordSchema.nullable(),
45
+ depositTerms: jsonRecordSchema.nullable(),
46
+ notes: z.string().nullable(),
47
+ metadata: jsonRecordSchema.nullable(),
48
+ createdAt: z.string(),
49
+ updatedAt: z.string(),
50
+ });
51
+ /**
52
+ * Wire shape of a `room_block_pickups` ledger row. `checkIn`/`checkOut` are
53
+ * `date` columns and `pickedUpAt`/`reversedAt` are timestamps — strings over
54
+ * the wire (§17).
55
+ */
56
+ export const roomBlockPickupRowSchema = z.object({
57
+ id: z.string(),
58
+ blockId: z.string(),
59
+ bookingId: z.string().nullable(),
60
+ stayBookingItemId: z.string().nullable(),
61
+ checkIn: z.string(),
62
+ checkOut: z.string(),
63
+ rooms: z.number().int(),
64
+ status: z.enum(ALLOTMENT_PICKUP_STATUSES),
65
+ pickedUpAt: z.string(),
66
+ reversedAt: z.string().nullable(),
67
+ });
68
+ /** Derived ops view — counters + pickup progress for a block. */
69
+ export const roomBlockSummarySchema = z.object({
70
+ blockId: z.string(),
71
+ status: z.enum(ALLOTMENT_STATUSES),
72
+ totalHeld: z.number().int(),
73
+ totalPickedUp: z.number().int(),
74
+ totalReleased: z.number().int(),
75
+ totalRemaining: z.number().int(),
76
+ pickupProgress: z.enum(PICKUP_PROGRESS_VALUES),
77
+ });
78
+ const roomBlockDetailResponseSchema = z.object({
79
+ data: z.object({ block: roomBlockSchema, summary: roomBlockSummarySchema.nullable() }),
80
+ });
81
+ const roomBlockResponseSchema = z.object({ data: roomBlockSchema });
82
+ const roomBlockSummaryResponseSchema = z.object({ data: roomBlockSummarySchema.nullable() });
83
+ const roomBlockPickupResponseSchema = z.object({ data: roomBlockPickupRowSchema });
84
+ const roomBlockReleaseResponseSchema = z.object({
85
+ data: z.object({ releasedRooms: z.number().int(), block: roomBlockSchema }),
86
+ });
87
+ const nightUnavailableResponseSchema = z.object({
88
+ error: z.string(),
89
+ detail: z.object({
90
+ date: z.string(),
91
+ remaining: z.number().int(),
92
+ needed: z.number().int(),
93
+ }),
94
+ });
95
+ const createRoomBlockRoute = createRoute({
96
+ method: "post",
97
+ path: "/room-blocks",
98
+ request: {
99
+ body: {
100
+ required: true,
101
+ content: { "application/json": { schema: createRoomBlockSchema } },
102
+ },
103
+ },
104
+ responses: {
105
+ 201: {
106
+ description: "The created room block (starts in `inquiry`; nights are set separately)",
107
+ content: { "application/json": { schema: roomBlockResponseSchema } },
108
+ },
109
+ 400: {
110
+ description: "Invalid request body",
111
+ content: { "application/json": { schema: errorResponseSchema } },
112
+ },
113
+ },
114
+ });
115
+ const getRoomBlockRoute = createRoute({
116
+ method: "get",
117
+ path: "/room-blocks/{id}",
118
+ request: { params: idParamSchema },
119
+ responses: {
120
+ 200: {
121
+ description: "The room block header plus its derived pickup summary",
122
+ content: { "application/json": { schema: roomBlockDetailResponseSchema } },
123
+ },
124
+ 404: {
125
+ description: "Room block not found",
126
+ content: { "application/json": { schema: errorResponseSchema } },
127
+ },
128
+ },
129
+ });
130
+ const setRoomBlockNightsRoute = createRoute({
131
+ method: "put",
132
+ path: "/room-blocks/{id}/nights",
133
+ request: {
134
+ params: idParamSchema,
135
+ body: {
136
+ required: true,
137
+ description: "Upsert the held inventory (and optional rate overrides) per night. Only " +
138
+ "`roomsHeld` and the rate overrides are caller-controlled; the pickup / " +
139
+ "release counters are owned by the lifecycle actions and never set here.",
140
+ content: { "application/json": { schema: setRoomBlockNightsSchema } },
141
+ },
142
+ },
143
+ responses: {
144
+ 200: {
145
+ description: "The recomputed block summary after the nights upsert",
146
+ content: { "application/json": { schema: roomBlockSummaryResponseSchema } },
147
+ },
148
+ 400: {
149
+ description: "Invalid request body",
150
+ content: { "application/json": { schema: errorResponseSchema } },
151
+ },
152
+ 404: {
153
+ description: "Room block not found",
154
+ content: { "application/json": { schema: errorResponseSchema } },
155
+ },
156
+ },
157
+ });
158
+ const pickupRoomBlockRoute = createRoute({
159
+ method: "post",
160
+ path: "/room-blocks/{id}/pickups",
161
+ request: {
162
+ params: idParamSchema,
163
+ body: {
164
+ required: true,
165
+ description: "Record a pickup against the block. Idempotent on `stayBookingItemId`: " +
166
+ "re-processing the same stay item returns the existing active pickup with " +
167
+ "a 200 instead of a 201.",
168
+ content: { "application/json": { schema: roomBlockPickupSchema } },
169
+ },
170
+ },
171
+ responses: {
172
+ 200: {
173
+ description: "An existing active pickup (idempotent replay on `stayBookingItemId`)",
174
+ content: { "application/json": { schema: roomBlockPickupResponseSchema } },
175
+ },
176
+ 201: {
177
+ description: "The newly recorded pickup",
178
+ content: { "application/json": { schema: roomBlockPickupResponseSchema } },
179
+ },
180
+ 400: {
181
+ description: "Invalid check-in/check-out range",
182
+ content: { "application/json": { schema: errorResponseSchema } },
183
+ },
184
+ 404: {
185
+ description: "Room block not found",
186
+ content: { "application/json": { schema: errorResponseSchema } },
187
+ },
188
+ 409: {
189
+ description: "The block is no longer accepting pickups, or a night has insufficient " +
190
+ "inventory (the latter carries a `detail` with the short night)",
191
+ content: {
192
+ "application/json": {
193
+ schema: z.union([errorResponseSchema, nightUnavailableResponseSchema]),
194
+ },
195
+ },
196
+ },
197
+ },
198
+ });
199
+ const reverseRoomBlockPickupRoute = createRoute({
200
+ method: "post",
201
+ path: "/room-blocks/{id}/pickups/reverse",
202
+ request: {
203
+ params: idParamSchema,
204
+ body: {
205
+ required: true,
206
+ description: "Reverse a pickup (booking cancelled / rooms reduced). Exactly one of " +
207
+ "`pickupId` or `stayBookingItemId` is required; the body schema enforces " +
208
+ "this, so a request with neither (or both) fails request validation with a " +
209
+ "400 `invalid_request`. A well-formed request whose pickup isn't found (or " +
210
+ "belongs to a different block) returns 404.",
211
+ content: { "application/json": { schema: reverseRoomBlockPickupSchema } },
212
+ },
213
+ },
214
+ responses: {
215
+ 200: {
216
+ description: "The reversed pickup ledger row",
217
+ content: { "application/json": { schema: roomBlockPickupResponseSchema } },
218
+ },
219
+ 400: {
220
+ description: "Invalid body — neither (or both) of `pickupId`/`stayBookingItemId` supplied",
221
+ content: { "application/json": { schema: errorResponseSchema } },
222
+ },
223
+ 404: {
224
+ description: "Active pickup not found (or it belongs to a different block)",
225
+ content: { "application/json": { schema: errorResponseSchema } },
226
+ },
227
+ },
228
+ });
229
+ const releaseRoomBlockRoute = createRoute({
230
+ method: "post",
231
+ path: "/room-blocks/{id}/release",
232
+ request: { params: idParamSchema },
233
+ responses: {
234
+ 200: {
235
+ description: "Unpicked rooms released back to the property; the block moves to `released`",
236
+ content: { "application/json": { schema: roomBlockReleaseResponseSchema } },
237
+ },
238
+ 404: {
239
+ description: "Room block not found",
240
+ content: { "application/json": { schema: errorResponseSchema } },
241
+ },
242
+ },
243
+ });
244
+ export const roomBlockAdminRoutes = new OpenAPIHono({ defaultHook: openApiValidationHook })
245
+ .openapi(createRoomBlockRoute, async (c) => c.json({ data: await createRoomBlock(c.get("db"), c.req.valid("json")) }, 201))
246
+ .openapi(getRoomBlockRoute, async (c) => {
247
+ const id = c.req.valid("param").id;
21
248
  const block = await getRoomBlock(c.get("db"), id);
22
249
  if (!block)
23
250
  return c.json({ error: "Room block not found" }, 404);
24
251
  const summary = await summarizeRoomBlock(c.get("db"), id);
25
- return c.json({ data: { block, summary } });
252
+ return c.json({ data: { block, summary } }, 200);
26
253
  })
27
- .put("/room-blocks/:id/nights", async (c) => {
28
- const id = c.req.param("id");
254
+ .openapi(setRoomBlockNightsRoute, async (c) => {
255
+ const id = c.req.valid("param").id;
29
256
  const block = await getRoomBlock(c.get("db"), id);
30
257
  if (!block)
31
258
  return c.json({ error: "Room block not found" }, 404);
32
- const { nights } = await parseJsonBody(c, setRoomBlockNightsSchema);
259
+ const { nights } = c.req.valid("json");
33
260
  await setRoomBlockNights(c.get("db"), id, nights);
34
- return c.json({ data: await summarizeRoomBlock(c.get("db"), id) });
261
+ return c.json({ data: await summarizeRoomBlock(c.get("db"), id) }, 200);
35
262
  })
36
- .post("/room-blocks/:id/pickups", async (c) => {
37
- const body = await parseJsonBody(c, roomBlockPickupSchema);
38
- const outcome = await pickupRoomBlock(c.get("db"), { blockId: c.req.param("id"), ...body });
263
+ .openapi(pickupRoomBlockRoute, async (c) => {
264
+ const body = c.req.valid("json");
265
+ const outcome = await pickupRoomBlock(c.get("db"), {
266
+ blockId: c.req.valid("param").id,
267
+ ...body,
268
+ });
39
269
  switch (outcome.status) {
40
270
  case "ok":
41
271
  return c.json({ data: outcome.pickup }, outcome.idempotent ? 200 : 201);
@@ -52,21 +282,23 @@ export const roomBlockAdminRoutes = new Hono()
52
282
  }, 409);
53
283
  }
54
284
  })
55
- .post("/room-blocks/:id/pickups/reverse", async (c) => {
56
- const body = await parseJsonBody(c, reverseRoomBlockPickupSchema);
285
+ .openapi(reverseRoomBlockPickupRoute, async (c) => {
286
+ const body = c.req.valid("json");
57
287
  const outcome = await reverseRoomBlockPickup(c.get("db"), {
58
- blockId: c.req.param("id"),
288
+ blockId: c.req.valid("param").id,
59
289
  ...body,
60
290
  });
61
291
  if (outcome.status === "pickup_not_found") {
62
292
  return c.json({ error: "Active pickup not found" }, 404);
63
293
  }
64
- return c.json({ data: outcome.pickup });
294
+ return c.json({ data: outcome.pickup }, 200);
65
295
  })
66
- .post("/room-blocks/:id/release", async (c) => {
67
- const outcome = await releaseRoomBlockAtCutoff(c.get("db"), { blockId: c.req.param("id") });
296
+ .openapi(releaseRoomBlockRoute, async (c) => {
297
+ const outcome = await releaseRoomBlockAtCutoff(c.get("db"), {
298
+ blockId: c.req.valid("param").id,
299
+ });
68
300
  if (outcome.status === "block_not_found") {
69
301
  return c.json({ error: "Room block not found" }, 404);
70
302
  }
71
- return c.json({ data: { releasedRooms: outcome.releasedRooms, block: outcome.block } });
303
+ return c.json({ data: { releasedRooms: outcome.releasedRooms, block: outcome.block } }, 200);
72
304
  });
@@ -279,7 +279,7 @@ export declare const stayBookingItems: import("drizzle-orm/pg-core").PgTableWith
279
279
  tableName: "stay_booking_items";
280
280
  dataType: "string";
281
281
  columnType: "PgEnumColumn";
282
- data: "cancelled" | "no_show" | "reserved";
282
+ data: "reserved" | "cancelled" | "no_show";
283
283
  driverParam: string;
284
284
  notNull: true;
285
285
  hasDefault: true;
@@ -109,7 +109,7 @@ export declare const roomTypes: import("drizzle-orm/pg-core").PgTableWithColumns
109
109
  tableName: "room_types";
110
110
  dataType: "string";
111
111
  columnType: "PgEnumColumn";
112
- data: "virtual" | "pooled" | "serialized";
112
+ data: "pooled" | "serialized" | "virtual";
113
113
  driverParam: string;
114
114
  notNull: true;
115
115
  hasDefault: true;
@@ -1018,7 +1018,7 @@ export declare const ratePlans: import("drizzle-orm/pg-core").PgTableWithColumns
1018
1018
  tableName: "rate_plans";
1019
1019
  dataType: "string";
1020
1020
  columnType: "PgEnumColumn";
1021
- data: "none" | "on_request" | "deposit" | "card_hold" | "full_prepay";
1021
+ data: "none" | "card_hold" | "deposit" | "full_prepay" | "on_request";
1022
1022
  driverParam: string;
1023
1023
  notNull: true;
1024
1024
  hasDefault: true;
@@ -121,7 +121,7 @@ export declare const roomBlocks: import("drizzle-orm/pg-core").PgTableWithColumn
121
121
  tableName: "room_blocks";
122
122
  dataType: "string";
123
123
  columnType: "PgEnumColumn";
124
- data: "held" | "confirmed" | "cancelled" | "expired" | "inquiry" | "released";
124
+ data: "inquiry" | "cancelled" | "held" | "confirmed" | "released" | "expired";
125
125
  driverParam: string;
126
126
  notNull: true;
127
127
  hasDefault: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/accommodations",
3
- "version": "0.108.2",
3
+ "version": "0.108.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -61,17 +61,18 @@
61
61
  }
62
62
  },
63
63
  "dependencies": {
64
+ "@hono/zod-openapi": "^1.4.0",
64
65
  "drizzle-orm": "^0.45.2",
65
66
  "hono": "^4.12.10",
66
67
  "zod": "^4.3.6",
67
68
  "@voyant-travel/accommodations-contracts": "^0.105.3",
68
69
  "@voyant-travel/allotments": "^0.2.0",
69
- "@voyant-travel/bookings": "^0.137.0",
70
- "@voyant-travel/catalog": "^0.135.0",
70
+ "@voyant-travel/bookings": "^0.137.1",
71
+ "@voyant-travel/catalog": "^0.135.1",
71
72
  "@voyant-travel/core": "^0.111.0",
72
73
  "@voyant-travel/db": "^0.109.4",
73
- "@voyant-travel/hono": "^0.117.0",
74
- "@voyant-travel/operations": "^0.5.2"
74
+ "@voyant-travel/hono": "^0.118.0",
75
+ "@voyant-travel/operations": "^0.5.3"
75
76
  },
76
77
  "devDependencies": {
77
78
  "drizzle-kit": "^0.31.10",
@@ -101,10 +102,10 @@
101
102
  ]
102
103
  },
103
104
  "scripts": {
104
- "typecheck": "tsc --noEmit",
105
+ "typecheck": "tsc -p tsconfig.typecheck.json",
105
106
  "lint": "biome check src/",
106
107
  "test": "vitest run",
107
- "build": "tsc -p tsconfig.json",
108
+ "build": "tsc -p tsconfig.build.json",
108
109
  "clean": "rm -rf dist tsconfig.tsbuildinfo",
109
110
  "db:generate": "drizzle-kit generate --config=drizzle.migrations.config.ts --name=accommodations_baseline && node ../../scripts/migrations/guard-create-type.mjs ./migrations && node ../../scripts/migrations/ensure-extensions.mjs ./migrations"
110
111
  },