mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(dashboard): keep hotels out of the upcoming reservations widget
A stay covers a range rather than a moment, so one week in a hotel would hold a slot in a six-entry widget for the whole week and push out the bookings that actually happen on a day. The accommodation belongs to the day plan, which shows it across its span. They never worked here either, which is what the report is about. A hotel keeps its dates on the linked stay, not on the reservation, so the two ways one can exist behaved differently: The booking form writes reservation_time = NULL and sends no day_id, so a form-created hotel matched neither arm of the date test and was silently absent. That is the "incomplete" half. The accommodation panel auto-creates its booking with day_id and with the start day's date stamped into reservation_time. That row did show, but updateAccommodation syncs only metadata and the confirmation number to its linked bookings, so moving the stay left the stamp untouched and the widget kept the old date. That is the "no effect when I change the date" half. The type test is NULL-safe: the column defaults to 'other' but nothing stops a NULL, and a bare `type != 'hotel'` evaluates to NULL there and would have dropped those rows from the widget instead. Closes #1934
This commit is contained in:
@@ -384,6 +384,20 @@ export class ReservationsService {
|
||||
* as upcoming when its own time is in the future, or — for timeless entries —
|
||||
* when its day falls on or after today. Cancelled bookings are skipped.
|
||||
* The default limit (6) matches the legacy inline handler.
|
||||
*
|
||||
* Hotels are left out on purpose (#1934). A stay covers a range rather than a
|
||||
* moment, so a week in one hotel would hold a slot in a six-entry widget for
|
||||
* the whole week and push out the bookings that actually happen on a day. The
|
||||
* accommodation belongs to the day plan, which shows it across its span.
|
||||
*
|
||||
* They also never worked here. A hotel keeps its dates on the linked stay, not
|
||||
* on the reservation: the booking form writes reservation_time = NULL and no
|
||||
* day_id, so a form-created hotel matched neither arm of the date test and was
|
||||
* invisible. The one path that did produce a visible row, the accommodation
|
||||
* 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.
|
||||
*/
|
||||
listUpcoming(userId: number, limit = 6) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
@@ -402,6 +416,7 @@ export class ReservationsService {
|
||||
WHERE (t.user_id = ? OR tm.user_id IS NOT NULL)
|
||||
AND t.is_archived = 0
|
||||
AND r.status != 'cancelled'
|
||||
AND COALESCE(r.type, '') != 'hotel'
|
||||
AND (
|
||||
(r.reservation_time IS NOT NULL AND r.reservation_time >= ?)
|
||||
OR (r.reservation_time IS NULL AND d.date IS NOT NULL AND d.date >= ?)
|
||||
|
||||
@@ -112,11 +112,14 @@ describe('Reservations + accommodations e2e (real auth guard + temp SQLite, real
|
||||
expect((await request(server).get('/api/reservations/upcoming')).status).toBe(401);
|
||||
});
|
||||
|
||||
it('200 cross-trip upcoming reservations feed', async () => {
|
||||
it('200 cross-trip upcoming reservations feed, without the hotels (#1934)', async () => {
|
||||
db.prepare("INSERT INTO reservations (trip_id, title, type, reservation_time) VALUES (?, 'Flight', 'flight', '2999-01-01T10:00:00')").run(tripId);
|
||||
db.prepare("INSERT INTO reservations (trip_id, title, type, reservation_time) VALUES (?, 'Stay', 'hotel', '2999-01-01T09:00:00')").run(tripId);
|
||||
const res = await request(server).get('/api/reservations/upcoming').set('Cookie', sessionCookie(1));
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.reservations.map((r: { title: string }) => r.title)).toContain('Flight');
|
||||
const titles = res.body.reservations.map((r: { title: string }) => r.title);
|
||||
expect(titles).toContain('Flight');
|
||||
expect(titles).not.toContain('Stay');
|
||||
});
|
||||
|
||||
it('404 when trip not accessible (reservations)', async () => {
|
||||
|
||||
@@ -301,6 +301,39 @@ describe('ReservationsService (DI-native, real SQL)', () => {
|
||||
const rows = svc.listUpcoming(user.id) as { title: string }[];
|
||||
expect(rows.map(r => r.title)).toEqual(['Future']);
|
||||
});
|
||||
|
||||
// #1934 — a stay covers a range, so it would hold a widget slot for its
|
||||
// whole span. Both ways a hotel can be dated are kept out.
|
||||
it('RESV-SVC-017b: leaves out a hotel dated through its own reservation_time', () => {
|
||||
const { user, trip } = ownerTrip();
|
||||
const hotel = createReservation(testDb, trip.id, { title: 'Hotel', type: 'hotel' });
|
||||
testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?').run('2999-01-01T10:00:00', hotel.id);
|
||||
const flight = createReservation(testDb, trip.id, { title: 'Flight' });
|
||||
testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?').run('2999-01-02T10:00:00', flight.id);
|
||||
|
||||
const rows = svc.listUpcoming(user.id) as { title: string }[];
|
||||
expect(rows.map(r => r.title)).toEqual(['Flight']);
|
||||
});
|
||||
|
||||
it('RESV-SVC-017c: leaves out a hotel dated through its day', () => {
|
||||
const { user, trip } = ownerTrip();
|
||||
const day = createDay(testDb, trip.id, { date: '2999-06-01' });
|
||||
createReservation(testDb, trip.id, { title: 'Hotel', type: 'hotel', day_id: day.id });
|
||||
|
||||
const rows = svc.listUpcoming(user.id) as { title: string }[];
|
||||
expect(rows.map(r => r.title)).toEqual([]);
|
||||
});
|
||||
|
||||
// The type column defaults to 'other' but nothing stops a NULL, and a
|
||||
// NULL-unsafe `type != 'hotel'` would silently drop those rows.
|
||||
it('RESV-SVC-017d: a reservation with no type at all still shows', () => {
|
||||
const { user, trip } = ownerTrip();
|
||||
const res = createReservation(testDb, trip.id, { title: 'Untyped' });
|
||||
testDb.prepare('UPDATE reservations SET type = NULL, reservation_time = ? WHERE id = ?').run('2999-01-01T10:00:00', res.id);
|
||||
|
||||
const rows = svc.listUpcoming(user.id) as { title: string }[];
|
||||
expect(rows.map(r => r.title)).toEqual(['Untyped']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resyncReservationDays', () => {
|
||||
|
||||
Reference in New Issue
Block a user