const db = require("../db"); const { reportError } = require("../lib/report-error"); // Bus-line repository methods (PostgreSQL mode). // The client-based methods keep their original `db.pool.connect()` call before // the try block, preserving the exact original control flow (including the // connection/rollback/release dance) for byte-identical behavior. async function listLineStops(req) { try { const { busLineId } = req.params; const result = await db.query( ` SELECT rs.id, rs.stop_order, s.name as stop_name, s.latitude, s.longitude, rs.estimated_arrival_minutes FROM route_stops rs JOIN stops s ON s.id = rs.stop_id WHERE rs.bus_line_id = $1 ORDER BY rs.stop_order ASC `, [busLineId], ); const stops = result.rows.map((row) => ({ id: row.id, order: row.stop_order, name: row.stop_name, latitude: parseFloat(row.latitude), longitude: parseFloat(row.longitude), estimatedArrivalMinutes: row.estimated_arrival_minutes, })); return { statusCode: 200, body: { stops } }; } 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 listLines(req) { try { const result = await db.query(` SELECT id, line_number, start_stop, end_stop, estimated_duration_minutes, description FROM bus_lines ORDER BY line_number ASC, id ASC `); return { statusCode: 200, body: { busLines: result.rows.map((row) => ({ id: row.id, lineNumber: row.line_number, startStop: row.start_stop, endStop: row.end_stop, estimatedDurationMinutes: row.estimated_duration_minutes, description: row.description, })), }, }; } 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 createLine(req) { try { const { lineNumber, startStop, endStop, estimatedDurationMinutes, description, } = req.body; if ( lineNumber === undefined || !startStop || !endStop || estimatedDurationMinutes === undefined ) { return { statusCode: 400, body: { error: "missing_required_fields" } }; } const result = await db.query( ` WITH next_id AS ( SELECT COALESCE(MAX(id), 0) + 1 AS id FROM bus_lines ) INSERT INTO bus_lines (id, line_number, start_stop, end_stop, estimated_duration_minutes, description) SELECT next_id.id, $1, $2, $3, $4, $5 FROM next_id RETURNING id, line_number, start_stop, end_stop, estimated_duration_minutes, description `, [ lineNumber, startStop, endStop, estimatedDurationMinutes, description || null, ], ); const busLine = result.rows[0]; return { statusCode: 201, body: { busLine: { id: busLine.id, lineNumber: busLine.line_number, startStop: busLine.start_stop, endStop: busLine.end_stop, estimatedDurationMinutes: busLine.estimated_duration_minutes, description: busLine.description, }, }, }; } 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 updateLine(req) { try { const { busLineId } = req.params; const { lineNumber, startStop, endStop, estimatedDurationMinutes, description, } = req.body; const result = await db.query( ` UPDATE bus_lines SET line_number = COALESCE($1, line_number), start_stop = COALESCE($2, start_stop), end_stop = COALESCE($3, end_stop), estimated_duration_minutes = COALESCE($4, estimated_duration_minutes), description = COALESCE($5, description) WHERE id = $6 RETURNING id, line_number, start_stop, end_stop, estimated_duration_minutes, description `, [ lineNumber ?? null, startStop ?? null, endStop ?? null, estimatedDurationMinutes ?? null, description ?? null, busLineId, ], ); if (!result.rows[0]) return { statusCode: 404, body: { error: "bus_line_not_found" } }; const busLine = result.rows[0]; return { statusCode: 200, body: { busLine: { id: busLine.id, lineNumber: busLine.line_number, startStop: busLine.start_stop, endStop: busLine.end_stop, estimatedDurationMinutes: busLine.estimated_duration_minutes, description: busLine.description, }, }, }; } 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 listLineStopsDetailed(req) { try { const { busLineId } = req.params; if (!busLineId || !Number.isFinite(Number(busLineId))) { return { statusCode: 400, body: { error: "invalid_bus_line_id" } }; } const result = await db.query( ` SELECT rs.id, rs.bus_line_id, rs.stop_id, rs.stop_order, rs.estimated_arrival_minutes, s.name as stop_name, s.latitude, s.longitude FROM route_stops rs JOIN stops s ON s.id = rs.stop_id WHERE rs.bus_line_id = $1 ORDER BY rs.stop_order ASC `, [busLineId], ); return { statusCode: 200, body: { routeStops: result.rows.map((row) => ({ id: row.id, busLineId: row.bus_line_id, stopId: row.stop_id, stopOrder: row.stop_order, estimatedArrivalMinutes: row.estimated_arrival_minutes, name: row.stop_name, latitude: parseFloat(row.latitude), longitude: parseFloat(row.longitude), })), }, }; } 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 addLineStop(req) { try { const { busLineId } = req.params; const { stopId, stopOrder, estimatedArrivalMinutes } = req.body; if (!stopId || stopOrder === undefined) { return { statusCode: 400, body: { error: "missing_required_fields" } }; } const result = await db.query( ` WITH next_id AS ( SELECT COALESCE(MAX(id), 0) + 1 AS id FROM route_stops ) INSERT INTO route_stops (id, bus_line_id, stop_id, stop_order, estimated_arrival_minutes) SELECT next_id.id, $1, $2, $3, $4 FROM next_id ON CONFLICT (bus_line_id, stop_order) DO UPDATE SET stop_id = EXCLUDED.stop_id, estimated_arrival_minutes = EXCLUDED.estimated_arrival_minutes, updated_at = NOW() RETURNING id, bus_line_id, stop_id, stop_order, estimated_arrival_minutes, created_at, updated_at `, [busLineId, stopId, stopOrder, estimatedArrivalMinutes ?? null], ); const link = result.rows[0]; return { statusCode: 201, body: { routeStop: { id: link.id, busLineId: link.bus_line_id, stopId: link.stop_id, stopOrder: link.stop_order, estimatedArrivalMinutes: link.estimated_arrival_minutes, createdAt: link.created_at, updatedAt: link.updated_at, }, }, }; } 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 reorderLineStops(req) { const client = await db.pool.connect(); try { const { busLineId } = req.params; const { routeStopIds } = req.body || {}; if (!Array.isArray(routeStopIds) || routeStopIds.length === 0) { return { statusCode: 400, body: { error: "routeStopIds array required" } }; } const normalizedIds = routeStopIds .map((id) => Number(id)) .filter((id) => Number.isFinite(id)); if (normalizedIds.length !== routeStopIds.length) { return { statusCode: 400, body: { error: "routeStopIds must contain only numeric ids" } }; } const current = await client.query( `SELECT id FROM route_stops WHERE bus_line_id = $1 ORDER BY stop_order ASC`, [busLineId], ); const currentIds = current.rows.map((row) => Number(row.id)); const sameCount = currentIds.length === normalizedIds.length; const sameIds = sameCount && normalizedIds.every((id) => currentIds.includes(id)); if (!sameIds) { return { statusCode: 400, body: { error: "routeStopIds must match current bus line stops" } }; } await client.query("BEGIN"); for (let index = 0; index < normalizedIds.length; index += 1) { await client.query( `UPDATE route_stops SET stop_order = $1, updated_at = NOW() WHERE id = $2 AND bus_line_id = $3`, [-(index + 1), normalizedIds[index], busLineId], ); } for (let index = 0; index < normalizedIds.length; index += 1) { await client.query( `UPDATE route_stops SET stop_order = $1, updated_at = NOW() WHERE id = $2 AND bus_line_id = $3`, [index + 1, normalizedIds[index], busLineId], ); } const result = await client.query( `SELECT rs.id, rs.stop_order, s.name as stop_name, s.latitude, s.longitude, rs.estimated_arrival_minutes FROM route_stops rs JOIN stops s ON s.id = rs.stop_id WHERE rs.bus_line_id = $1 ORDER BY rs.stop_order ASC`, [busLineId], ); await client.query("COMMIT"); return { statusCode: 200, body: { routeStops: result.rows.map((row) => ({ id: row.id, order: row.stop_order, name: row.stop_name, latitude: parseFloat(row.latitude), longitude: parseFloat(row.longitude), estimatedArrivalMinutes: row.estimated_arrival_minutes, })), }, }; } catch (err) { await client.query("ROLLBACK").catch(() => {}); console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } finally { client.release(); } } async function deleteLine(req) { const client = await db.pool.connect(); try { const { busLineId } = req.params; if (!busLineId || !Number.isFinite(Number(busLineId))) { return { statusCode: 400, body: { error: "invalid_bus_line_id" } }; } await client.query("BEGIN"); // Delete associated route_stops first await client.query( `DELETE FROM route_stops WHERE bus_line_id = $1`, [busLineId], ); // Delete the bus line const result = await client.query( `DELETE FROM bus_lines WHERE id = $1 RETURNING id, line_number`, [busLineId], ); if (result.rows.length === 0) { await client.query("ROLLBACK"); return { statusCode: 404, body: { error: "bus_line_not_found" } }; } await client.query("COMMIT"); return { statusCode: 200, body: { success: true, busLine: result.rows[0] } }; } catch (err) { await client.query("ROLLBACK").catch(() => {}); console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } finally { client.release(); } } async function deleteLineStop(req) { const client = await db.pool.connect(); try { const { busLineId, routeStopId } = req.params; if (!busLineId || !Number.isFinite(Number(busLineId))) { return { statusCode: 400, body: { error: "invalid_bus_line_id" } }; } if (!routeStopId || !Number.isFinite(Number(routeStopId))) { return { statusCode: 400, body: { error: "invalid_route_stop_id" } }; } await client.query("BEGIN"); // Find and delete the route_stop const result = await client.query( `DELETE FROM route_stops WHERE id = $1 AND bus_line_id = $2 RETURNING id, stop_order`, [routeStopId, busLineId], ); if (result.rows.length === 0) { await client.query("ROLLBACK"); return { statusCode: 404, body: { error: "route_stop_not_found" } }; } const deletedOrder = result.rows[0].stop_order; // Re-order remaining stops await client.query( `UPDATE route_stops SET stop_order = stop_order - 1, updated_at = NOW() WHERE bus_line_id = $1 AND stop_order > $2`, [busLineId, deletedOrder], ); await client.query("COMMIT"); return { statusCode: 200, body: { success: true, routeStop: result.rows[0] } }; } catch (err) { await client.query("ROLLBACK").catch(() => {}); console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } finally { client.release(); } } module.exports = { listLineStops, listLines, createLine, updateLine, listLineStopsDetailed, addLineStop, reorderLineStops, deleteLine, deleteLineStop, };