fix(reservations): keep dated bookings on their date when the trip range shifts (#1288)
Changing a trip's start date positionally re-dates the day rows (keeping their ids), so a dated booking's day_id stayed glued to a now-re-dated day and the booking visually shifted by the offset — until you re-opened and saved it. After a date-range change, non-hotel bookings are now re-anchored to the day matching their absolute reservation_time (the same derivation create/update already use). Bookings whose date falls outside the new range are left untouched; hotels and the relative positional shift of places/notes are unaffected.
This commit is contained in:
parent
649735726f
commit
1ec2d62b1c
@ -59,6 +59,34 @@ function resolveDayIdFromTime(
|
|||||||
return row?.id ?? null;
|
return row?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After a trip's date range changes, generateDays positionally re-dates the day rows
|
||||||
|
// (keeping their ids), so a dated booking's day_id stays glued to a now-re-dated day and
|
||||||
|
// the booking visually shifts by the offset (#1288). Re-anchor non-hotel bookings to the
|
||||||
|
// day matching their absolute reservation_time — the same derivation create/updateReservation
|
||||||
|
// use. Only updates when a matching day exists, so a booking whose date now falls outside
|
||||||
|
// the new range is left untouched. Hotels keep their range on the linked day_accommodation.
|
||||||
|
export function resyncReservationDays(tripId: string | number): void {
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT id, reservation_time, reservation_end_time, day_id, end_day_id
|
||||||
|
FROM reservations
|
||||||
|
WHERE trip_id = ? AND type != 'hotel' AND reservation_time IS NOT NULL`,
|
||||||
|
).all(tripId) as {
|
||||||
|
id: number; reservation_time: string | null; reservation_end_time: string | null;
|
||||||
|
day_id: number | null; end_day_id: number | null;
|
||||||
|
}[];
|
||||||
|
const update = db.prepare('UPDATE reservations SET day_id = ?, end_day_id = ? WHERE id = ?');
|
||||||
|
for (const r of rows) {
|
||||||
|
const newDayId = resolveDayIdFromTime(tripId, r.reservation_time);
|
||||||
|
if (newDayId == null) continue;
|
||||||
|
const newEndDayId = r.reservation_end_time
|
||||||
|
? (resolveDayIdFromTime(tripId, r.reservation_end_time) ?? r.end_day_id)
|
||||||
|
: r.end_day_id;
|
||||||
|
if (newDayId !== r.day_id || newEndDayId !== r.end_day_id) {
|
||||||
|
update.run(newDayId, newEndDayId, r.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveEndpoints(reservationId: number, endpoints: EndpointInput[]): void {
|
function saveEndpoints(reservationId: number, endpoints: EndpointInput[]): void {
|
||||||
// Bind the transaction lazily on each call. Binding at module load time
|
// Bind the transaction lazily on each call. Binding at module load time
|
||||||
// captures the DB connection that was open then, which becomes invalid
|
// captures the DB connection that was open then, which becomes invalid
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Trip, User } from '../types';
|
|||||||
import { listDays, listAccommodations } from './dayService';
|
import { listDays, listAccommodations } from './dayService';
|
||||||
import { listBudgetItems } from './budgetService';
|
import { listBudgetItems } from './budgetService';
|
||||||
import { listItems as listPackingItems } from './packingService';
|
import { listItems as listPackingItems } from './packingService';
|
||||||
import { listReservations, loadEndpointsByTrip } from './reservationService';
|
import { listReservations, loadEndpointsByTrip, resyncReservationDays } from './reservationService';
|
||||||
import { listNotes as listCollabNotes } from './collabService';
|
import { listNotes as listCollabNotes } from './collabService';
|
||||||
import { shiftOwnerEntriesForTripWindow } from './vacayService';
|
import { shiftOwnerEntriesForTripWindow } from './vacayService';
|
||||||
|
|
||||||
@ -256,8 +256,12 @@ export function updateTrip(tripId: string | number, userId: number, data: Update
|
|||||||
shiftOwnerEntriesForTripWindow(trip.user_id, trip.start_date, trip.end_date, newStart);
|
shiftOwnerEntriesForTripWindow(trip.user_id, trip.start_date, trip.end_date, newStart);
|
||||||
|
|
||||||
const dayCount = data.day_count ? Math.min(Math.max(Number(data.day_count) || 7, 1), MAX_TRIP_DAYS) : undefined;
|
const dayCount = data.day_count ? Math.min(Math.max(Number(data.day_count) || 7, 1), MAX_TRIP_DAYS) : undefined;
|
||||||
if (newStart !== trip.start_date || newEnd !== trip.end_date || dayCount)
|
if (newStart !== trip.start_date || newEnd !== trip.end_date || dayCount) {
|
||||||
generateDays(tripId, newStart || null, newEnd || null, undefined, dayCount);
|
generateDays(tripId, newStart || null, newEnd || null, undefined, dayCount);
|
||||||
|
// generateDays re-dates day rows positionally; re-anchor dated bookings to the day
|
||||||
|
// matching their absolute reservation_time so they don't shift with it (#1288).
|
||||||
|
resyncReservationDays(tripId);
|
||||||
|
}
|
||||||
|
|
||||||
const changes: Record<string, unknown> = {};
|
const changes: Record<string, unknown> = {};
|
||||||
if (title && title !== trip.title) changes.title = title;
|
if (title && title !== trip.title) changes.title = title;
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { createTables } from '../../../src/db/schema';
|
|||||||
import { runMigrations } from '../../../src/db/migrations';
|
import { runMigrations } from '../../../src/db/migrations';
|
||||||
import { resetTestDb } from '../../helpers/test-db';
|
import { resetTestDb } from '../../helpers/test-db';
|
||||||
import { createUser, createTrip, createReservation, createPlace, createDay, createDayAssignment, createDayNote } from '../../helpers/factories';
|
import { createUser, createTrip, createReservation, createPlace, createDay, createDayAssignment, createDayNote } from '../../helpers/factories';
|
||||||
import { exportICS, generateDays, deleteOldCover } from '../../../src/services/tripService';
|
import { exportICS, generateDays, deleteOldCover, updateTrip } from '../../../src/services/tripService';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -476,3 +476,34 @@ describe('deleteOldCover', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resyncReservationDays (#1288)', () => {
|
||||||
|
const dayFor = (tripId: number, date: string) =>
|
||||||
|
(testDb.prepare('SELECT id FROM days WHERE trip_id = ? AND date = ?').get(tripId, date) as { id: number }).id;
|
||||||
|
const insertDatedReservation = (tripId: number, dayId: number, time: string) =>
|
||||||
|
Number(testDb.prepare(
|
||||||
|
"INSERT INTO reservations (trip_id, day_id, title, reservation_time, type, status) VALUES (?, ?, 'Dinner', ?, 'restaurant', 'pending')",
|
||||||
|
).run(tripId, dayId, time).lastInsertRowid);
|
||||||
|
|
||||||
|
it('TRIP-SVC-018: changing the start date re-anchors a dated reservation to the day matching its time', () => {
|
||||||
|
const { user } = createUser(testDb);
|
||||||
|
const trip = createTrip(testDb, user.id, { start_date: '2025-06-01', end_date: '2025-06-05' });
|
||||||
|
const resId = insertDatedReservation(trip.id, dayFor(trip.id, '2025-06-02'), '2025-06-02T19:00:00');
|
||||||
|
// Shift the whole range one day forward (days become 2025-06-02..06).
|
||||||
|
updateTrip(trip.id, user.id, { start_date: '2025-06-02', end_date: '2025-06-06' }, 'user');
|
||||||
|
const res = testDb.prepare('SELECT day_id FROM reservations WHERE id = ?').get(resId) as { day_id: number };
|
||||||
|
// The booking stays on its absolute date (2025-06-02) instead of shifting with its old day row.
|
||||||
|
expect(res.day_id).toBe(dayFor(trip.id, '2025-06-02'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TRIP-SVC-019: a reservation whose date falls outside the new range keeps its day_id (not nulled)', () => {
|
||||||
|
const { user } = createUser(testDb);
|
||||||
|
const trip = createTrip(testDb, user.id, { start_date: '2025-06-01', end_date: '2025-06-05' });
|
||||||
|
const origDayId = dayFor(trip.id, '2025-06-02');
|
||||||
|
const resId = insertDatedReservation(trip.id, origDayId, '2025-06-02T19:00:00');
|
||||||
|
// Shift far forward so 2025-06-02 is no longer covered by any day.
|
||||||
|
updateTrip(trip.id, user.id, { start_date: '2025-06-10', end_date: '2025-06-14' }, 'user');
|
||||||
|
const res = testDb.prepare('SELECT day_id FROM reservations WHERE id = ?').get(resId) as { day_id: number };
|
||||||
|
expect(res.day_id).toBe(origDayId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user