Files
TREK/client/src/utils/plannedPlaces.test.ts
T
Maurice 4d6820a4d4 @
fix(planner): route a layover flight per leg, and count linked places as planned

Two v4.0.0 reports, both a surface answering a question with less than it had.

The map built its own waypoint order. It read the BOOKING position and never
expanded metadata.legs, while a multi-leg booking stores its position per leg —
so a Frankfurt to Vancouver flight via Reykjavik was dropped from the list and
the airport before it was joined to the airport after it in one driving run
across the Atlantic. It now takes the order from getMergedItems, the same one
the day plan renders, which brings the span filter and the leg expansion with
it. getMergedItems in turn had never read day_plan_position, the one slot the
map did read, which is how the two could disagree at all.

getTransportRouteEndpoints handed back the booking outermost airports for every
leg. A stop between two legs was therefore routed from the arrival airport back
to the departure one — a drive from Vancouver to Frankfurt with a cafe in
Reykjavik on the way. A leg now spans its own pair, when the endpoints line up
one per stop with the legs; anything ambiguous keeps the role lookup.

Being planned meant one thing: a day_assignments row. A hotel is linked through
its stay and a venue through its booking, and neither is ever dragged onto a
day, so both sat under Unplanned while the day header printed their names. The
three places that asked the question — desktop sidebar, map markers, phone
browser — now ask one helper that also counts a stay and a day-anchored
booking. A booking that names no day still counts as unplanned, because nothing
says when it happens, and the day plan renders nothing for it either.
@
2026-08-27 19:21:06 +02:00

104 lines
4.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { plannedPlaceIds, plannedPlaceIdsForDay } from './plannedPlaces'
import type { AssignmentsMap, Day } from '../types'
// FE-UTIL-PLANNED-001 to FE-UTIL-PLANNED-010
//
// #2072 — a hotel linked to a booking sat under "Unplanned" while its name was
// printed in the day header, because being planned meant one thing only: a
// day_assignments row.
const assignments = (map: Record<string, number[]>): AssignmentsMap =>
Object.fromEntries(
Object.entries(map).map(([dayId, ids]) => [dayId, ids.map((id, i) => ({
id: 900 + id, day_id: Number(dayId), order_index: i, place: { id },
}))]),
) as unknown as AssignmentsMap
const DAYS = [
{ id: 10, day_number: 1 }, { id: 11, day_number: 2 }, { id: 12, day_number: 3 }, { id: 13, day_number: 4 },
] as unknown as Day[]
describe('plannedPlaceIds', () => {
it('FE-UTIL-PLANNED-001: an assigned place is planned, as it always was', () => {
expect([...plannedPlaceIds({ assignments: assignments({ 10: [1, 2] }) })]).toEqual([1, 2])
})
it('FE-UTIL-PLANNED-002: a stay plans its place without any assignment', () => {
const ids = plannedPlaceIds({
assignments: assignments({}),
accommodations: [{ place_id: 7, start_day_id: 10, end_day_id: 12 }],
})
expect(ids.has(7)).toBe(true)
})
it('FE-UTIL-PLANNED-003: a stay whose place was deleted contributes nothing', () => {
const ids = plannedPlaceIds({
assignments: assignments({}),
accommodations: [{ place_id: null, start_day_id: 10, end_day_id: 12 }],
})
expect(ids.size).toBe(0)
})
it('FE-UTIL-PLANNED-004: a booking on a day plans the place it points at', () => {
const ids = plannedPlaceIds({
assignments: assignments({}),
reservations: [{ place_id: 5, day_id: 10 }],
})
expect(ids.has(5)).toBe(true)
})
// The boundary the issue itself calls out: a booking that names no day is not
// on the plan, and the day view renders nothing for it either.
it('FE-UTIL-PLANNED-005: a booking with no day plans nothing', () => {
const ids = plannedPlaceIds({
assignments: assignments({}),
reservations: [{ place_id: 5, day_id: null }],
})
expect(ids.size).toBe(0)
})
it('FE-UTIL-PLANNED-006: a place reachable two ways is counted once', () => {
const ids = plannedPlaceIds({
assignments: assignments({ 10: [7] }),
accommodations: [{ place_id: 7, start_day_id: 10, end_day_id: 12 }],
reservations: [{ place_id: 7, day_id: 10 }],
})
expect([...ids]).toEqual([7])
})
it('FE-UTIL-PLANNED-007: no sources at all is an empty set, not a throw', () => {
expect(plannedPlaceIds({ assignments: {} as AssignmentsMap }).size).toBe(0)
})
})
describe('plannedPlaceIdsForDay', () => {
it('FE-UTIL-PLANNED-008: a stay covers the nights in between, not just its two ends', () => {
const sources = {
assignments: assignments({}),
accommodations: [{ place_id: 7, start_day_id: 10, end_day_id: 12 }],
}
expect(plannedPlaceIdsForDay(10, DAYS, sources).has(7)).toBe(true)
expect(plannedPlaceIdsForDay(11, DAYS, sources).has(7)).toBe(true)
expect(plannedPlaceIdsForDay(12, DAYS, sources).has(7)).toBe(true)
expect(plannedPlaceIdsForDay(13, DAYS, sources).has(7)).toBe(false)
})
// The day rows are the only thing that can order a stay. Without them the
// planner falls back to the id range rather than dropping the middle nights.
it('FE-UTIL-PLANNED-009: an unloaded day list still keeps the middle of a stay', () => {
const sources = {
assignments: assignments({}),
accommodations: [{ place_id: 7, start_day_id: 10, end_day_id: 12 }],
}
expect(plannedPlaceIdsForDay(11, [], sources).has(7)).toBe(true)
expect(plannedPlaceIdsForDay(13, [], sources).has(7)).toBe(false)
})
it('FE-UTIL-PLANNED-010: a booking counts only on the day it names', () => {
const sources = { assignments: assignments({}), reservations: [{ place_id: 5, day_id: 11 }] }
expect(plannedPlaceIdsForDay(11, DAYS, sources).has(5)).toBe(true)
expect(plannedPlaceIdsForDay(10, DAYS, sources).has(5)).toBe(false)
})
})