diff --git a/server/src/db/reseat-booked-nights.ts b/server/src/db/reseat-booked-nights.ts index 458e969ed..cfe904449 100644 --- a/server/src/db/reseat-booked-nights.ts +++ b/server/src/db/reseat-booked-nights.ts @@ -31,10 +31,13 @@ export function reseatBookedNights(db: Database.Database): number { JOIN day_accommodations a ON a.id = da.accommodation_id ORDER BY da.day_id, da.order_index, da.id `).all() as Array<{ id: number; day_id: number; check_in: string | null }>; + // Another night on the same day counts by its check-in, so two bookings on one + // day settle by the clock instead of by whichever was seated last. const dayRows = db.prepare(` - SELECT da.id, COALESCE(da.assignment_time, p.place_time) AS at, + SELECT da.id, COALESCE(da.assignment_time, p.place_time, other.check_in) AS at, (p.lat IS NOT NULL AND p.lng IS NOT NULL) AS located FROM day_assignments da JOIN places p ON p.id = da.place_id + LEFT JOIN day_accommodations other ON other.id = da.accommodation_id WHERE da.day_id = ? ORDER BY da.order_index ASC, da.created_at ASC, da.id ASC `); diff --git a/server/src/nest/accommodations/accommodations.service.ts b/server/src/nest/accommodations/accommodations.service.ts index 1d0eade62..a7403fe7d 100644 --- a/server/src/nest/accommodations/accommodations.service.ts +++ b/server/src/nest/accommodations/accommodations.service.ts @@ -296,9 +296,12 @@ export class AccommodationsService { if (!checkIn) return 0; // excludeId leaves the row being re-seated out of the chain it is measured // against. Without it a night parked at the end of the day can find itself. + // Another booked night on the day counts by its check-in, so two bookings on + // one day settle by the clock. const rows = this.db.all<{ order_index: number; at: string | null }>(` - SELECT da.order_index, COALESCE(da.assignment_time, p.place_time) AS at + SELECT da.order_index, COALESCE(da.assignment_time, p.place_time, other.check_in) AS at FROM day_assignments da JOIN places p ON p.id = da.place_id + LEFT JOIN day_accommodations other ON other.id = da.accommodation_id WHERE da.day_id = ? AND da.id != ? ORDER BY da.order_index `, dayId, excludeId ?? -1); let seat = 0; @@ -319,8 +322,9 @@ export class AccommodationsService { private seatedByCheckIn(dayId: number, ownId: number, checkIn: string | null | undefined): boolean { if (!checkIn) return true; const rows = this.db.all<{ id: number; at: string | null }>(` - SELECT da.id, COALESCE(da.assignment_time, p.place_time) AS at + SELECT da.id, COALESCE(da.assignment_time, p.place_time, other.check_in) AS at FROM day_assignments da JOIN places p ON p.id = da.place_id + LEFT JOIN day_accommodations other ON other.id = da.accommodation_id WHERE da.day_id = ? ORDER BY da.order_index `, dayId); const own = rows.findIndex(row => row.id === ownId); diff --git a/server/tests/unit/db/booked-night-reseat.test.ts b/server/tests/unit/db/booked-night-reseat.test.ts index 14246ef25..58797af18 100644 --- a/server/tests/unit/db/booked-night-reseat.test.ts +++ b/server/tests/unit/db/booked-night-reseat.test.ts @@ -177,6 +177,18 @@ describe('booked night reseat', () => { expect(vias(db).map((v) => v.id)).not.toContain(afterA); }); + it('RESEAT-010: two nights on one day settle by their check-ins', () => { + const db = freshDb(); + const [later, earlier] = [place(db, 'Rue de Paris'), place(db, 'Brandenburger Tor')]; + stop(db, later, 0, null, night(db, later, '12:00')); + stop(db, earlier, 1, null, night(db, earlier, '10:00')); + + reseatBookedNights(db); + + expect(order(db)).toEqual([earlier, later]); + expect(reseatBookedNights(db)).toBe(0); + }); + it('RESEAT-008: runs again without moving anything', () => { const db = freshDb(); const [fuel, hotel] = [place(db, 'Aral'), place(db, 'Rostock')]; diff --git a/server/tests/unit/nest/accommodations.service.test.ts b/server/tests/unit/nest/accommodations.service.test.ts index 758a363d1..d017e2332 100644 --- a/server/tests/unit/nest/accommodations.service.test.ts +++ b/server/tests/unit/nest/accommodations.service.test.ts @@ -743,6 +743,20 @@ describe('the day stop a booking implies', () => { expect(stopsOn(day.id).map(a => a.place_id)).toEqual([afternoon.id, hotel.id]); }); + it('ACC-022i a second night on the day counts by its check-in', () => { + const { user } = createUser(testDb); + const trip = createTrip(testDb, user.id); + const day = createDay(testDb, trip.id); + const [noon, morning] = ['Rue de Paris', 'Brandenburger Tor'].map(name => createPlace(testDb, trip.id, { name })); + svc.createAccommodation(trip.id, { place_id: noon.id, start_day_id: day.id, end_day_id: day.id, check_in: '12:00' }); + svc.createAccommodation(trip.id, { place_id: morning.id, start_day_id: day.id, end_day_id: day.id, check_in: '10:00' }); + expect(stopsOn(day.id).map(a => a.place_id)).toEqual([morning.id, noon.id]); + + const evening = createPlace(testDb, trip.id, { name: 'Late' }); + svc.createAccommodation(trip.id, { place_id: evening.id, start_day_id: day.id, end_day_id: day.id, check_in: '18:00' }); + expect(stopsOn(day.id).map(a => a.place_id)).toEqual([morning.id, noon.id, evening.id]); + }); + it('ACC-022h an edit that leaves the check-in alone still corrects a night the clocks contradict', () => { // Dragged behind a stop pinned to the afternoon, with a check-in at ten: a change of // notes puts it back ahead of that stop, because a night sitting after an afternoon it