import { test, expect } from "@playwright/test"; import { API_URL, STATIONS, staffToken } from "../../fixtures/data"; import { UI_IDS } from "../../../apps/edr-passenger-api/test/fixtures/seed-ui"; /** * Track B — per-station check-in cutoff, driven by the backoffice route config API. Follows the * same convention as config-validation.spec.ts: hits the passenger-api directly with a staff * bearer token rather than driving the real DOM — the route-stop form has no data-testid hooks, * so browser automation here would be selector-fragile for no extra coverage value. * * Creates its OWN route (not the shared ROUTE_ID from seed-ui) since updateRoute deletes and * recreates all stops — mutating the shared fixture route would break every other spec that * depends on its distances/times staying stable for the whole suite run. */ function auth() { return { Authorization: `Bearer ${staffToken()}` }; } test("BC-11 ✅ travelMinutesToStop drives each stop's estimated arrival independently of route-wide departure", async ({ request }) => { const routeRes = await request.post(`${API_URL}/routes`, { headers: auth(), data: { code: `E2E-CUTOFF-${Date.now()}`, name: "E2E Check-in Cutoff Route", effectiveFrom: "2020-01-01T00:00:00Z", stops: [ { stationId: STATIONS.A, sequence: 1 }, { stationId: STATIONS.B, sequence: 2, travelMinutesToStop: 60 }, { stationId: STATIONS.C, sequence: 3, travelMinutesToStop: 40 }, ], }, }); expect(routeRes.ok()).toBeTruthy(); const route = (await routeRes.json())?.data ?? (await routeRes.json()); const routeId = route.id; // Reuse the seeded coach via a route coach template so schedule creation auto-assigns real // seats (createSchedule auto-applies any route coach template — see schedules.service.ts). const templateRes = await request.put(`${API_URL}/routes/${routeId}/coaches`, { headers: auth(), data: { coaches: [{ coachId: UI_IDS.coach, positionNumber: 1 }] }, }); expect(templateRes.ok()).toBeTruthy(); // dep only 5 min out: A's cutoff (dep - 30min default) is already ~25 min in the past by the // time this schedule is queried, but B's arrival (dep + 60min) keeps its own cutoff (arrival - // 30min default) about 35 min in the future — proving the two stations close independently. const dep = new Date(Date.now() + 5 * 60_000); const arr = new Date(dep.getTime() + 100 * 60_000); // A->B 60min + B->C 40min const scheduleRes = await request.post(`${API_URL}/schedules`, { headers: auth(), data: { trainId: UI_IDS.train, routeId, departureAt: dep.toISOString(), arrivalAt: arr.toISOString() }, }); expect(scheduleRes.ok()).toBeTruthy(); const schedule = (await scheduleRes.json())?.data ?? (await scheduleRes.json()); const getRes = await request.get(`${API_URL}/schedules/${schedule.id}`, { headers: auth() }); expect(getRes.ok()).toBeTruthy(); const full = (await getRes.json())?.data ?? (await getRes.json()); const stopTimes: any[] = full.stopTimes ?? []; const bStop = stopTimes.find((s) => s.stationId === STATIONS.B); const cStop = stopTimes.find((s) => s.stationId === STATIONS.C); expect(new Date(bStop.plannedArrivalAt).getTime()).toBe(dep.getTime() + 60 * 60_000); expect(new Date(cStop.plannedArrivalAt).getTime()).toBe(arr.getTime()); // last stop locked to overall arrival // A's own segment (no arrival — falls back to its departure) is already past its cutoff... const rejectRes = await request.post(`${API_URL}/seats/hold`, { headers: auth(), data: { scheduleId: schedule.id, originStationId: STATIONS.A, destinationStationId: STATIONS.B, passengers: [{ passengerId: "44444444-4444-4444-8444-444444444444", seatId: "00000000-0000-4000-8000-000000009999" }], }, }); expect(rejectRes.status()).toBe(400); expect((await rejectRes.json())?.message ?? "").toMatch(/cannot be held within/i); // ...while B, whose own arrival is comfortably later, remains independently bookable. const seatmapRes = await request.get(`${API_URL}/seats/seatmap/${schedule.id}`, { headers: auth() }); expect(seatmapRes.ok()).toBeTruthy(); const seatmap = (await seatmapRes.json())?.data ?? (await seatmapRes.json()); const seatId = seatmap.coaches?.[0]?.seats?.[0]?.id; expect(seatId).toBeTruthy(); const holdRes = await request.post(`${API_URL}/seats/hold`, { headers: auth(), data: { scheduleId: schedule.id, originStationId: STATIONS.B, destinationStationId: STATIONS.C, passengers: [{ passengerId: "55555555-5555-4555-8555-555555555555", seatId }], }, }); expect(holdRes.ok()).toBeTruthy(); await request.delete(`${API_URL}/schedules/${schedule.id}`, { headers: auth() }).catch(() => {}); await request.delete(`${API_URL}/routes/${routeId}?cascade=true`, { headers: auth() }).catch(() => {}); });