mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(trips): preserve day content when trip date range changes
Rewrites generateDays to remap days positionally by day_number instead of matching by date identity. Previously any date range shift with no overlap would cascade-delete all day_assignments, day_notes, and day_accommodations. New behaviour: - Shift/partial overlap: existing days remapped to new dates in order - Shrink: overflow days become dateless (date=NULL) instead of deleted, preserving all child data for manual reassignment - Grow: existing days kept, new empty days appended - Clear dates: all days nullified, content intact Also fixes a UNIQUE(trip_id, day_number) collision that would occur when spare dateless days remained after growing into a partially-dateless trip (maxAssigned base was wrong). Closes #646
This commit is contained in:
@@ -34,27 +34,44 @@ export { isOwner };
|
||||
|
||||
export function generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, maxDays?: number, dayCount?: number) {
|
||||
const existing = db.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ?').all(tripId) as { id: number; day_number: number; date: string | null }[];
|
||||
const setDayNumber = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||
|
||||
// Helper: two-phase renumber to avoid UNIQUE(trip_id, day_number) collisions
|
||||
function renumber(days: { id: number }[]) {
|
||||
days.forEach((d, i) => setDayNumber.run(-(i + 1), d.id));
|
||||
days.forEach((d, i) => setDayNumber.run(i + 1, d.id));
|
||||
}
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
const datelessExisting = existing.filter(d => !d.date).sort((a, b) => a.day_number - b.day_number);
|
||||
// Nullify all dated days instead of deleting them — preserves assignments/notes/accommodations
|
||||
const withDates = existing.filter(d => d.date);
|
||||
if (withDates.length > 0) {
|
||||
db.prepare(`DELETE FROM days WHERE trip_id = ? AND date IS NOT NULL`).run(tripId);
|
||||
const nullify = db.prepare('UPDATE days SET date = NULL WHERE id = ?');
|
||||
for (const d of withDates) nullify.run(d.id);
|
||||
}
|
||||
const targetCount = Math.min(Math.max(dayCount ?? (datelessExisting.length || 7), 1), MAX_TRIP_DAYS);
|
||||
const needed = targetCount - datelessExisting.length;
|
||||
// Now all days are dateless — adjust count toward dayCount target
|
||||
const allDays = db.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as { id: number }[];
|
||||
const targetCount = Math.min(Math.max(dayCount ?? (allDays.length || 7), 1), MAX_TRIP_DAYS);
|
||||
const needed = targetCount - allDays.length;
|
||||
if (needed > 0) {
|
||||
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)');
|
||||
for (let i = 0; i < needed; i++) insert.run(tripId, datelessExisting.length + i + 1);
|
||||
for (let i = 0; i < needed; i++) insert.run(tripId, allDays.length + i + 1);
|
||||
} else if (needed < 0) {
|
||||
const toRemove = datelessExisting.slice(targetCount);
|
||||
// Only trim trailing empty days to avoid destroying content
|
||||
const candidates = db.prepare(
|
||||
`SELECT d.id FROM days d
|
||||
WHERE d.trip_id = ?
|
||||
AND NOT EXISTS (SELECT 1 FROM day_assignments da WHERE da.day_id = d.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM day_notes dn WHERE dn.day_id = d.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM day_accommodations dac WHERE dac.start_day_id = d.id OR dac.end_day_id = d.id)
|
||||
ORDER BY d.day_number DESC
|
||||
LIMIT ?`
|
||||
).all(tripId, -needed) as { id: number }[];
|
||||
const del = db.prepare('DELETE FROM days WHERE id = ?');
|
||||
for (const d of toRemove) del.run(d.id);
|
||||
for (const d of candidates) del.run(d.id);
|
||||
}
|
||||
const remaining = db.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as { id: number }[];
|
||||
const tmpUpd = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||
remaining.forEach((d, i) => tmpUpd.run(-(i + 1), d.id));
|
||||
remaining.forEach((d, i) => tmpUpd.run(i + 1, d.id));
|
||||
renumber(remaining);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,45 +90,50 @@ export function generateDays(tripId: number | bigint | string, startDate: string
|
||||
targetDates.push(`${yyyy}-${mm}-${dd}`);
|
||||
}
|
||||
|
||||
const existingByDate = new Map<string, { id: number; day_number: number; date: string | null }>();
|
||||
for (const d of existing) {
|
||||
if (d.date) existingByDate.set(d.date, d);
|
||||
}
|
||||
|
||||
const targetDateSet = new Set(targetDates);
|
||||
|
||||
const toDelete = existing.filter(d => d.date && !targetDateSet.has(d.date));
|
||||
// Split into dated (sorted by day_number = position) and dateless (spare pool)
|
||||
const dated = existing.filter(d => d.date).sort((a, b) => a.day_number - b.day_number);
|
||||
const dateless = existing.filter(d => !d.date).sort((a, b) => a.day_number - b.day_number);
|
||||
const del = db.prepare('DELETE FROM days WHERE id = ?');
|
||||
for (const d of toDelete) del.run(d.id);
|
||||
|
||||
// Reassign dateless days to the first unmatched target dates (preserves content)
|
||||
const assignDate = db.prepare('UPDATE days SET date = ?, day_number = ? WHERE id = ?');
|
||||
let datelessIdx = 0;
|
||||
|
||||
const setTemp = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||
const kept = existing.filter(d => d.date && targetDateSet.has(d.date));
|
||||
for (let i = 0; i < kept.length; i++) setTemp.run(-(i + 1), kept[i].id);
|
||||
// Phase 1: stamp all existing days with negative day_numbers to free up slots
|
||||
const allExisting = [...dated, ...dateless];
|
||||
allExisting.forEach((d, i) => setDayNumber.run(-(i + 1), d.id));
|
||||
|
||||
const assignDay = db.prepare('UPDATE days SET date = ?, day_number = ? WHERE id = ?');
|
||||
const insert = db.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, ?)');
|
||||
const update = db.prepare('UPDATE days SET day_number = ? WHERE id = ?');
|
||||
|
||||
let datelessIdx = 0;
|
||||
|
||||
for (let i = 0; i < targetDates.length; i++) {
|
||||
const date = targetDates[i];
|
||||
const ex = existingByDate.get(date);
|
||||
if (ex) {
|
||||
update.run(i + 1, ex.id);
|
||||
if (i < dated.length) {
|
||||
// Positional remap: existing dated day i gets new date — keeps all children
|
||||
assignDay.run(date, i + 1, dated[i].id);
|
||||
} else if (datelessIdx < dateless.length) {
|
||||
// Reuse a dateless day — keeps its assignments, notes, etc.
|
||||
assignDate.run(date, i + 1, dateless[datelessIdx].id);
|
||||
assignDay.run(date, i + 1, dateless[datelessIdx].id);
|
||||
datelessIdx++;
|
||||
} else {
|
||||
insert.run(tripId, i + 1, date);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any remaining unused dateless days
|
||||
for (let i = datelessIdx; i < dateless.length; i++) del.run(dateless[i].id);
|
||||
// Overflow dated days (trip shrunk): convert to dateless instead of deleting
|
||||
const nullify = db.prepare('UPDATE days SET date = NULL, day_number = ? WHERE id = ?');
|
||||
for (let i = targetDates.length; i < dated.length; i++) {
|
||||
nullify.run(targetDates.length + (i - targetDates.length) + 1, dated[i].id);
|
||||
}
|
||||
|
||||
// Any remaining unused dateless days: keep as dateless, just renumber.
|
||||
// Base must be max(targetDates.length, dated.length) to avoid colliding with
|
||||
// positives already assigned by the main loop or the overflow loop above.
|
||||
const maxAssigned = Math.max(targetDates.length, dated.length);
|
||||
for (let i = datelessIdx; i < dateless.length; i++) {
|
||||
setDayNumber.run(maxAssigned + (i - datelessIdx) + 1, dateless[i].id);
|
||||
}
|
||||
|
||||
// Final renumber to compact and eliminate any gaps/negatives
|
||||
const remaining = db.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as { id: number }[];
|
||||
renumber(remaining);
|
||||
}
|
||||
|
||||
// ── Trip CRUD ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -49,7 +49,7 @@ import { createApp } from '../../src/app';
|
||||
import { createTables } from '../../src/db/schema';
|
||||
import { runMigrations } from '../../src/db/migrations';
|
||||
import { resetTestDb } from '../helpers/test-db';
|
||||
import { createUser, createAdmin, createTrip, addTripMember, createPlace, createReservation, createTag, createDayAccommodation, createBudgetItem, createPackingItem, createDayNote } from '../helpers/factories';
|
||||
import { createUser, createAdmin, createTrip, addTripMember, createPlace, createReservation, createTag, createDayAccommodation, createBudgetItem, createPackingItem, createDayNote, createDayAssignment } from '../helpers/factories';
|
||||
import { authCookie } from '../helpers/auth';
|
||||
import { loginAttempts, mfaAttempts } from '../../src/routes/auth';
|
||||
import { invalidatePermissionsCache } from '../../src/services/permissions';
|
||||
@@ -430,6 +430,65 @@ describe('Update trip', () => {
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('TRIP-023 — Shifting trip date range preserves day assignments positionally', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-08-01', end_date: '2026-08-05' });
|
||||
|
||||
const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string }[];
|
||||
expect(days).toHaveLength(5);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, days[0].id, place.id);
|
||||
const note = createDayNote(testDb, days[1].id, trip.id, { text: 'pack sunscreen' });
|
||||
|
||||
// Shift forward 10 days (zero overlap with original range)
|
||||
const res = await request(app)
|
||||
.put(`/api/trips/${trip.id}`)
|
||||
.set('Cookie', authCookie(user.id))
|
||||
.send({ start_date: '2026-08-11', end_date: '2026-08-15' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const daysAfter = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[];
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
expect(daysAfter.map(d => d.date)).toEqual(['2026-08-11', '2026-08-12', '2026-08-13', '2026-08-14', '2026-08-15']);
|
||||
|
||||
const assignmentsAfter = testDb.prepare('SELECT * FROM day_assignments WHERE id = ?').get(assignment.id) as { day_id: number } | undefined;
|
||||
expect(assignmentsAfter).toBeDefined();
|
||||
expect(assignmentsAfter!.day_id).toBe(daysAfter[0].id);
|
||||
|
||||
const notesAfter = testDb.prepare('SELECT * FROM day_notes WHERE id = ?').get(note.id) as { day_id: number } | undefined;
|
||||
expect(notesAfter).toBeDefined();
|
||||
expect(notesAfter!.day_id).toBe(daysAfter[1].id);
|
||||
});
|
||||
|
||||
it('TRIP-024 — Shrinking trip date range keeps overflow days as dateless with content intact', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2026-09-01', end_date: '2026-09-05' });
|
||||
|
||||
const days = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number }[];
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const a4 = createDayAssignment(testDb, days[3].id, place.id);
|
||||
const a5 = createDayAssignment(testDb, days[4].id, place.id);
|
||||
|
||||
// Shrink from 5 to 3 days
|
||||
const res = await request(app)
|
||||
.put(`/api/trips/${trip.id}`)
|
||||
.set('Cookie', authCookie(user.id))
|
||||
.send({ start_date: '2026-09-01', end_date: '2026-09-03' });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const daysAfter = testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(trip.id) as { id: number; date: string | null }[];
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
expect(daysAfter.filter(d => d.date !== null)).toHaveLength(3);
|
||||
expect(daysAfter.filter(d => d.date === null)).toHaveLength(2);
|
||||
|
||||
// Overflow assignments survived
|
||||
const all = testDb.prepare('SELECT * FROM day_assignments WHERE id IN (?, ?)').all(a4.id, a5.id) as { id: number }[];
|
||||
expect(all).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -33,8 +33,8 @@ vi.mock('../../../src/config', () => ({
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, createReservation } from '../../helpers/factories';
|
||||
import { exportICS } from '../../../src/services/tripService';
|
||||
import { createUser, createTrip, createReservation, createPlace, createDay, createDayAssignment, createDayNote } from '../../helpers/factories';
|
||||
import { exportICS, generateDays } from '../../../src/services/tripService';
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
@@ -49,8 +49,197 @@ afterAll(() => {
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function getDays(tripId: number) {
|
||||
return testDb.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as {
|
||||
id: number; trip_id: number; day_number: number; date: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
function getAssignments(dayId: number) {
|
||||
return testDb.prepare('SELECT * FROM day_assignments WHERE day_id = ?').all(dayId) as { id: number; day_id: number }[];
|
||||
}
|
||||
|
||||
function getNotes(dayId: number) {
|
||||
return testDb.prepare('SELECT * FROM day_notes WHERE day_id = ?').all(dayId) as { id: number; day_id: number }[];
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('generateDays', () => {
|
||||
it('TRIP-SVC-010: full range shift preserves day assignments and notes positionally', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-06-01', end_date: '2025-06-05' });
|
||||
const daysBefore = getDays(trip.id);
|
||||
expect(daysBefore).toHaveLength(5);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, daysBefore[0].id, place.id);
|
||||
const note = createDayNote(testDb, daysBefore[1].id, trip.id, { text: 'packed' });
|
||||
|
||||
// Shift forward 9 days — zero overlap with original dates
|
||||
generateDays(trip.id, '2025-06-10', '2025-06-14');
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
expect(daysAfter.map(d => d.date)).toEqual([
|
||||
'2025-06-10', '2025-06-11', '2025-06-12', '2025-06-13', '2025-06-14',
|
||||
]);
|
||||
|
||||
// day_number 1 (formerly June 1) now has date June 10 — assignment still attached
|
||||
const day1 = daysAfter[0];
|
||||
const day2 = daysAfter[1];
|
||||
expect(getAssignments(day1.id)).toHaveLength(1);
|
||||
expect(getAssignments(day1.id)[0].id).toBe(assignment.id);
|
||||
expect(getNotes(day2.id)).toHaveLength(1);
|
||||
expect(getNotes(day2.id)[0].id).toBe(note.id);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-011: shrinking range converts overflow days to dateless, preserves their assignments', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-07-01', end_date: '2025-07-05' });
|
||||
const daysBefore = getDays(trip.id);
|
||||
expect(daysBefore).toHaveLength(5);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
// Assign places to days 4 and 5 (will become overflow)
|
||||
const a4 = createDayAssignment(testDb, daysBefore[3].id, place.id);
|
||||
const a5 = createDayAssignment(testDb, daysBefore[4].id, place.id);
|
||||
|
||||
// Shrink from 5 to 3 days
|
||||
generateDays(trip.id, '2025-07-01', '2025-07-03');
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(5); // no rows deleted
|
||||
|
||||
const dated = daysAfter.filter(d => d.date !== null);
|
||||
const dateless = daysAfter.filter(d => d.date === null);
|
||||
expect(dated).toHaveLength(3);
|
||||
expect(dateless).toHaveLength(2);
|
||||
|
||||
// Overflow days still have their assignments
|
||||
expect(getAssignments(dateless[0].id)).toHaveLength(1);
|
||||
expect(getAssignments(dateless[0].id)[0].id).toBe(a4.id);
|
||||
expect(getAssignments(dateless[1].id)).toHaveLength(1);
|
||||
expect(getAssignments(dateless[1].id)[0].id).toBe(a5.id);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-012: growing range keeps existing day content and appends new empty days', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-08-01', end_date: '2025-08-03' });
|
||||
const daysBefore = getDays(trip.id);
|
||||
expect(daysBefore).toHaveLength(3);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, daysBefore[0].id, place.id);
|
||||
|
||||
// Grow to 5 days
|
||||
generateDays(trip.id, '2025-08-01', '2025-08-05');
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
expect(daysAfter.map(d => d.date)).toEqual([
|
||||
'2025-08-01', '2025-08-02', '2025-08-03', '2025-08-04', '2025-08-05',
|
||||
]);
|
||||
|
||||
// Existing day 1 retains its assignment
|
||||
expect(getAssignments(daysAfter[0].id)).toHaveLength(1);
|
||||
expect(getAssignments(daysAfter[0].id)[0].id).toBe(assignment.id);
|
||||
|
||||
// New days 4 and 5 are empty
|
||||
expect(getAssignments(daysAfter[3].id)).toHaveLength(0);
|
||||
expect(getAssignments(daysAfter[4].id)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-013: clearing dates converts all days to dateless without destroying assignments', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-09-01', end_date: '2025-09-04' });
|
||||
const daysBefore = getDays(trip.id);
|
||||
expect(daysBefore).toHaveLength(4);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
const assignment = createDayAssignment(testDb, daysBefore[1].id, place.id);
|
||||
|
||||
// Clear both dates
|
||||
generateDays(trip.id, null, null);
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(4);
|
||||
expect(daysAfter.every(d => d.date === null)).toBe(true);
|
||||
|
||||
// The assignment on the former day 2 still exists
|
||||
const formerDay2 = daysAfter.find(d => d.id === daysBefore[1].id);
|
||||
expect(formerDay2).toBeDefined();
|
||||
expect(getAssignments(formerDay2!.id)).toHaveLength(1);
|
||||
expect(getAssignments(formerDay2!.id)[0].id).toBe(assignment.id);
|
||||
});
|
||||
|
||||
it('TRIP-SVC-014: partial overlap shift remaps by position (day 1→3 kept, 4-5 overflow)', () => {
|
||||
// Original: Jun 1-5. New: Jun 3-7 (overlap on Jun 3-5, but we map by position)
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-10-01', end_date: '2025-10-05' });
|
||||
const daysBefore = getDays(trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
// Assign to each of the 5 days
|
||||
for (const day of daysBefore) createDayAssignment(testDb, day.id, place.id);
|
||||
|
||||
// Shift forward 2 days (partial overlap with original range)
|
||||
generateDays(trip.id, '2025-10-03', '2025-10-07');
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
expect(daysAfter.map(d => d.date)).toEqual([
|
||||
'2025-10-03', '2025-10-04', '2025-10-05', '2025-10-06', '2025-10-07',
|
||||
]);
|
||||
|
||||
// All 5 assignments survive
|
||||
for (const day of daysAfter) {
|
||||
expect(getAssignments(day.id)).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('TRIP-SVC-015: growing into dateless days reuses them; leftover dateless renumber without UNIQUE collision', () => {
|
||||
// 3 dated days + 2 pre-existing dateless days. Resize to 4 dated days.
|
||||
// Main loop: dated[0..2] → positions 1-3, dateless[0] → position 4 (consumed).
|
||||
// Unused dateless: dateless[1] should land at position 5, NOT 4 (collision bug).
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { start_date: '2025-11-01', end_date: '2025-11-03' });
|
||||
|
||||
// Insert 2 dateless days directly
|
||||
const daysBefore = getDays(trip.id);
|
||||
testDb.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)').run(trip.id, 4);
|
||||
testDb.prepare('INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, NULL)').run(trip.id, 5);
|
||||
|
||||
const allDays = getDays(trip.id);
|
||||
expect(allDays).toHaveLength(5);
|
||||
|
||||
const place = createPlace(testDb, trip.id);
|
||||
// Put an assignment on the second dateless day (day_number=5) — it should survive
|
||||
const assignment = createDayAssignment(testDb, allDays[4].id, place.id);
|
||||
|
||||
// Grow from 3 to 4 dated days — consumes dateless[0], leaves dateless[1] unused
|
||||
// This is the scenario that triggered the UNIQUE collision bug
|
||||
generateDays(trip.id, '2025-11-01', '2025-11-04');
|
||||
|
||||
const daysAfter = getDays(trip.id);
|
||||
expect(daysAfter).toHaveLength(5);
|
||||
|
||||
const dated = daysAfter.filter(d => d.date !== null);
|
||||
const dateless = daysAfter.filter(d => d.date === null);
|
||||
expect(dated).toHaveLength(4);
|
||||
expect(dateless).toHaveLength(1);
|
||||
|
||||
// The remaining dateless day still has its assignment
|
||||
expect(getAssignments(dateless[0].id)).toHaveLength(1);
|
||||
expect(getAssignments(dateless[0].id)[0].id).toBe(assignment.id);
|
||||
|
||||
// All day_numbers are unique 1..5
|
||||
const nums = daysAfter.map(d => d.day_number).sort((a, b) => a - b);
|
||||
expect(nums).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportICS', () => {
|
||||
it('TRIP-SVC-001: returns VCALENDAR wrapper', () => {
|
||||
const { user } = createUser(testDb);
|
||||
|
||||
Reference in New Issue
Block a user