fix(feeds): keep a staged booking out of the public feed and the shared trip

The ICS feed is public with a token that never expires and prints the
confirmation number into the DESCRIPTION, and the shared-trip payload does a
SELECT * over reservations with share_bookings on by default. Neither filters
anything, so whatever lands on a trip is published the moment it is written.
That is fine while a person types every booking in, and it stops being fine as
soon as something writes on their behalf.

reservations.ingest_state is that gate. It defaults to live, SQLite backfills
every existing row on the ALTER, and no current writer names the column, so
every booking visible today stays visible. Nothing writes staged yet.

The predicate takes its table alias because share.service.ts queried
reservations without one. A stay is withheld only when bookings point at it and
all of them are staged: a hand-added hotel has no booking at all and has to keep
showing up.
This commit is contained in:
Maurice
2026-08-28 13:23:07 +02:00
committed by Maurice
parent 8641788de0
commit 15588d20c9
12 changed files with 389 additions and 8 deletions
@@ -153,6 +153,9 @@ export const reservationSchema = z.object({
accommodation_id: z.union([z.number(), z.string()]).nullable().optional(),
metadata: z.string().nullable().optional(),
needs_review: z.number().optional(),
// 'live' for anything a person put on the trip, 'staged' while an automated
// ingest waits for confirmation. Only the anonymous exports filter on it.
ingest_state: z.string().optional(),
day_plan_position: z.number().nullable().optional(),
created_at: z.string().optional(),
// AirTrail (or future provider) linkage — drives the "synced" badge (#214).
@@ -309,6 +312,12 @@ const bookingImportAccommodationSchema = z.object({
export const bookingImportPreviewItemSchema = z.object({
type: z.string(),
/**
* The extractor could not read a type and filled one in, so the import UI
* should offer the form the user was importing into rather than trusting the
* placeholder (#2076).
*/
type_guessed: z.boolean().optional(),
title: z.string().min(1),
reservation_time: z.string().nullable().optional(),
reservation_end_time: z.string().nullable().optional(),