mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
fix(roadtrip): a check-in is a door opening, not an appointment
It is the hour a room becomes available. Reaching it later is reaching it; only a time somebody pinned to a stop can be missed. Read as an anchor it did the exact opposite of what it means: arrive at 16:14 at a hotel that checks in from 11:00 and the rail reported five hours fourteen late, then read that 11:00 as earlier than the 15:00 before it, took it for a new day and pushed the hotel onto tomorrow. The day it came from was left with one stop and dropped out of the list altogether, so the stop before it appeared to vanish. The schedule now takes it as a floor: waited for when the drive gets there first, and otherwise silent. Waiting still prints in ink, because the hour is one somebody named; arriving late to it prints as computed, because nothing was decided. Same shape of mistake as the check-out, from the other side. One says the earliest you may be somewhere, the other the latest you may still be — neither says when you drive.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AccommodationsService } from './accommodations.service';
|
||||
import { AssignmentsDomainModule } from '../assignments/assignments-domain.module';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { RealtimeModule } from '../realtime/realtime.module';
|
||||
|
||||
/**
|
||||
* The accommodations SERVICE, split from the controller/MCP/RPC surfaces (the
|
||||
* assignments-domain precedent). AccommodationsService itself never touches
|
||||
* PlacesService -- only AccommodationsMcp does, for the one tool that writes a
|
||||
* place and a stay together -- so the service can live in a module that stays
|
||||
* off the AccommodationsModule -> PlacesModule edge.
|
||||
*
|
||||
* That is what lets PlacesModule import this: deleting a place has to take the
|
||||
* nights booked at it with it, and the cascade behind a stay (its partner
|
||||
* booking, that booking's expense, the day stop it wrote) belongs in one place
|
||||
* rather than copied into the delete path.
|
||||
*/
|
||||
@Module({
|
||||
imports: [PermissionsModule, RealtimeModule, AssignmentsDomainModule],
|
||||
providers: [AccommodationsService],
|
||||
exports: [AccommodationsService],
|
||||
})
|
||||
export class AccommodationsDomainModule {}
|
||||
@@ -88,7 +88,8 @@ export function assembleRoadtrip({
|
||||
spills: [],
|
||||
schedule: computeSchedule(
|
||||
d.stops.map((s) => ({
|
||||
anchor: s.time ?? s.checkInTime ?? null,
|
||||
anchor: s.time ?? null,
|
||||
earliest: s.checkInTime ?? null,
|
||||
dwellMinutes: s.dwellMinutes,
|
||||
})),
|
||||
d.stops.slice(0, -1).map((s, i) => storedLegFor(s, d.stops[i + 1]!)?.seg.duration),
|
||||
|
||||
@@ -94,7 +94,8 @@ export function spillChains(plan: PlanDay[], quietDays: QuietDay[], legFor: LegL
|
||||
const legs = routed.map((l) => l?.seg);
|
||||
const schedule = computeSchedule(
|
||||
d.stops.map((s) => ({
|
||||
anchor: s.time ?? s.checkInTime ?? null,
|
||||
anchor: s.time ?? null,
|
||||
earliest: s.checkInTime ?? null,
|
||||
dwellMinutes: s.dwellMinutes,
|
||||
})),
|
||||
legs.map((l) => l?.duration),
|
||||
|
||||
@@ -847,3 +847,43 @@ describe('hasChosenArrival', () => {
|
||||
expect(hasChosenArrival({ time: '22:00', automaticNight: { phase: 'end' } })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a check-in is a door opening, not an appointment', () => {
|
||||
// The hour a room becomes available. Reaching it later is reaching it; only a time
|
||||
// somebody pinned to a stop can be missed. Read as an anchor it did the opposite:
|
||||
// arriving at 16:14 was reported as 5 h 14 late against an 11:00 check-in, and the
|
||||
// hotel was pushed onto the next day because 11:00 reads as earlier than the 15:00
|
||||
// before it.
|
||||
it('waits for it when the drive gets there first', () => {
|
||||
const schedule = computeSchedule(
|
||||
[{ anchor: '09:00', dwellMinutes: 0 }, { anchor: null, earliest: '15:00', dwellMinutes: 0 }],
|
||||
[3600],
|
||||
);
|
||||
expect(schedule.entries[1]!.arrival).toBe('15:00');
|
||||
// Waiting for a door is waiting for a time somebody named, so it is printed as one.
|
||||
expect(schedule.entries[1]!.anchored).toBe(true);
|
||||
expect(schedule.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('is simply arrived at when the drive gets there later', () => {
|
||||
const schedule = computeSchedule(
|
||||
[{ anchor: '15:00', dwellMinutes: 60 }, { anchor: null, earliest: '11:00', dwellMinutes: 60 }],
|
||||
[14 * 60],
|
||||
);
|
||||
expect(schedule.entries[1]!.arrival).toBe('16:14');
|
||||
// Nothing was decided here, so it reads as computed.
|
||||
expect(schedule.entries[1]!.anchored).toBe(false);
|
||||
// And nothing was missed.
|
||||
expect(schedule.warnings).toEqual([]);
|
||||
// Still the same day, which is what the day split reads.
|
||||
expect(schedule.entries[1]!.dayOffset).toBe(0);
|
||||
});
|
||||
|
||||
it('still reports a pinned time that cannot be made', () => {
|
||||
const schedule = computeSchedule(
|
||||
[{ anchor: '15:00', dwellMinutes: 60 }, { anchor: '11:00', dwellMinutes: 0 }],
|
||||
[14 * 60],
|
||||
);
|
||||
expect(schedule.warnings).toContainEqual({ index: 1, code: 'late', minutes: 314 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,8 +74,17 @@ export function formatClock(minutes: number): string {
|
||||
|
||||
export interface ScheduleStop {
|
||||
departureAt?: number;
|
||||
/** A time somebody fixed this stop to. The chain restarts from it, and arriving
|
||||
* after it is being late. */
|
||||
anchor: string | null;
|
||||
|
||||
/**
|
||||
* The earliest this stop can be entered, when something says so — a check-in is
|
||||
* the hour a room becomes available, not an appointment. Arriving before it means
|
||||
* waiting for it; arriving after it means arriving, with nothing to report.
|
||||
*/
|
||||
earliest?: string | null;
|
||||
|
||||
dwellMinutes: number | null;
|
||||
}
|
||||
|
||||
@@ -186,7 +195,16 @@ export function computeSchedule(
|
||||
for (let i = 0; i < stops.length; i++) {
|
||||
const stop = stops[i]!;
|
||||
const anchor = parseClock(stop.anchor);
|
||||
const { arrival, lateBy } = resolveArrival(anchor, cursor, dayOffset);
|
||||
const resolved = resolveArrival(anchor, cursor, dayOffset);
|
||||
// A door that opens at eleven is not an appointment at eleven. Reaching the stop
|
||||
// later than that is simply reaching it; only a time somebody pinned can be missed.
|
||||
const opens = parseClock(stop.earliest ?? null);
|
||||
const waited = anchor === null && opens !== null && resolved.arrival !== null
|
||||
? opens + Math.round((resolved.arrival - opens) / DAY_MINUTES) * DAY_MINUTES
|
||||
: null;
|
||||
const held = waited !== null && waited > resolved.arrival!;
|
||||
const arrival = held ? waited : resolved.arrival;
|
||||
const lateBy = resolved.lateBy;
|
||||
if (lateBy !== null) warnings.push({ index: i, code: 'late', minutes: lateBy });
|
||||
|
||||
if (arrival === null) {
|
||||
@@ -199,7 +217,9 @@ export function computeSchedule(
|
||||
if (offset > dayOffset) dayOffset = offset;
|
||||
|
||||
arrivals[i]! = arrival;
|
||||
anchored[i]! = anchor !== null;
|
||||
// Ink for a time somebody decided: a pinned one always, a check-in only where the
|
||||
// drive actually had to wait for it.
|
||||
anchored[i]! = anchor !== null || held;
|
||||
|
||||
const leg = legSeconds[i];
|
||||
cursor =
|
||||
|
||||
Reference in New Issue
Block a user