3623 lines
125 KiB
JavaScript
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
|
|
};
|