Compare commits
No commits in common. "12364bcb44963504fc123e82af50a667ca6bc4f7" and "5828395907af28b49171410088c4df166a4c0f97" have entirely different histories.
12364bcb44
...
5828395907
@ -1,11 +1,11 @@
|
||||
PORT=3001
|
||||
DB_HOST=localhost
|
||||
DB_HOST=194.164.175.51
|
||||
DB_USER=roganet
|
||||
DB_PASSWORD=bdIRGLnet2905*/
|
||||
DB_NAME=abian_app_produccion
|
||||
JWT_SECRET=9c64f2727d53bfefaaa17a5fda5009ffe93cae904860c659bd18d2d14ad6b467
|
||||
|
||||
DB_HOST_P = localhost
|
||||
DB_HOST_P = 194.164.175.51
|
||||
DB_USER_P = roganet
|
||||
DB_PASSWORD_P = bdIRGLnet2905*/
|
||||
DB_NAME_P = abian_app_produccion
|
||||
@ -26,7 +26,7 @@ DRIVER_LICENSE_RETENTION_DAYS=365
|
||||
|
||||
#Carga de fotos dual
|
||||
TRIP_STATUS_PHOTO_STORAGE_MODE=local
|
||||
TRIP_STATUS_SFTP_HOST=localhost
|
||||
TRIP_STATUS_SFTP_HOST=194.164.175.51
|
||||
TRIP_STATUS_SFTP_PORT=22
|
||||
TRIP_STATUS_SFTP_USERNAME=ssh_fotos_estado
|
||||
TRIP_STATUS_SFTP_PASSWORD=IZYj%c0FiIlCc@rI%W0Z
|
||||
|
||||
@ -96,7 +96,7 @@ Ejemplo de modo temporal dual:
|
||||
|
||||
```bash
|
||||
TRIP_STATUS_PHOTO_STORAGE_MODE=dual
|
||||
TRIP_STATUS_SFTP_HOST=localhost
|
||||
TRIP_STATUS_SFTP_HOST=194.164.175.51
|
||||
TRIP_STATUS_SFTP_PORT=22
|
||||
TRIP_STATUS_SFTP_USERNAME=ssh_fotos_estado
|
||||
TRIP_STATUS_SFTP_PASSWORD=********
|
||||
|
||||
@ -96,7 +96,7 @@ Ejemplo de modo temporal dual:
|
||||
|
||||
```bash
|
||||
TRIP_STATUS_PHOTO_STORAGE_MODE=dual
|
||||
TRIP_STATUS_SFTP_HOST=localhost
|
||||
TRIP_STATUS_SFTP_HOST=194.164.175.51
|
||||
TRIP_STATUS_SFTP_PORT=22
|
||||
TRIP_STATUS_SFTP_USERNAME=ssh_fotos_estado
|
||||
TRIP_STATUS_SFTP_PASSWORD=********
|
||||
|
||||
12
app.js
12
app.js
@ -13,7 +13,6 @@ dotenv.config({ path: path.resolve(__dirname, '.env'), override: true });
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const uploadsDir = path.resolve(__dirname, 'uploads');
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
@ -89,17 +88,6 @@ const limiter = rateLimit({
|
||||
legacyHeaders: false,
|
||||
message: "Too many requests from this IP, please try again after 15 minutes"
|
||||
});
|
||||
app.get(["/health", "/healthcheck"], (req, res) => {
|
||||
res.set("Cache-Control", "no-store");
|
||||
res.status(200).json({
|
||||
status: "ok",
|
||||
service: "node-gestion-api",
|
||||
pid: process.pid,
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
timestamp_utc: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
app.use(limiter);
|
||||
|
||||
// Routes
|
||||
|
||||
Binary file not shown.
@ -1,191 +0,0 @@
|
||||
<?php
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
include("mysql.php");
|
||||
|
||||
$id_viaje = $_POST['id_viaje'];
|
||||
$n_proveedor = $_POST['n_proveedor'];
|
||||
$usuario = $_POST['usuario'];
|
||||
$id_estado = $_POST['id_estado'];
|
||||
$incidencia = $_POST['incidencia'];
|
||||
$latitud = $_POST['latitud'];
|
||||
$longitud = $_POST['longitud'];
|
||||
$nombre = $_POST['nombre'] . '.jpg';
|
||||
|
||||
if ($nombre == "vacio.jpg") {
|
||||
$nombre = NULL;
|
||||
}
|
||||
|
||||
$consulta = "SELECT count(*) from c_cambios_estado where id_viaje='" . $id_viaje . "' and id_estado='" . $id_estado . "' and foto='" . $nombre . "'";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$duplicado = $fila[0];
|
||||
|
||||
if ($duplicado > 0) {
|
||||
echo json_encode("0");
|
||||
die();
|
||||
}
|
||||
|
||||
$sQuery2 = "UPDATE c_viajes SET id_estado = $id_estado, ind_edi_app = 1 WHERE id_viaje = $id_viaje";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $sQuery2) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
|
||||
$sQuery2 = "INSERT INTO c_cambios_estado (id_viaje, n_proveedor, id_transportista, id_estado, incidencia, latitud, longitud, fecha_y_hora, foto) VALUES ('" . $id_viaje . "','" . $n_proveedor . "','" . $usuario . "','" . $id_estado . "','" . $incidencia . "','" . $latitud . "','" . $longitud . "',NOW(),'" . $nombre . "')";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $sQuery2) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
|
||||
$consulta = "SELECT id_viaje_padre, id_cliente, cod_viaje from c_viajes where id_viaje='" . $id_viaje . "'";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$id_viaje_padre = $fila[0];
|
||||
$id_cliente = $fila[1];
|
||||
$cod_viaje = $fila[2];
|
||||
|
||||
while ($id_viaje_padre > 0) {
|
||||
$consulta = "UPDATE `c_viajes` SET id_estado='" . $id_estado . "', ind_edi_app = 1 where id_viaje='" . $id_viaje_padre . "'";
|
||||
$query_res = mysqli_query($gaSql['link'], $consulta);
|
||||
|
||||
$consulta = "INSERT INTO c_cambios_estado (id_viaje, n_proveedor, id_transportista, id_estado, incidencia, latitud, longitud, fecha_y_hora, foto) VALUES ('" . $id_viaje . "','" . $n_proveedor . "','" . $usuario . "','" . $id_estado . "','" . $incidencia . "','" . $latitud . "','" . $longitud . "',NOW(),'" . $nombre . "')";
|
||||
$query_res = mysqli_query($gaSql['link'], $consulta);
|
||||
|
||||
//vover consultar para ver si se sale del bucle
|
||||
$consulta = "SELECT id_viaje_padre, id_cr from c_viajes where id_viaje='" . $id_viaje_padre . "'";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$id_viaje_padre = $fila[0];
|
||||
}
|
||||
|
||||
$resultado = "0";
|
||||
|
||||
$consulta = "SELECT html FROM html_correo WHERE id_mensaje = 34";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta);
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$mensaje = $fila[0];
|
||||
|
||||
$consulta = "SELECT user_smtp_admin, user_smtp_admin, pass_smtp_admin, host_smtp, puerto_smtp, date_format(NOW(), '%Y-%m-%d_%H_%i_%s') as ahora, email_operaciones FROM m_cr WHERE id_cr = $cr";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta);
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$email_operaciones = $fila[0];
|
||||
$user_smtp = $fila[1];
|
||||
$pass_smtp = $fila[2];
|
||||
$host_smtp = $fila[3];
|
||||
$puerto_smtp = $fila[4];
|
||||
$ahora = $fila[5];
|
||||
$email_operaciones = $fila[6];
|
||||
|
||||
$consulta = "SELECT estado FROM t_viaje_estados WHERE id_estado = $id_estado";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta);
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$estado = $fila[0];
|
||||
|
||||
if (strlen($user_smtp) > 4 && strlen($pass_smtp) > 4 && strlen($host_smtp) > 4) {
|
||||
|
||||
$mail = new PHPMailer();
|
||||
$mail->IsSMTP();
|
||||
$mail->From = $user_smtp;
|
||||
$mail->FromName = "Abian Service";
|
||||
|
||||
$html = '<br>Se ha cambiado el estado del viaje ' . $cod_viaje . ' a ' . $estado . ' mediante la APP';
|
||||
|
||||
$mail->AddAddress($email_operaciones);
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->Host = $host_smtp;
|
||||
$mail->Port = $puerto_smtp;
|
||||
$mail->Username = $user_smtp;
|
||||
$mail->Password = $pass_smtp;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Subject = "Facturas pendientes Abian Service";
|
||||
$mail->Body = $mensaje . "<br>" . $html;
|
||||
$mail->IsHTML(true);
|
||||
$mail->SMTPDebug = 0;
|
||||
|
||||
if ($mail->Send()) {
|
||||
echo json_encode("0");
|
||||
} else {
|
||||
echo json_encode("Error al enviar el correo");
|
||||
}
|
||||
|
||||
} else {
|
||||
echo json_encode("Error al enviar el correo: fallo con usuario/contraseña");
|
||||
}
|
||||
|
||||
if ($id_cliente == 532) {
|
||||
$consulta = "SELECT id_tipovehiculo AS matricula FROM c_viajes_proveedor WHERE id_viaje = $id_viaje AND n_proveedor = $n_proveedor";
|
||||
$rResult2 = mysqli_query($gaSql['link'], $consulta) or fatal_error('MySQL Error: ' . mysqli_errno($gaSql['link']));
|
||||
$fila = mysqli_fetch_row($rResult2);
|
||||
$matricula = $fila[0];
|
||||
|
||||
$xml = '
|
||||
<S:Envelope
|
||||
xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<SOAP-ENV:Header/>
|
||||
<S:Body>
|
||||
<ns2:SubmitPosition xmlns="http://schemas.datacontract.org/2004/07/QESE.QFV.Classes.QAP"
|
||||
xmlns:ns2="QESE.QFV.QAP" xmlns:ns3="http://schemas.datacontract.org/2004/07/System"
|
||||
xmlns:ns4="http://schemas.microsoft.com/2003/10/Serialization/Arrays"
|
||||
xmlns:ns5="http://schemas.datacontract.org/2004/07/QESE.QFV.Classes"
|
||||
xmlns:ns6="http://schemas.datacontract.org/2004/07/FV.Enums"
|
||||
xmlns:ns7="http://schemas.datacontract.org/2004/07/FV.Enums.DriverHours"
|
||||
xmlns:ns8="http://schemas.microsoft.com/2003/10/Serialization/">
|
||||
<ns2:credentials>
|
||||
<Client>abian</Client>
|
||||
<Password>TVwuW57u0^</Password>
|
||||
<UserName>abian-5567</UserName>
|
||||
<Version>1</Version>
|
||||
</ns2:credentials>
|
||||
<ns2:positions>
|
||||
<Position>
|
||||
<AssetCategory>1</AssetCategory>
|
||||
<CID>5567</CID>
|
||||
<CustomerName/>
|
||||
<DT>
|
||||
<ns3:DateTime>?fecha_hora?</ns3:DateTime>
|
||||
<ns3:OffsetMinutes>0</ns3:OffsetMinutes>
|
||||
</DT>
|
||||
<DeviceType>14</DeviceType>
|
||||
<From>' . $matricula . '</From>
|
||||
<GatewayMsgId/>
|
||||
<IdentifierType>12</IdentifierType>
|
||||
<MSISDN>' . $matricula . '</MSISDN>
|
||||
<RequestId>1</RequestId>
|
||||
<Version>1</Version>
|
||||
<Latitude>?latitud?</Latitude>
|
||||
<Longitude>?longitud?</Longitude>
|
||||
</Position>
|
||||
</ns2:positions>
|
||||
</ns2:SubmitPosition>
|
||||
</S:Body>
|
||||
</S:Envelope>';
|
||||
|
||||
$url = 'https://export.fleetvisor.eu/wsQAP/Positions.svc/ssl';
|
||||
$str = date("Y-m-d");
|
||||
$strb = date("H:i:s");
|
||||
|
||||
$fecha_hora = $str . "T" . $strb . "Z";
|
||||
|
||||
$xml = str_replace("?fecha_hora?", $fecha_hora, $xml);
|
||||
$xml = str_replace("?latitud?", $latitud, $xml);
|
||||
$xml = str_replace("?longitud?", $longitud, $xml);
|
||||
|
||||
//echo $xml;
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$headers = array(
|
||||
"Content-Type: text/xml",
|
||||
"SOAPAction: QESE.QFV.QAP/PositionsService/SubmitPosition"
|
||||
);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$data = $xml;
|
||||
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
||||
|
||||
$resp = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
}
|
||||
|
||||
echo json_encode($resultado);
|
||||
?>
|
||||
109
docu/correo.txt
109
docu/correo.txt
@ -1,109 +0,0 @@
|
||||
|
||||
|
||||
Dear Gorka,
|
||||
thanks for your reply!
|
||||
|
||||
We have a standard API that we provide openly - please see the attached documentation for more details.
|
||||
The attached document also includes the URL and apikey for our TEST instance.
|
||||
|
||||
- endpoint URL for test and production
|
||||
|
||||
PROD URL: https://push-dhl.agheera.com/Telematics/positions
|
||||
apiKey for Abian: 2NL7G-0QTMP-T0N54-7ERGY-S7U6U-SXKWF
|
||||
TEST URL: https://push-test.agheera.com/Telematics/Positions
|
||||
apiKey for testing purposes: 0eec610a-aad5-4f60-91c7-7f9a4089d5b5
|
||||
|
||||
- authentication method and credentials process
|
||||
Authentication is done by sending an apikey in a HTTP header "apiKey" with each request.
|
||||
There is no further handshake (oauth flow or similar). If the apikey is correct and accepted, data will be assigned to the customer (in this case Abian)
|
||||
Apikey is provided by Agheera on request.
|
||||
1 Apikey is bound to 1 customer and 1 environment (prod/test).
|
||||
|
||||
- required payload format
|
||||
Please see attached documentation for more details.
|
||||
We encourage to send data batches whenever possible.
|
||||
|
||||
- required vehicle identifier: license plate, device ID, MSISDN, etc.
|
||||
Device ID and License Plate shall both be provided
|
||||
|
||||
- timestamp format/timezone
|
||||
Must be in UTC timezone
|
||||
Format: yyyy-MM-ddTHH:mm:ssZ
|
||||
|
||||
- required fields for position updates
|
||||
Minimum required are: latitude, longitude, vehicleid(deviceid), licensePlate, measurementTime
|
||||
We appreciate the following fields to have better Trip ETA calculation: speed, direction, assetType
|
||||
|
||||
- expected response codes
|
||||
200 - OK
|
||||
If data was accepted and was syntactically correct
|
||||
400 - Bad request
|
||||
If data was syntactically wrong/malformed
|
||||
401 - Unauthorized
|
||||
Wrong apikey or apikey header missing
|
||||
405 - Method not available
|
||||
If request method was not POST
|
||||
|
||||
- retry/error handling recommendations
|
||||
We are not validating the semantics of position data at the point of response generation, so we return 200 - OK on almost all requests unless the request is badly malformed or unauthorized.
|
||||
This also means a retry is not very likely needed, unless the Agheera server is down.
|
||||
For sending position data to us, it is fine to "fire and forget" and not schedule a retry.
|
||||
In case you notice any errors, please contact ops@agheera.com and we will investigate.
|
||||
|
||||
- whether positions should be sent only for active DHL trips/customer id 532 or for all authorized vehicles
|
||||
Agheera does already filter the data so that DHL can only see data that is relevant for DHL trips.
|
||||
So as far as we are concerned, you can send for all vehicles, and we will filter it anyway.
|
||||
But I think this should be decided by ABIAN if they want this.
|
||||
|
||||
- any IP allowlist requirements
|
||||
No IP whitelist in place. We do not need to know your IPs upfront.
|
||||
|
||||
|
||||
Let me know if you have any open questions!
|
||||
|
||||
Kind Regards / Freundliche Grüße
|
||||
Stephan Wahlen
|
||||
Head of IoT Hardware
|
||||
Agheera GmbH – a DHL Group company
|
||||
|
||||
stephan.wahlen@agheera.com
|
||||
+49 2203 29757-23
|
||||
Office: August-Horch-Straße 5, 51149 Köln
|
||||
Warehouse: Kasinostraße 24, 53840 Troisdorf, Deutschland
|
||||
Registered office Cologne; Register court Bonn; HRB 18111
|
||||
VAT ID no. DE 273 231 181
|
||||
Managing Directors: Pierre Lynch, Sven Kefferpütz
|
||||
|
||||
|
||||
|
||||
-----Ursprüngliche Nachricht-----
|
||||
Von: Gorka Leceta <gleceta@roganet.es>
|
||||
Gesendet: Donnerstag, 28. Mai 2026 11:52
|
||||
An: Operations <ops@agheera.com>; Stephan Wahlen <stephan.wahlen@agheera.com>
|
||||
Betreff: ABIAN SERVICE - Agheera Push API documentation request for GPS position integration
|
||||
|
||||
Hello Stephan / Agheera team,
|
||||
|
||||
We are Roganet, GPS provider for ABIAN SERVICE.
|
||||
|
||||
ABIAN has asked us to integrate active position pushing from their mobile app backend to Agheera for DHL transports.
|
||||
|
||||
Could you please send us the technical documentation for Agheera's push API?
|
||||
|
||||
We need:
|
||||
- endpoint URL for test and production
|
||||
- authentication method and credentials process
|
||||
- required payload format
|
||||
- required vehicle identifier: license plate, device ID, MSISDN, etc.
|
||||
- timestamp format/timezone
|
||||
- required fields for position updates
|
||||
- expected response codes
|
||||
- retry/error handling recommendations
|
||||
- whether positions should be sent only for active DHL trips/customer id
|
||||
532 or for all authorized vehicles
|
||||
- any IP allowlist requirements
|
||||
|
||||
ABIAN registration was submitted on 2024-01-23 and the authorization document is AFTemplateV2-ABIAN SERVICE_2024-01-23T11_55_17Z.PDF.
|
||||
|
||||
Kind regards
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
const db = require('../config/db');
|
||||
const agheeraPushClient = require('../services/agheeraPushClient');
|
||||
|
||||
const AGHEERA_CLIENT_ID = 532;
|
||||
|
||||
const getDniFromLocation = (locationData) => {
|
||||
if (locationData?.extras?.alias) {
|
||||
@ -73,137 +70,6 @@ const getTripIdFromLocation = (locationData) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRawTimestampFromLocation = (locationData) => {
|
||||
const candidates = [
|
||||
locationData?.timestamp,
|
||||
locationData?.location?.timestamp
|
||||
];
|
||||
|
||||
for (const value of candidates) {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
return String(value).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPersistedTimestampFromLocation = (locationData) => {
|
||||
const rawTimestamp = getRawTimestampFromLocation(locationData);
|
||||
|
||||
if (rawTimestamp === null) {
|
||||
return {
|
||||
value: new Date(),
|
||||
usedFallback: true,
|
||||
reason: 'missing'
|
||||
};
|
||||
}
|
||||
|
||||
const parsedTimestamp = new Date(rawTimestamp);
|
||||
if (Number.isNaN(parsedTimestamp.getTime())) {
|
||||
return {
|
||||
value: new Date(),
|
||||
usedFallback: true,
|
||||
reason: 'invalid',
|
||||
rawTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: parsedTimestamp,
|
||||
usedFallback: false,
|
||||
reason: null,
|
||||
rawTimestamp
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const pushLocationToAgheera = async ({ latitude, longitude, dni, tripId, measurementTime }) => {
|
||||
if (!tripId || !dni) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [tripRows] = await db.query(
|
||||
`SELECT id_cliente
|
||||
FROM c_viajes
|
||||
WHERE id_viaje = ?
|
||||
LIMIT 1`,
|
||||
[tripId]
|
||||
);
|
||||
|
||||
if (Number.parseInt(tripRows[0]?.id_cliente, 10) !== AGHEERA_CLIENT_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseResult = {
|
||||
trip_id: tripId,
|
||||
attempted: true,
|
||||
success: false,
|
||||
http_status: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
const [authorizationRows] = await db.query(
|
||||
`SELECT id_tipovehiculo AS matricula
|
||||
FROM c_viajes_proveedor
|
||||
WHERE id_viaje = ?
|
||||
AND dni = ?
|
||||
ORDER BY n_proveedor ASC
|
||||
LIMIT 1`,
|
||||
[tripId, dni]
|
||||
);
|
||||
|
||||
const licensePlate = String(authorizationRows[0]?.matricula || '').trim();
|
||||
if (!licensePlate) {
|
||||
return {
|
||||
...baseResult,
|
||||
error: 'LICENSE_PLATE_NOT_FOUND'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await agheeraPushClient.pushPosition({
|
||||
latitude,
|
||||
longitude,
|
||||
vehicleId: licensePlate,
|
||||
licensePlate,
|
||||
measurementTime
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
success: true,
|
||||
http_status: result?.status ?? null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Agheera push failed after location update:', {
|
||||
tripId,
|
||||
dni,
|
||||
message: error.message,
|
||||
status: error.status || null
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
http_status: error.status || null,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const pushLocationsToAgheera = async (locationsToPush) => {
|
||||
const results = [];
|
||||
|
||||
for (const location of locationsToPush) {
|
||||
const result = await pushLocationToAgheera(location);
|
||||
if (result) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const saveLocation = async (req, res) => {
|
||||
try {
|
||||
const data = req.body;
|
||||
@ -234,8 +100,8 @@ const saveLocation = async (req, res) => {
|
||||
globalTripId = getTripIdFromLocation(data);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const rowsToInsert = [];
|
||||
const locationsToPush = [];
|
||||
|
||||
for (const loc of locations) {
|
||||
const coords = getCoordinatesFromLocation(loc);
|
||||
@ -243,32 +109,13 @@ const saveLocation = async (req, res) => {
|
||||
const tripId = globalTripId !== null ? globalTripId : getTripIdFromLocation(loc);
|
||||
|
||||
if (coords && coords.lat !== undefined && coords.lat !== null && coords.lng !== undefined && coords.lng !== null) {
|
||||
const persistedTimestamp = getPersistedTimestampFromLocation(loc);
|
||||
|
||||
if (persistedTimestamp.usedFallback) {
|
||||
console.warn('Location timestamp fallback applied:', {
|
||||
reason: persistedTimestamp.reason,
|
||||
rawTimestamp: persistedTimestamp.rawTimestamp || null,
|
||||
uuid: loc?.uuid || loc?.location?.uuid || null,
|
||||
tripId,
|
||||
dni: dni || null
|
||||
});
|
||||
}
|
||||
|
||||
rowsToInsert.push([
|
||||
String(coords.lat),
|
||||
String(coords.lng),
|
||||
dni || null,
|
||||
persistedTimestamp.value,
|
||||
now,
|
||||
tripId
|
||||
]);
|
||||
locationsToPush.push({
|
||||
latitude: coords.lat,
|
||||
longitude: coords.lng,
|
||||
dni,
|
||||
tripId,
|
||||
measurementTime: persistedTimestamp.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,18 +134,11 @@ const saveLocation = async (req, res) => {
|
||||
[rowsToInsert]
|
||||
);
|
||||
|
||||
const agheeraResults = await pushLocationsToAgheera(locationsToPush);
|
||||
const responseBody = {
|
||||
return res.json({
|
||||
success: true,
|
||||
count: rowsToInsert.length,
|
||||
message: 'Locations saved'
|
||||
};
|
||||
|
||||
if (agheeraResults.length > 0) {
|
||||
responseBody.agheera_push = agheeraResults.length === 1 ? agheeraResults[0] : agheeraResults;
|
||||
}
|
||||
|
||||
return res.json(responseBody);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving location:', error);
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
|
||||
@ -2,7 +2,6 @@ const db = require('../config/db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const tripIncidenceMailer = require('../services/tripIncidenceMailer');
|
||||
const agheeraPushClient = require('../services/agheeraPushClient');
|
||||
const {
|
||||
collectUploadedTripStatusFiles,
|
||||
removeUploadedTripStatusFiles,
|
||||
@ -24,7 +23,6 @@ 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 AGHEERA_CLIENT_ID = 532;
|
||||
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([
|
||||
@ -240,85 +238,6 @@ const normalizeCoordinateValue = (rawValue) => {
|
||||
return String(parsedNumericValue);
|
||||
};
|
||||
|
||||
const pushTripStatusPositionToAgheera = async ({
|
||||
tripRow,
|
||||
authorizationRow,
|
||||
latitud,
|
||||
longitud,
|
||||
measurementTime,
|
||||
tripId,
|
||||
requestId,
|
||||
flow
|
||||
}) => {
|
||||
const clientId = Number.parseInt(tripRow?.id_cliente, 10);
|
||||
if (clientId !== AGHEERA_CLIENT_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseResult = {
|
||||
trip_id: tripId,
|
||||
attempted: false,
|
||||
success: false,
|
||||
http_status: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
if (latitud === null || longitud === null) {
|
||||
return {
|
||||
...baseResult,
|
||||
error: 'COORDINATES_MISSING'
|
||||
};
|
||||
}
|
||||
|
||||
const licensePlate = String(authorizationRow?.matricula || '').trim();
|
||||
if (!licensePlate) {
|
||||
return {
|
||||
...baseResult,
|
||||
error: 'LICENSE_PLATE_NOT_FOUND'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await agheeraPushClient.pushPosition({
|
||||
latitude: latitud,
|
||||
longitude: longitud,
|
||||
vehicleId: licensePlate,
|
||||
licensePlate,
|
||||
measurementTime
|
||||
});
|
||||
|
||||
appendTripStatusDebugLog({
|
||||
stage: 'update_trip_status:agheera_push_success',
|
||||
request_id: requestId,
|
||||
flow,
|
||||
trip_id: tripId,
|
||||
http_status: result?.status ?? null
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
attempted: true,
|
||||
success: true,
|
||||
http_status: result?.status ?? null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Agheera push failed after trip status update:', {
|
||||
tripId,
|
||||
requestId,
|
||||
flow,
|
||||
message: error.message,
|
||||
status: error.status || null
|
||||
});
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
attempted: true,
|
||||
http_status: error.status || null,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeSqlDateTimeValue = (rawValue) => {
|
||||
if (typeof rawValue !== 'string') {
|
||||
return null;
|
||||
@ -1501,7 +1420,7 @@ const updateTripStatus = async (req, res) => {
|
||||
await connection.beginTransaction();
|
||||
|
||||
const [tripRows] = await connection.query(
|
||||
`SELECT id_viaje, id_estado, id_viaje_padre, id_cliente
|
||||
`SELECT id_viaje, id_estado, id_viaje_padre
|
||||
FROM c_viajes
|
||||
WHERE id_viaje = ?
|
||||
LIMIT 1
|
||||
@ -1534,7 +1453,7 @@ const updateTripStatus = async (req, res) => {
|
||||
}
|
||||
|
||||
const [authorizationRows] = await connection.query(
|
||||
`SELECT n_proveedor, id_proveedor, id_tipovehiculo AS matricula
|
||||
`SELECT n_proveedor, id_proveedor
|
||||
FROM c_viajes_proveedor
|
||||
WHERE id_viaje = ?
|
||||
AND dni = ?
|
||||
@ -1797,18 +1716,8 @@ const updateTripStatus = async (req, res) => {
|
||||
idEstadoLogged: FAILED_TRIP_STATE,
|
||||
idPuntoLogged: idPunto
|
||||
});
|
||||
const agheeraPushResult = await pushTripStatusPositionToAgheera({
|
||||
tripRow: tripRows[0],
|
||||
authorizationRow: authorizationRows[0],
|
||||
latitud,
|
||||
longitud,
|
||||
measurementTime: now,
|
||||
tripId,
|
||||
requestId,
|
||||
flow: 'failed_branch_manual'
|
||||
});
|
||||
|
||||
const responseBody = {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
trip_id: tripId,
|
||||
updated_status_id: idEstado,
|
||||
@ -1820,13 +1729,7 @@ const updateTripStatus = async (req, res) => {
|
||||
failed_marked: true,
|
||||
fotos_concat: fotosConcat || '',
|
||||
updated_at: now.toISOString()
|
||||
};
|
||||
|
||||
if (agheeraPushResult) {
|
||||
responseBody.agheera_push = agheeraPushResult;
|
||||
}
|
||||
|
||||
return res.status(200).json(responseBody);
|
||||
});
|
||||
} catch (error) {
|
||||
if (connection) {
|
||||
try {
|
||||
@ -1849,7 +1752,7 @@ const updateTripStatus = async (req, res) => {
|
||||
}
|
||||
|
||||
const [tripRows] = await db.query(
|
||||
`SELECT id_viaje, id_viaje_padre, id_cliente
|
||||
`SELECT id_viaje, id_viaje_padre
|
||||
FROM c_viajes
|
||||
WHERE id_viaje = ?
|
||||
LIMIT 1`,
|
||||
@ -1880,7 +1783,7 @@ const updateTripStatus = async (req, res) => {
|
||||
}
|
||||
|
||||
const [authorizationRows] = await db.query(
|
||||
`SELECT n_proveedor, id_proveedor, id_tipovehiculo AS matricula
|
||||
`SELECT n_proveedor, id_proveedor
|
||||
FROM c_viajes_proveedor
|
||||
WHERE id_viaje = ?
|
||||
AND dni = ?
|
||||
@ -2009,18 +1912,8 @@ const updateTripStatus = async (req, res) => {
|
||||
idEstadoLogged: idEstado,
|
||||
idPuntoLogged: idPunto
|
||||
});
|
||||
const agheeraPushResult = await pushTripStatusPositionToAgheera({
|
||||
tripRow: tripRows[0],
|
||||
authorizationRow: authorizationRows[0],
|
||||
latitud,
|
||||
longitud,
|
||||
measurementTime: now,
|
||||
tripId,
|
||||
requestId,
|
||||
flow: 'normal_manual'
|
||||
});
|
||||
|
||||
const responseBody = {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
trip_id: tripId,
|
||||
updated_status_id: idEstado,
|
||||
@ -2032,13 +1925,7 @@ const updateTripStatus = async (req, res) => {
|
||||
failed_marked: false,
|
||||
fotos_concat: fotosConcat || '',
|
||||
updated_at: now.toISOString()
|
||||
};
|
||||
|
||||
if (agheeraPushResult) {
|
||||
responseBody.agheera_push = agheeraPushResult;
|
||||
}
|
||||
|
||||
return res.status(200).json(responseBody);
|
||||
});
|
||||
} catch (error) {
|
||||
const parsedTripId = Number.parseInt(req.params?.id, 10);
|
||||
const parsedStatusId = Number.parseInt(req.body?.id_estado, 10);
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
const DEFAULT_AGHEERA_PUSH_URL = 'https://push-test.agheera.com/Telematics/Positions';
|
||||
|
||||
let httpClientOverride = null;
|
||||
|
||||
const getPushUrl = () =>
|
||||
String(process.env.AGHEERA_PUSH_URL || DEFAULT_AGHEERA_PUSH_URL).trim();
|
||||
|
||||
const getApiKey = () =>
|
||||
String(process.env.AGHEERA_API_KEY || '').trim();
|
||||
|
||||
const formatMeasurementTime = (dateValue) => {
|
||||
const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
|
||||
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
};
|
||||
|
||||
const normalizeNumber = (rawValue) => {
|
||||
const numericValue = Number(rawValue);
|
||||
return Number.isFinite(numericValue) ? numericValue : null;
|
||||
};
|
||||
|
||||
const getHttpClient = () => {
|
||||
if (typeof httpClientOverride === 'function') {
|
||||
return httpClientOverride;
|
||||
}
|
||||
|
||||
if (typeof fetch === 'function') {
|
||||
return fetch;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildPositionPayload = ({
|
||||
latitude,
|
||||
longitude,
|
||||
vehicleId,
|
||||
licensePlate,
|
||||
measurementTime
|
||||
}) => ({
|
||||
Vehicles: [
|
||||
{
|
||||
latitude: normalizeNumber(latitude),
|
||||
longitude: normalizeNumber(longitude),
|
||||
vehicleId: String(vehicleId || '').trim(),
|
||||
licensePlate: String(licensePlate || '').trim(),
|
||||
measurementTime: formatMeasurementTime(measurementTime)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const pushPosition = async ({
|
||||
latitude,
|
||||
longitude,
|
||||
vehicleId,
|
||||
licensePlate,
|
||||
measurementTime
|
||||
}) => {
|
||||
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 payload = buildPositionPayload({
|
||||
latitude,
|
||||
longitude,
|
||||
vehicleId,
|
||||
licensePlate,
|
||||
measurementTime
|
||||
});
|
||||
|
||||
const response = await httpClient(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const responseBody = typeof response?.text === 'function' ? await response.text() : '';
|
||||
if (!response?.ok) {
|
||||
const error = new Error('Agheera push failed');
|
||||
error.status = response?.status || null;
|
||||
error.body = responseBody;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
body: responseBody
|
||||
};
|
||||
};
|
||||
|
||||
const __setHttpClientForTests = (httpClient) => {
|
||||
httpClientOverride = httpClient;
|
||||
};
|
||||
|
||||
const __resetHttpClientForTests = () => {
|
||||
httpClientOverride = null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_AGHEERA_PUSH_URL,
|
||||
pushPosition,
|
||||
__setHttpClientForTests,
|
||||
__resetHttpClientForTests
|
||||
};
|
||||
@ -1,289 +0,0 @@
|
||||
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 agheeraPushClient = require('../src/services/agheeraPushClient');
|
||||
|
||||
const TEST_JWT_SECRET = 'test-jwt-secret';
|
||||
|
||||
let originalQuery;
|
||||
let originalJwtSecret;
|
||||
let originalAgheeraApiKey;
|
||||
|
||||
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 postJson = async ({ port, path, authorization, body }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const rawBody = JSON.stringify(body);
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: '127.0.0.1',
|
||||
port,
|
||||
method: 'POST',
|
||||
path,
|
||||
headers: {
|
||||
authorization,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(rawBody)
|
||||
}
|
||||
},
|
||||
(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);
|
||||
req.write(rawBody);
|
||||
req.end();
|
||||
});
|
||||
|
||||
|
||||
test.before(() => {
|
||||
originalQuery = db.query;
|
||||
originalJwtSecret = process.env.JWT_SECRET;
|
||||
originalAgheeraApiKey = process.env.AGHEERA_API_KEY;
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
db.query = originalQuery;
|
||||
process.env.JWT_SECRET = originalJwtSecret;
|
||||
process.env.AGHEERA_API_KEY = originalAgheeraApiKey;
|
||||
agheeraPushClient.__resetHttpClientForTests();
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
db.query = originalQuery;
|
||||
agheeraPushClient.__resetHttpClientForTests();
|
||||
});
|
||||
|
||||
test('POST /api/locations envia posicion a Agheera para cliente 532', async () => {
|
||||
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return { ok: true, status: 200, text: async () => 'Messages received.' };
|
||||
});
|
||||
|
||||
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']);
|
||||
assert.equal(params[0][0][4], 248230);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
assert.match(sql, /FROM c_viajes/);
|
||||
assert.deepEqual(params, [248230]);
|
||||
return [[{ id_cliente: 532 }]];
|
||||
}
|
||||
|
||||
assert.match(sql, /FROM c_viajes_proveedor/);
|
||||
assert.deepEqual(params, [248230, '58045340X']);
|
||||
return [[{ matricula: '6599LCN' }]];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
postJson({
|
||||
port: server.address().port,
|
||||
path: '/api/locations',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
latitude: 40.416775,
|
||||
longitude: -3.70379,
|
||||
id_viaje: 248230,
|
||||
timestamp: '2026-06-01T13:20:00Z'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.body, {
|
||||
success: true,
|
||||
count: 1,
|
||||
message: 'Locations saved',
|
||||
agheera_push: {
|
||||
trip_id: 248230,
|
||||
attempted: true,
|
||||
success: true,
|
||||
http_status: 200,
|
||||
error: null
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(agheeraCalls.length, 1);
|
||||
|
||||
const call = agheeraCalls[0];
|
||||
assert.equal(call.url, 'https://push-test.agheera.com/Telematics/Positions');
|
||||
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
||||
|
||||
const payload = JSON.parse(call.options.body);
|
||||
assert.deepEqual(payload, {
|
||||
Vehicles: [
|
||||
{
|
||||
latitude: 40.416775,
|
||||
longitude: -3.70379,
|
||||
vehicleId: '6599LCN',
|
||||
licensePlate: '6599LCN',
|
||||
measurementTime: '2026-06-01T13:20:00Z'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('POST /api/locations no envia a Agheera para clientes distintos de 532', async () => {
|
||||
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return { ok: true, status: 200, text: async () => 'Messages received.' };
|
||||
});
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql, params) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
assert.match(sql, /INSERT INTO c_trazabilidad_transportista/);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
assert.match(sql, /FROM c_viajes/);
|
||||
assert.deepEqual(params, [248230]);
|
||||
return [[{ id_cliente: 700 }]];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
postJson({
|
||||
port: server.address().port,
|
||||
path: '/api/locations',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
latitude: 40.416775,
|
||||
longitude: -3.70379,
|
||||
id_viaje: 248230,
|
||||
timestamp: '2026-06-01T13:20:00Z'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.body, {
|
||||
success: true,
|
||||
count: 1,
|
||||
message: 'Locations saved'
|
||||
});
|
||||
assert.equal(agheeraCalls.length, 0);
|
||||
});
|
||||
|
||||
test('POST /api/locations devuelve error de Agheera sin romper guardado local', async () => {
|
||||
process.env.JWT_SECRET = TEST_JWT_SECRET;
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
|
||||
agheeraPushClient.__setHttpClientForTests(async () => ({
|
||||
ok: false,
|
||||
status: 401,
|
||||
text: async () => 'Unauthorized'
|
||||
}));
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql, params) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
assert.match(sql, /INSERT INTO c_trazabilidad_transportista/);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
assert.match(sql, /FROM c_viajes/);
|
||||
assert.deepEqual(params, [248230]);
|
||||
return [[{ id_cliente: 532 }]];
|
||||
}
|
||||
|
||||
assert.match(sql, /FROM c_viajes_proveedor/);
|
||||
assert.deepEqual(params, [248230, '58045340X']);
|
||||
return [[{ matricula: '6599LCN' }]];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
postJson({
|
||||
port: server.address().port,
|
||||
path: '/api/locations',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
latitude: 40.416775,
|
||||
longitude: -3.70379,
|
||||
id_viaje: 248230,
|
||||
timestamp: '2026-06-01T13:20:00Z'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.body, {
|
||||
success: true,
|
||||
count: 1,
|
||||
message: 'Locations saved',
|
||||
agheera_push: {
|
||||
trip_id: 248230,
|
||||
attempted: true,
|
||||
success: false,
|
||||
http_status: 401,
|
||||
error: 'Agheera push failed'
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -12,11 +12,8 @@ process.env.TRIP_STATUS_UPLOAD_DIR = TEST_UPLOAD_DIR;
|
||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual'
|
||||
|
||||
const app = require('../app');
|
||||
process.env.TRIP_STATUS_UPLOAD_DIR = TEST_UPLOAD_DIR;
|
||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||
const db = require('../src/config/db');
|
||||
const tripStatusPhotoStorage = require('../src/services/tripStatusPhotoStorage');
|
||||
const agheeraPushClient = require('../src/services/agheeraPushClient');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret';
|
||||
process.env.JWT_SECRET = JWT_SECRET;
|
||||
@ -236,7 +233,6 @@ test.after(() => {
|
||||
db.query = originalQuery;
|
||||
db.getConnection = originalGetConnection;
|
||||
tripStatusPhotoStorage.__resetSftpClientFactoryForTests();
|
||||
agheeraPushClient.__resetHttpClientForTests();
|
||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||
delete process.env.TRIP_STATUS_SFTP_HOST;
|
||||
delete process.env.TRIP_STATUS_SFTP_PORT;
|
||||
@ -245,8 +241,6 @@ test.after(() => {
|
||||
delete process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR;
|
||||
delete process.env.POSTS_LOG_PATH;
|
||||
delete process.env.TRIP_STATUS_UPDATES_LOG_PATH;
|
||||
delete process.env.AGHEERA_PUSH_URL;
|
||||
delete process.env.AGHEERA_API_KEY;
|
||||
fs.rmSync(TEST_UPLOAD_DIR, { recursive: true, force: true });
|
||||
fs.rmSync(TEST_POSTS_LOG_PATH, { force: true });
|
||||
fs.rmSync(TEST_STATUS_LOG_PATH, { force: true });
|
||||
@ -256,7 +250,6 @@ test.afterEach(() => {
|
||||
db.query = originalQuery;
|
||||
db.getConnection = originalGetConnection;
|
||||
tripStatusPhotoStorage.__resetSftpClientFactoryForTests();
|
||||
agheeraPushClient.__resetHttpClientForTests();
|
||||
process.env.TRIP_STATUS_PHOTO_STORAGE_MODE = 'dual';
|
||||
delete process.env.TRIP_STATUS_SFTP_HOST;
|
||||
delete process.env.TRIP_STATUS_SFTP_PORT;
|
||||
@ -265,8 +258,6 @@ test.afterEach(() => {
|
||||
delete process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR;
|
||||
delete process.env.POSTS_LOG_PATH;
|
||||
delete process.env.TRIP_STATUS_UPDATES_LOG_PATH;
|
||||
delete process.env.AGHEERA_PUSH_URL;
|
||||
delete process.env.AGHEERA_API_KEY;
|
||||
fs.rmSync(TEST_POSTS_LOG_PATH, { force: true });
|
||||
fs.rmSync(TEST_STATUS_LOG_PATH, { force: true });
|
||||
});
|
||||
@ -719,7 +710,7 @@ test('POST /api/trips/:id/status modo dual replica foto a SFTP y mantiene local'
|
||||
const recorder = createSftpRecorder();
|
||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||
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_USERNAME = 'ssh_fotos_estado';
|
||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||
@ -796,7 +787,7 @@ test('POST /api/trips/:id/status modo dual con fallo SFTP mantiene fallback loca
|
||||
createFakeSftpClientFactory(recorder, { failPut: true })
|
||||
);
|
||||
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_USERNAME = 'ssh_fotos_estado';
|
||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||
@ -862,7 +853,7 @@ test('POST /api/trips/:id/status payload inválido tras upload limpia remoto y l
|
||||
const recorder = createSftpRecorder();
|
||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||
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_USERNAME = 'ssh_fotos_estado';
|
||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||
@ -1130,269 +1121,6 @@ test('POST /api/trips/:id/status propaga estado global al viaje padre', async ()
|
||||
assert.equal(step, 7);
|
||||
});
|
||||
|
||||
test('POST /api/trips/:id/status cliente 532 envia posicion a Agheera en estado global', async () => {
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => 'Messages received.'
|
||||
};
|
||||
});
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql, params) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
assert.match(sql, /FROM t_viaje_estados/);
|
||||
assert.deepEqual(params, [6]);
|
||||
return [[{ id_estado: 6 }]];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
assert.match(sql, /FROM c_viajes/);
|
||||
assert.match(sql, /id_cliente/);
|
||||
assert.deepEqual(params, [248230]);
|
||||
return [[{ id_viaje: 248230, id_viaje_padre: 0, id_cliente: 532 }]];
|
||||
}
|
||||
|
||||
if (step === 3) {
|
||||
assert.match(sql, /FROM c_viajes_proveedor/);
|
||||
assert.match(sql, /id_tipovehiculo AS matricula/);
|
||||
assert.deepEqual(params, [248230, '58045340X']);
|
||||
return [[{ n_proveedor: 1, id_proveedor: 675, matricula: '1234ABC' }]];
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
assert.match(sql, /UPDATE c_viajes/);
|
||||
assert.deepEqual(params, [6, 1, 248230]);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
||||
assert.equal(params[6], '40.416775');
|
||||
assert.equal(params[7], '-3.70379');
|
||||
return [{ insertId: 6, affectedRows: 1 }];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
requestJson({
|
||||
port: server.address().port,
|
||||
method: 'POST',
|
||||
path: '/api/trips/248230/status',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
id_estado: 6,
|
||||
latitud: '40,416775',
|
||||
longitud: '-3.703790'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.body.success, true);
|
||||
assert.deepEqual(response.body.agheera_push, {
|
||||
trip_id: 248230,
|
||||
attempted: true,
|
||||
success: true,
|
||||
http_status: 200,
|
||||
error: null
|
||||
});
|
||||
assert.equal(agheeraCalls.length, 1);
|
||||
|
||||
const call = agheeraCalls[0];
|
||||
assert.equal(call.url, 'https://push-test.agheera.com/Telematics/Positions');
|
||||
assert.equal(call.options.method, 'POST');
|
||||
assert.equal(call.options.headers.apiKey, 'test-api-key');
|
||||
assert.equal(call.options.headers['Content-Type'], 'application/json');
|
||||
|
||||
const payload = JSON.parse(call.options.body);
|
||||
assert.deepEqual(Object.keys(payload), ['Vehicles']);
|
||||
assert.equal(payload.Vehicles.length, 1);
|
||||
assert.equal(payload.Vehicles[0].latitude, 40.416775);
|
||||
assert.equal(payload.Vehicles[0].longitude, -3.70379);
|
||||
assert.equal(payload.Vehicles[0].vehicleId, '1234ABC');
|
||||
assert.equal(payload.Vehicles[0].licensePlate, '1234ABC');
|
||||
assert.match(payload.Vehicles[0].measurementTime, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
||||
});
|
||||
|
||||
test('POST /api/trips/:id/status cliente distinto de 532 no envia a Agheera', async () => {
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return { ok: true, status: 200, text: async () => 'Messages received.' };
|
||||
});
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql, params) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
return [[{ id_estado: 6 }]];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
return [[{ id_viaje: 248230, id_viaje_padre: 0, id_cliente: 700 }]];
|
||||
}
|
||||
|
||||
if (step === 3) {
|
||||
return [[{ n_proveedor: 1, id_proveedor: 675, matricula: '1234ABC' }]];
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
assert.match(sql, /UPDATE c_viajes/);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
||||
return [{ insertId: 6, affectedRows: 1 }];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
requestJson({
|
||||
port: server.address().port,
|
||||
method: 'POST',
|
||||
path: '/api/trips/248230/status',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
id_estado: 6,
|
||||
latitud: '40.416775',
|
||||
longitud: '-3.703790'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.body.success, true);
|
||||
assert.equal(response.body.agheera_push, undefined);
|
||||
assert.equal(agheeraCalls.length, 0);
|
||||
});
|
||||
|
||||
test('POST /api/trips/:id/status estado intermedio con id_punto no envia a Agheera', async () => {
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return { ok: true, status: 200, text: async () => 'Messages received.' };
|
||||
});
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql, params) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
assert.match(sql, /FROM t_viaje_estados/);
|
||||
return [[{ id_estado: 5 }]];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
assert.match(sql, /FROM c_viajes/);
|
||||
return [[{ id_viaje: 248230 }]];
|
||||
}
|
||||
|
||||
if (step === 3) {
|
||||
assert.match(sql, /FROM c_viajes_proveedor/);
|
||||
return [[{ n_proveedor: 1 }]];
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
assert.match(sql, /FROM c_viajes_puntos/);
|
||||
assert.deepEqual(params, [8123, 248230]);
|
||||
return [[{ id_punto: 8123, id_estado_intermedio: 4, valor: null, foto: null }]];
|
||||
}
|
||||
|
||||
assert.match(sql, /UPDATE c_viajes_puntos/);
|
||||
return [{ affectedRows: 1 }];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
requestJson({
|
||||
port: server.address().port,
|
||||
method: 'POST',
|
||||
path: '/api/trips/248230/status',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
id_estado: 5,
|
||||
id_punto: 8123,
|
||||
latitud: '40.416775',
|
||||
longitud: '-3.703790'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.body.success, true);
|
||||
assert.equal(response.body.agheera_push, undefined);
|
||||
assert.equal(agheeraCalls.length, 0);
|
||||
});
|
||||
|
||||
test('POST /api/trips/:id/status fallo de Agheera mantiene respuesta 200', async () => {
|
||||
process.env.AGHEERA_API_KEY = 'test-api-key';
|
||||
const agheeraCalls = [];
|
||||
agheeraPushClient.__setHttpClientForTests(async (url, options) => {
|
||||
agheeraCalls.push({ url, options });
|
||||
return {
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => 'temporary error'
|
||||
};
|
||||
});
|
||||
|
||||
let step = 0;
|
||||
db.query = async (sql) => {
|
||||
step += 1;
|
||||
|
||||
if (step === 1) {
|
||||
return [[{ id_estado: 6 }]];
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
return [[{ id_viaje: 248230, id_viaje_padre: 0, id_cliente: 532 }]];
|
||||
}
|
||||
|
||||
if (step === 3) {
|
||||
return [[{ n_proveedor: 1, id_proveedor: 675, matricula: '1234ABC' }]];
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
assert.match(sql, /UPDATE c_viajes/);
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
|
||||
assert.match(sql, /INSERT INTO c_cambios_estado/);
|
||||
return [{ insertId: 6, affectedRows: 1 }];
|
||||
};
|
||||
|
||||
const response = await withServer(async (server) =>
|
||||
requestJson({
|
||||
port: server.address().port,
|
||||
method: 'POST',
|
||||
path: '/api/trips/248230/status',
|
||||
authorization: `Bearer ${createToken()}`,
|
||||
body: {
|
||||
id_estado: 6,
|
||||
latitud: '40.416775',
|
||||
longitud: '-3.703790'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.body.success, true);
|
||||
assert.deepEqual(response.body.agheera_push, {
|
||||
trip_id: 248230,
|
||||
attempted: true,
|
||||
success: false,
|
||||
http_status: 500,
|
||||
error: 'Agheera push failed'
|
||||
});
|
||||
assert.equal(agheeraCalls.length, 1);
|
||||
});
|
||||
|
||||
test('POST /api/trips/:id/status estado intermedio con id_punto inválido => 400', async () => {
|
||||
db.query = async () => {
|
||||
throw new Error('db.query should not run for invalid id_punto');
|
||||
@ -2795,7 +2523,7 @@ test('DELETE /api/trips/:id/status en modo dual no borra foto en remoto ni local
|
||||
const recorder = createSftpRecorder();
|
||||
tripStatusPhotoStorage.__setSftpClientFactoryForTests(createFakeSftpClientFactory(recorder));
|
||||
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_USERNAME = 'ssh_fotos_estado';
|
||||
process.env.TRIP_STATUS_SFTP_PASSWORD = 'test-password';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user