Files
Sven laptop 7d55580d87 Refactor transit_api into a repo/routes module split
Split the 5,441-line index.js into an 86-line bootstrap, thin domain
route modules, shared lib/ helpers, and a repo/ layer that isolates the
DEV_MOCK branches (repo/*.mock.js). Repo methods return {statusCode, body};
routes are one-line wrappers. Pin search_path to "transit.bus" and
document the /bus/ prefix with legacy root aliases (Deprecation header).

Parity verified: 57/57 byte-identical on the stub differential harness
plus 24/24 live read-only responses vs the original against real
PostgreSQL. The only intentional behavior change: mock reorder/delete
bus-line endpoints no longer require a live DB (previously 500 in
DEV_MOCK=1 without PG). DEV_MOCK gate and node --check clean.
2026-09-24 20:18:32 +02:00

545 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { store: _mock } = require("../mock/store");
const { toDateKey, expandRecurringScheduleRows, scheduleMatchesFilters, parseScheduleFilters, mapRecurringOccurrenceRows, materializeScheduleOverviewRow } = require("../lib/schedule");
const { reportError } = require("../lib/report-error");
// Schedules repository methods (DEV_MOCK mode). Exact copies of the original
// handlers’ mock branches, returning { statusCode, body }. Note: the markStopPassed
// mock branch reproduces the original ReferenceError (it reads an undeclared
// `delayMinutes`), so it always returns 500 in mock mode, exactly as before.
async function listSchedules(req) {
try {
const filters = parseScheduleFilters(req.query, {
statuses: ["planned", "active"],
});
const routeStopsByLine = new Map();
for (const routeStop of _mock.routeStops || []) {
const busLineId = Number(routeStop.busLineId);
if (!routeStopsByLine.has(busLineId)) {
routeStopsByLine.set(busLineId, []);
}
routeStopsByLine.get(busLineId).push({
stop_id: routeStop.stopId,
stop_order: routeStop.stopOrder,
});
}
const schedules = expandRecurringScheduleRows(
(_mock.schedules || []).map((s) => {
const busLine = _mock.busLines.find((bl) => bl.id === s.busLineId);
const bus = _mock.fleetBuses.find((b) => b.id === s.busId);
return {
id: s.id,
busLineId: s.busLineId,
busId: s.busId,
startTime: s.startTime,
endTime: s.endTime,
weekdays: s.weekdays || [],
status: s.status,
lineNumber: busLine?.lineNumber || 0,
busName: bus?.name || "Unknown Bus",
driverId: null,
estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60,
};
}),
).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine));
return { statusCode: 200, body: { schedules } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function getSchedulesOverview(req) {
try {
const filters = parseScheduleFilters(req.query, {
statuses: ["planned", "active"],
});
const overviewDateKey = filters.date || null;
const routeStopsByLine = new Map();
for (const routeStop of _mock.routeStops || []) {
const busLineId = Number(routeStop.busLineId);
if (!routeStopsByLine.has(busLineId)) {
routeStopsByLine.set(busLineId, []);
}
routeStopsByLine.get(busLineId).push({
bus_line_id: busLineId,
stop_id: routeStop.stopId,
stop_order: routeStop.stopOrder,
estimated_arrival_minutes: routeStop.estimatedArrivalMinutes,
stop_name: routeStop.name || "",
});
}
const baseRows = (_mock.schedules || []).map((schedule) => {
const busLine = _mock.busLines.find(
(line) => Number(line.id) === Number(schedule.busLineId),
);
const bus = _mock.fleetBuses.find(
(vehicle) => Number(vehicle.id) === Number(schedule.busId),
);
const driver = _mock.drivers.find(
(person) => Number(person.id) === Number(schedule.driverId),
);
return {
id: schedule.id,
busLineId: schedule.busLineId,
busId: schedule.busId,
driverId: schedule.driverId ?? null,
driverName: driver ? driver.name : null,
startTime: schedule.startTime,
endTime: schedule.endTime,
weekdays: schedule.weekdays || [],
status: schedule.status,
lineNumber: busLine?.lineNumber || 0,
busName: bus?.name || "Unknown Bus",
licensePlate: bus?.licensePlate || "",
createdAt: schedule.createdAt,
};
});
const schedules = mapRecurringOccurrenceRows(
baseRows,
overviewDateKey,
routeStopsByLine,
).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine));
return {
statusCode: 200,
body: {
date: overviewDateKey,
summary: {
total: schedules.length,
recurring: schedules.filter(
(schedule) => schedule.scheduleType === "recurring",
).length,
oneTime: schedules.filter(
(schedule) => schedule.scheduleType === "one_time",
).length,
},
schedules,
},
};
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function listRecurringSchedules(req) {
try {
const filters = parseScheduleFilters(req.query);
const routeStopsByLine = new Map();
for (const routeStop of _mock.routeStops || []) {
const busLineId = Number(routeStop.busLineId);
if (!routeStopsByLine.has(busLineId)) {
routeStopsByLine.set(busLineId, []);
}
routeStopsByLine.get(busLineId).push({
bus_line_id: busLineId,
stop_id: routeStop.stopId,
stop_order: routeStop.stopOrder,
estimated_arrival_minutes: routeStop.estimatedArrivalMinutes,
stop_name: routeStop.name || "",
});
}
const baseRows = (_mock.schedules || [])
.filter(
(schedule) =>
Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0,
)
.map((schedule) => {
const busLine = _mock.busLines.find(
(line) => Number(line.id) === Number(schedule.busLineId),
);
const bus = _mock.fleetBuses.find(
(vehicle) => Number(vehicle.id) === Number(schedule.busId),
);
const driver = _mock.drivers.find(
(person) => Number(person.id) === Number(schedule.driverId),
);
return {
id: schedule.id,
busLineId: schedule.busLineId,
busId: schedule.busId,
driverId: schedule.driverId ?? null,
driverName: driver ? driver.name : null,
startTime: schedule.startTime,
endTime: schedule.endTime,
weekdays: schedule.weekdays || [],
lineNumber: busLine?.lineNumber || 0,
busName: bus?.name || "Unknown Bus",
licensePlate: bus?.licensePlate || "",
createdAt: schedule.createdAt,
};
});
const schedules = baseRows
.filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine))
.map((row) => {
const materialized = materializeScheduleOverviewRow(
row,
null,
routeStopsByLine,
);
return {
...materialized,
status: undefined,
};
});
return { statusCode: 200, body: { schedules } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function getScheduleById(req) {
try {
const { scheduleId } = req.params;
const schedule = (_mock.schedules || []).find(
(s) => Number(s.id) === Number(scheduleId),
);
if (!schedule) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const busLine = _mock.busLines.find((bl) => bl.id === schedule.busLineId);
const bus = _mock.fleetBuses.find((b) => b.id === schedule.busId);
return {
statusCode: 200,
body: {
schedule: {
id: schedule.id,
busLineId: schedule.busLineId,
busId: schedule.busId,
startTime: schedule.startTime,
endTime: schedule.endTime,
weekdays: schedule.weekdays || [],
status: schedule.status,
lineNumber: busLine?.lineNumber || 0,
busName: bus?.name || "Unknown Bus",
},
},
};
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function getScheduleStops(req) {
try {
const { scheduleId } = req.params;
const schedule = (_mock.schedules || []).find(
(s) => Number(s.id) === Number(scheduleId),
);
if (!schedule) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const routeStops = (_mock.routeStops || [])
.filter((rs) => Number(rs.busLineId) === Number(schedule.busLineId))
.sort((a, b) => a.stopOrder - b.stopOrder);
const formattedStops = routeStops.map((rs) => {
const stop = (_mock.stops || []).find(
(s) => Number(s.id) === Number(rs.stopId),
);
return {
id: rs.id,
order: rs.stopOrder,
name: stop?.name || rs.name || "Unknown Stop",
latitude: stop?.latitude ?? rs.latitude ?? 0,
longitude: stop?.longitude ?? rs.longitude ?? 0,
estimatedArrivalMinutes: rs.estimatedArrivalMinutes || 0,
};
});
return { statusCode: 200, body: { stops: formattedStops } };
} catch (err) {
console.error(err);
reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function markStopPassed(req) {
try {
const scheduleId = Number(req.params.scheduleId);
const stopOrder = Number(req.params.stopOrder);
if (
!Number.isFinite(scheduleId) ||
!Number.isFinite(stopOrder) ||
stopOrder < 1
) {
return {
statusCode: 400,
body: { error: "invalid_schedule_or_stop_order" },
};
}
const actualPassedAt = req.body?.actualPassedAt
? new Date(req.body.actualPassedAt)
: new Date();
if (Number.isNaN(actualPassedAt.getTime())) {
return { statusCode: 400, body: { error: "invalid_actual_passed_at" } };
}
const schedule = (_mock.schedules || []).find(
(s) => Number(s.id) === scheduleId,
);
if (!schedule) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const routeStop = (_mock.routeStops || []).find(
(rs) =>
Number(rs.busLineId) === Number(schedule.busLineId) &&
Number(rs.stopOrder) === stopOrder,
);
if (!routeStop) {
return { statusCode: 404, body: { error: "stop_not_found_for_schedule" } };
}
const stop = (_mock.stops || []).find(
(s) => Number(s.id) === Number(routeStop.stopId),
);
const occurrenceDateKey =
req.query.date || req.body.date || toDateKey(schedule.startTime);
const templateStart = new Date(schedule.startTime);
const scheduledPassedAt = new Date(`${occurrenceDateKey}T00:00:00`);
scheduledPassedAt.setHours(
templateStart.getHours(),
templateStart.getMinutes(),
templateStart.getSeconds(),
templateStart.getMilliseconds(),
);
scheduledPassedAt.setMinutes(
scheduledPassedAt.getMinutes() +
(Number(routeStop.estimatedArrivalMinutes) || 0),
);
const existingIndex = (_mock.scheduleStopProgress || []).findIndex(
(p) =>
Number(p.scheduleId) === scheduleId &&
Number(p.stopOrder) === stopOrder,
);
const progressRow = {
id:
existingIndex >= 0
? _mock.scheduleStopProgress[existingIndex].id
: _mock.nextScheduleStopProgressId++,
scheduleId,
busLineId: Number(schedule.busLineId),
stopId: Number(routeStop.stopId),
stopOrder,
stopName: stop?.name || routeStop.name || "Unknown Stop",
estimatedArrivalMinutes:
Number(routeStop.estimatedArrivalMinutes) || 0,
scheduledPassedAt: scheduledPassedAt.toISOString(),
scheduledPassedDate: occurrenceDateKey,
actualPassedAt: actualPassedAt.toISOString(),
delayMinutes,
markedByUserId: Number(req.user?.userId) || null,
createdAt: new Date().toISOString(),
};
if (existingIndex >= 0) {
_mock.scheduleStopProgress[existingIndex] = progressRow;
} else {
_mock.scheduleStopProgress.push(progressRow);
}
const totalStops = (_mock.routeStops || []).filter(
(rs) => Number(rs.busLineId) === Number(schedule.busLineId),
).length;
const passedCount = (_mock.scheduleStopProgress || []).filter(
(p) =>
Number(p.scheduleId) === scheduleId &&
p.scheduledPassedDate === occurrenceDateKey,
).length;
if (passedCount >= totalStops) {
schedule.status = "completed";
schedule.endTime = actualPassedAt.toISOString();
} else if (schedule.status === "planned") {
schedule.status = "in_progress";
}
return {
statusCode: 200,
body: {
scheduleId,
stopOrder,
status: schedule.status,
delayMinutes,
passedStops: passedCount,
totalStops,
occurrenceDate: occurrenceDateKey,
},
};
} catch (err) {
console.error(
"POST /driver/schedules/:scheduleId/stops/:stopOrder/passed error:",
err,
);
reportError({ partofservice: "POST /driver/schedules/:scheduleId/stops/:stopOrder/passed", error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function getPublicScheduleProgress(req) {
try {
const scheduleId = Number(req.params.scheduleId);
if (!Number.isFinite(scheduleId)) {
return { statusCode: 400, body: { error: "invalid_schedule_id" } };
}
const schedule = (_mock.schedules || []).find(
(s) => Number(s.id) === scheduleId,
);
if (!schedule) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const busLine = (_mock.busLines || []).find(
(bl) => Number(bl.id) === Number(schedule.busLineId),
);
const routeStops = (_mock.routeStops || [])
.filter((rs) => Number(rs.busLineId) === Number(schedule.busLineId))
.sort((a, b) => Number(a.stopOrder) - Number(b.stopOrder));
const mockProgressDate =
req.query.date || req.query.occurrenceDate || null;
let progressEvents = (_mock.scheduleStopProgress || []).filter(
(p) => Number(p.scheduleId) === scheduleId,
);
if (mockProgressDate) {
progressEvents = progressEvents.filter(
(p) => p.scheduledPassedDate === mockProgressDate,
);
}
progressEvents = progressEvents.sort(
(a, b) => Number(a.stopOrder) - Number(b.stopOrder),
);
const progressByOrder = new Map(
progressEvents.map((p) => [Number(p.stopOrder), p]),
);
const latestProgress =
progressEvents.length > 0
? progressEvents[progressEvents.length - 1]
: null;
const currentDelayMinutes = latestProgress
? Number(latestProgress.delayMinutes) || 0
: 0;
const occurrenceDateKey =
req.query.date ||
req.query.occurrenceDate ||
toDateKey(schedule.startTime);
const stops = routeStops.map((rs) => {
const stop = (_mock.stops || []).find(
(s) => Number(s.id) === Number(rs.stopId),
);
const templateStart = new Date(schedule.startTime);
const scheduledTime = new Date(`${occurrenceDateKey}T00:00:00`);
scheduledTime.setHours(
templateStart.getHours(),
templateStart.getMinutes(),
templateStart.getSeconds(),
templateStart.getMilliseconds(),
);
scheduledTime.setMinutes(
scheduledTime.getMinutes() +
(Number(rs.estimatedArrivalMinutes) || 0),
);
const progress = progressByOrder.get(Number(rs.stopOrder));
return {
stopId: Number(rs.stopId),
stopOrder: Number(rs.stopOrder),
stopName: stop?.name || rs.name || "Unknown Stop",
estimatedArrivalMinutes: Number(rs.estimatedArrivalMinutes) || 0,
scheduledPassedAt: scheduledTime.toISOString(),
actualPassedAt: progress?.actualPassedAt || null,
delayMinutes: progress ? Number(progress.delayMinutes) || 0 : null,
status: progress ? "passed" : "pending",
};
});
const passedStops = stops.filter((s) => s.status === "passed");
const nextStop = stops.find((s) => s.status === "pending") || null;
const driverState =
passedStops.length === 0
? "not_started"
: passedStops.length >= stops.length
? "completed"
: "en_route";
return {
statusCode: 200,
body: {
schedule: {
id: scheduleId,
lineNumber: Number(busLine?.lineNumber || 0),
startStop: busLine?.startStop || null,
endStop: busLine?.endStop || null,
startTime: schedule.startTime,
endTime: schedule.endTime,
status: schedule.status,
},
driverState,
currentDelayMinutes,
passedStopCount: passedStops.length,
totalStopCount: stops.length,
nextStop: nextStop
? {
...nextStop,
predictedPassedAt: new Date(
new Date(nextStop.scheduledPassedAt).getTime() +
currentDelayMinutes * 60000,
).toISOString(),
}
: null,
stops,
updatedAt: latestProgress?.actualPassedAt || null,
},
};
} catch (err) {
console.error("GET /public/schedules/:scheduleId/progress error:", err);
reportError({ partofservice: "GET /public/schedules/:scheduleId/progress", error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
module.exports = {
listSchedules,
getSchedulesOverview,
listRecurringSchedules,
getScheduleById,
getScheduleStops,
markStopPassed,
getPublicScheduleProgress,
};