Compare commits
5
Commits
12364bcb44
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c093c3f7c2 | ||
|
|
73c1416f7a | ||
|
|
9b4ea0b415 | ||
|
|
8a5cdba7df | ||
|
|
5212bbad71 |
@@ -7,6 +7,7 @@ const authRoutes = require('./src/routes/authRoutes');
|
|||||||
const profileRoutes = require('./src/routes/profileRoutes');
|
const profileRoutes = require('./src/routes/profileRoutes');
|
||||||
const tripsRoutes = require('./src/routes/tripsRoutes');
|
const tripsRoutes = require('./src/routes/tripsRoutes');
|
||||||
const driverLicenseRoutes = require('./src/routes/driverLicenseRoutes');
|
const driverLicenseRoutes = require('./src/routes/driverLicenseRoutes');
|
||||||
|
const availabilityRoutes = require('./src/routes/availabilityRoutes');
|
||||||
const { appendPostLog } = require('./src/utils/postLog');
|
const { appendPostLog } = require('./src/utils/postLog');
|
||||||
|
|
||||||
dotenv.config({ path: path.resolve(__dirname, '.env'), override: true });
|
dotenv.config({ path: path.resolve(__dirname, '.env'), override: true });
|
||||||
@@ -54,6 +55,76 @@ app.use((req, res, next) => {
|
|||||||
contentType.toLowerCase().startsWith('multipart/form-data');
|
contentType.toLowerCase().startsWith('multipart/form-data');
|
||||||
|
|
||||||
if (isMultipartFormData) {
|
if (isMultipartFormData) {
|
||||||
|
const startedAt = process.hrtime.bigint();
|
||||||
|
const baseLogPayload = {
|
||||||
|
request_id: requestId,
|
||||||
|
method: req.method,
|
||||||
|
path: req.originalUrl || req.url,
|
||||||
|
ip: req.ip || null,
|
||||||
|
content_type: contentType,
|
||||||
|
content_length: Number.parseInt(req.get('content-length'), 10) || null,
|
||||||
|
has_authorization_header: Boolean(authorizationHeader),
|
||||||
|
user_agent: String(req.get('user-agent') || '').slice(0, 255) || null
|
||||||
|
};
|
||||||
|
let responseFinished = false;
|
||||||
|
|
||||||
|
appendPostLog({
|
||||||
|
event: 'upload_request_started',
|
||||||
|
...baseLogPayload
|
||||||
|
});
|
||||||
|
|
||||||
|
res.once('finish', () => {
|
||||||
|
responseFinished = true;
|
||||||
|
const parserStatus = req.uploadDiagnostics?.parser_status || 'not_reached';
|
||||||
|
let failureStage = null;
|
||||||
|
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
if (parserStatus === 'rejected') {
|
||||||
|
failureStage = 'multipart_parser';
|
||||||
|
} else if (parserStatus === 'parsed') {
|
||||||
|
failureStage = 'controller_or_persistence';
|
||||||
|
} else {
|
||||||
|
failureStage = 'authentication_rate_limit_or_route';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendPostLog({
|
||||||
|
event: 'upload_request_finished',
|
||||||
|
...baseLogPayload,
|
||||||
|
status_code: res.statusCode,
|
||||||
|
duration_ms: Number(
|
||||||
|
(Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2)
|
||||||
|
),
|
||||||
|
request_complete: req.complete,
|
||||||
|
outcome: res.statusCode < 400 ? 'success' : 'error',
|
||||||
|
failure_stage: failureStage,
|
||||||
|
upload: req.uploadDiagnostics || {
|
||||||
|
parser_status: 'not_reached'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
res.once('close', () => {
|
||||||
|
if (responseFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendPostLog({
|
||||||
|
event: 'upload_request_interrupted',
|
||||||
|
...baseLogPayload,
|
||||||
|
status_code: res.statusCode,
|
||||||
|
duration_ms: Number(
|
||||||
|
(Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2)
|
||||||
|
),
|
||||||
|
request_complete: req.complete,
|
||||||
|
outcome: 'interrupted',
|
||||||
|
failure_stage: 'transport_or_client_disconnect',
|
||||||
|
upload: req.uploadDiagnostics || {
|
||||||
|
parser_status: 'not_reached'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +178,7 @@ app.use('/', authRoutes);
|
|||||||
app.use('/', profileRoutes);
|
app.use('/', profileRoutes);
|
||||||
app.use('/', require('./src/routes/locationRoutes'));
|
app.use('/', require('./src/routes/locationRoutes'));
|
||||||
app.use('/api', require('./src/routes/stressRoutes')); // Stress Test Endpoint
|
app.use('/api', require('./src/routes/stressRoutes')); // Stress Test Endpoint
|
||||||
|
app.use('/api', availabilityRoutes);
|
||||||
app.use('/api', tripsRoutes);
|
app.use('/api', tripsRoutes);
|
||||||
app.use('/api', driverLicenseRoutes);
|
app.use('/api', driverLicenseRoutes);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
const db = require('../config/db');
|
||||||
|
|
||||||
|
const getCoordinatesFromBody = (body) => {
|
||||||
|
const lat = body?.latitud ?? body?.latitude;
|
||||||
|
const lng = body?.longitud ?? body?.longitude;
|
||||||
|
|
||||||
|
if (lat === undefined || lat === null || lng === undefined || lng === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lat, lng };
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertOnlineAvailability = async (dni, lat, lng) => {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT id_usuario
|
||||||
|
FROM c_trazabilidad_online
|
||||||
|
WHERE id_usuario = ?
|
||||||
|
LIMIT 1`,
|
||||||
|
[dni]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE c_trazabilidad_online
|
||||||
|
SET latitud = ?, longitud = ?, fecha = NOW()
|
||||||
|
WHERE id_usuario = ?`,
|
||||||
|
[String(lat), String(lng), dni]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO c_trazabilidad_online
|
||||||
|
(latitud, longitud, id_usuario, fecha)
|
||||||
|
VALUES (?, ?, ?, NOW())`,
|
||||||
|
[String(lat), String(lng), dni]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailability = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const dni = String(req.user.dni);
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT COUNT(*) AS total
|
||||||
|
FROM c_trazabilidad_online
|
||||||
|
WHERE id_usuario = ?`,
|
||||||
|
[dni]
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
available: Number(rows[0]?.total || 0) > 0
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting availability:', error);
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAvailability = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const coords = getCoordinatesFromBody(req.body);
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'missing_coords',
|
||||||
|
message: 'latitud/longitud or latitude/longitude are required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await upsertOnlineAvailability(String(req.user.dni), coords.lat, coords.lng);
|
||||||
|
|
||||||
|
return res.json({ success: true, available: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting availability:', error);
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAvailability = async (req, res) => {
|
||||||
|
try {
|
||||||
|
await db.query(
|
||||||
|
`DELETE FROM c_trazabilidad_online
|
||||||
|
WHERE id_usuario = ?`,
|
||||||
|
[String(req.user.dni)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ success: true, available: false });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting availability:', error);
|
||||||
|
return res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
deleteAvailability,
|
||||||
|
getAvailability,
|
||||||
|
setAvailability,
|
||||||
|
upsertOnlineAvailability
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const db = require('../config/db');
|
const db = require('../config/db');
|
||||||
const agheeraPushClient = require('../services/agheeraPushClient');
|
const agheeraPushClient = require('../services/agheeraPushClient');
|
||||||
|
const { upsertOnlineAvailability } = require('./availabilityController');
|
||||||
|
|
||||||
const AGHEERA_CLIENT_ID = 532;
|
const AGHEERA_CLIENT_ID = 532;
|
||||||
|
|
||||||
@@ -73,6 +74,22 @@ const getTripIdFromLocation = (locationData) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAvailabilityModeEnabled = (value) =>
|
||||||
|
value === true || value === 'true' || value === 1 || value === '1';
|
||||||
|
|
||||||
|
const hasAvailabilityModeEnabled = (data, loc) => {
|
||||||
|
const candidates = [
|
||||||
|
data?.availability_mode,
|
||||||
|
data?.params?.availability_mode,
|
||||||
|
data?.extras?.availability_mode,
|
||||||
|
loc?.availability_mode,
|
||||||
|
loc?.params?.availability_mode,
|
||||||
|
loc?.extras?.availability_mode
|
||||||
|
];
|
||||||
|
|
||||||
|
return candidates.some(isAvailabilityModeEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
const getRawTimestampFromLocation = (locationData) => {
|
const getRawTimestampFromLocation = (locationData) => {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
locationData?.timestamp,
|
locationData?.timestamp,
|
||||||
@@ -167,7 +184,12 @@ const pushLocationToAgheera = async ({ latitude, longitude, dni, tripId, measure
|
|||||||
longitude,
|
longitude,
|
||||||
vehicleId: licensePlate,
|
vehicleId: licensePlate,
|
||||||
licensePlate,
|
licensePlate,
|
||||||
measurementTime
|
measurementTime,
|
||||||
|
metadata: {
|
||||||
|
source: 'location',
|
||||||
|
trip_id: tripId,
|
||||||
|
dni
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -236,6 +258,7 @@ const saveLocation = async (req, res) => {
|
|||||||
|
|
||||||
const rowsToInsert = [];
|
const rowsToInsert = [];
|
||||||
const locationsToPush = [];
|
const locationsToPush = [];
|
||||||
|
const onlineAvailabilityUpdates = [];
|
||||||
|
|
||||||
for (const loc of locations) {
|
for (const loc of locations) {
|
||||||
const coords = getCoordinatesFromLocation(loc);
|
const coords = getCoordinatesFromLocation(loc);
|
||||||
@@ -269,6 +292,13 @@ const saveLocation = async (req, res) => {
|
|||||||
tripId,
|
tripId,
|
||||||
measurementTime: persistedTimestamp.value
|
measurementTime: persistedTimestamp.value
|
||||||
});
|
});
|
||||||
|
if (dni && hasAvailabilityModeEnabled(data, loc)) {
|
||||||
|
onlineAvailabilityUpdates.push({
|
||||||
|
dni,
|
||||||
|
lat: coords.lat,
|
||||||
|
lng: coords.lng
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +318,11 @@ const saveLocation = async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const agheeraResults = await pushLocationsToAgheera(locationsToPush);
|
const agheeraResults = await pushLocationsToAgheera(locationsToPush);
|
||||||
|
|
||||||
|
for (const update of onlineAvailabilityUpdates) {
|
||||||
|
await upsertOnlineAvailability(update.dni, update.lat, update.lng);
|
||||||
|
}
|
||||||
|
|
||||||
const responseBody = {
|
const responseBody = {
|
||||||
success: true,
|
success: true,
|
||||||
count: rowsToInsert.length,
|
count: rowsToInsert.length,
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const LEGACY_STATUS_PHOTO_FIELD_MAX_LENGTH = 100;
|
|||||||
const LEGACY_INTERMEDIATE_POINT_VALUE_SEPARATOR = ':|:';
|
const LEGACY_INTERMEDIATE_POINT_VALUE_SEPARATOR = ':|:';
|
||||||
const LEGACY_INTERMEDIATE_POINT_REFERENCE_REGEX = /^[0-9]+$/;
|
const LEGACY_INTERMEDIATE_POINT_REFERENCE_REGEX = /^[0-9]+$/;
|
||||||
const MOBILE_TRIPS_ALLOWED_STATES = [7, 8, 9, 1];
|
const MOBILE_TRIPS_ALLOWED_STATES = [7, 8, 9, 1];
|
||||||
|
const MOBILE_TRIPS_DEFAULT_PAGE = 1;
|
||||||
|
const MOBILE_TRIPS_DEFAULT_LIMIT = 25;
|
||||||
|
const MOBILE_TRIPS_MAX_LIMIT = 100;
|
||||||
const CLEAR_STATUS_FALLBACK_STATE = 1;
|
const CLEAR_STATUS_FALLBACK_STATE = 1;
|
||||||
const INTERMEDIATE_POINT_ALLOWED_STATES = [3, 4, 5];
|
const INTERMEDIATE_POINT_ALLOWED_STATES = [3, 4, 5];
|
||||||
const INTERMEDIATE_POINT_ALLOWED_STATES_SET = new Set(INTERMEDIATE_POINT_ALLOWED_STATES);
|
const INTERMEDIATE_POINT_ALLOWED_STATES_SET = new Set(INTERMEDIATE_POINT_ALLOWED_STATES);
|
||||||
@@ -27,6 +30,7 @@ const FAILED_TRIP_STATE = 9;
|
|||||||
const AGHEERA_CLIENT_ID = 532;
|
const AGHEERA_CLIENT_ID = 532;
|
||||||
const INTERMEDIATE_POINT_STATUS_IDS = new Set([3, 4, 5]);
|
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 INCIDENCE_TEXT_CONTROL_CHARACTERS_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
||||||
|
const SQL_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
const GLOBAL_STATUS_KEYS_BY_STATE_ID = new Map([
|
const GLOBAL_STATUS_KEYS_BY_STATE_ID = new Map([
|
||||||
[1, 'assigned'],
|
[1, 'assigned'],
|
||||||
[2, 'en_camino'],
|
[2, 'en_camino'],
|
||||||
@@ -48,6 +52,59 @@ const appendTripStatusDebugLog = (payload) => {
|
|||||||
|
|
||||||
console.info('[TripStatusDebug]', payload);
|
console.info('[TripStatusDebug]', payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseTripsPositiveInteger = (value, defaultValue) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(String(value))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = Number.parseInt(value, 10);
|
||||||
|
return parsedValue >= 1 ? parsedValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTripsStatusIds = (value) => {
|
||||||
|
if (value === undefined || value === '') {
|
||||||
|
return MOBILE_TRIPS_ALLOWED_STATES;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawStatusIds = String(value).split(',');
|
||||||
|
|
||||||
|
if (rawStatusIds.some((statusId) => !/^\d+$/.test(statusId))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawStatusIds.map((statusId) => Number.parseInt(statusId, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidSqlDate = (value) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SQL_DATE_REGEX.test(String(value))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [year, month, day] = String(value).split('-').map(Number);
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
|
||||||
|
return (
|
||||||
|
date.getUTCFullYear() === year &&
|
||||||
|
date.getUTCMonth() === month - 1 &&
|
||||||
|
date.getUTCDate() === day
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOneDayToSqlDate = (value) => {
|
||||||
|
const [year, month, day] = String(value).split('-').map(Number);
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day + 1));
|
||||||
|
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
};
|
||||||
const getTripStatusUpdatesLogPath = () =>
|
const getTripStatusUpdatesLogPath = () =>
|
||||||
process.env.TRIP_STATUS_UPDATES_LOG_PATH ||
|
process.env.TRIP_STATUS_UPDATES_LOG_PATH ||
|
||||||
'/var/log/status.log';
|
'/var/log/status.log';
|
||||||
@@ -284,7 +341,13 @@ const pushTripStatusPositionToAgheera = async ({
|
|||||||
longitude: longitud,
|
longitude: longitud,
|
||||||
vehicleId: licensePlate,
|
vehicleId: licensePlate,
|
||||||
licensePlate,
|
licensePlate,
|
||||||
measurementTime
|
measurementTime,
|
||||||
|
metadata: {
|
||||||
|
source: 'trip_status',
|
||||||
|
request_id: requestId,
|
||||||
|
flow,
|
||||||
|
trip_id: tripId
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
appendTripStatusDebugLog({
|
appendTripStatusDebugLog({
|
||||||
@@ -3581,6 +3644,8 @@ const getActiveTrip = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTrips = async (req, res) => {
|
const getTrips = async (req, res) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dni = req.user?.dni;
|
const dni = req.user?.dni;
|
||||||
|
|
||||||
@@ -3588,6 +3653,79 @@ const getTrips = async (req, res) => {
|
|||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const page = parseTripsPositiveInteger(req.query.page, MOBILE_TRIPS_DEFAULT_PAGE);
|
||||||
|
const requestedLimit = parseTripsPositiveInteger(req.query.limit, MOBILE_TRIPS_DEFAULT_LIMIT);
|
||||||
|
const statusIds = parseTripsStatusIds(req.query.status_ids);
|
||||||
|
|
||||||
|
if (page === null || requestedLimit === null || requestedLimit > MOBILE_TRIPS_MAX_LIMIT) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid pagination parameters'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusIds || statusIds.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid status_ids parameter'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidSqlDate(req.query.date_from) || !isValidSqlDate(req.query.date_to)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid date parameter'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.query.date_from !== undefined &&
|
||||||
|
req.query.date_to !== undefined &&
|
||||||
|
req.query.date_from > req.query.date_to
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid date range'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = requestedLimit;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const whereClauses = [
|
||||||
|
'p.dni = ?',
|
||||||
|
'v.id_estado IN (' + statusIds.map(() => '?').join(', ') + ')'
|
||||||
|
];
|
||||||
|
const queryParams = [dni, ...statusIds];
|
||||||
|
|
||||||
|
if (req.query.date_from !== undefined) {
|
||||||
|
whereClauses.push('COALESCE(p.fecha_salida, v.fecha_salida) >= ?');
|
||||||
|
queryParams.push(req.query.date_from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.date_to !== undefined) {
|
||||||
|
whereClauses.push('COALESCE(p.fecha_salida, v.fecha_salida) < ?');
|
||||||
|
queryParams.push(addOneDayToSqlDate(req.query.date_to));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromAndWhereSql = `
|
||||||
|
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 ${whereClauses.join('\n AND ')}`;
|
||||||
|
|
||||||
|
const [[countRow]] = await db.query(
|
||||||
|
`SELECT COUNT(*) AS total ${fromAndWhereSql}`,
|
||||||
|
queryParams
|
||||||
|
);
|
||||||
|
const total = Number.parseInt(countRow?.total, 10) || 0;
|
||||||
|
|
||||||
const [rows] = await db.query(
|
const [rows] = await db.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
p.id_viaje AS id_viaje,
|
p.id_viaje AS id_viaje,
|
||||||
@@ -3682,30 +3820,32 @@ const getTrips = async (req, res) => {
|
|||||||
END AS fecha_llegada,
|
END AS fecha_llegada,
|
||||||
NULLIF(TRIM(v.observaciones_mercancia), '') AS observaciones_mercancia,
|
NULLIF(TRIM(v.observaciones_mercancia), '') AS observaciones_mercancia,
|
||||||
NULLIF(TRIM(v.observaciones_cliente), '') AS observaciones_cliente
|
NULLIF(TRIM(v.observaciones_cliente), '') AS observaciones_cliente
|
||||||
FROM c_viajes_proveedor p
|
${fromAndWhereSql}
|
||||||
INNER JOIN c_viajes v
|
ORDER BY COALESCE(p.fecha_salida, v.fecha_salida) DESC, p.id_viaje DESC
|
||||||
ON v.id_viaje = p.id_viaje
|
LIMIT ? OFFSET ?`,
|
||||||
INNER JOIN m_proveedores_trasportistas t
|
[...queryParams, limit, offset]
|
||||||
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]
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.info('[TripsList]', {
|
||||||
|
dni,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
filters: {
|
||||||
|
status_ids: statusIds,
|
||||||
|
date_from: req.query.date_from || null,
|
||||||
|
date_to: req.query.date_to || null
|
||||||
|
},
|
||||||
|
rows: rows.length,
|
||||||
|
total,
|
||||||
|
elapsed_ms: Date.now() - startedAt
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
trips: rows
|
trips: rows,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
has_more: offset + rows.length < total
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting trips list:', {
|
console.error('Error getting trips list:', {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
} = require('../utils/uploadDiagnostics');
|
||||||
|
|
||||||
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
|
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
|
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
|
||||||
@@ -40,14 +45,31 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadDriverLicense = (req, res, next) => {
|
const uploadDriverLicense = (req, res, next) => {
|
||||||
|
const flow = 'driver_license';
|
||||||
|
|
||||||
|
beginUploadParsing(req, flow);
|
||||||
internalUpload.fields([
|
internalUpload.fields([
|
||||||
{ name: FRONT_FILE_FIELD, maxCount: 1 },
|
{ name: FRONT_FILE_FIELD, maxCount: 1 },
|
||||||
{ name: BACK_FILE_FIELD, maxCount: 1 }
|
{ name: BACK_FILE_FIELD, maxCount: 1 }
|
||||||
])(req, res, (error) => {
|
])(req, res, (error) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
const uploadedFiles = [
|
||||||
|
...(Array.isArray(req.files?.[FRONT_FILE_FIELD])
|
||||||
|
? req.files[FRONT_FILE_FIELD]
|
||||||
|
: []),
|
||||||
|
...(Array.isArray(req.files?.[BACK_FILE_FIELD])
|
||||||
|
? req.files[BACK_FILE_FIELD]
|
||||||
|
: [])
|
||||||
|
];
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: uploadedFiles
|
||||||
|
});
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const crypto = require('crypto');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
} = require('../utils/uploadDiagnostics');
|
||||||
|
|
||||||
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
|
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
|
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
|
||||||
@@ -57,11 +62,20 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadProfilePhoto = (req, res, next) => {
|
const uploadProfilePhoto = (req, res, next) => {
|
||||||
|
const flow = 'profile_photo';
|
||||||
|
|
||||||
|
beginUploadParsing(req, flow);
|
||||||
internalUpload.single('foto_perfil')(req, res, (error) => {
|
internalUpload.single('foto_perfil')(req, res, (error) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: req.file ? [req.file] : []
|
||||||
|
});
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
if (error.code === 'LIMIT_FILE_SIZE') {
|
if (error.code === 'LIMIT_FILE_SIZE') {
|
||||||
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
|
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ const multer = require('multer');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const {
|
const {
|
||||||
getTripStatusUploadsDir,
|
getTripStatusUploadsDir,
|
||||||
getTripStatusFallbackUploadsDir,
|
|
||||||
replicateUploadedFilesToRemote,
|
replicateUploadedFilesToRemote,
|
||||||
removeUploadedTripStatusFiles
|
removeUploadedTripStatusFiles
|
||||||
} = require('../services/tripStatusPhotoStorage');
|
} = require('../services/tripStatusPhotoStorage');
|
||||||
const { appendPostLog } = require('../utils/postLog');
|
const { appendPostLog } = require('../utils/postLog');
|
||||||
|
const {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
} = require('../utils/uploadDiagnostics');
|
||||||
|
|
||||||
const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
|
const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
|
||||||
const MAX_TRIP_STATUS_FILES = 5;
|
const MAX_TRIP_STATUS_FILES = 5;
|
||||||
@@ -28,21 +32,9 @@ const getTripDirectorySegment = (req) => {
|
|||||||
|
|
||||||
const getTripStatusUploadsTripDir = (req) =>
|
const getTripStatusUploadsTripDir = (req) =>
|
||||||
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
||||||
const getTripStatusFallbackUploadsTripDir = (req) =>
|
|
||||||
path.join(getTripStatusFallbackUploadsDir(), getTripDirectorySegment(req));
|
|
||||||
|
|
||||||
const ensureTripStatusUploadsDir = (req) => {
|
const ensureTripStatusUploadsDir = (req) => {
|
||||||
const primaryTripDir = getTripStatusUploadsTripDir(req);
|
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(primaryTripDir, { recursive: true });
|
|
||||||
return primaryTripDir;
|
|
||||||
} catch (primaryError) {
|
|
||||||
const fallbackTripDir = getTripStatusFallbackUploadsTripDir(req);
|
|
||||||
|
|
||||||
fs.mkdirSync(fallbackTripDir, { recursive: true });
|
|
||||||
return fallbackTripDir;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExtensionFromMimeType = (mimeType) => {
|
const getExtensionFromMimeType = (mimeType) => {
|
||||||
@@ -68,7 +60,8 @@ const getExtensionFromMimeType = (mimeType) => {
|
|||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => {
|
destination: (req, file, cb) => {
|
||||||
try {
|
try {
|
||||||
cb(null, ensureTripStatusUploadsDir(req));
|
ensureTripStatusUploadsDir(req);
|
||||||
|
cb(null, getTripStatusUploadsTripDir(req));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cb(error);
|
cb(error);
|
||||||
}
|
}
|
||||||
@@ -96,6 +89,12 @@ const internalUpload = multer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadTripStatusPhotos = (req, res, next) => {
|
const uploadTripStatusPhotos = (req, res, next) => {
|
||||||
|
const flow = 'trip_status_photos';
|
||||||
|
const storageMode = String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
beginUploadParsing(req, flow);
|
||||||
internalUpload.fields([
|
internalUpload.fields([
|
||||||
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
||||||
{ name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES }
|
{ name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES }
|
||||||
@@ -104,6 +103,11 @@ const uploadTripStatusPhotos = (req, res, next) => {
|
|||||||
const uploadedFiles = collectUploadedTripStatusFiles(req);
|
const uploadedFiles = collectUploadedTripStatusFiles(req);
|
||||||
const authorizationHeader = req.get('authorization');
|
const authorizationHeader = req.get('authorization');
|
||||||
|
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: uploadedFiles,
|
||||||
|
storageMode
|
||||||
|
});
|
||||||
appendPostLog({
|
appendPostLog({
|
||||||
request_id: req.requestId || null,
|
request_id: req.requestId || null,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
@@ -127,9 +131,16 @@ const uploadTripStatusPhotos = (req, res, next) => {
|
|||||||
tripId: req.params?.id,
|
tripId: req.params?.id,
|
||||||
files: uploadedFiles
|
files: uploadedFiles
|
||||||
});
|
});
|
||||||
|
markUploadParsed(req, {
|
||||||
|
flow,
|
||||||
|
files: uploadedFiles,
|
||||||
|
storageMode
|
||||||
|
});
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markUploadRejected(req, { flow, error });
|
||||||
|
|
||||||
if (error instanceof multer.MulterError) {
|
if (error instanceof multer.MulterError) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const availabilityController = require('../controllers/availabilityController');
|
||||||
|
const authenticateDevice = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/availability', authenticateDevice, availabilityController.getAvailability);
|
||||||
|
router.post('/availability', authenticateDevice, availabilityController.setAvailability);
|
||||||
|
router.delete('/availability', authenticateDevice, availabilityController.deleteAvailability);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
const DEFAULT_AGHEERA_PUSH_URL = 'https://push-test.agheera.com/Telematics/Positions';
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const DEFAULT_AGHEERA_PUSH_URL = 'https://push-dhl.agheera.com/Telematics/positions';
|
||||||
|
const DEFAULT_AGHEERA_PUSH_LOG_PATH = '/var/log/agheera_push.log';
|
||||||
|
|
||||||
let httpClientOverride = null;
|
let httpClientOverride = null;
|
||||||
|
|
||||||
@@ -8,6 +12,41 @@ const getPushUrl = () =>
|
|||||||
const getApiKey = () =>
|
const getApiKey = () =>
|
||||||
String(process.env.AGHEERA_API_KEY || '').trim();
|
String(process.env.AGHEERA_API_KEY || '').trim();
|
||||||
|
|
||||||
|
const getPushLogPath = () =>
|
||||||
|
String(process.env.AGHEERA_PUSH_LOG_PATH || DEFAULT_AGHEERA_PUSH_LOG_PATH).trim();
|
||||||
|
|
||||||
|
const appendPushLog = async ({ metadata, url, payload, success, status, responseBody, error }) => {
|
||||||
|
if (process.env.AGHEERA_PUSH_LOGS === '0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstVehicle = Array.isArray(payload?.Vehicles) ? payload.Vehicles[0] : null;
|
||||||
|
const entry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...(metadata || {}),
|
||||||
|
url,
|
||||||
|
vehicleId: firstVehicle?.vehicleId ?? null,
|
||||||
|
licensePlate: firstVehicle?.licensePlate ?? null,
|
||||||
|
latitude: firstVehicle?.latitude ?? null,
|
||||||
|
longitude: firstVehicle?.longitude ?? null,
|
||||||
|
measurementTime: firstVehicle?.measurementTime ?? null,
|
||||||
|
payload,
|
||||||
|
success,
|
||||||
|
http_status: status ?? null,
|
||||||
|
response_body: responseBody || '',
|
||||||
|
error: error || null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const logPath = getPushLogPath();
|
||||||
|
await fs.promises.mkdir(path.dirname(logPath), { recursive: true });
|
||||||
|
await fs.promises.appendFile(logPath, `${JSON.stringify(entry)}
|
||||||
|
`);
|
||||||
|
} catch (logError) {
|
||||||
|
console.error('Failed to append Agheera push log:', { message: logError.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatMeasurementTime = (dateValue) => {
|
const formatMeasurementTime = (dateValue) => {
|
||||||
const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
|
const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
|
||||||
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||||
@@ -53,18 +92,9 @@ const pushPosition = async ({
|
|||||||
longitude,
|
longitude,
|
||||||
vehicleId,
|
vehicleId,
|
||||||
licensePlate,
|
licensePlate,
|
||||||
measurementTime
|
measurementTime,
|
||||||
|
metadata
|
||||||
}) => {
|
}) => {
|
||||||
const httpClient = getHttpClient();
|
|
||||||
if (!httpClient) {
|
|
||||||
throw new Error('Agheera HTTP client unavailable');
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = getApiKey();
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new Error('Agheera API key missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = getPushUrl();
|
const url = getPushUrl();
|
||||||
const payload = buildPositionPayload({
|
const payload = buildPositionPayload({
|
||||||
latitude,
|
latitude,
|
||||||
@@ -74,7 +104,23 @@ const pushPosition = async ({
|
|||||||
measurementTime
|
measurementTime
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await httpClient(url, {
|
const httpClient = getHttpClient();
|
||||||
|
if (!httpClient) {
|
||||||
|
const message = 'Agheera HTTP client unavailable';
|
||||||
|
await appendPushLog({ metadata, url, payload, success: false, error: message });
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = getApiKey();
|
||||||
|
if (!apiKey) {
|
||||||
|
const message = 'Agheera API key missing';
|
||||||
|
await appendPushLog({ metadata, url, payload, success: false, error: message });
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await httpClient(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -82,15 +128,43 @@ const pushPosition = async ({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await appendPushLog({
|
||||||
|
metadata,
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const responseBody = typeof response?.text === 'function' ? await response.text() : '';
|
const responseBody = typeof response?.text === 'function' ? await response.text() : '';
|
||||||
if (!response?.ok) {
|
if (!response?.ok) {
|
||||||
const error = new Error('Agheera push failed');
|
const error = new Error('Agheera push failed');
|
||||||
error.status = response?.status || null;
|
error.status = response?.status || null;
|
||||||
error.body = responseBody;
|
error.body = responseBody;
|
||||||
|
await appendPushLog({
|
||||||
|
metadata,
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
success: false,
|
||||||
|
status: error.status,
|
||||||
|
responseBody,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await appendPushLog({
|
||||||
|
metadata,
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
success: true,
|
||||||
|
status: response.status,
|
||||||
|
responseBody
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
body: responseBody
|
body: responseBody
|
||||||
@@ -107,6 +181,7 @@ const __resetHttpClientForTests = () => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_AGHEERA_PUSH_URL,
|
DEFAULT_AGHEERA_PUSH_URL,
|
||||||
|
DEFAULT_AGHEERA_PUSH_LOG_PATH,
|
||||||
pushPosition,
|
pushPosition,
|
||||||
__setHttpClientForTests,
|
__setHttpClientForTests,
|
||||||
__resetHttpClientForTests
|
__resetHttpClientForTests
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ const DEFAULT_SFTP_PORT = 22;
|
|||||||
|
|
||||||
let sftpClientFactoryOverride = null;
|
let sftpClientFactoryOverride = null;
|
||||||
|
|
||||||
const PRIMARY_TRIP_STATUS_UPLOAD_DIR =
|
|
||||||
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/trips/status';
|
|
||||||
const FALLBACK_TRIP_STATUS_UPLOAD_DIR =
|
|
||||||
path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
|
|
||||||
|
|
||||||
const resolveUploadDirCandidate = (uploadDir) =>
|
const resolveUploadDirCandidate = (uploadDir) =>
|
||||||
path.isAbsolute(uploadDir)
|
path.isAbsolute(uploadDir)
|
||||||
? uploadDir
|
? uploadDir
|
||||||
@@ -42,10 +37,19 @@ const selectUploadDirCandidate = (uploadDirs) =>
|
|||||||
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
|
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
|
||||||
|
|
||||||
const getTripStatusUploadsDir = () => {
|
const getTripStatusUploadsDir = () => {
|
||||||
return PRIMARY_TRIP_STATUS_UPLOAD_DIR;
|
const configuredUploadDir = String(process.env.TRIP_STATUS_UPLOAD_DIR || '').trim();
|
||||||
};
|
|
||||||
|
|
||||||
const getTripStatusFallbackUploadsDir = () => FALLBACK_TRIP_STATUS_UPLOAD_DIR;
|
if (configuredUploadDir) {
|
||||||
|
const configuredUploadDirs = configuredUploadDir
|
||||||
|
.split(';')
|
||||||
|
.map((uploadDir) => uploadDir.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return selectUploadDirCandidate(configuredUploadDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
|
||||||
|
};
|
||||||
|
|
||||||
const getTripStatusPhotoStorageMode = () =>
|
const getTripStatusPhotoStorageMode = () =>
|
||||||
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
||||||
@@ -205,7 +209,7 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
|
|||||||
file.tripStatusTripId = tripDirectorySegment;
|
file.tripStatusTripId = tripDirectorySegment;
|
||||||
}
|
}
|
||||||
|
|
||||||
await withSftpClient(
|
const remoteOperationSucceeded = await withSftpClient(
|
||||||
async (client, sftpConfig) => {
|
async (client, sftpConfig) => {
|
||||||
await ensureRemoteTripDirectory(client, {
|
await ensureRemoteTripDirectory(client, {
|
||||||
remoteBaseDir: sftpConfig.remoteBaseDir,
|
remoteBaseDir: sftpConfig.remoteBaseDir,
|
||||||
@@ -238,6 +242,12 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
|
|||||||
logContext: 'replicate_upload'
|
logContext: 'replicate_upload'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (shouldUseRemoteStorage() && !remoteOperationSucceeded) {
|
||||||
|
for (const file of normalizedFiles) {
|
||||||
|
file.tripStatusRemoteUploaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
|
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
|
||||||
@@ -389,7 +399,6 @@ const __resetSftpClientFactoryForTests = () => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getTripStatusUploadsDir,
|
getTripStatusUploadsDir,
|
||||||
getTripStatusFallbackUploadsDir,
|
|
||||||
replicateUploadedFilesToRemote,
|
replicateUploadedFilesToRemote,
|
||||||
removeUploadedTripStatusFiles,
|
removeUploadedTripStatusFiles,
|
||||||
removeStatusPhotosByName,
|
removeStatusPhotosByName,
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const getBodyFieldNames = (req) =>
|
||||||
|
Object.keys(req.body || {}).sort();
|
||||||
|
|
||||||
|
const getFileMetadata = (file) => {
|
||||||
|
const originalExtension = path.extname(String(file?.originalname || '')).toLowerCase();
|
||||||
|
let remoteUploadStatus = null;
|
||||||
|
|
||||||
|
if (file?.tripStatusRemoteUploaded === true) {
|
||||||
|
remoteUploadStatus = 'success';
|
||||||
|
} else if (file?.tripStatusRemoteUploaded === false) {
|
||||||
|
remoteUploadStatus = 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
field_name: file?.fieldname || null,
|
||||||
|
original_extension: originalExtension || null,
|
||||||
|
mimetype: file?.mimetype || null,
|
||||||
|
size_bytes: Number.isFinite(file?.size) ? file.size : null,
|
||||||
|
stored_filename: file?.filename || null,
|
||||||
|
local_file_created: Boolean(file?.path),
|
||||||
|
remote_upload_status: remoteUploadStatus
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const beginUploadParsing = (req, flow) => {
|
||||||
|
req.uploadDiagnostics = {
|
||||||
|
flow,
|
||||||
|
parser: 'multer',
|
||||||
|
parser_status: 'started'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const markUploadParsed = (req, { flow, files, storageMode = null }) => {
|
||||||
|
const normalizedFiles = Array.isArray(files) ? files : [];
|
||||||
|
|
||||||
|
req.uploadDiagnostics = {
|
||||||
|
flow,
|
||||||
|
parser: 'multer',
|
||||||
|
parser_status: 'parsed',
|
||||||
|
body_fields: getBodyFieldNames(req),
|
||||||
|
file_count: normalizedFiles.length,
|
||||||
|
files: normalizedFiles.map(getFileMetadata),
|
||||||
|
storage_mode: storageMode
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const markUploadRejected = (req, { flow, error }) => {
|
||||||
|
req.uploadDiagnostics = {
|
||||||
|
flow,
|
||||||
|
parser: 'multer',
|
||||||
|
parser_status: 'rejected',
|
||||||
|
body_fields: getBodyFieldNames(req),
|
||||||
|
error: {
|
||||||
|
type: error?.constructor?.name || 'Error',
|
||||||
|
code: error?.code || error?.message || null,
|
||||||
|
field_name: error?.field || null,
|
||||||
|
message: String(error?.message || 'Unknown upload error').slice(0, 500)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
beginUploadParsing,
|
||||||
|
markUploadParsed,
|
||||||
|
markUploadRejected
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const agheeraPushClient = require('../src/services/agheeraPushClient');
|
||||||
|
|
||||||
|
let originalApiKey;
|
||||||
|
let originalPushLogPath;
|
||||||
|
let originalPushLogs;
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
originalApiKey = process.env.AGHEERA_API_KEY;
|
||||||
|
originalPushLogPath = process.env.AGHEERA_PUSH_LOG_PATH;
|
||||||
|
originalPushLogs = process.env.AGHEERA_PUSH_LOGS;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
process.env.AGHEERA_API_KEY = originalApiKey;
|
||||||
|
process.env.AGHEERA_PUSH_LOG_PATH = originalPushLogPath;
|
||||||
|
process.env.AGHEERA_PUSH_LOGS = originalPushLogs;
|
||||||
|
agheeraPushClient.__resetHttpClientForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(() => {
|
||||||
|
agheeraPushClient.__resetHttpClientForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pushPosition escribe log dedicado sin apiKey', async () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agheera-log-'));
|
||||||
|
const logPath = path.join(tempDir, 'agheera_push.log');
|
||||||
|
|
||||||
|
process.env.AGHEERA_API_KEY = 'secret-api-key';
|
||||||
|
process.env.AGHEERA_PUSH_LOG_PATH = logPath;
|
||||||
|
delete process.env.AGHEERA_PUSH_LOGS;
|
||||||
|
|
||||||
|
agheeraPushClient.__setHttpClientForTests(async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => 'Messages received.'
|
||||||
|
}));
|
||||||
|
|
||||||
|
await agheeraPushClient.pushPosition({
|
||||||
|
latitude: '40.416775',
|
||||||
|
longitude: '-3.703790',
|
||||||
|
vehicleId: '6599LCN',
|
||||||
|
licensePlate: '6599LCN',
|
||||||
|
measurementTime: '2026-06-01T13:38:31Z',
|
||||||
|
metadata: {
|
||||||
|
source: 'test',
|
||||||
|
trip_id: 306075
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n');
|
||||||
|
assert.equal(lines.length, 1);
|
||||||
|
|
||||||
|
const entry = JSON.parse(lines[0]);
|
||||||
|
assert.equal(entry.source, 'test');
|
||||||
|
assert.equal(entry.trip_id, 306075);
|
||||||
|
assert.equal(entry.vehicleId, '6599LCN');
|
||||||
|
assert.equal(entry.licensePlate, '6599LCN');
|
||||||
|
assert.equal(entry.latitude, 40.416775);
|
||||||
|
assert.equal(entry.longitude, -3.70379);
|
||||||
|
assert.equal(entry.measurementTime, '2026-06-01T13:38:31Z');
|
||||||
|
assert.equal(entry.success, true);
|
||||||
|
assert.equal(entry.http_status, 200);
|
||||||
|
assert.equal(entry.response_body, 'Messages received.');
|
||||||
|
assert.equal(entry.error, null);
|
||||||
|
assert.equal(JSON.stringify(entry).includes('secret-api-key'), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const http = require('node:http');
|
||||||
|
const test = require('node:test');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const app = require('../app');
|
||||||
|
const db = require('../src/config/db');
|
||||||
|
|
||||||
|
const TEST_JWT_SECRET = 'test-jwt-secret';
|
||||||
|
|
||||||
|
let originalQuery;
|
||||||
|
let originalJwtSecret;
|
||||||
|
|
||||||
|
const createToken = (payload = {}) =>
|
||||||
|
jwt.sign(
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
dni: '58045340X',
|
||||||
|
id_proveedor: 675,
|
||||||
|
...payload
|
||||||
|
},
|
||||||
|
TEST_JWT_SECRET,
|
||||||
|
{ expiresIn: '1h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const withServer = async (callback) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const server = app.listen(0, '127.0.0.1');
|
||||||
|
|
||||||
|
server.on('error', reject);
|
||||||
|
server.on('listening', async () => {
|
||||||
|
try {
|
||||||
|
const result = await callback(server);
|
||||||
|
server.close((closeError) => {
|
||||||
|
if (closeError) {
|
||||||
|
reject(closeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
server.close(() => reject(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestJson = async ({ port, method, path, authorization, body }) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const rawBody = body === undefined ? null : JSON.stringify(body);
|
||||||
|
const headers = {};
|
||||||
|
|
||||||
|
if (authorization) {
|
||||||
|
headers.authorization = authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawBody !== null) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
headers['Content-Length'] = Buffer.byteLength(rawBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
headers
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let responseBody = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve({
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
body: responseBody ? JSON.parse(responseBody) : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
if (rawBody !== null) {
|
||||||
|
req.write(rawBody);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
originalQuery = db.query;
|
||||||
|
originalJwtSecret = process.env.JWT_SECRET;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
db.query = originalQuery;
|
||||||
|
process.env.JWT_SECRET = originalJwtSecret;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(() => {
|
||||||
|
db.query = originalQuery;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/availability devuelve available false si no hay fila', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
assert.match(sql, /COUNT\(\*\) AS total/);
|
||||||
|
assert.match(sql, /FROM c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [[{ total: 0 }]];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/availability',
|
||||||
|
authorization: `Bearer ${createToken()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { success: true, available: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/availability devuelve available true si hay fila', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
assert.match(sql, /COUNT\(\*\) AS total/);
|
||||||
|
assert.match(sql, /FROM c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [[{ total: 1 }]];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/availability',
|
||||||
|
authorization: `Bearer ${createToken()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { success: true, available: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/availability hace INSERT si no existe', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
let step = 0;
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
step += 1;
|
||||||
|
|
||||||
|
if (step === 1) {
|
||||||
|
assert.match(sql, /SELECT id_usuario/);
|
||||||
|
assert.match(sql, /FROM c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(sql, /INSERT INTO c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['40.416775', '-3.70379', '58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/availability',
|
||||||
|
authorization: `Bearer ${createToken()}`,
|
||||||
|
body: {
|
||||||
|
latitud: 40.416775,
|
||||||
|
longitud: -3.70379,
|
||||||
|
usuario: 'OTHER'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(step, 2);
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { success: true, available: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/availability hace UPDATE si existe', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
let step = 0;
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
step += 1;
|
||||||
|
|
||||||
|
if (step === 1) {
|
||||||
|
assert.match(sql, /SELECT id_usuario/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [[{ id_usuario: '58045340X' }]];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(sql, /UPDATE c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['40.416775', '-3.70379', '58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/availability',
|
||||||
|
authorization: `Bearer ${createToken()}`,
|
||||||
|
body: {
|
||||||
|
latitude: 40.416775,
|
||||||
|
longitude: -3.70379
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(step, 2);
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { success: true, available: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELETE /api/availability borra la fila', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
assert.match(sql, /DELETE FROM c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/api/availability',
|
||||||
|
authorization: `Bearer ${createToken()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { success: true, available: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/locations con availability_mode true actualiza disponibilidad online', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
let step = 0;
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
step += 1;
|
||||||
|
|
||||||
|
if (step === 1) {
|
||||||
|
assert.match(sql, /INSERT INTO c_trazabilidad_transportista/);
|
||||||
|
assert.equal(params[0].length, 1);
|
||||||
|
assert.deepEqual(params[0][0].slice(0, 3), ['40.416775', '-3.70379', '58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 2) {
|
||||||
|
assert.match(sql, /SELECT id_usuario/);
|
||||||
|
assert.match(sql, /FROM c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['58045340X']);
|
||||||
|
return [[{ id_usuario: '58045340X' }]];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(sql, /UPDATE c_trazabilidad_online/);
|
||||||
|
assert.deepEqual(params, ['40.416775', '-3.70379', '58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/locations',
|
||||||
|
authorization: `Bearer ${createToken()}`,
|
||||||
|
body: {
|
||||||
|
location: [
|
||||||
|
{
|
||||||
|
coords: {
|
||||||
|
latitude: 40.416775,
|
||||||
|
longitude: -3.70379
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
availability_mode: 'true'
|
||||||
|
},
|
||||||
|
timestamp: '2026-06-01T13:20:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(step, 3);
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, {
|
||||||
|
success: true,
|
||||||
|
count: 1,
|
||||||
|
message: 'Locations saved'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api/locations sin availability_mode no toca disponibilidad online', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
let calls = 0;
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
calls += 1;
|
||||||
|
assert.match(sql, /INSERT INTO c_trazabilidad_transportista/);
|
||||||
|
assert.deepEqual(params[0][0].slice(0, 3), ['40.416775', '-3.70379', '58045340X']);
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer((server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/locations',
|
||||||
|
authorization: `Bearer ${createToken()}`,
|
||||||
|
body: {
|
||||||
|
latitude: 40.416775,
|
||||||
|
longitude: -3.70379,
|
||||||
|
timestamp: '2026-06-01T13:20:00Z'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(calls, 1);
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, {
|
||||||
|
success: true,
|
||||||
|
count: 1,
|
||||||
|
message: 'Locations saved'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('todas las rutas nuevas requieren JWT valido', async () => {
|
||||||
|
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||||
|
|
||||||
|
db.query = async () => {
|
||||||
|
throw new Error('db.query should not be called without token');
|
||||||
|
};
|
||||||
|
|
||||||
|
const responses = await withServer(async (server) => {
|
||||||
|
const port = server.address().port;
|
||||||
|
return Promise.all([
|
||||||
|
requestJson({ port, method: 'GET', path: '/api/availability' }),
|
||||||
|
requestJson({ port, method: 'POST', path: '/api/availability', body: { latitude: 1, longitude: 2 } }),
|
||||||
|
requestJson({ port, method: 'DELETE', path: '/api/availability' })
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
responses.map((response) => response.statusCode),
|
||||||
|
[401, 401, 401]
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -165,7 +165,7 @@ test('POST /api/locations envia posicion a Agheera para cliente 532', async () =
|
|||||||
assert.equal(agheeraCalls.length, 1);
|
assert.equal(agheeraCalls.length, 1);
|
||||||
|
|
||||||
const call = agheeraCalls[0];
|
const call = agheeraCalls[0];
|
||||||
assert.equal(call.url, 'https://push-test.agheera.com/Telematics/Positions');
|
assert.equal(call.url, 'https://push-dhl.agheera.com/Telematics/positions');
|
||||||
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
||||||
|
|
||||||
const payload = JSON.parse(call.options.body);
|
const payload = JSON.parse(call.options.body);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ test('GET /api/trips está registrado en /api', () => {
|
|||||||
assert.ok(tripsRouteLayer, 'GET /api/trips route is not defined');
|
assert.ok(tripsRouteLayer, 'GET /api/trips route is not defined');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /api/trips devuelve viajes del transportista autenticado con aliases legacy', async () => {
|
test('GET /api/trips devuelve viajes del transportista autenticado con aliases legacy e incluye asignados', async () => {
|
||||||
const mockedTrips = [
|
const mockedTrips = [
|
||||||
{
|
{
|
||||||
id_viaje: 84919,
|
id_viaje: 84919,
|
||||||
@@ -116,26 +116,36 @@ test('GET /api/trips devuelve viajes del transportista autenticado con aliases l
|
|||||||
{
|
{
|
||||||
id_viaje: 84918,
|
id_viaje: 84918,
|
||||||
cod_viaje: 'VIA-2026-0000',
|
cod_viaje: 'VIA-2026-0000',
|
||||||
id_estado: 4,
|
id_estado: 1,
|
||||||
nombrea: 'Madrid, ES',
|
nombrea: 'Madrid, ES',
|
||||||
nombreb: 'Bilbao, ES',
|
nombreb: 'Bilbao, ES',
|
||||||
fecha_salida: '2026-01-21',
|
fecha_salida: '2026-01-21',
|
||||||
fecha_llegada: '2026-01-21'
|
fecha_llegada: '2026-01-21'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
let callCount = 0;
|
||||||
|
|
||||||
db.query = async (sql, params) => {
|
db.query = async (sql, params) => {
|
||||||
|
callCount += 1;
|
||||||
assert.match(sql, /FROM c_viajes_proveedor p/);
|
assert.match(sql, /FROM c_viajes_proveedor p/);
|
||||||
assert.match(sql, /INNER JOIN c_viajes v/);
|
assert.match(sql, /INNER JOIN c_viajes v/);
|
||||||
assert.match(sql, /INNER JOIN m_proveedores_trasportistas t/);
|
assert.match(sql, /INNER JOIN m_proveedores_trasportistas t/);
|
||||||
assert.match(sql, /id_estado IN \(\?, \?, \?\)/);
|
assert.match(sql, /id_estado IN \(\?, \?, \?, \?\)/);
|
||||||
|
|
||||||
|
if (callCount === 1) {
|
||||||
|
assert.match(sql, /COUNT\(\*\) AS total/);
|
||||||
|
assert.deepEqual(params, ['58045340X', 7, 8, 9, 1]);
|
||||||
|
return [[{ total: 2 }]];
|
||||||
|
}
|
||||||
|
|
||||||
assert.match(sql, /AS id_viaje/);
|
assert.match(sql, /AS id_viaje/);
|
||||||
assert.match(sql, /AS id_estado/);
|
assert.match(sql, /AS id_estado/);
|
||||||
assert.match(sql, /AS nombrea/);
|
assert.match(sql, /AS nombrea/);
|
||||||
assert.match(sql, /AS nombreb/);
|
assert.match(sql, /AS nombreb/);
|
||||||
assert.match(sql, /AS fecha_salida/);
|
assert.match(sql, /AS fecha_salida/);
|
||||||
assert.match(sql, /AS fecha_llegada/);
|
assert.match(sql, /AS fecha_llegada/);
|
||||||
assert.deepEqual(params, ['58045340X', 7, 8, 9]);
|
assert.match(sql, /LIMIT \? OFFSET \?/);
|
||||||
|
assert.deepEqual(params, ['58045340X', 7, 8, 9, 1, 25, 0]);
|
||||||
return [mockedTrips];
|
return [mockedTrips];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,13 +159,23 @@ test('GET /api/trips devuelve viajes del transportista autenticado con aliases l
|
|||||||
|
|
||||||
assert.equal(response.statusCode, 200);
|
assert.equal(response.statusCode, 200);
|
||||||
assert.deepEqual(response.body, {
|
assert.deepEqual(response.body, {
|
||||||
trips: mockedTrips
|
trips: mockedTrips,
|
||||||
|
page: 1,
|
||||||
|
limit: 25,
|
||||||
|
total: 2,
|
||||||
|
has_more: false
|
||||||
});
|
});
|
||||||
assert.equal(response.body.trips[0].id_estado, 7);
|
assert.equal(response.body.trips[0].id_estado, 7);
|
||||||
|
assert.equal(response.body.trips[1].id_estado, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /api/trips devuelve lista vacia cuando no hay viajes', async () => {
|
test('GET /api/trips devuelve lista vacia cuando no hay viajes', async () => {
|
||||||
db.query = async () => [[]];
|
let callCount = 0;
|
||||||
|
|
||||||
|
db.query = async () => {
|
||||||
|
callCount += 1;
|
||||||
|
return callCount === 1 ? [[{ total: 0 }]] : [[]];
|
||||||
|
};
|
||||||
|
|
||||||
const response = await withServer(async (server) =>
|
const response = await withServer(async (server) =>
|
||||||
requestJson({
|
requestJson({
|
||||||
@@ -167,12 +187,16 @@ test('GET /api/trips devuelve lista vacia cuando no hay viajes', async () => {
|
|||||||
|
|
||||||
assert.equal(response.statusCode, 200);
|
assert.equal(response.statusCode, 200);
|
||||||
assert.deepEqual(response.body, {
|
assert.deepEqual(response.body, {
|
||||||
trips: []
|
trips: [],
|
||||||
|
page: 1,
|
||||||
|
limit: 25,
|
||||||
|
total: 0,
|
||||||
|
has_more: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET /api/trips responde en menos de 1s para 500 viajes mockeados', async () => {
|
test('GET /api/trips responde en menos de 1s para 25 viajes mockeados', async () => {
|
||||||
const mockedTrips = Array.from({ length: 500 }, (_, index) => ({
|
const mockedTrips = Array.from({ length: 25 }, (_, index) => ({
|
||||||
id_viaje: 100000 + index,
|
id_viaje: 100000 + index,
|
||||||
cod_viaje: `VIA-2026-${String(index + 1).padStart(4, '0')}`,
|
cod_viaje: `VIA-2026-${String(index + 1).padStart(4, '0')}`,
|
||||||
id_estado: index % 2 === 0 ? 7 : 4,
|
id_estado: index % 2 === 0 ? 7 : 4,
|
||||||
@@ -182,7 +206,12 @@ test('GET /api/trips responde en menos de 1s para 500 viajes mockeados', async (
|
|||||||
fecha_llegada: '2026-01-22 18:11:00'
|
fecha_llegada: '2026-01-22 18:11:00'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
db.query = async () => [mockedTrips];
|
let callCount = 0;
|
||||||
|
|
||||||
|
db.query = async () => {
|
||||||
|
callCount += 1;
|
||||||
|
return callCount === 1 ? [[{ total: 500 }]] : [mockedTrips];
|
||||||
|
};
|
||||||
|
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const response = await withServer(async (server) =>
|
const response = await withServer(async (server) =>
|
||||||
@@ -195,10 +224,88 @@ test('GET /api/trips responde en menos de 1s para 500 viajes mockeados', async (
|
|||||||
const elapsedMs = Date.now() - startedAt;
|
const elapsedMs = Date.now() - startedAt;
|
||||||
|
|
||||||
assert.equal(response.statusCode, 200);
|
assert.equal(response.statusCode, 200);
|
||||||
assert.equal(response.body.trips.length, 500);
|
assert.equal(response.body.trips.length, 25);
|
||||||
|
assert.equal(response.body.total, 500);
|
||||||
|
assert.equal(response.body.has_more, true);
|
||||||
assert.ok(elapsedMs < 1000, `Expected < 1000ms, got ${elapsedMs}ms`);
|
assert.ok(elapsedMs < 1000, `Expected < 1000ms, got ${elapsedMs}ms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('GET /api/trips aplica paginacion y filtros en SQL', async () => {
|
||||||
|
const mockedTrips = [
|
||||||
|
{
|
||||||
|
id_viaje: 84919,
|
||||||
|
cod_viaje: 'VIA-2026-0001',
|
||||||
|
id_estado: 7,
|
||||||
|
nombrea: 'Barcelona, ES',
|
||||||
|
nombreb: 'Lyon, FR',
|
||||||
|
fecha_salida: '2026-06-30 06:00:00',
|
||||||
|
fecha_llegada: '2026-06-30 18:11:00'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let callCount = 0;
|
||||||
|
|
||||||
|
db.query = async (sql, params) => {
|
||||||
|
callCount += 1;
|
||||||
|
assert.match(sql, /v\.id_estado IN \(\?, \?, \?\)/);
|
||||||
|
assert.match(sql, /COALESCE\(p\.fecha_salida, v\.fecha_salida\) >= \?/);
|
||||||
|
assert.match(sql, /COALESCE\(p\.fecha_salida, v\.fecha_salida\) < \?/);
|
||||||
|
|
||||||
|
if (callCount === 1) {
|
||||||
|
assert.match(sql, /COUNT\(\*\) AS total/);
|
||||||
|
assert.deepEqual(params, ['58045340X', 7, 8, 9, '2026-06-01', '2026-07-01']);
|
||||||
|
return [[{ total: 26 }]];
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.match(sql, /ORDER BY COALESCE\(p\.fecha_salida, v\.fecha_salida\) DESC, p\.id_viaje DESC/);
|
||||||
|
assert.match(sql, /LIMIT \? OFFSET \?/);
|
||||||
|
assert.deepEqual(params, ['58045340X', 7, 8, 9, '2026-06-01', '2026-07-01', 25, 25]);
|
||||||
|
return [mockedTrips];
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await withServer(async (server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
path: '/api/trips?page=2&limit=25&status_ids=7,8,9&date_from=2026-06-01&date_to=2026-06-30',
|
||||||
|
authorization: `Bearer ${createToken()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.equal(response.body.page, 2);
|
||||||
|
assert.equal(response.body.limit, 25);
|
||||||
|
assert.equal(response.body.total, 26);
|
||||||
|
assert.equal(response.body.has_more, false);
|
||||||
|
assert.deepEqual(response.body.trips, mockedTrips);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api/trips valida parametros invalidos', async () => {
|
||||||
|
db.query = async () => {
|
||||||
|
throw new Error('db.query should not be called with invalid params');
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidPaths = [
|
||||||
|
'/api/trips?page=0',
|
||||||
|
'/api/trips?limit=101',
|
||||||
|
'/api/trips?status_ids=7,x',
|
||||||
|
'/api/trips?date_from=2026-02-30',
|
||||||
|
'/api/trips?date_from=2026-07-01&date_to=2026-06-30'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of invalidPaths) {
|
||||||
|
const response = await withServer(async (server) =>
|
||||||
|
requestJson({
|
||||||
|
port: server.address().port,
|
||||||
|
path,
|
||||||
|
authorization: `Bearer ${createToken()}`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.statusCode, 400, path);
|
||||||
|
assert.equal(response.body.success, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('GET /api/trips devuelve 401 sin token', async () => {
|
test('GET /api/trips devuelve 401 sin token', async () => {
|
||||||
db.query = async () => {
|
db.query = async () => {
|
||||||
throw new Error('db.query should not be called without token');
|
throw new Error('db.query should not be called without token');
|
||||||
|
|||||||
@@ -719,7 +719,7 @@ test('POST /api/trips/:id/status modo dual replica foto a SFTP y mantiene local'
|
|||||||
const recorder = createSftpRecorder();
|
const recorder = createSftpRecorder();
|
||||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||||
process.env.TRIP_STATUS_SFTP_HOST = 'localhost';
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
||||||
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
||||||
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
||||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||||
@@ -796,7 +796,7 @@ test('POST /api/trips/:id/status modo dual con fallo SFTP mantiene fallback loca
|
|||||||
createFakeSftpClientFactory(recorder, { failPut: true })
|
createFakeSftpClientFactory(recorder, { failPut: true })
|
||||||
);
|
);
|
||||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||||
process.env.TRIP_STATUS_SFTP_HOST = 'localhost';
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
||||||
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
||||||
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
||||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||||
@@ -862,7 +862,7 @@ test('POST /api/trips/:id/status payload inválido tras upload limpia remoto y l
|
|||||||
const recorder = createSftpRecorder();
|
const recorder = createSftpRecorder();
|
||||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||||
process.env.TRIP_STATUS_SFTP_HOST = 'localhost';
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
||||||
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
||||||
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
||||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||||
@@ -1204,7 +1204,7 @@ test('POST /api/trips/:id/status cliente 532 envia posicion a Agheera en estado
|
|||||||
assert.equal(agheeraCalls.length, 1);
|
assert.equal(agheeraCalls.length, 1);
|
||||||
|
|
||||||
const call = agheeraCalls[0];
|
const call = agheeraCalls[0];
|
||||||
assert.equal(call.url, 'https://push-test.agheera.com/Telematics/Positions');
|
assert.equal(call.url, 'https://push-dhl.agheera.com/Telematics/positions');
|
||||||
assert.equal(call.options.method, 'POST');
|
assert.equal(call.options.method, 'POST');
|
||||||
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
||||||
assert.equal(call.options.headers['Content-Type'], 'application/json');
|
assert.equal(call.options.headers['Content-Type'], 'application/json');
|
||||||
@@ -2795,7 +2795,7 @@ test('DELETE /api/trips/:id/status en modo dual no borra foto en remoto ni local
|
|||||||
const recorder = createSftpRecorder();
|
const recorder = createSftpRecorder();
|
||||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||||
process.env.TRIP_STATUS_SFTP_HOST = 'localhost';
|
process.env.TRIP_STATUS_SFTP_HOST = '194.164.175.51';
|
||||||
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
process.env.TRIP_STATUS_SFTP_PORT = '22';
|
||||||
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
process.env.TRIP_STATUS_SFTP_USERNAME = 'ssh_fotos_estado';
|
||||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||||
|
|||||||
Reference in New Issue
Block a user