feat(api): expose shortlisted places, detached bookings and the bucket list

Reading the surface back against real data turned up three things it dropped
without saying so.

Places with no day were invisible, because the payload only reached places
through day_assignments. On a live instance that is half of them, and they
are the ones carrying a coordinate and no claim about when — exactly what an
integration can act on. They come back under `unplanned_places`, minus the
hotel, which is already reported as an accommodation.

Bookings with no day were dropped by the day filter. reservations.day_id is
nullable and deleting a day detaches its bookings rather than removing them,
so this is not a hypothetical.

The bucket list had no endpoint at all. It hangs off the user rather than a
trip, so it gets its own, and it answers whether or not the Atlas addon is
switched on: the addon decides what TREK shows, not what a key can read.

Also fixes ?include=notes returning the bare trip: places, notes and
reservations are reported on days, so asking for one now implies days
instead of quietly answering with nothing.

The module stays a leaf. Reaching for AtlasService pulled AuthModule and the
storage registry in behind it and broke the surface's own boot, so the one
SELECT lives here like every other query in the file.
This commit is contained in:
Maurice
2026-08-28 02:12:37 +02:00
committed by Maurice
parent d559fdda1d
commit 1e1a80c199
8 changed files with 341 additions and 24 deletions
@@ -106,6 +106,32 @@ export const publicApiTravellerSchema = z.object({
});
export type PublicApiTraveller = z.infer<typeof publicApiTravellerSchema>;
/**
* An entry on the traveller's bucket list: somewhere they want to go, with no
* trip attached yet.
*
* Lives on the user rather than on a trip, which is why it has its own endpoint
* rather than an `include`. It is the one part of TREK that is explicitly about
* places not yet visited, so a consumer that knows where someone actually went
* can tell them they got there.
*/
export const publicApiBucketListItemSchema = z.object({
name: z.string(),
lat: z.number().nullable(),
lng: z.number().nullable(),
/** ISO 3166-1 alpha-2, when the entry is pinned to a country. */
country_code: z.string().nullable(),
notes: z.string().nullable(),
/** An optional date the traveller is aiming for. Not a booking. */
target_date: z.string().nullable(),
});
export type PublicApiBucketListItem = z.infer<typeof publicApiBucketListItemSchema>;
export const publicApiBucketListSchema = z.object({
items: z.array(publicApiBucketListItemSchema),
});
export type PublicApiBucketList = z.infer<typeof publicApiBucketListSchema>;
/** Trip without its itinerary — what the list endpoint returns. */
export const publicApiTripSummarySchema = z.object({
id: z.number(),
@@ -136,6 +162,24 @@ export const publicApiTripSchema = publicApiTripSummarySchema.extend({
days: z.array(publicApiDaySchema).optional(),
accommodations: z.array(publicApiAccommodationSchema).optional(),
travellers: z.array(publicApiTravellerSchema).optional(),
/**
* Places on the trip that sit on no day yet — the shortlist a traveller
* collects before deciding when to go where.
*
* Comes with `include=places`, because a section called "places" that silently
* omitted half of them would be worse than one that did not exist. On a real
* instance these are routinely half of a trip's places, and they carry the
* coordinates that make them worth matching against.
*/
unplanned_places: z.array(publicApiPlaceSchema).optional(),
/**
* Bookings not pinned to a day. A reservation keeps its `day_id` as a nullable
* reference, and deleting a day sets it null rather than deleting the booking,
* so these exist in the wild and are not an edge case.
*
* Comes with `include=reservations`, for the same reason as above.
*/
unscheduled_reservations: z.array(publicApiReservationSchema).optional(),
});
export type PublicApiTrip = z.infer<typeof publicApiTripSchema>;