fix(dashboard): leave planned public transit out of upcoming reservations

Journeys from the transit search are routing, not bookings, and a day of them
pushed the real bookings out of the six slots. Booked trains and buses keep
their own types and stay. The MCP tool reads the same feed.
This commit is contained in:
Maurice
2026-10-14 10:45:24 +02:00
committed by Maurice
parent 76b7b305b2
commit 775b35d5af
8 changed files with 37 additions and 5 deletions
@@ -1084,7 +1084,7 @@ export class ReservationsRepository extends TrekRepository<Reservations> {
.as('at_time'),
])
.where('r.status', '!=', 'cancelled')
.where((eb) => eb(eb.fn.coalesce(eb.ref('r.type'), eb.val('')), '!=', 'hotel'))
.where((eb) => eb(eb.fn.coalesce(eb.ref('r.type'), eb.val('')), 'not in', ['hotel', 'transit']))
.$castTo<EntriesRow>();
const checkins = qb
@@ -607,7 +607,7 @@ export class ReservationsMcp {
@Tool({
name: 'list_upcoming_reservations',
description: 'The next bookings across ALL of the user\'s trips, soonest first: "what is coming up?", "when is my next flight?". Prefer this over get_trip_summary when no particular trip is in question, or when the trip is unknown. Hotel stays appear as their check-in and check-out moments (the stay itself covers a range and is not a point in time), which no per-trip reservation list reports. Cancelled bookings and archived trips are left out.',
description: 'The next bookings across ALL of the user\'s trips, soonest first: "what is coming up?", "when is my next flight?". Prefer this over get_trip_summary when no particular trip is in question, or when the trip is unknown. Hotel stays appear as their check-in and check-out moments (the stay itself covers a range and is not a point in time), which no per-trip reservation list reports. Cancelled bookings, planned public-transit journeys and archived trips are left out.',
inputSchema: {
limit: z.number().int().min(1).max(50).optional().describe('How many entries to return, soonest first (default 6, the dashboard widget\'s size)'),
},
@@ -423,7 +423,7 @@ export class ReservationsService {
* panel's auto-created booking, stamps the start day into reservation_time
* once and never restamps it, so moving the stay left the widget showing the
* old date. Both halves of the report come from reading a hotel's date off
* fields hotels do not use.
* fields hotels do not use. Planned transit journeys (type 'transit') are routing, not bookings.
*
* RS20 — Task 4 (calendar + listUpcoming + the visibility predicate
* consumers). Ruling (ii): ONE fully typed Kysely statement, in
+11
View File
@@ -173,6 +173,17 @@ describe('Reservations + accommodations e2e (real auth guard + temp SQLite, real
expect(titles).not.toContain('Stay');
});
it('200 upcoming feed leaves planned public transit out and keeps booked trains and buses', async () => {
await seedReservation(tripId, 'Metro leg', 'transit', { reservation_time: '2998-12-30T08:00:00' });
await seedReservation(tripId, 'Booked train', 'train', { reservation_time: '2998-12-30T09:00:00' });
await seedReservation(tripId, 'Booked bus', 'bus', { reservation_time: '2998-12-30T10:00:00' });
const res = await request(server).get('/api/reservations/upcoming').set('Cookie', sessionCookie(1));
expect(res.status).toBe(200);
const titles = res.body.reservations.map((r: { title: string }) => r.title);
expect(titles).toEqual(expect.arrayContaining(['Booked train', 'Booked bus']));
expect(titles).not.toContain('Metro leg');
});
it('404 when trip not accessible (reservations)', async () => {
// Plan 3c Task 0b: TripAccessGuard reads TripsRepository.findAccessible
// directly now, a real query — `canAccessTrip.mockReturnValue(...)` no
@@ -312,6 +312,7 @@ const LEGACY_LIST_UPCOMING = `
LEFT JOIN places p ON r.place_id = p.id
WHERE r.status != 'cancelled'
AND COALESCE(r.type, '') != 'hotel'
AND COALESCE(r.type, '') != 'transit'
UNION ALL
@@ -397,6 +398,11 @@ describe('ReservationsRepository — RS20 (listUpcomingForUser)', () => {
await setReservation(cancelled.id, { reservation_time: '2999-01-03T10:00:00', status: 'cancelled' });
const hotelDated = createReservation(testDb, tripOwned.id, { title: 'Hotel dated', type: 'hotel' });
await setReservation(hotelDated.id, { reservation_time: '2999-01-03T10:00:00' });
// A planned public-transit journey is routing, not a booking; a booked train is one.
const transit = createReservation(testDb, tripOwned.id, { title: 'Metro A to B', type: 'transit' });
await setReservation(transit.id, { reservation_time: '2999-01-03T11:00:00', status: 'confirmed' });
const train = createReservation(testDb, tripOwned.id, { title: 'Booked train', type: 'train' });
await setReservation(train.id, { reservation_time: '2999-01-03T12:00:00', status: 'confirmed' });
// A NULL-typed booking still shows.
const untyped = createReservation(testDb, tripOwned.id, { title: 'Untyped' });
@@ -456,6 +462,8 @@ describe('ReservationsRepository — RS20 (listUpcomingForUser)', () => {
// linked "<id>.0" booking.
const baseline = await reservationsRepo.listUpcomingForUser(owner.id, '2000-01-01', '2000-01-01T00:00', 20);
expect(baseline.map((r) => r.title)).not.toContain('Hidden');
expect(baseline.map((r) => r.title)).not.toContain('Metro A to B');
expect(baseline.map((r) => r.title)).toContain('Booked train');
const tieIndex = baseline.findIndex((r) => r.title === 'Tie 1');
expect(baseline[tieIndex + 1]?.title).toBe('Tie 2');
expect(baseline.some((r) => r.title === 'Hotel Ibis')).toBe(true);
@@ -1067,6 +1067,19 @@ describe('Tool: list_upcoming_reservations', () => {
expect(rows.map((r) => r.title)).toEqual(['Still on']);
});
it('leaves out planned public-transit journeys and keeps a booked train, as the dashboard does', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
await seedBooking(trip.id, 'Metro to the museum', `${dateInDays(1)}T09:00:00`, {
type: 'transit',
status: 'confirmed',
});
await seedBooking(trip.id, 'Night train', `${dateInDays(2)}T21:00:00`, { type: 'train', status: 'confirmed' });
const rows = await upcoming(user.id);
expect(rows.map((r) => r.title)).toEqual(['Night train']);
});
it('returns the dashboard six by default and honours a limit', async () => {
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
+1 -1
View File
@@ -58,7 +58,7 @@ Clocks refresh every 30 seconds and are always shown in 24-hour time — the 12-
## Upcoming reservations
**Upcoming reservations** lists the next six bookings across all your trips that are not archived, soonest first. A booking counts when its time is still ahead, or, without a time, when its day is today or later; cancelled bookings are left out. A hotel stay is not listed as a whole: its **Check-in** and **Check-out** appear as two entries of their own. Each row shows the date, the time in your 12 or 24 hour format, the title with a **Pending** tag while the booking is not confirmed, and the booking's type as an icon. Click a row to open its trip. With nothing ahead the widget reads *Nothing booked yet.*
**Upcoming reservations** lists the next six bookings across all your trips that are not archived, soonest first. A booking counts when its time is still ahead, or, without a time, when its day is today or later; cancelled bookings are left out. A hotel stay is not listed as a whole: its **Check-in** and **Check-out** appear as two entries of their own. Public-transit connections planned with the transit search are not listed either; a train or bus you booked is. Each row shows the date, the time in your 12 or 24 hour format, the title with a **Pending** tag while the booking is not confirmed, and the booking's type as an icon. Click a row to open its trip. With nothing ahead the widget reads *Nothing booked yet.*
---
+1 -1
View File
@@ -147,7 +147,7 @@ Requires `reservations:read` or `reservations:write` scope.
| `reorder_reservations` | Reorder reservations within a day. |
| `set_reservation_travelers` | Set who is travelling on a booking, from the trip roster (members and guests). Replaces the list; an empty array clears it. Ids that are not on the trip come back under `ignored_user_ids` rather than being attached. |
| `link_hotel_accommodation` | Set or update a hotel reservation's check-in/out day links and place. |
| `list_upcoming_reservations` | The next bookings across all of the user's trips, soonest first, for "what is coming up?" when no particular trip is in question. Hotel stays appear as their check-in and check-out moments. Cancelled bookings and archived trips are left out. `limit` defaults to 6, at most 50. Requires `reservations:read`. |
| `list_upcoming_reservations` | The next bookings across all of the user's trips, soonest first, for "what is coming up?" when no particular trip is in question. Hotel stays appear as their check-in and check-out moments. Cancelled bookings, planned public-transit journeys and archived trips are left out. `limit` defaults to 6, at most 50. Requires `reservations:read`. |
### Budget