mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
d6ed7a60a0
Bookings get a first-class url column (migration) instead of users pasting links into notes. It's editable in the booking modal and rendered as a clickable link on the reservation card. The reservation request schemas are open passthroughs, so only the entity schema + service SQL enumerate it.
221 lines
9.3 KiB
TypeScript
221 lines
9.3 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* Reservation + accommodation API contract — single source of truth for the
|
|
* /api/trips/:tripId/reservations and /api/trips/:tripId/accommodations endpoints.
|
|
*
|
|
* Trip-scoped. Reservations use the 'reservation_edit' permission; accommodations
|
|
* use 'day_edit' (they live in the day/accommodation service). The legacy routes
|
|
* (server/src/routes/reservations.ts + the accommodations sub-router in
|
|
* routes/days.ts) carry several side effects — auto-creating/updating/deleting a
|
|
* linked budget item, accommodation broadcasts and booking notifications — which
|
|
* the Nest service reproduces 1:1. Reservation bodies are wide and provider-ish,
|
|
* so the create/update payloads stay mostly open with `title` pinned.
|
|
*/
|
|
|
|
const open = z.record(z.string(), z.unknown());
|
|
|
|
/**
|
|
* A reservation endpoint (flight/train leg terminal) — row of the
|
|
* reservation_endpoints table (server/src/services/reservationService.ts).
|
|
*/
|
|
export const reservationEndpointSchema = z.object({
|
|
id: z.number().optional(),
|
|
reservation_id: z.number().optional(),
|
|
role: z.enum(['from', 'to', 'stop']),
|
|
sequence: z.number(),
|
|
name: z.string(),
|
|
code: z.string().nullable(),
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
timezone: z.string().nullable(),
|
|
local_time: z.string().nullable(),
|
|
local_date: z.string().nullable(),
|
|
});
|
|
export type ReservationEndpoint = z.infer<typeof reservationEndpointSchema>;
|
|
|
|
/**
|
|
* Reservation entity as returned by the reservation list endpoint
|
|
* (server/src/services/reservationService.ts -> listReservations). Columns of
|
|
* the `reservations` table plus the joined day_number / place_name / linked
|
|
* accommodation fields and the computed `day_positions` + `endpoints`.
|
|
* `accommodation_id` is stored as TEXT in the DB.
|
|
*/
|
|
export const reservationSchema = z.object({
|
|
id: z.number(),
|
|
trip_id: z.number(),
|
|
day_id: z.number().nullable().optional(),
|
|
end_day_id: z.number().nullable().optional(),
|
|
place_id: z.number().nullable().optional(),
|
|
assignment_id: z.number().nullable().optional(),
|
|
title: z.string(),
|
|
reservation_time: z.string().nullable().optional(),
|
|
reservation_end_time: z.string().nullable().optional(),
|
|
location: z.string().nullable().optional(),
|
|
confirmation_number: z.string().nullable().optional(),
|
|
notes: z.string().nullable().optional(),
|
|
url: z.string().nullable().optional(),
|
|
status: z.string(),
|
|
type: z.string(),
|
|
accommodation_id: z.union([z.number(), z.string()]).nullable().optional(),
|
|
metadata: z.string().nullable().optional(),
|
|
needs_review: z.number().optional(),
|
|
day_plan_position: z.number().nullable().optional(),
|
|
created_at: z.string().optional(),
|
|
// AirTrail (or future provider) linkage — drives the "synced" badge (#214).
|
|
external_source: z.string().nullable().optional(),
|
|
external_id: z.string().nullable().optional(),
|
|
external_owner_user_id: z.number().nullable().optional(),
|
|
external_synced_at: z.string().nullable().optional(),
|
|
sync_enabled: z.number().nullable().optional(),
|
|
// joined / computed in listReservations
|
|
day_number: z.number().nullable().optional(),
|
|
place_name: z.string().nullable().optional(),
|
|
accommodation_place_id: z.number().nullable().optional(),
|
|
accommodation_name: z.string().nullable().optional(),
|
|
accommodation_start_day_id: z.number().nullable().optional(),
|
|
accommodation_end_day_id: z.number().nullable().optional(),
|
|
day_positions: z.record(z.string(), z.number()).nullable().optional(),
|
|
endpoints: z.array(reservationEndpointSchema).optional(),
|
|
});
|
|
export type Reservation = z.infer<typeof reservationSchema>;
|
|
|
|
/**
|
|
* Accommodation entity as returned by listAccommodations / getAccommodationWithPlace
|
|
* (server/src/services/dayService.ts). Columns of the day_accommodations table
|
|
* plus the joined place fields and (on list) the linked reservation_title.
|
|
*/
|
|
export const accommodationSchema = z.object({
|
|
id: z.number(),
|
|
trip_id: z.number(),
|
|
place_id: z.number().nullable().optional(),
|
|
start_day_id: z.number(),
|
|
end_day_id: z.number(),
|
|
check_in: z.string().nullable().optional(),
|
|
check_in_end: z.string().nullable().optional(),
|
|
check_out: z.string().nullable().optional(),
|
|
confirmation: z.string().nullable().optional(),
|
|
notes: z.string().nullable().optional(),
|
|
created_at: z.string().optional(),
|
|
// joined in listAccommodations / getAccommodationWithPlace
|
|
place_name: z.string().nullable().optional(),
|
|
place_address: z.string().nullable().optional(),
|
|
place_image: z.string().nullable().optional(),
|
|
place_lat: z.number().nullable().optional(),
|
|
place_lng: z.number().nullable().optional(),
|
|
reservation_title: z.string().nullable().optional(),
|
|
});
|
|
export type Accommodation = z.infer<typeof accommodationSchema>;
|
|
|
|
/** Reservation create: title is required; the many optional fields stay open. */
|
|
export const reservationCreateRequestSchema = open.and(z.object({ title: z.string().min(1) }));
|
|
export type ReservationCreateRequest = z.infer<typeof reservationCreateRequestSchema>;
|
|
|
|
export const reservationUpdateRequestSchema = open;
|
|
export type ReservationUpdateRequest = z.infer<typeof reservationUpdateRequestSchema>;
|
|
|
|
export const reservationPositionsRequestSchema = z.object({
|
|
positions: z.array(z.object({ id: z.number(), day_plan_position: z.number() })),
|
|
day_id: z.union([z.number(), z.string()]).nullable().optional(),
|
|
});
|
|
export type ReservationPositionsRequest = z.infer<typeof reservationPositionsRequestSchema>;
|
|
|
|
export const accommodationCreateRequestSchema = z.object({
|
|
place_id: z.union([z.number(), z.string()]),
|
|
start_day_id: z.union([z.number(), z.string()]),
|
|
end_day_id: z.union([z.number(), z.string()]),
|
|
check_in: z.string().nullable().optional(),
|
|
check_in_end: z.string().nullable().optional(),
|
|
check_out: z.string().nullable().optional(),
|
|
confirmation: z.string().nullable().optional(),
|
|
notes: z.string().nullable().optional(),
|
|
});
|
|
export type AccommodationCreateRequest = z.infer<typeof accommodationCreateRequestSchema>;
|
|
|
|
export const accommodationUpdateRequestSchema = open;
|
|
export type AccommodationUpdateRequest = z.infer<typeof accommodationUpdateRequestSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Booking import (KItinerary)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const bookingImportEndpointSchema = z.object({
|
|
role: z.enum(['from', 'to', 'stop']),
|
|
sequence: z.number(),
|
|
name: z.string(),
|
|
code: z.string().nullable(),
|
|
// Nullable: the mapper emits named endpoints without coords; confirm() geocodes
|
|
// them, and only the coord'd ones are persisted.
|
|
lat: z.number().nullable(),
|
|
lng: z.number().nullable(),
|
|
timezone: z.string().nullable(),
|
|
local_time: z.string().nullable(),
|
|
local_date: z.string().nullable(),
|
|
});
|
|
|
|
const bookingImportVenueSchema = z.object({
|
|
name: z.string(),
|
|
lat: z.number().optional(),
|
|
lng: z.number().optional(),
|
|
address: z.string().optional(),
|
|
website: z.string().optional(),
|
|
phone: z.string().optional(),
|
|
});
|
|
|
|
const bookingImportAccommodationSchema = z.object({
|
|
check_in: z.string().optional(),
|
|
check_out: z.string().optional(),
|
|
confirmation: z.string().optional(),
|
|
});
|
|
|
|
export const bookingImportPreviewItemSchema = z.object({
|
|
type: z.string(),
|
|
title: z.string().min(1),
|
|
reservation_time: z.string().nullable().optional(),
|
|
reservation_end_time: z.string().nullable().optional(),
|
|
confirmation_number: z.string().nullable().optional(),
|
|
location: z.string().nullable().optional(),
|
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
endpoints: z.array(bookingImportEndpointSchema).optional(),
|
|
needs_review: z.boolean().optional(),
|
|
_venue: bookingImportVenueSchema.optional(),
|
|
_accommodation: bookingImportAccommodationSchema.optional(),
|
|
source: z.object({ fileName: z.string(), index: z.number() }),
|
|
});
|
|
export type BookingImportPreviewItem = z.infer<typeof bookingImportPreviewItemSchema>;
|
|
|
|
/**
|
|
* How the preview endpoint should treat the LLM fallback:
|
|
* - `no-ai` — kitinerary only (default; existing behaviour)
|
|
* - `fallback-on-empty` — run the LLM for files kitinerary returns nothing for
|
|
* - `force-ai` — run the LLM on every submitted file (skip kitinerary)
|
|
*/
|
|
export const bookingImportModeSchema = z.enum(['no-ai', 'fallback-on-empty', 'force-ai']);
|
|
export type BookingImportMode = z.infer<typeof bookingImportModeSchema>;
|
|
|
|
/** Per-file AI report so the preview UI can offer "Try AI parsing" only where it applies. */
|
|
export const bookingImportFileReportSchema = z.object({
|
|
fileName: z.string(),
|
|
aiAvailable: z.boolean(),
|
|
aiUsed: z.boolean(),
|
|
});
|
|
export type BookingImportFileReport = z.infer<typeof bookingImportFileReportSchema>;
|
|
|
|
export const bookingImportPreviewResponseSchema = z.object({
|
|
items: z.array(bookingImportPreviewItemSchema),
|
|
warnings: z.array(z.string()),
|
|
// Optional so existing/no-AI responses stay byte-compatible.
|
|
files: z.array(bookingImportFileReportSchema).optional(),
|
|
});
|
|
export type BookingImportPreviewResponse = z.infer<typeof bookingImportPreviewResponseSchema>;
|
|
|
|
export const bookingImportConfirmRequestSchema = z.object({
|
|
items: z.array(bookingImportPreviewItemSchema).min(1),
|
|
});
|
|
export type BookingImportConfirmRequest = z.infer<typeof bookingImportConfirmRequestSchema>;
|
|
|
|
export const bookingImportConfirmResponseSchema = z.object({
|
|
created: z.array(reservationSchema),
|
|
});
|
|
export type BookingImportConfirmResponse = z.infer<typeof bookingImportConfirmResponseSchema>;
|