mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(server): the booked night settles in one pass, and the sibling paths seat it too
The audit over the 4.3.1 release found the seating rule reading a night by one value and its neighbours by another, so two nights on one day never agreed: the migration reported moves that changed nothing, flipped the pair on the next run and dropped the road drawn between them, and in the service every notes edit swapped them. One rule now lives in accommodations/night-seat.ts, measures every night by its own check-in, breaks a tie by the booking made first, and carries the vias; the service, the reseat migration and a trip date change all seat through it, the last of which used to append the night at the end of its day again. An account created by a first SSO login writes the user.register row the password path writes. Tests cover the equal and absent check-ins, the date change, the booking form path and a day that is nothing but its night.
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
type DayRow = { id: number; at: string | null; located: number };
|
||||
type Via = { id: number; after_order_index: number; sequence: number };
|
||||
import { carryVias, dayStops, locatedIds, seatAmong } from '../nest/accommodations/night-seat';
|
||||
|
||||
/**
|
||||
* Seat every booked night where its check-in says, the way a night booked today is
|
||||
* seated (AccommodationsService.positionForCheckIn).
|
||||
* seated (night-seat.ts, the rule AccommodationsService applies).
|
||||
*
|
||||
* The stop a booking puts on its check-in day used to go last unless a stop pinned to
|
||||
* a later hour pulled it forward, and the backfill that gave older bookings their stop
|
||||
@@ -16,103 +14,46 @@ type Via = { id: number; after_order_index: number; sequence: number };
|
||||
*
|
||||
* Only the stops a booking owns move (accommodation_id). A stop the traveller placed
|
||||
* themselves and then booked a night on is theirs, wherever it sits. The drawn roads
|
||||
* (roadtrip_vias) are pinned to positions among the day's located stops, so they are
|
||||
* carried along by the rules the service applies (AccommodationsService.reanchorVias):
|
||||
* a via follows the stop it was drawn after, a via behind a stop that is last stays
|
||||
* with it, and one left without a leg goes.
|
||||
* (roadtrip_vias) are pinned to positions among the day's located stops and are
|
||||
* carried along by the rules the service applies (carryVias): a via follows the stop
|
||||
* it was drawn after, a via behind a stop that is last stays with it, and one left
|
||||
* without a leg goes.
|
||||
*
|
||||
* Runs inside the migration's transaction. Re-runnable: a night already in its seat is
|
||||
* passed over. Returns how many nights moved.
|
||||
* Runs inside the migration's transaction. Re-runnable: the nights of a day are taken
|
||||
* in the order the rule seats them (a night without a check-in first, then by
|
||||
* check-in, ties by booking), so one pass leaves every night in its seat and a second
|
||||
* pass moves nothing. Returns how many nights moved.
|
||||
*/
|
||||
export function reseatBookedNights(db: Database.Database): number {
|
||||
const nights = db.prepare(`
|
||||
SELECT da.id, da.day_id, a.check_in
|
||||
SELECT da.id, da.day_id, a.id AS accommodation_id, a.check_in
|
||||
FROM day_assignments da
|
||||
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, 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
|
||||
`);
|
||||
ORDER BY da.day_id, a.check_in IS NOT NULL, a.check_in, a.id
|
||||
`).all() as Array<{ id: number; day_id: number; accommodation_id: number; check_in: string | null }>;
|
||||
const setIndex = db.prepare('UPDATE day_assignments SET order_index = ? WHERE id = ?');
|
||||
const viasOf = db.prepare('SELECT id, after_order_index, sequence FROM roadtrip_vias WHERE day_id = ?');
|
||||
const dropVia = db.prepare('DELETE FROM roadtrip_vias WHERE id = ?');
|
||||
const moveVia = db.prepare('UPDATE roadtrip_vias SET after_order_index = ? WHERE id = ?');
|
||||
const setSequence = db.prepare('UPDATE roadtrip_vias SET sequence = ? WHERE id = ?');
|
||||
|
||||
let seated = 0;
|
||||
const roads = { moved: 0, removed: 0 };
|
||||
for (const night of nights) {
|
||||
const rows = dayRows.all(night.day_id) as DayRow[];
|
||||
const rows = dayStops(db, night.day_id);
|
||||
const own = rows.findIndex((row) => row.id === night.id);
|
||||
if (own < 0) continue;
|
||||
const others = rows.filter((row) => row.id !== night.id);
|
||||
let seat = 0;
|
||||
if (night.check_in) {
|
||||
others.forEach((row, i) => {
|
||||
if (row.at !== null && row.at <= night.check_in!) seat = i + 1;
|
||||
});
|
||||
}
|
||||
const next = [...others.slice(0, seat), rows[own]!, ...others.slice(seat)];
|
||||
if (next.every((row, i) => row.id === rows[i]!.id)) continue;
|
||||
const seat = seatAmong(others, { id: night.accommodation_id, check_in: night.check_in });
|
||||
const next = [...others.slice(0, seat), rows[own], ...others.slice(seat)];
|
||||
if (next.every((row, i) => row.id === rows[i].id)) continue;
|
||||
|
||||
const previousLocated = rows.filter((row) => row.located).map((row) => row.id);
|
||||
next.forEach((row, i) => setIndex.run(i, row.id));
|
||||
seated += 1;
|
||||
|
||||
const nextLocated = next.filter((row) => row.located).map((row) => row.id);
|
||||
const vias = viasOf.all(night.day_id) as Via[];
|
||||
if (!vias.length) continue;
|
||||
const previousLast = previousLocated.length - 1;
|
||||
const nextLast = nextLocated.length - 1;
|
||||
const lastStayed = previousLast >= 0 && previousLocated[previousLast] === nextLocated[nextLast];
|
||||
const landed = new Set<number>();
|
||||
const kept: Array<Via & { was: number }> = [];
|
||||
for (const via of vias) {
|
||||
const leg = lastStayed && via.after_order_index === previousLast
|
||||
? nextLast
|
||||
: legAfter(via.after_order_index, previousLocated, nextLocated);
|
||||
if (leg === null) {
|
||||
dropVia.run(via.id);
|
||||
continue;
|
||||
}
|
||||
if (leg !== via.after_order_index) {
|
||||
moveVia.run(leg, via.id);
|
||||
landed.add(via.id);
|
||||
}
|
||||
kept.push({ ...via, was: via.after_order_index, after_order_index: leg });
|
||||
}
|
||||
// Two legs that merged carry two sequence series side by side. Renumbered the way
|
||||
// the service does it: the earlier leg's points first, then by their old sequence,
|
||||
// then by id. Only a leg that received a via is touched.
|
||||
const byLeg = new Map<number, Array<Via & { was: number }>>();
|
||||
for (const via of kept) byLeg.set(via.after_order_index, [...(byLeg.get(via.after_order_index) ?? []), via]);
|
||||
for (const onLeg of byLeg.values()) {
|
||||
if (!onLeg.some((via) => landed.has(via.id))) continue;
|
||||
onLeg.sort((a, b) => a.was - b.was || a.sequence - b.sequence || a.id - b.id);
|
||||
onLeg.forEach((via, i) => {
|
||||
if (via.sequence !== i) setSequence.run(i, via.id);
|
||||
});
|
||||
const carried = carryVias(db, night.day_id, locatedIds(rows), locatedIds(next));
|
||||
if (carried) {
|
||||
roads.moved += carried.moved;
|
||||
roads.removed += carried.removed;
|
||||
}
|
||||
}
|
||||
if (roads.moved || roads.removed) {
|
||||
console.log(`[DB] Re-pinned ${roads.moved} drawn road(s) behind the reseated nights, dropped ${roads.removed} left without a leg`);
|
||||
}
|
||||
return seated;
|
||||
}
|
||||
|
||||
/**
|
||||
* The leg a via pinned behind the n-th stop of the old order is on in the new one
|
||||
* (AccommodationsService.legAfter). Null when no leg is left for it.
|
||||
*/
|
||||
function legAfter(index: number, previousIds: number[], nextIds: number[]): number | null {
|
||||
if (index > previousIds.length - 1) return null;
|
||||
let at = index;
|
||||
while (at >= 0 && !nextIds.includes(previousIds[at]!)) at -= 1;
|
||||
if (at < 0) return null;
|
||||
const next = nextIds.indexOf(previousIds[at]!);
|
||||
return next >= nextIds.length - 1 ? null : next;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user