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.
This commit is contained in:
Sven laptop
2026-09-24 20:18:32 +02:00
parent 2ae1720383
commit 7d55580d87
44 changed files with 7033 additions and 5343 deletions
+545
View File
@@ -0,0 +1,545 @@
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,
};