ABIANAPP_NODE_PRODUCCION/src/controllers/tripsController.js
2026-04-13 11:27:30 +02:00

3623 lines
125 KiB
JavaScript

const db = require('../config/db');
const fs = require('fs');
const path = require('path');
const tripIncidenceMailer = require('../services/tripIncidenceMailer');
const {
collectUploadedTripStatusFiles,
removeUploadedTripStatusFiles,
MAX_TRIP_STATUS_FILES
} = require('../middleware/tripStatusUpload');
const ACTIVE_TRIP_MIN_STATE = 2;
const ACTIVE_TRIP_MAX_STATE = 6;
const ASSIGNED_TRIP_STATE = 1;
const START_TRIP_TARGET_STATE = 2;
const STATES_CATALOG_MIN_STATE = 2;
const STATES_CATALOG_MAX_STATE = 7;
const LEGACY_STATUS_PHOTO_FIELD_MAX_LENGTH = 100;
const LEGACY_INTERMEDIATE_POINT_VALUE_SEPARATOR = ':|:';
const LEGACY_INTERMEDIATE_POINT_REFERENCE_REGEX = /^[0-9]+$/;
const MOBILE_TRIPS_ALLOWED_STATES = [7, 8, 9, 1];
const CLEAR_STATUS_FALLBACK_STATE = 1;
const INTERMEDIATE_POINT_ALLOWED_STATES = [3, 4, 5];
const INTERMEDIATE_POINT_ALLOWED_STATES_SET = new Set(INTERMEDIATE_POINT_ALLOWED_STATES);
const SQL_DATETIME_REGEX = /^(\d{4})-(\d{2})-(\d{2}) ([0-2]\d):([0-5]\d):([0-5]\d)$/;
const FAILED_TRIP_STATE = 9;
const INTERMEDIATE_POINT_STATUS_IDS = new Set([3, 4, 5]);
const INCIDENCE_TEXT_CONTROL_CHARACTERS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
const GLOBAL_STATUS_KEYS_BY_STATE_ID = new Map([
[1, 'assigned'],
[2, 'en_camino'],
[3, 'positioned'],
[4, 'loading'],
[5, 'transit'],
[6, 'arrival'],
[7, 'closed']
]);
const INTERMEDIATE_STATUS_KEY_SUFFIX_BY_STATE_ID = new Map([
[3, 'positioned'],
[4, 'loading_unloading'],
[5, 'transit']
]);
const appendTripStatusDebugLog = (payload) => {
if (process.env.TRIP_STATUS_DEBUG_LOGS === '0') {
return;
}
console.info('[TripStatusDebug]', payload);
};
const getTripStatusUpdatesLogPath = () =>
process.env.TRIP_STATUS_UPDATES_LOG_PATH ||
'/var/log/status.log';
const formatLogDateTime = (dateValue) => {
const year = dateValue.getFullYear();
const month = String(dateValue.getMonth() + 1).padStart(2, '0');
const day = String(dateValue.getDate()).padStart(2, '0');
const hour = String(dateValue.getHours()).padStart(2, '0');
const minute = String(dateValue.getMinutes()).padStart(2, '0');
const second = String(dateValue.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
};
const appendTripStatusUpdateLog = (payload) => {
const now = new Date();
const logPath = getTripStatusUpdatesLogPath();
const entry = {
timestamp: now.toISOString(),
date_time: formatLogDateTime(now),
...payload
};
fs.promises
.mkdir(path.dirname(logPath), { recursive: true })
.then(() => fs.promises.appendFile(logPath, `${JSON.stringify(entry)}\n`))
.catch((error) => {
console.error('Failed to append trip status update log:', {
message: error.message
});
});
};
const appendIntermediatePointStatusUpdateLog = ({
updateType = 'manual',
flow,
operation,
requestId,
success,
result,
httpStatus,
errorCode,
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido,
valorWritten,
fotoWritten,
fechaFotoWritten,
valorCleared,
fotoCleared,
actualizadoAutomaticamente
}) => {
appendTripStatusUpdateLog({
update_type: updateType,
flow,
operation,
success,
result,
http_status: httpStatus,
error_code: errorCode || null,
request_id: requestId || null,
trip_id: Number.isInteger(tripId) ? tripId : null,
id_punto: Number.isInteger(idPunto) ? idPunto : null,
id_estado: Number.isInteger(idEstado) ? idEstado : null,
previous_intermediate_status_id: Number.isInteger(previousIntermediateStatusId)
? previousIntermediateStatusId
: null,
new_intermediate_status_id: Number.isInteger(newIntermediateStatusId)
? newIntermediateStatusId
: null,
user_id: userId || null,
dni: dni || null,
fecha_y_hora: fechaYHora || null,
latitud: latitud ?? null,
longitud: longitud ?? null,
ind_fallido: Number.isInteger(indFallido) ? indFallido : null,
valor_written: valorWritten ?? null,
foto_written: fotoWritten ?? null,
fecha_foto_written: fechaFotoWritten || null,
valor_cleared: typeof valorCleared === 'boolean' ? valorCleared : null,
foto_cleared: typeof fotoCleared === 'boolean' ? fotoCleared : null,
actualizado_automaticamente:
Number.isInteger(actualizadoAutomaticamente) ? actualizadoAutomaticamente : null
});
};
const ACTIVE_TRIP_QUERY = fs
.readFileSync(
path.resolve(__dirname, 'query_Active_Trip_en_curso_y_asignados_proximos.sql'),
'utf8'
)
.trim();
const normalizeLegacyPhotoReferences = (rawReferences) =>
String(rawReferences || '')
.split(';')
.map((item) => item.trim())
.filter(Boolean)
.join(';')
.slice(0, LEGACY_STATUS_PHOTO_FIELD_MAX_LENGTH);
const parseLegacyIntermediatePointValue = (rawValue) => {
const normalizedRawValue = typeof rawValue === 'string' ? rawValue.trim() : '';
if (!normalizedRawValue) {
return {
hasLegacyFormat: false,
refPuntoId: null,
obs: null,
fecha: null,
hora: null,
plainTextObs: null
};
}
const parts = String(rawValue).split(LEGACY_INTERMEDIATE_POINT_VALUE_SEPARATOR);
const refRaw = typeof parts[0] === 'string' ? parts[0].trim() : '';
const obsRaw = typeof parts[1] === 'string' ? parts[1].trim() : '';
const fechaRaw = typeof parts[2] === 'string' ? parts[2].trim() : '';
const horaRaw = typeof parts[3] === 'string' ? parts[3].trim() : '';
const hasLegacyFormat = LEGACY_INTERMEDIATE_POINT_REFERENCE_REGEX.test(refRaw);
return {
hasLegacyFormat,
refPuntoId: hasLegacyFormat ? Number.parseInt(refRaw, 10) : null,
obs: hasLegacyFormat && obsRaw ? obsRaw : null,
fecha: hasLegacyFormat && fechaRaw ? fechaRaw : null,
hora: hasLegacyFormat && horaRaw ? horaRaw : null,
plainTextObs: hasLegacyFormat ? null : normalizedRawValue
};
};
const buildLegacyIntermediatePointValue = ({ refPuntoId, obs, fecha, hora }) => {
if (!Number.isInteger(refPuntoId) || refPuntoId <= 0) {
return null;
}
const normalizedObs = typeof obs === 'string' ? obs.trim() : '';
const normalizedFecha = typeof fecha === 'string' ? fecha.trim() : '';
const normalizedHora = typeof hora === 'string' ? hora.trim() : '';
return [
String(refPuntoId),
normalizedObs,
normalizedFecha,
normalizedHora
].join(LEGACY_INTERMEDIATE_POINT_VALUE_SEPARATOR);
};
const resolveStoredIntermediatePointValue = ({ currentValor, nextObservation, clearObservation = false }) => {
const parsedCurrentValue = parseLegacyIntermediatePointValue(currentValor);
if (!parsedCurrentValue.hasLegacyFormat) {
return clearObservation ? null : nextObservation || null;
}
return buildLegacyIntermediatePointValue({
refPuntoId: parsedCurrentValue.refPuntoId,
obs: clearObservation ? '' : nextObservation || '',
fecha: parsedCurrentValue.fecha,
hora: parsedCurrentValue.hora
});
};
const normalizeCoordinateValue = (rawValue) => {
if (rawValue === undefined || rawValue === null) {
return null;
}
const normalizedRawValue = String(rawValue).trim();
if (!normalizedRawValue) {
return null;
}
const normalizedDecimalValue = normalizedRawValue.replace(',', '.');
const parsedNumericValue = Number(normalizedDecimalValue);
if (!Number.isFinite(parsedNumericValue)) {
return null;
}
return String(parsedNumericValue);
};
const normalizeSqlDateTimeValue = (rawValue) => {
if (typeof rawValue !== 'string') {
return null;
}
const normalizedRawValue = rawValue.trim();
const match = SQL_DATETIME_REGEX.exec(normalizedRawValue);
if (!match) {
return null;
}
const year = Number.parseInt(match[1], 10);
const month = Number.parseInt(match[2], 10);
const day = Number.parseInt(match[3], 10);
const hour = Number.parseInt(match[4], 10);
const minute = Number.parseInt(match[5], 10);
const second = Number.parseInt(match[6], 10);
const candidateDate = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
if (
candidateDate.getUTCFullYear() !== year ||
candidateDate.getUTCMonth() + 1 !== month ||
candidateDate.getUTCDate() !== day ||
candidateDate.getUTCHours() !== hour ||
candidateDate.getUTCMinutes() !== minute ||
candidateDate.getUTCSeconds() !== second
) {
return null;
}
return `${match[1]}-${match[2]}-${match[3]} ${match[4]}:${match[5]}:${match[6]}`;
};
const parseTripStatusIdFromRequest = (req) => {
if (Object.prototype.hasOwnProperty.call(req.body || {}, 'id_estado')) {
return Number.parseInt(req.body.id_estado, 10);
}
return Number.parseInt(req.query?.id_estado, 10);
};
const getRequestId = (req) => {
const rawRequestId = req?.requestId;
if (typeof rawRequestId !== 'string') {
return null;
}
const normalizedRequestId = rawRequestId.trim();
return normalizedRequestId || null;
};
const parseTripPointIdFromRequest = (req) => {
if (Object.prototype.hasOwnProperty.call(req.body || {}, 'id_punto')) {
return Number.parseInt(req.body.id_punto, 10);
}
return Number.parseInt(req.query?.id_punto, 10);
};
const hasTripPointIdInRequest = (req) =>
Object.prototype.hasOwnProperty.call(req.body || {}, 'id_punto') ||
Object.prototype.hasOwnProperty.call(req.query || {}, 'id_punto');
const parseStatusKeyFromHistoryEvent = ({ idEstado, idPunto }) => {
if (Number.isInteger(idPunto) && INTERMEDIATE_POINT_STATUS_IDS.has(idEstado)) {
const intermediateStatusKeySuffix = INTERMEDIATE_STATUS_KEY_SUFFIX_BY_STATE_ID.get(idEstado);
return intermediateStatusKeySuffix ? `point_${idPunto}_${intermediateStatusKeySuffix}` : null;
}
return GLOBAL_STATUS_KEYS_BY_STATE_ID.get(idEstado) || null;
};
const getLatestManualTripStatus = async (queryable, tripId) => {
if (!Number.isInteger(tripId) || tripId <= 0) {
return null;
}
const [rows] = await queryable.query(
`SELECT
latest_history.id_estado,
states.estado,
states.estado_en
FROM (
SELECT
history_events.id_estado
FROM (
SELECT
CAST(c.id_estado AS SIGNED) AS id_estado,
c.fecha_y_hora AS fecha_hora,
c.id_cambio AS sort_event_id,
0 AS sort_source,
NULL AS sort_point_id
FROM c_cambios_estado c
WHERE c.id_viaje = ?
AND c.fecha_y_hora IS NOT NULL
AND COALESCE(c.actualizado_automaticamente, 0) = 0
UNION ALL
SELECT
CAST(vp.id_estado_intermedio AS SIGNED) AS id_estado,
COALESCE(vp.fecha_y_hora, vp.fecha_foto) AS fecha_hora,
vp.id_punto AS sort_event_id,
1 AS sort_source,
CAST(vp.id_punto AS SIGNED) AS sort_point_id
FROM c_viajes_puntos vp
WHERE vp.id_viaje = ?
AND vp.id_estado_intermedio IN (?, ?, ?)
AND COALESCE(vp.fecha_y_hora, vp.fecha_foto) IS NOT NULL
AND COALESCE(vp.actualizado_automaticamente, 0) = 0
) AS history_events
ORDER BY
history_events.fecha_hora DESC,
history_events.sort_source DESC,
history_events.sort_point_id DESC,
history_events.sort_event_id DESC
LIMIT 1
) AS latest_history
LEFT JOIN t_viaje_estados states
ON states.id_estado = latest_history.id_estado`,
[tripId, tripId, ...Array.from(INTERMEDIATE_POINT_STATUS_IDS)]
);
if (rows.length === 0) {
return null;
}
const parsedStatusId = Number.parseInt(rows[0].id_estado, 10);
if (!Number.isInteger(parsedStatusId)) {
return null;
}
return {
id_estado: parsedStatusId,
estado: rows[0].estado || null,
estado_en: rows[0].estado_en || null
};
};
const parseTripFailedMarkFromRequest = (req) => {
const rawBodyValue = req?.body?.ind_fallido;
const rawQueryValue = req?.query?.ind_fallido;
const requestId = getRequestId(req);
appendTripStatusDebugLog({
stage: 'parse_failed_mark:input',
request_id: requestId,
route: req?.originalUrl,
method: req?.method,
content_type: req?.headers?.['content-type'] || null,
raw_body_ind_fallido: rawBodyValue ?? null,
raw_query_ind_fallido: rawQueryValue ?? null
});
if (!Object.prototype.hasOwnProperty.call(req.body || {}, 'ind_fallido')) {
const result = { isValid: true, value: 0 };
appendTripStatusDebugLog({
stage: 'parse_failed_mark:result',
reason: 'missing_in_body',
result
});
return result;
}
const rawValue = req.body.ind_fallido;
if (rawValue === undefined || rawValue === null) {
const result = { isValid: true, value: 0 };
appendTripStatusDebugLog({
stage: 'parse_failed_mark:result',
reason: 'null_or_undefined',
result
});
return result;
}
const normalizedValue = String(rawValue).trim().toLowerCase();
if (!normalizedValue || normalizedValue === '0' || normalizedValue === 'false') {
const result = { isValid: true, value: 0 };
appendTripStatusDebugLog({
stage: 'parse_failed_mark:result',
reason: 'false_like_value',
normalized_value: normalizedValue,
result
});
return result;
}
if (normalizedValue === '1' || normalizedValue === 'true') {
const result = { isValid: true, value: 1 };
appendTripStatusDebugLog({
stage: 'parse_failed_mark:result',
reason: 'true_like_value',
normalized_value: normalizedValue,
result
});
return result;
}
const result = { isValid: false, value: 0 };
appendTripStatusDebugLog({
stage: 'parse_failed_mark:result',
reason: 'invalid_value',
normalized_value: normalizedValue,
result
});
return result;
};
const logTripStatusAudit = ({ userId, dni, tripId, statusId, failedMark, result, errorCode, requestId }) => {
console.info('[TripStatusAudit]', {
request_id: requestId || null,
user_id: userId || null,
dni: dni || null,
trip_id: tripId || null,
id_estado: Number.isInteger(statusId) ? statusId : null,
ind_fallido: failedMark ? 1 : 0,
result,
error_code: errorCode || null
});
};
const sanitizeTripIncidenceText = (rawText) =>
String(rawText || '')
.replace(INCIDENCE_TEXT_CONTROL_CHARACTERS_REGEX, '')
.trim();
const logTripIncidenceAudit = ({ tripId, userId, result, emailStatus, errorCode }) => {
console.info('[TripIncidenceAudit]', {
trip_id: Number.isInteger(tripId) ? tripId : null,
user_id: Number.isInteger(userId) ? userId : null,
result: result || null,
email_status: emailStatus || null,
error_code: errorCode || null
});
};
const normalizeParentTripId = (value) => {
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
};
const propagateTripStatusToParentTrips = async (
queryExecutor,
{ initialParentTripId, statusId, userId, requestId, flow }
) => {
let parentTripId = normalizeParentTripId(initialParentTripId);
const visitedParentTripIds = new Set();
const updatedParentTripIds = [];
while (parentTripId !== null) {
if (visitedParentTripIds.has(parentTripId)) {
throw new Error(`Detected c_viajes parent cycle at id_viaje ${parentTripId}`);
}
visitedParentTripIds.add(parentTripId);
const [updateParentTripResult] = await queryExecutor.query(
`UPDATE c_viajes
SET id_estado = ?,
ind_edi_app = 1,
fecha_ultima_edicion = NOW(),
id_usuarios_ultima_edicion = ?
WHERE id_viaje = ?`,
[statusId, userId, parentTripId]
);
updatedParentTripIds.push(parentTripId);
appendTripStatusDebugLog({
stage: 'trip_status:parent_trip_updated',
request_id: requestId || null,
flow: flow || null,
parent_trip_id: parentTripId,
id_estado: statusId,
affected_rows: updateParentTripResult?.affectedRows ?? null
});
const [parentTripRows] = await queryExecutor.query(
`SELECT id_viaje_padre
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[parentTripId]
);
parentTripId = normalizeParentTripId(parentTripRows[0]?.id_viaje_padre);
}
return updatedParentTripIds;
};
const startTrip = async (req, res) => {
const dni = req.user?.dni;
const userId = req.user?.id || null;
const tripId = Number.parseInt(req.params.tripId, 10);
const requestId = getRequestId(req);
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!Number.isInteger(tripId) || tripId <= 0) {
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
let connection;
try {
connection = await db.getConnection();
await connection.beginTransaction();
await connection.query(
`SELECT id_viaje
FROM c_viajes_proveedor
WHERE dni = ?
ORDER BY id_viaje ASC
FOR UPDATE`,
[dni]
);
const [tripRows] = await connection.query(
`SELECT id_viaje, cod_viaje, id_estado, id_viaje_padre
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1
FOR UPDATE`,
[tripId]
);
if (tripRows.length === 0) {
await connection.rollback();
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await connection.query(
`SELECT n_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1
FOR UPDATE`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
await connection.rollback();
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const currentTripStatusId = Number.parseInt(tripRows[0].id_estado, 10);
if (currentTripStatusId !== ASSIGNED_TRIP_STATE) {
await connection.rollback();
return res.status(422).json({
success: false,
code: 'INVALID_STATUS',
message: 'El viaje no está en estado asignado'
});
}
const [activeTripRows] = await connection.query(
`SELECT
v.id_viaje,
v.cod_viaje,
v.id_estado
FROM c_viajes_proveedor p
INNER JOIN c_viajes v
ON v.id_viaje = p.id_viaje
WHERE p.dni = ?
AND v.id_viaje <> ?
AND v.id_estado BETWEEN ? AND ?
ORDER BY COALESCE(p.fecha_salida, v.fecha_salida) DESC, v.id_viaje DESC
LIMIT 1
FOR UPDATE`,
[dni, tripId, ACTIVE_TRIP_MIN_STATE, ACTIVE_TRIP_MAX_STATE]
);
if (activeTripRows.length > 0) {
await connection.rollback();
return res.status(409).json({
success: false,
code: 'ACTIVE_TRIP_EXISTS',
message: 'Ya existe un viaje en curso',
activeTrip: {
id_viaje: activeTripRows[0].id_viaje,
cod_viaje: activeTripRows[0].cod_viaje,
id_estado: activeTripRows[0].id_estado
}
});
}
const now = new Date();
const startStatusFailedFlag = 0;
await connection.query(
`UPDATE c_viajes
SET id_estado = ?,
ind_edi_app = 1,
fecha_inicio_real = ?,
fecha_ultima_edicion = NOW(),
id_usuarios_ultima_edicion = ?
WHERE id_viaje = ?`,
[START_TRIP_TARGET_STATE, now, userId, tripId]
);
await propagateTripStatusToParentTrips(connection, {
initialParentTripId: tripRows[0].id_viaje_padre,
statusId: START_TRIP_TARGET_STATE,
userId,
requestId,
flow: 'start_trip'
});
const [insertStartStatusResult] = await connection.query(
`INSERT INTO c_cambios_estado
(id_viaje, n_proveedor, id_transportista, id_estado, ind_fallido, incidencia, latitud, longitud, fecha_y_hora, foto, id_usuario)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripId,
authorizationRows[0].n_proveedor,
dni,
START_TRIP_TARGET_STATE,
startStatusFailedFlag,
null,
null,
null,
now,
null,
userId
]
);
appendTripStatusDebugLog({
stage: 'start_trip:c_cambios_estado_inserted',
request_id: requestId,
action: 'insert',
trip_id: tripId,
id_estado: START_TRIP_TARGET_STATE,
n_proveedor: authorizationRows[0].n_proveedor ?? null,
id_transportista: dni || null,
latitud: null,
longitud: null,
affected_rows: insertStartStatusResult?.affectedRows ?? null,
insert_id: insertStartStatusResult?.insertId ?? null
});
const [updatedTripRows] = await connection.query(
`SELECT
id_viaje,
cod_viaje,
id_estado,
DATE_FORMAT(fecha_inicio_real, '%Y-%m-%d %H:%i:%s') AS fecha_inicio_real
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
await connection.commit();
return res.status(200).json({
success: true,
trip: updatedTripRows[0] || {
id_viaje: tripRows[0].id_viaje,
cod_viaje: tripRows[0].cod_viaje,
id_estado: START_TRIP_TARGET_STATE,
fecha_inicio_real: now.toISOString()
}
});
} catch (error) {
if (connection) {
try {
await connection.rollback();
} catch (rollbackError) {
console.error('Rollback failed while starting trip:', {
message: rollbackError.message,
dni,
tripId
});
}
}
console.error('Error starting trip:', {
message: error.message,
dni,
tripId: req.params?.tripId
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
} finally {
if (connection) {
connection.release();
}
}
};
const createTripIncidence = async (req, res) => {
const dni = req.user?.dni;
const parsedUserId = Number.parseInt(req.user?.id, 10);
const userId = Number.isInteger(parsedUserId) ? parsedUserId : null;
const tripId = Number.parseInt(req.params.tripId, 10);
const hasRawIncidenciaField = Object.prototype.hasOwnProperty.call(req.body || {}, 'incidencia');
const incidencia =
typeof req.body?.incidencia === 'string' ? sanitizeTripIncidenceText(req.body.incidencia) : '';
if (!dni || !Number.isInteger(userId) || userId <= 0) {
logTripIncidenceAudit({
tripId,
userId,
result: 'UNAUTHORIZED',
errorCode: 'UNAUTHORIZED'
});
return res.status(401).json({ error: 'Unauthorized' });
}
if (
!Number.isInteger(tripId) ||
tripId <= 0 ||
!hasRawIncidenciaField ||
typeof req.body?.incidencia !== 'string' ||
!incidencia
) {
logTripIncidenceAudit({
tripId,
userId,
result: 'INVALID_PAYLOAD',
errorCode: 'INVALID_PAYLOAD'
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
try {
const [tripRows] = await db.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
logTripIncidenceAudit({
tripId,
userId,
result: 'TRIP_NOT_FOUND',
errorCode: 'TRIP_NOT_FOUND'
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT n_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
logTripIncidenceAudit({
tripId,
userId,
result: 'FORBIDDEN',
errorCode: 'FORBIDDEN'
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
await db.query(
`INSERT INTO c_viajes_incidencias
(id_viaje, incidencia, id_usuarios, dni_conductor, ind_aviso_cli, ind_aviso_cr)
VALUES (?, ?, ?, ?, ?, ?)`,
[tripId, incidencia, null, dni, 1, 1]
);
let emailStatus = 'not_attempted';
let emailWarning = null;
try {
const emailResult = await tripIncidenceMailer.sendTripIncidenceEmail({
tripId,
incidencia,
userId
});
emailStatus = emailResult?.status || 'unknown';
} catch (emailError) {
emailStatus = 'failed';
emailWarning = 'email_failed';
console.error('Trip incidence email failed:', {
tripId,
userId,
message: emailError.message
});
}
logTripIncidenceAudit({
tripId,
userId,
result: 'SUCCESS',
emailStatus
});
const responseBody = {
success: true,
message: 'correcto'
};
if (emailWarning) {
responseBody.warning = emailWarning;
}
return res.status(201).json(responseBody);
} catch (error) {
logTripIncidenceAudit({
tripId,
userId,
result: 'ERROR',
errorCode: 'INTERNAL_ERROR'
});
console.error('Error creating trip incidence:', {
message: error.message,
tripId: req.params?.tripId,
userId
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const updateTripStatus = async (req, res) => {
const uploadedFiles = collectUploadedTripStatusFiles(req);
const requestId = getRequestId(req);
try {
const dni = req.user?.dni;
const userId = req.user?.id || null;
const tripId = Number.parseInt(req.params.id, 10);
const idEstado = Number.parseInt(req.body?.id_estado, 10);
const rawIdPunto = req.body?.id_punto;
const hasIdPunto =
rawIdPunto !== undefined &&
rawIdPunto !== null &&
String(rawIdPunto).trim() !== '';
const idPunto = Number.parseInt(rawIdPunto, 10);
const isIntermediatePointStatus = INTERMEDIATE_POINT_STATUS_IDS.has(idEstado);
const useIntermediatePointFlow = isIntermediatePointStatus && hasIdPunto;
const failedMarkParseResult = parseTripFailedMarkFromRequest(req);
const observaciones =
typeof req.body?.observaciones === 'string' ? req.body.observaciones.trim() : '';
const rawFotosConcat =
typeof req.body?.fotos_concat === 'string' ? req.body.fotos_concat : '';
const normalizedClientFotosConcat = normalizeLegacyPhotoReferences(rawFotosConcat);
const latitud = normalizeCoordinateValue(req.body?.latitud);
const longitud = normalizeCoordinateValue(req.body?.longitud);
const hasLatitudInPayload = Object.prototype.hasOwnProperty.call(req.body || {}, 'latitud');
const hasLongitudInPayload = Object.prototype.hasOwnProperty.call(req.body || {}, 'longitud');
const contentType = req?.headers?.['content-type'] || null;
const failedMarkRequested = failedMarkParseResult.value === 1;
const selectedStatusFailedFlag =
failedMarkRequested && idEstado !== FAILED_TRIP_STATE ? 1 : 0;
const failedStateInsertFlag = 0;
appendTripStatusDebugLog({
stage: 'update_trip_status:request_parsed',
route: req?.originalUrl,
method: req?.method,
request_id: requestId,
content_type: contentType,
trip_id: tripId,
id_estado: idEstado,
is_status_7: idEstado === 7,
id_punto: Number.isInteger(idPunto) ? idPunto : null,
has_id_punto: hasIdPunto,
has_latitud_key: hasLatitudInPayload,
has_longitud_key: hasLongitudInPayload,
raw_latitud: req.body?.latitud ?? null,
raw_longitud: req.body?.longitud ?? null,
latitud_normalized: latitud,
longitud_normalized: longitud,
failed_mark_parse: failedMarkParseResult,
failed_mark_requested: failedMarkRequested,
is_intermediate_point_status: isIntermediatePointStatus,
use_intermediate_point_flow: useIntermediatePointFlow,
selected_status_failed_flag: selectedStatusFailedFlag,
failed_state_insert_flag: failedStateInsertFlag,
body_keys: Object.keys(req.body || {}),
raw_body: req.body || null,
files_count: uploadedFiles.length
});
const logManualStatusUpdateAttempt = ({ result, httpStatus, errorCode, idEstadoLogged, idPuntoLogged }) => {
appendTripStatusUpdateLog({
update_type: 'manual',
success: result === 'SUCCESS',
result,
http_status: httpStatus,
error_code: errorCode || null,
request_id: requestId,
trip_id: Number.isInteger(tripId) ? tripId : null,
id_estado: Number.isInteger(idEstadoLogged) ? idEstadoLogged : null,
id_punto: Number.isInteger(idPuntoLogged) ? idPuntoLogged : null,
user_id: userId,
dni,
failed_marked: failedMarkRequested
});
};
const logManualTripStatusAudit = (payload) =>
logTripStatusAudit({
...payload,
requestId
});
if (!dni) {
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: failedMarkRequested,
result: 'UNAUTHORIZED'
});
logManualStatusUpdateAttempt({
result: 'UNAUTHORIZED',
httpStatus: 401,
errorCode: 'UNAUTHORIZED',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(401).json({ error: 'Unauthorized' });
}
if (process.env.DEBUG_TRIP_STATUS_COORDS === '1') {
if ((hasLatitudInPayload && latitud === null) || (hasLongitudInPayload && longitud === null)) {
console.info('[TripStatus] coordinates missing or invalid', {
tripId,
request_id: requestId,
hasLatitudInPayload,
hasLongitudInPayload
});
}
}
if (
!Number.isInteger(tripId) ||
tripId <= 0 ||
!Number.isInteger(idEstado) ||
idEstado <= 0 ||
!failedMarkParseResult.isValid ||
(useIntermediatePointFlow &&
(!Number.isInteger(idPunto) || idPunto <= 0))
) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: failedMarkRequested,
result: 'INVALID_PAYLOAD'
});
logManualStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
if (uploadedFiles.length > MAX_TRIP_STATUS_FILES) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: failedMarkRequested,
result: 'INVALID_PAYLOAD'
});
logManualStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
if (idEstado === 7 && uploadedFiles.length === 0 && !normalizedClientFotosConcat) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: failedMarkRequested,
result: 'PHOTO_REQUIRED'
});
logManualStatusUpdateAttempt({
result: 'PHOTO_REQUIRED',
httpStatus: 422,
errorCode: 'PHOTO_REQUIRED',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(422).json({
success: false,
error: 'Photo required for status 7'
});
}
const [stateRows] = await db.query(
`SELECT id_estado
FROM t_viaje_estados
WHERE id_estado = ?
LIMIT 1`,
[idEstado]
);
if (stateRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: failedMarkRequested,
result: 'INVALID_PAYLOAD'
});
logManualStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
const photoNames = uploadedFiles.map((file) => file.filename).filter(Boolean);
const fotosConcat = normalizeLegacyPhotoReferences(
photoNames.length > 0 ? photoNames.join(';') : normalizedClientFotosConcat
);
const now = new Date();
if (useIntermediatePointFlow) {
const [tripRows] = await db.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'TRIP_NOT_FOUND'
});
logManualStatusUpdateAttempt({
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'upsert_point_status',
requestId,
success: false,
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
tripId,
idPunto,
idEstado,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT n_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length > 0) {
appendTripStatusDebugLog({
stage: 'update_trip_status:authorization_resolved',
request_id: requestId,
flow: 'intermediate_point_manual',
trip_id: tripId,
dni_from_token: dni || null,
resolved_n_proveedor: authorizationRows[0].n_proveedor ?? null,
resolved_id_proveedor: null,
provider_is_null: authorizationRows[0].n_proveedor == null,
dni_is_null: !dni
});
}
if (authorizationRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'FORBIDDEN'
});
logManualStatusUpdateAttempt({
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'upsert_point_status',
requestId,
success: false,
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
tripId,
idPunto,
idEstado,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const [tripPointRows] = await db.query(
`SELECT
id_punto,
id_estado_intermedio,
valor,
foto,
fecha_foto
FROM c_viajes_puntos
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1`,
[idPunto, tripId]
);
if (tripPointRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
result: 'POINT_NOT_FOUND'
});
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'POINT_NOT_FOUND'
});
logManualStatusUpdateAttempt({
result: 'POINT_NOT_FOUND',
httpStatus: 404,
errorCode: 'POINT_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'upsert_point_status',
requestId,
success: false,
result: 'POINT_NOT_FOUND',
httpStatus: 404,
errorCode: 'POINT_NOT_FOUND',
tripId,
idPunto,
idEstado,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(404).json({
success: false,
error: 'Trip point not found'
});
}
const previousIntermediateStatusId = Number.parseInt(
tripPointRows[0].id_estado_intermedio,
10
);
const nextIntermediateValor = resolveStoredIntermediatePointValue({
currentValor: tripPointRows[0].valor,
nextObservation: observaciones || null,
clearObservation: !observaciones
});
const nextIntermediateFoto = fotosConcat || null;
const nextIntermediateFechaFoto = nextIntermediateFoto ? now : null;
await db.query(
`UPDATE c_viajes_puntos
SET id_estado_intermedio = ?,
valor = ?,
foto = ?,
fecha_foto = ?,
fecha_y_hora = ?,
latitud = ?,
longitud = ?,
borrado_en_app = 0,
actualizado_automaticamente = 0
WHERE id_punto = ?
AND id_viaje = ?`,
[
idEstado,
nextIntermediateValor,
nextIntermediateFoto,
nextIntermediateFechaFoto,
now,
latitud,
longitud,
idPunto,
tripId
]
);
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
result: 'SUCCESS'
});
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'SUCCESS'
});
logManualStatusUpdateAttempt({
result: 'SUCCESS',
httpStatus: 200,
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'upsert_point_status',
requestId,
success: true,
result: 'SUCCESS',
httpStatus: 200,
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId: idEstado,
userId,
dni,
fechaYHora: now.toISOString(),
latitud,
longitud,
valorWritten: nextIntermediateValor,
fotoWritten: nextIntermediateFoto,
fechaFotoWritten: nextIntermediateFechaFoto ? nextIntermediateFechaFoto.toISOString() : null,
valorCleared: nextIntermediateValor === null,
fotoCleared: nextIntermediateFoto === null,
actualizadoAutomaticamente: 0
});
return res.status(200).json({
success: true,
trip_id: tripId,
status_id: idEstado,
id_estado: idEstado,
id_punto: idPunto,
updated_at: now.toISOString()
});
}
if (failedMarkRequested) {
const [failedStateRows] = await db.query(
`SELECT id_estado
FROM t_viaje_estados
WHERE id_estado = ?
LIMIT 1`,
[FAILED_TRIP_STATE]
);
if (failedStateRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: true,
result: 'INVALID_PAYLOAD'
});
logManualStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
}
if (failedMarkRequested) {
let connection;
try {
connection = await db.getConnection();
await connection.beginTransaction();
const [tripRows] = await connection.query(
`SELECT id_viaje, id_estado, id_viaje_padre
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1
FOR UPDATE`,
[tripId]
);
if (tripRows.length === 0) {
await connection.rollback();
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: true,
result: 'TRIP_NOT_FOUND'
});
logManualStatusUpdateAttempt({
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await connection.query(
`SELECT n_proveedor, id_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1
FOR UPDATE`,
[tripId, dni]
);
if (authorizationRows.length > 0) {
appendTripStatusDebugLog({
stage: 'update_trip_status:authorization_resolved',
request_id: requestId,
flow: 'failed_branch_manual',
trip_id: tripId,
dni_from_token: dni || null,
resolved_n_proveedor: authorizationRows[0].n_proveedor ?? null,
resolved_id_proveedor: authorizationRows[0].id_proveedor ?? null,
provider_is_null: authorizationRows[0].n_proveedor == null,
dni_is_null: !dni
});
}
if (authorizationRows.length === 0) {
await connection.rollback();
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: true,
result: 'FORBIDDEN'
});
logManualStatusUpdateAttempt({
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const providerNumber = authorizationRows[0].n_proveedor;
if (Object.prototype.hasOwnProperty.call(req.body, 'observaciones')) {
const targetObservationField = idEstado >= 6 ? 'obs_descarga' : 'obs_carga';
await connection.query(
`UPDATE c_viajes_proveedor
SET ${targetObservationField} = ?
WHERE id_viaje = ?
AND dni = ?`,
[observaciones, tripId, dni]
);
}
const [updatedExistingStatusResult] = await connection.query(
`UPDATE c_cambios_estado
SET ind_fallido = ?,
incidencia = ?,
latitud = ?,
longitud = ?,
foto = ?,
id_usuario = ?
WHERE id_viaje = ?
AND n_proveedor = ?
AND id_transportista = ?
AND id_estado = ?
ORDER BY fecha_y_hora DESC
LIMIT 1`,
[
selectedStatusFailedFlag,
observaciones,
latitud,
longitud,
fotosConcat || null,
userId,
tripId,
providerNumber,
dni,
idEstado
]
);
appendTripStatusDebugLog({
stage: 'update_trip_status:c_cambios_estado_persisted',
request_id: requestId,
flow: 'failed_branch_manual',
action: 'update',
trip_id: tripId,
id_estado: idEstado,
n_proveedor: providerNumber ?? null,
id_transportista: dni || null,
latitud,
longitud,
affected_rows: updatedExistingStatusResult?.affectedRows ?? null,
insert_id: updatedExistingStatusResult?.insertId ?? null
});
let statusAction = 'update';
if (updatedExistingStatusResult.affectedRows === 0) {
const [insertSelectedStatusResult] = await connection.query(
`INSERT INTO c_cambios_estado
(id_viaje, n_proveedor, id_transportista, id_estado, ind_fallido, incidencia, latitud, longitud, fecha_y_hora, foto, id_usuario)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripId,
providerNumber,
dni,
idEstado,
selectedStatusFailedFlag,
observaciones,
latitud,
longitud,
now,
fotosConcat || null,
userId
]
);
appendTripStatusDebugLog({
stage: 'update_trip_status:c_cambios_estado_persisted',
request_id: requestId,
flow: 'failed_branch_manual',
action: 'insert',
trip_id: tripId,
id_estado: idEstado,
n_proveedor: providerNumber ?? null,
id_transportista: dni || null,
latitud,
longitud,
affected_rows: insertSelectedStatusResult?.affectedRows ?? null,
insert_id: insertSelectedStatusResult?.insertId ?? null
});
statusAction = 'insert';
}
appendTripStatusDebugLog({
stage: 'update_trip_status:failed_branch_selected_status_saved',
request_id: requestId,
trip_id: tripId,
id_estado: idEstado,
selected_status_failed_flag: selectedStatusFailedFlag,
affected_rows: updatedExistingStatusResult.affectedRows,
action: statusAction
});
const [lastStatusRows] = await connection.query(
`SELECT id_estado
FROM c_cambios_estado
WHERE id_viaje = ?
ORDER BY fecha_y_hora DESC
LIMIT 1
FOR UPDATE`,
[tripId]
);
const lastStatusId =
lastStatusRows.length > 0
? Number.parseInt(lastStatusRows[0].id_estado, 10)
: null;
let insertedFailedStatusId = null;
if (lastStatusId !== FAILED_TRIP_STATE) {
const [insertFailedStateResult] = await connection.query(
`INSERT INTO c_cambios_estado
(id_viaje, n_proveedor, id_transportista, id_estado, ind_fallido, incidencia, latitud, longitud, fecha_y_hora, foto, id_usuario)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripId,
providerNumber,
dni,
FAILED_TRIP_STATE,
failedStateInsertFlag,
observaciones,
latitud,
longitud,
now,
fotosConcat || null,
userId
]
);
appendTripStatusDebugLog({
stage: 'update_trip_status:c_cambios_estado_persisted',
request_id: requestId,
flow: 'failed_branch_manual',
action: 'insert_failed_state',
trip_id: tripId,
id_estado: FAILED_TRIP_STATE,
n_proveedor: providerNumber ?? null,
id_transportista: dni || null,
latitud,
longitud,
affected_rows: insertFailedStateResult?.affectedRows ?? null,
insert_id: insertFailedStateResult?.insertId ?? null
});
insertedFailedStatusId = FAILED_TRIP_STATE;
}
appendTripStatusDebugLog({
stage: 'update_trip_status:failed_branch_failed_state_insert',
request_id: requestId,
trip_id: tripId,
id_estado: idEstado,
last_status_id_before_insert: lastStatusId,
inserted_failed_status_id: insertedFailedStatusId,
failed_state_insert_flag: failedStateInsertFlag
});
await connection.query(
`UPDATE c_viajes
SET id_estado = ?,
ind_edi_app = 1,
fecha_ultima_edicion = NOW(),
id_usuarios_ultima_edicion = ?
WHERE id_viaje = ?`,
[FAILED_TRIP_STATE, userId, tripId]
);
await propagateTripStatusToParentTrips(connection, {
initialParentTripId: tripRows[0].id_viaje_padre,
statusId: FAILED_TRIP_STATE,
userId,
requestId,
flow: 'failed_branch_manual'
});
await connection.commit();
const actionTaken =
insertedFailedStatusId === FAILED_TRIP_STATE
? `${statusAction}+insert_failed`
: `${statusAction}+skip_failed_insert`;
const responseMessage =
insertedFailedStatusId === FAILED_TRIP_STATE
? 'Estado marcado como fallido y estado 9 registrado'
: 'Estado marcado como fallido; no se insertó 9 porque ya era el último estado';
appendTripStatusDebugLog({
stage: 'update_trip_status:failed_branch_success',
request_id: requestId,
trip_id: tripId,
id_estado: idEstado,
action_taken: actionTaken,
inserted_failed_status_id: insertedFailedStatusId
});
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: true,
result: 'SUCCESS'
});
logManualStatusUpdateAttempt({
result: 'SUCCESS',
httpStatus: 200,
idEstadoLogged: FAILED_TRIP_STATE,
idPuntoLogged: idPunto
});
return res.status(200).json({
success: true,
trip_id: tripId,
updated_status_id: idEstado,
inserted_failed_status_id: insertedFailedStatusId,
action_taken: actionTaken,
message: responseMessage,
status_id: FAILED_TRIP_STATE,
id_estado: FAILED_TRIP_STATE,
failed_marked: true,
fotos_concat: fotosConcat || '',
updated_at: now.toISOString()
});
} catch (error) {
if (connection) {
try {
await connection.rollback();
} catch (rollbackError) {
console.error('Rollback failed while marking trip as failed:', {
message: rollbackError.message,
dni,
tripId
});
}
}
throw error;
} finally {
if (connection) {
connection.release();
}
}
}
const [tripRows] = await db.query(
`SELECT id_viaje, id_viaje_padre
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'TRIP_NOT_FOUND'
});
logManualStatusUpdateAttempt({
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT n_proveedor, id_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length > 0) {
appendTripStatusDebugLog({
stage: 'update_trip_status:authorization_resolved',
request_id: requestId,
flow: 'normal_manual',
trip_id: tripId,
dni_from_token: dni || null,
resolved_n_proveedor: authorizationRows[0].n_proveedor ?? null,
resolved_id_proveedor: authorizationRows[0].id_proveedor ?? null,
provider_is_null: authorizationRows[0].n_proveedor == null,
dni_is_null: !dni
});
}
if (authorizationRows.length === 0) {
removeUploadedTripStatusFiles(uploadedFiles);
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'FORBIDDEN'
});
logManualStatusUpdateAttempt({
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
await db.query(
`UPDATE c_viajes
SET id_estado = ?,
ind_edi_app = 1,
fecha_ultima_edicion = NOW(),
id_usuarios_ultima_edicion = ?
WHERE id_viaje = ?`,
[idEstado, userId, tripId]
);
if (Object.prototype.hasOwnProperty.call(req.body, 'observaciones')) {
const targetObservationField = idEstado >= 6 ? 'obs_descarga' : 'obs_carga';
await db.query(
`UPDATE c_viajes_proveedor
SET ${targetObservationField} = ?
WHERE id_viaje = ?
AND dni = ?`,
[observaciones, tripId, dni]
);
}
const [insertStatusResult] = await db.query(
`INSERT INTO c_cambios_estado
(id_viaje, n_proveedor, id_transportista, id_estado, ind_fallido, incidencia, latitud, longitud, fecha_y_hora, foto, id_usuario)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripId,
authorizationRows[0].n_proveedor,
dni,
idEstado,
selectedStatusFailedFlag,
observaciones,
latitud,
longitud,
now,
fotosConcat || null,
userId
]
);
await propagateTripStatusToParentTrips(db, {
initialParentTripId: tripRows[0].id_viaje_padre,
statusId: idEstado,
userId,
requestId,
flow: 'normal_manual'
});
appendTripStatusDebugLog({
stage: 'update_trip_status:c_cambios_estado_persisted',
request_id: requestId,
flow: 'normal_manual',
action: 'insert',
trip_id: tripId,
id_estado: idEstado,
n_proveedor: authorizationRows[0].n_proveedor ?? null,
id_transportista: dni || null,
latitud,
longitud,
affected_rows: insertStatusResult?.affectedRows ?? null,
insert_id: insertStatusResult?.insertId ?? null
});
appendTripStatusDebugLog({
stage: 'update_trip_status:normal_branch_success',
request_id: requestId,
trip_id: tripId,
id_estado: idEstado,
selected_status_failed_flag: selectedStatusFailedFlag
});
logManualTripStatusAudit({
userId,
dni,
tripId,
statusId: idEstado,
failedMark: false,
result: 'SUCCESS'
});
logManualStatusUpdateAttempt({
result: 'SUCCESS',
httpStatus: 200,
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(200).json({
success: true,
trip_id: tripId,
updated_status_id: idEstado,
inserted_failed_status_id: null,
action_taken: 'insert',
message: 'Estado actualizado',
status_id: idEstado,
id_estado: idEstado,
failed_marked: false,
fotos_concat: fotosConcat || '',
updated_at: now.toISOString()
});
} catch (error) {
const parsedTripId = Number.parseInt(req.params?.id, 10);
const parsedStatusId = Number.parseInt(req.body?.id_estado, 10);
const parsedPointId = Number.parseInt(req.body?.id_punto, 10);
const isIntermediatePointFlowOnError =
INTERMEDIATE_POINT_STATUS_IDS.has(parsedStatusId) && Number.isInteger(parsedPointId);
appendTripStatusDebugLog({
stage: 'update_trip_status:error',
route: req?.originalUrl,
method: req?.method,
request_id: requestId,
trip_id: parsedTripId,
id_estado: parsedStatusId,
raw_ind_fallido: req.body?.ind_fallido,
error_message: error?.message,
error_stack: error?.stack || null
});
removeUploadedTripStatusFiles(uploadedFiles);
logTripStatusAudit({
userId: req.user?.id || null,
dni: req.user?.dni,
tripId: Number.parseInt(req.params?.id, 10),
statusId: Number.parseInt(req.body?.id_estado, 10),
failedMark: req.body?.ind_fallido === '1' || req.body?.ind_fallido === 1,
result: 'ERROR',
requestId
});
appendTripStatusUpdateLog({
update_type: 'manual',
success: false,
result: 'ERROR',
http_status: 500,
error_code: 'INTERNAL_SERVER_ERROR',
request_id: requestId,
trip_id: parsedTripId || null,
id_estado: parsedStatusId || null,
id_punto: parsedPointId || null,
user_id: req.user?.id || null,
dni: req.user?.dni || null,
failed_marked: req.body?.ind_fallido === '1' || req.body?.ind_fallido === 1
});
if (isIntermediatePointFlowOnError) {
const uploadedPhotoNames = uploadedFiles
.map((file) => file?.filename)
.filter(Boolean);
const errorFotoWritten = normalizeLegacyPhotoReferences(
uploadedPhotoNames.length > 0
? uploadedPhotoNames.join(';')
: typeof req.body?.fotos_concat === 'string'
? req.body.fotos_concat
: ''
) || null;
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'upsert_point_status',
requestId,
success: false,
result: 'ERROR',
httpStatus: 500,
errorCode: 'INTERNAL_SERVER_ERROR',
tripId: parsedTripId,
idPunto: parsedPointId,
idEstado: parsedStatusId,
userId: req.user?.id || null,
dni: req.user?.dni || null,
latitud: normalizeCoordinateValue(req.body?.latitud),
longitud: normalizeCoordinateValue(req.body?.longitud),
valorWritten:
typeof req.body?.observaciones === 'string' && req.body.observaciones.trim()
? req.body.observaciones.trim()
: null,
fotoWritten: errorFotoWritten,
valorCleared:
!(typeof req.body?.observaciones === 'string' && req.body.observaciones.trim()),
fotoCleared: errorFotoWritten === null,
actualizadoAutomaticamente: 0
});
}
console.error('Error updating trip status:', {
message: error.message,
dni: req.user?.dni,
tripId: req.params?.id
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const updateTripStatusAutomatically = async (req, res) => {
const dni = req.user?.dni;
const userId = req.user?.id || null;
const requestId = getRequestId(req);
const tripId = Number.parseInt(req.params.id, 10);
const idEstado = Number.parseInt(req.body?.id_estado, 10);
const rawIdPunto = req.body?.id_punto;
const hasIdPunto =
rawIdPunto !== undefined &&
rawIdPunto !== null &&
String(rawIdPunto).trim() !== '';
const idPunto = Number.parseInt(rawIdPunto, 10);
const failedMarkParseResult = parseTripFailedMarkFromRequest(req);
const observaciones =
typeof req.body?.observaciones === 'string' ? req.body.observaciones.trim() : '';
const latitud = normalizeCoordinateValue(req.body?.latitud);
const longitud = normalizeCoordinateValue(req.body?.longitud);
const parsedFechaYHora = normalizeSqlDateTimeValue(req.body?.fecha_y_hora);
const hasUnsupportedPhotoPayload =
Object.prototype.hasOwnProperty.call(req.body || {}, 'fotos_concat') ||
Object.prototype.hasOwnProperty.call(req.body || {}, 'foto') ||
(Array.isArray(req.files) && req.files.length > 0);
const logAutomaticStatusUpdateAttempt = ({ result, httpStatus, errorCode, idEstadoLogged, idPuntoLogged }) => {
appendTripStatusUpdateLog({
update_type: 'automatic',
success: result === 'SUCCESS',
result,
http_status: httpStatus,
error_code: errorCode || null,
request_id: requestId,
trip_id: Number.isInteger(tripId) ? tripId : null,
id_estado: Number.isInteger(idEstadoLogged) ? idEstadoLogged : null,
id_punto: Number.isInteger(idPuntoLogged) ? idPuntoLogged : null,
user_id: userId,
dni,
failed_marked: failedMarkParseResult.value === 1
});
};
if (!dni) {
logAutomaticStatusUpdateAttempt({
result: 'UNAUTHORIZED',
httpStatus: 401,
errorCode: 'UNAUTHORIZED',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(401).json({ error: 'Unauthorized' });
}
if (
!Number.isInteger(tripId) ||
tripId <= 0 ||
!Number.isInteger(idEstado) ||
idEstado <= 0 ||
!failedMarkParseResult.isValid ||
hasUnsupportedPhotoPayload ||
(hasIdPunto && (!Number.isInteger(idPunto) || idPunto <= 0))
) {
logAutomaticStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
if (hasIdPunto && !INTERMEDIATE_POINT_STATUS_IDS.has(idEstado)) {
logAutomaticStatusUpdateAttempt({
result: 'INVALID_POINT_STATUS',
httpStatus: 422,
errorCode: 'INVALID_POINT_STATUS',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(422).json({
success: false,
error: 'Invalid point status'
});
}
let connection;
try {
connection = await db.getConnection();
await connection.beginTransaction();
const [stateRows] = await connection.query(
`SELECT id_estado
FROM t_viaje_estados
WHERE id_estado = ?
LIMIT 1`,
[idEstado]
);
if (stateRows.length === 0) {
await connection.rollback();
logAutomaticStatusUpdateAttempt({
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
const [tripRows] = await connection.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1
FOR UPDATE`,
[tripId]
);
if (tripRows.length === 0) {
await connection.rollback();
logAutomaticStatusUpdateAttempt({
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await connection.query(
`SELECT n_proveedor, id_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1
FOR UPDATE`,
[tripId, dni]
);
if (authorizationRows.length > 0) {
appendTripStatusDebugLog({
stage: 'update_trip_status_automatic:authorization_resolved',
request_id: requestId,
trip_id: tripId,
dni_from_token: dni || null,
resolved_n_proveedor: authorizationRows[0].n_proveedor ?? null,
resolved_id_proveedor: authorizationRows[0].id_proveedor ?? null,
provider_is_null: authorizationRows[0].n_proveedor == null,
dni_is_null: !dni
});
}
if (authorizationRows.length === 0) {
await connection.rollback();
logAutomaticStatusUpdateAttempt({
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const eventDate = parsedFechaYHora || new Date();
const failedFlag = failedMarkParseResult.value;
const [automaticInsertStatusResult] = await connection.query(
`INSERT INTO c_cambios_estado
(id_viaje, n_proveedor, id_transportista, id_estado, ind_fallido, incidencia, latitud, longitud, fecha_y_hora, foto, id_usuario, actualizado_automaticamente)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripId,
authorizationRows[0].n_proveedor,
dni,
idEstado,
failedFlag,
observaciones,
latitud,
longitud,
eventDate,
null,
userId,
1
]
);
appendTripStatusDebugLog({
stage: 'update_trip_status_automatic:c_cambios_estado_persisted',
request_id: requestId,
flow: 'automatic',
action: 'insert',
trip_id: tripId,
id_estado: idEstado,
n_proveedor: authorizationRows[0].n_proveedor ?? null,
id_transportista: dni || null,
latitud,
longitud,
affected_rows: automaticInsertStatusResult?.affectedRows ?? null,
insert_id: automaticInsertStatusResult?.insertId ?? null
});
if (hasIdPunto) {
const [tripPointRows] = await connection.query(
`SELECT id_punto
FROM c_viajes_puntos
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1
FOR UPDATE`,
[idPunto, tripId]
);
if (tripPointRows.length === 0) {
await connection.rollback();
logAutomaticStatusUpdateAttempt({
result: 'POINT_NOT_FOUND',
httpStatus: 404,
errorCode: 'POINT_NOT_FOUND',
idEstadoLogged: idEstado,
idPuntoLogged: idPunto
});
return res.status(404).json({
success: false,
error: 'Trip point not found'
});
}
await connection.query(
`UPDATE c_viajes_puntos
SET id_estado_intermedio = ?,
fecha_y_hora = ?,
ind_fallido = ?,
latitud = ?,
longitud = ?,
borrado_en_app = 0,
actualizado_automaticamente = 1
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1`,
[
idEstado,
eventDate,
failedFlag,
latitud,
longitud,
idPunto,
tripId
]
);
}
await connection.commit();
const responseBody = {
success: true,
trip_id: tripId,
id_estado: idEstado,
actualizado_automaticamente: 1,
updated_at: new Date().toISOString()
};
if (hasIdPunto) {
responseBody.id_punto = idPunto;
}
logAutomaticStatusUpdateAttempt({
result: 'SUCCESS',
httpStatus: 200,
idEstadoLogged: idEstado,
idPuntoLogged: hasIdPunto ? idPunto : null
});
return res.status(200).json(responseBody);
} catch (error) {
if (connection) {
try {
await connection.rollback();
} catch (rollbackError) {
console.error('Rollback failed while updating automatic trip status:', {
message: rollbackError.message,
tripId,
idEstado
});
}
}
console.error('Error updating automatic trip status:', {
message: error.message,
dni,
tripId: req.params?.id
});
appendTripStatusDebugLog({
stage: 'update_trip_status_automatic:error',
request_id: requestId,
trip_id: Number.parseInt(req.params?.id, 10) || null,
id_estado: Number.parseInt(req.body?.id_estado, 10) || null,
error_message: error?.message || null
});
appendTripStatusUpdateLog({
update_type: 'automatic',
success: false,
result: 'ERROR',
http_status: 500,
error_code: 'INTERNAL_SERVER_ERROR',
request_id: requestId,
trip_id: Number.parseInt(req.params?.id, 10) || null,
id_estado: Number.parseInt(req.body?.id_estado, 10) || null,
id_punto: Number.parseInt(req.body?.id_punto, 10) || null,
user_id: req.user?.id || null,
dni: req.user?.dni || null,
failed_marked: req.body?.ind_fallido === '1' || req.body?.ind_fallido === 1
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
} finally {
if (connection) {
connection.release();
}
}
};
const clearTripStatus = async (req, res) => {
const dni = req.user?.dni;
const userId = req.user?.id || null;
const requestId = getRequestId(req);
const tripId = Number.parseInt(req.params.id, 10);
const idEstado = parseTripStatusIdFromRequest(req);
const rawIdPunto = hasTripPointIdInRequest(req)
? Object.prototype.hasOwnProperty.call(req.body || {}, 'id_punto')
? req.body?.id_punto
: req.query?.id_punto
: null;
const hasIdPunto =
rawIdPunto !== undefined &&
rawIdPunto !== null &&
String(rawIdPunto).trim() !== '';
const idPunto = Number.parseInt(rawIdPunto, 10);
const isIntermediatePointStatus = INTERMEDIATE_POINT_STATUS_IDS.has(idEstado);
const useIntermediatePointFlow = isIntermediatePointStatus && hasIdPunto;
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (
!Number.isInteger(tripId) ||
tripId <= 0 ||
!Number.isInteger(idEstado) ||
idEstado <= 0 ||
(useIntermediatePointFlow && (!Number.isInteger(idPunto) || idPunto <= 0))
) {
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
try {
const [stateRows] = await db.query(
`SELECT id_estado
FROM t_viaje_estados
WHERE id_estado = ?
LIMIT 1`,
[idEstado]
);
if (stateRows.length === 0) {
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
const [tripRows] = await db.query(
`SELECT id_viaje, id_estado, id_viaje_padre
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT n_proveedor
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
ORDER BY n_proveedor ASC
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
if (useIntermediatePointFlow) {
const [tripPointRows] = await db.query(
`SELECT
id_punto,
id_estado_intermedio,
valor,
COALESCE(actualizado_automaticamente, 0) AS actualizado_automaticamente
FROM c_viajes_puntos
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1`,
[idPunto, tripId]
);
if (tripPointRows.length === 0) {
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
dni: dni || null,
result: 'POINT_NOT_FOUND'
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: false,
result: 'POINT_NOT_FOUND',
httpStatus: 404,
errorCode: 'POINT_NOT_FOUND',
tripId,
idPunto,
idEstado,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(404).json({
success: false,
error: 'Trip point not found'
});
}
const previousIntermediateStatusId = Number.parseInt(
tripPointRows[0].id_estado_intermedio,
10
);
const isAutomaticIntermediateStatus =
Number.parseInt(tripPointRows[0].actualizado_automaticamente, 10) === 1;
if (isAutomaticIntermediateStatus) {
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
dni: dni || null,
previous_intermediate_status_id: Number.isInteger(previousIntermediateStatusId)
? previousIntermediateStatusId
: null,
new_intermediate_status_id: null,
result: 'INTERMEDIATE_STATUS_NOT_FOUND'
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: false,
result: 'INTERMEDIATE_STATUS_NOT_FOUND',
httpStatus: 404,
errorCode: 'INTERMEDIATE_STATUS_NOT_FOUND',
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId: null,
userId,
dni,
actualizadoAutomaticamente: 1
});
return res.status(404).json({
success: false,
error: 'Trip point intermediate status not found'
});
}
if (!INTERMEDIATE_POINT_STATUS_IDS.has(previousIntermediateStatusId)) {
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
dni: dni || null,
previous_intermediate_status_id: Number.isInteger(previousIntermediateStatusId)
? previousIntermediateStatusId
: null,
new_intermediate_status_id: null,
result: 'INTERMEDIATE_STATUS_NOT_FOUND'
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: false,
result: 'INTERMEDIATE_STATUS_NOT_FOUND',
httpStatus: 404,
errorCode: 'INTERMEDIATE_STATUS_NOT_FOUND',
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId: null,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(404).json({
success: false,
error: 'Trip point intermediate status not found'
});
}
const nextIntermediateValor = resolveStoredIntermediatePointValue({
currentValor: tripPointRows[0].valor,
nextObservation: null,
clearObservation: true
});
const newIntermediateStatusId =
previousIntermediateStatusId === 5
? 4
: previousIntermediateStatusId === 4
? 3
: null;
const [updateIntermediateStatusResult] = await db.query(
`UPDATE c_viajes_puntos
SET id_estado_intermedio = ?,
valor = ?,
foto = NULL,
fecha_foto = NULL,
borrado_en_app = 1
WHERE id_punto = ?
AND id_viaje = ?
AND COALESCE(actualizado_automaticamente, 0) = 0`,
[newIntermediateStatusId, nextIntermediateValor, idPunto, tripId]
);
if (updateIntermediateStatusResult.affectedRows === 0) {
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
dni: dni || null,
previous_intermediate_status_id: previousIntermediateStatusId,
new_intermediate_status_id: null,
result: 'INTERMEDIATE_STATUS_NOT_FOUND'
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: false,
result: 'INTERMEDIATE_STATUS_NOT_FOUND',
httpStatus: 404,
errorCode: 'INTERMEDIATE_STATUS_NOT_FOUND',
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId: null,
userId,
dni,
actualizadoAutomaticamente: 0
});
return res.status(404).json({
success: false,
error: 'Trip point intermediate status not found'
});
}
const currentStatusId = Number.parseInt(tripRows[0]?.id_estado, 10);
console.info('[TripPointStatusAudit]', {
trip_id: tripId,
id_punto: idPunto,
id_estado: idEstado,
user_id: userId || null,
dni: dni || null,
previous_intermediate_status_id: previousIntermediateStatusId,
new_intermediate_status_id: newIntermediateStatusId,
result: 'ROLLBACK_APPLIED'
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: true,
result: 'ROLLBACK_APPLIED',
httpStatus: 200,
tripId,
idPunto,
idEstado,
previousIntermediateStatusId,
newIntermediateStatusId,
userId,
dni,
valorWritten: null,
fotoWritten: null,
fechaFotoWritten: null,
valorCleared: true,
fotoCleared: true,
actualizadoAutomaticamente: 0
});
return res.status(200).json({
success: true,
trip_id: tripId,
removed_status_id: previousIntermediateStatusId,
new_intermediate_status_id: newIntermediateStatusId,
current_status_id: Number.isInteger(currentStatusId)
? currentStatusId
: CLEAR_STATUS_FALLBACK_STATE,
id_punto: idPunto
});
}
const providerNumber = authorizationRows[0].n_proveedor;
let connection;
let removedStatusId = idEstado;
let currentStatusId = CLEAR_STATUS_FALLBACK_STATE;
try {
connection = await db.getConnection();
await connection.beginTransaction();
const [eligibleChangeRows] = await connection.query(
`SELECT
id_estado
FROM c_cambios_estado
WHERE id_viaje = ?
AND n_proveedor = ?
AND id_transportista = ?
AND id_estado = ?
AND COALESCE(actualizado_automaticamente, 0) = 0
FOR UPDATE`,
[tripId, providerNumber, dni, idEstado]
);
if (eligibleChangeRows.length === 0) {
try {
await connection.rollback();
} catch (rollbackError) {
console.error('Rollback failed while clearing trip status:', {
message: rollbackError.message,
dni,
tripId
});
}
return res.status(404).json({
success: false,
error: 'Trip status change not found'
});
}
const [deleteResult] = await connection.query(
`DELETE FROM c_cambios_estado
WHERE id_viaje = ?
AND n_proveedor = ?
AND id_transportista = ?
AND id_estado = ?
AND COALESCE(actualizado_automaticamente, 0) = 0`,
[tripId, providerNumber, dni, idEstado]
);
if (deleteResult.affectedRows === 0) {
throw new Error('Failed to delete trip status changes');
}
removedStatusId = idEstado;
const [currentStateRows] = await connection.query(
`SELECT id_estado
FROM c_cambios_estado
WHERE id_viaje = ?
AND n_proveedor = ?
AND id_transportista = ?
AND COALESCE(actualizado_automaticamente, 0) = 0
ORDER BY fecha_y_hora DESC
LIMIT 1`,
[tripId, providerNumber, dni]
);
if (currentStateRows.length > 0) {
const parsedCurrentStatusId = Number.parseInt(currentStateRows[0].id_estado, 10);
if (Number.isInteger(parsedCurrentStatusId)) {
currentStatusId = parsedCurrentStatusId;
}
}
await connection.query(
`UPDATE c_viajes
SET id_estado = ?,
ind_edi_app = 1,
fecha_ultima_edicion = NOW(),
id_usuarios_ultima_edicion = ?
WHERE id_viaje = ?`,
[currentStatusId, userId, tripId]
);
await propagateTripStatusToParentTrips(connection, {
initialParentTripId: tripRows[0].id_viaje_padre,
statusId: currentStatusId,
userId,
requestId,
flow: 'clear_status_manual'
});
await connection.commit();
return res.status(200).json({
success: true,
trip_id: tripId,
removed_status_id: removedStatusId,
current_status_id: currentStatusId
});
} catch (error) {
if (connection) {
try {
await connection.rollback();
} catch (rollbackError) {
console.error('Rollback failed while clearing trip status:', {
message: rollbackError.message,
dni,
tripId
});
}
}
throw error;
} finally {
if (connection) {
connection.release();
}
}
} catch (error) {
if (useIntermediatePointFlow) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_manual',
operation: 'clear_point_status',
requestId,
success: false,
result: 'ERROR',
httpStatus: 500,
errorCode: 'INTERNAL_SERVER_ERROR',
tripId,
idPunto,
idEstado,
userId,
dni,
actualizadoAutomaticamente: 0
});
}
console.error('Error clearing trip status:', {
message: error.message,
dni: req.user?.dni,
tripId: req.params?.id
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const getTripStatusHistory = async (req, res) => {
try {
const dni = req.user?.dni;
const tripId = Number.parseInt(req.params.id, 10);
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!Number.isInteger(tripId) || tripId <= 0) {
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
const [tripRows] = await db.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT 1
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const [rows] = await db.query(
`SELECT
history_events.id_estado,
history_events.fecha_hora,
history_events.id_punto
FROM (
SELECT
CAST(c.id_estado AS SIGNED) AS id_estado,
DATE_FORMAT(c.fecha_y_hora, '%Y-%m-%d %H:%i:%s') AS fecha_hora,
NULL AS id_punto,
c.id_cambio AS sort_event_id,
0 AS sort_source
FROM c_cambios_estado c
WHERE c.id_viaje = ?
AND c.fecha_y_hora IS NOT NULL
AND COALESCE(c.actualizado_automaticamente, 0) = 0
UNION ALL
SELECT
CAST(vp.id_estado_intermedio AS SIGNED) AS id_estado,
DATE_FORMAT(COALESCE(vp.fecha_y_hora, vp.fecha_foto), '%Y-%m-%d %H:%i:%s') AS fecha_hora,
CAST(vp.id_punto AS SIGNED) AS id_punto,
vp.id_punto AS sort_event_id,
1 AS sort_source
FROM c_viajes_puntos vp
WHERE vp.id_viaje = ?
AND vp.id_estado_intermedio IN (?, ?, ?)
AND COALESCE(vp.fecha_y_hora, vp.fecha_foto) IS NOT NULL
AND COALESCE(vp.actualizado_automaticamente, 0) = 0
) AS history_events
ORDER BY
history_events.fecha_hora ASC,
history_events.sort_source ASC,
history_events.id_punto ASC,
history_events.sort_event_id ASC`,
[
tripId,
tripId,
...Array.from(INTERMEDIATE_POINT_STATUS_IDS)
]
);
const history = rows.map((row) => {
const parsedStatusId = Number.parseInt(row.id_estado, 10);
const normalizedStatusId = Number.isInteger(parsedStatusId) ? parsedStatusId : null;
const parsedPointId = Number.parseInt(row.id_punto, 10);
const normalizedPointId = Number.isInteger(parsedPointId) ? parsedPointId : null;
const statusKey = parseStatusKeyFromHistoryEvent({
idEstado: normalizedStatusId,
idPunto: normalizedPointId
});
const event = {
id_estado: normalizedStatusId,
fecha_hora: row.fecha_hora || null,
id_punto: normalizedPointId
};
if (statusKey) {
event.status_key = statusKey;
}
return event;
});
return res.status(200).json({
success: true,
trip_id: tripId,
history
});
} catch (error) {
console.error('Error getting trip status history:', {
message: error.message,
dni: req.user?.dni,
tripId: req.params?.id
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const updateIntermediatePointStatus = async (req, res) => {
try {
const dni = req.user?.dni;
const userId = req.user?.id || null;
const requestId = getRequestId(req);
const tripId = Number.parseInt(req.params.id, 10);
const pointId = Number.parseInt(req.params.pointId, 10);
const idEstadoIntermedio = Number.parseInt(req.body?.id_estado_intermedio, 10);
const fechaYHora = normalizeSqlDateTimeValue(req.body?.fecha_y_hora);
const latitud = normalizeCoordinateValue(req.body?.latitud);
const longitud = normalizeCoordinateValue(req.body?.longitud);
const indFallido = Number.parseInt(req.body?.ind_fallido, 10);
const hasFechaYHora = Object.prototype.hasOwnProperty.call(req.body || {}, 'fecha_y_hora');
const hasLatitud = Object.prototype.hasOwnProperty.call(req.body || {}, 'latitud');
const hasLongitud = Object.prototype.hasOwnProperty.call(req.body || {}, 'longitud');
const hasIndFallido = Object.prototype.hasOwnProperty.call(req.body || {}, 'ind_fallido');
if (!dni) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: false,
result: 'UNAUTHORIZED',
httpStatus: 401,
errorCode: 'UNAUTHORIZED',
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
userId,
dni
});
return res.status(401).json({ error: 'Unauthorized' });
}
if (
!Number.isInteger(tripId) ||
tripId <= 0 ||
!Number.isInteger(pointId) ||
pointId <= 0 ||
!Number.isInteger(idEstadoIntermedio) ||
!INTERMEDIATE_POINT_ALLOWED_STATES_SET.has(idEstadoIntermedio) ||
!hasFechaYHora ||
fechaYHora === null ||
!hasLatitud ||
latitud === null ||
!hasLongitud ||
longitud === null ||
!hasIndFallido ||
!Number.isInteger(indFallido) ||
(indFallido !== 0 && indFallido !== 1)
) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: false,
result: 'INVALID_PAYLOAD',
httpStatus: 400,
errorCode: 'INVALID_PAYLOAD',
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido
});
return res.status(400).json({
success: false,
error: 'Invalid payload'
});
}
const [tripRows] = await db.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: false,
result: 'TRIP_NOT_FOUND',
httpStatus: 404,
errorCode: 'TRIP_NOT_FOUND',
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido
});
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT 1
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: false,
result: 'FORBIDDEN',
httpStatus: 403,
errorCode: 'FORBIDDEN',
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido
});
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const [pointRows] = await db.query(
`SELECT id_punto
FROM c_viajes_puntos
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1`,
[pointId, tripId]
);
if (pointRows.length === 0) {
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: false,
result: 'POINT_NOT_FOUND',
httpStatus: 404,
errorCode: 'POINT_NOT_FOUND',
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido
});
return res.status(404).json({
success: false,
error: 'Intermediate point not found'
});
}
await db.query(
`UPDATE c_viajes_puntos
SET id_estado_intermedio = ?,
fecha_y_hora = ?,
ind_fallido = ?,
latitud = ?,
longitud = ?
WHERE id_punto = ?
AND id_viaje = ?
LIMIT 1`,
[idEstadoIntermedio, fechaYHora, indFallido, latitud, longitud, pointId, tripId]
);
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId,
success: true,
result: 'SUCCESS',
httpStatus: 200,
tripId,
idPunto: pointId,
idEstado: idEstadoIntermedio,
newIntermediateStatusId: idEstadoIntermedio,
userId,
dni,
fechaYHora,
latitud,
longitud,
indFallido
});
return res.status(200).json({
success: true,
trip_id: tripId,
id_punto: pointId,
id_estado_intermedio: idEstadoIntermedio,
fecha_y_hora: fechaYHora,
latitud,
longitud,
ind_fallido: indFallido
});
} catch (error) {
console.error('Error updating intermediate point status:', {
message: error.message,
dni: req.user?.dni,
tripId: req.params?.id,
pointId: req.params?.pointId
});
appendIntermediatePointStatusUpdateLog({
flow: 'intermediate_point_endpoint',
operation: 'update_point_status',
requestId: getRequestId(req),
success: false,
result: 'ERROR',
httpStatus: 500,
errorCode: 'INTERNAL_SERVER_ERROR',
tripId: Number.parseInt(req.params?.id, 10),
idPunto: Number.parseInt(req.params?.pointId, 10),
idEstado: Number.parseInt(req.body?.id_estado_intermedio, 10),
userId: req.user?.id || null,
dni: req.user?.dni || null,
fechaYHora: normalizeSqlDateTimeValue(req.body?.fecha_y_hora),
latitud: normalizeCoordinateValue(req.body?.latitud),
longitud: normalizeCoordinateValue(req.body?.longitud),
indFallido: Number.parseInt(req.body?.ind_fallido, 10)
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const getIntermediatePoints = async (req, res) => {
try {
const dni = req.user?.dni;
const tripId = Number.parseInt(req.params.id, 10);
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!Number.isInteger(tripId) || tripId <= 0) {
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [tripRows] = await db.query(
`SELECT id_viaje
FROM c_viajes
WHERE id_viaje = ?
LIMIT 1`,
[tripId]
);
if (tripRows.length === 0) {
return res.status(404).json({
success: false,
error: 'Trip not found'
});
}
const [authorizationRows] = await db.query(
`SELECT 1
FROM c_viajes_proveedor
WHERE id_viaje = ?
AND dni = ?
LIMIT 1`,
[tripId, dni]
);
if (authorizationRows.length === 0) {
return res.status(403).json({
success: false,
error: 'Forbidden'
});
}
const [rows] = await db.query(
`SELECT
CAST(vp.id_punto AS SIGNED) AS id_punto,
CAST(COALESCE(m.id_punto, vp.ref_punto_id) AS SIGNED) AS id_punto_ref,
vp.posicion,
COALESCE(m.nombre, '') AS nombre,
COALESCE(m.contacto, '') AS contacto,
COALESCE(m.telefono, '') AS telefono,
COALESCE(m.direccion, '') AS direccion,
CASE
WHEN REPLACE(TRIM(COALESCE(m.latitud, '')), ',', '.') REGEXP '^(-|)[0-9]+(\\.[0-9]+|)$'
THEN CAST(REPLACE(TRIM(m.latitud), ',', '.') AS DOUBLE)
ELSE NULL
END AS latitud,
CASE
WHEN REPLACE(TRIM(COALESCE(m.longitud, '')), ',', '.') REGEXP '^(-|)[0-9]+(\\.[0-9]+|)$'
THEN CAST(REPLACE(TRIM(m.longitud), ',', '.') AS DOUBLE)
ELSE NULL
END AS longitud,
COALESCE(
vp.obs,
COALESCE(m.observaciones, ''),
''
) AS obs,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.id_estado_intermedio
END AS id_estado_intermedio,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN ''
ELSE COALESCE(te.estado, '')
END AS estado_intermedio,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN ''
ELSE COALESCE(te.estado_en, '')
END AS estado_intermedio_en,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.fecha_y_hora
END AS fecha_y_hora,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.ind_fallido
END AS ind_fallido,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.latitud_estado
END AS latitud_estado,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.longitud_estado
END AS longitud_estado,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.fecha_hora
END AS fecha_hora,
CASE
WHEN vp.actualizado_automaticamente = 1 THEN NULL
ELSE vp.fecha_hora
END AS datetime,
NULLIF(TRIM(v.observaciones_mercancia), '') AS observaciones_mercancia,
NULLIF(TRIM(v.observaciones_cliente), '') AS observaciones_cliente
FROM (
SELECT
id_punto,
id_viaje,
posicion,
id_estado_intermedio,
DATE_FORMAT(fecha_y_hora, '%Y-%m-%d %H:%i:%s') AS fecha_y_hora,
COALESCE(ind_fallido, 0) AS ind_fallido,
latitud AS latitud_estado,
longitud AS longitud_estado,
fecha_foto,
COALESCE(actualizado_automaticamente, 0) AS actualizado_automaticamente,
NULLIF(TRIM(valor), '') AS valor_plain,
CASE
WHEN NULLIF(TRIM(SUBSTRING_INDEX(valor, ':|:', 1)), '') REGEXP '^[0-9]+$'
THEN CAST(NULLIF(TRIM(SUBSTRING_INDEX(valor, ':|:', 1)), '') AS UNSIGNED)
ELSE NULL
END AS ref_punto_id,
CASE
WHEN NULLIF(TRIM(SUBSTRING_INDEX(valor, ':|:', 1)), '') REGEXP '^[0-9]+$'
THEN NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 2), ':|:', -1)), '')
ELSE NULLIF(TRIM(valor), '')
END AS obs,
CASE
WHEN NULLIF(TRIM(SUBSTRING_INDEX(valor, ':|:', 1)), '') REGEXP '^[0-9]+$'
AND NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 3), ':|:', -1)), '') REGEXP '^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
AND NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 4), ':|:', -1)), '') REGEXP '^([0-2][0-9]):([0-5][0-9])$'
THEN CONCAT(
NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 3), ':|:', -1)), ''),
' ',
NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 4), ':|:', -1)), ''),
':00'
)
WHEN NULLIF(TRIM(SUBSTRING_INDEX(valor, ':|:', 1)), '') REGEXP '^[0-9]+$'
AND NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 3), ':|:', -1)), '') REGEXP '^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
AND NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 4), ':|:', -1)), '') REGEXP '^([0-2][0-9]):([0-5][0-9]):([0-5][0-9])$'
THEN CONCAT(
NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 3), ':|:', -1)), ''),
' ',
NULLIF(TRIM(SUBSTRING_INDEX(SUBSTRING_INDEX(valor, ':|:', 4), ':|:', -1)), '')
)
WHEN fecha_foto IS NOT NULL
THEN DATE_FORMAT(fecha_foto, '%Y-%m-%d %H:%i:%s')
ELSE NULL
END AS fecha_hora
FROM c_viajes_puntos
WHERE id_viaje = ?
) vp
LEFT JOIN m_puntos_envio_recogida m
ON m.id_punto = vp.ref_punto_id
LEFT JOIN t_viaje_estados te
ON te.id_estado = vp.id_estado_intermedio
LEFT JOIN c_viajes v
ON v.id_viaje = vp.id_viaje
ORDER BY vp.posicion ASC, vp.id_punto ASC;`,
[tripId]
);
return res.status(200).json({
success: true,
trip_id: tripId,
points: rows
});
} catch (error) {
console.error('Error getting intermediate points:', {
message: error.message,
dni: req.user?.dni,
tripId: req.params?.id
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const getTripStates = async (_req, res) => {
try {
const [rows] = await db.query(
`SELECT id_estado, estado, estado_en
FROM t_viaje_estados
WHERE id_estado BETWEEN ? AND ?
ORDER BY id_estado ASC`,
[STATES_CATALOG_MIN_STATE, STATES_CATALOG_MAX_STATE]
);
return res.status(200).json({
success: true,
states: rows
});
} catch (error) {
console.error('Error getting trip states:', {
message: error.message
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const getActiveTrip = async (req, res) => {
try {
const dni = req.user?.dni;
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (process.env.DEBUG_ACTIVE_TRIP === '1') {
console.info('[ActiveTrip] handler reached', {
dni,
endpoint: req.originalUrl
});
}
const [rows] = await db.query(ACTIVE_TRIP_QUERY, [
dni,
ACTIVE_TRIP_MIN_STATE,
ACTIVE_TRIP_MAX_STATE,
dni,
ASSIGNED_TRIP_STATE
]);
let activeTrip = rows.length > 0 ? { ...rows[0] } : null;
if (activeTrip) {
const parsedTripId = Number.parseInt(activeTrip.id_viaje, 10);
const latestManualStatus = await getLatestManualTripStatus(db, parsedTripId);
if (latestManualStatus) {
activeTrip.id_estado = latestManualStatus.id_estado;
activeTrip.estado = latestManualStatus.estado;
activeTrip.estado_en = latestManualStatus.estado_en;
}
const normalizedStatusId = Number.parseInt(activeTrip.id_estado, 10);
activeTrip.statusId = Number.isInteger(normalizedStatusId) ? normalizedStatusId : null;
}
return res.status(200).json({
success: true,
active_trip: activeTrip
});
} catch (error) {
console.error('Error getting active trip:', {
message: error.message,
dni: req.user?.dni
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
const getTrips = async (req, res) => {
try {
const dni = req.user?.dni;
if (!dni) {
return res.status(401).json({ error: 'Unauthorized' });
}
const [rows] = await db.query(
`SELECT
p.id_viaje AS id_viaje,
p.id_viaje AS id,
v.cod_viaje AS cod_viaje,
v.cod_viaje AS tripCode,
v.id_estado AS id_estado,
v.id_estado AS statusId,
COALESCE(p1.nombre, '') AS nombrea,
COALESCE(p1.nombre, '') AS origin,
COALESCE(p2.nombre, '') AS nombreb,
COALESCE(p2.nombre, '') AS destination,
p1.latitud AS latitud_origen,
p1.longitud AS longitud_origen,
p2.latitud AS latitud_destino,
p2.longitud AS longitud_destino,
CASE
WHEN COALESCE(
NULLIF(TRIM(p.origen_h_inicio), ''),
NULLIF(TRIM(v.origen_h_inicio), ''),
NULLIF(TRIM(p.origen_h_fin), ''),
NULLIF(TRIM(v.origen_h_fin), '')
) REGEXP '^[0-2][0-9]:[0-5][0-9]$'
THEN CONCAT(
DATE_FORMAT(COALESCE(p.fecha_salida, v.fecha_salida), '%Y-%m-%d'),
' ',
COALESCE(
NULLIF(TRIM(p.origen_h_inicio), ''),
NULLIF(TRIM(v.origen_h_inicio), ''),
NULLIF(TRIM(p.origen_h_fin), ''),
NULLIF(TRIM(v.origen_h_fin), '')
),
':00'
)
WHEN COALESCE(
NULLIF(TRIM(p.origen_h_inicio), ''),
NULLIF(TRIM(v.origen_h_inicio), ''),
NULLIF(TRIM(p.origen_h_fin), ''),
NULLIF(TRIM(v.origen_h_fin), '')
) REGEXP '^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$'
THEN CONCAT(
DATE_FORMAT(COALESCE(p.fecha_salida, v.fecha_salida), '%Y-%m-%d'),
' ',
COALESCE(
NULLIF(TRIM(p.origen_h_inicio), ''),
NULLIF(TRIM(v.origen_h_inicio), ''),
NULLIF(TRIM(p.origen_h_fin), ''),
NULLIF(TRIM(v.origen_h_fin), '')
)
)
WHEN COALESCE(p.fecha_salida, v.fecha_salida) IS NOT NULL
THEN DATE_FORMAT(COALESCE(p.fecha_salida, v.fecha_salida), '%Y-%m-%d')
ELSE NULL
END AS fecha_salida,
CASE
WHEN COALESCE(
NULLIF(TRIM(p.destino_h_fin), ''),
NULLIF(TRIM(v.destino_h_fin), ''),
NULLIF(TRIM(p.destino_h_inicio), ''),
NULLIF(TRIM(v.destino_h_inicio), '')
) REGEXP '^[0-2][0-9]:[0-5][0-9]$'
THEN CONCAT(
DATE_FORMAT(COALESCE(p.fecha_llegada, v.fecha_llegada), '%Y-%m-%d'),
' ',
COALESCE(
NULLIF(TRIM(p.destino_h_fin), ''),
NULLIF(TRIM(v.destino_h_fin), ''),
NULLIF(TRIM(p.destino_h_inicio), ''),
NULLIF(TRIM(v.destino_h_inicio), '')
),
':00'
)
WHEN COALESCE(
NULLIF(TRIM(p.destino_h_fin), ''),
NULLIF(TRIM(v.destino_h_fin), ''),
NULLIF(TRIM(p.destino_h_inicio), ''),
NULLIF(TRIM(v.destino_h_inicio), '')
) REGEXP '^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$'
THEN CONCAT(
DATE_FORMAT(COALESCE(p.fecha_llegada, v.fecha_llegada), '%Y-%m-%d'),
' ',
COALESCE(
NULLIF(TRIM(p.destino_h_fin), ''),
NULLIF(TRIM(v.destino_h_fin), ''),
NULLIF(TRIM(p.destino_h_inicio), ''),
NULLIF(TRIM(v.destino_h_inicio), '')
)
)
WHEN COALESCE(p.fecha_llegada, v.fecha_llegada) IS NOT NULL
THEN DATE_FORMAT(COALESCE(p.fecha_llegada, v.fecha_llegada), '%Y-%m-%d')
ELSE NULL
END AS fecha_llegada,
NULLIF(TRIM(v.observaciones_mercancia), '') AS observaciones_mercancia,
NULLIF(TRIM(v.observaciones_cliente), '') AS observaciones_cliente
FROM c_viajes_proveedor p
INNER JOIN c_viajes v
ON v.id_viaje = p.id_viaje
INNER JOIN m_proveedores_trasportistas t
ON t.dni = p.dni
AND t.desactivado = 0
LEFT JOIN m_puntos_envio_recogida p1
ON p1.id_punto = p.id_punto_recogida
LEFT JOIN m_puntos_envio_recogida p2
ON p2.id_punto = p.id_punto_entrega
WHERE p.dni = ?
AND v.id_estado IN (?, ?, ?, ?)
ORDER BY COALESCE(p.fecha_salida, v.fecha_salida) DESC, p.id_viaje DESC`,
[
dni,
MOBILE_TRIPS_ALLOWED_STATES[0],
MOBILE_TRIPS_ALLOWED_STATES[1],
MOBILE_TRIPS_ALLOWED_STATES[2],
MOBILE_TRIPS_ALLOWED_STATES[3]
]
);
return res.status(200).json({
trips: rows
});
} catch (error) {
console.error('Error getting trips list:', {
message: error.message,
dni: req.user?.dni
});
return res.status(500).json({
success: false,
error: 'Internal server error'
});
}
};
module.exports = {
createTripIncidence,
startTrip,
updateTripStatus,
updateTripStatusAutomatically,
clearTripStatus,
getTripStatusHistory,
updateIntermediatePointStatus,
getIntermediatePoints,
getTripStates,
getActiveTrip,
getTrips
};