fix(accommodations): a second night on the day counts by its check-in

Two bookings on one day used to settle by whichever was seated last, because
the other night's stop carries no hour of its own. Its check-in counts now,
in the service and in the migration alike.
This commit is contained in:
Maurice
2026-09-20 18:48:26 +02:00
parent 417d0445b9
commit be9a38e3b0
4 changed files with 36 additions and 3 deletions
+4 -1
View File
@@ -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
`);
@@ -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);
@@ -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')];
@@ -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