diff --git a/src/controllers/locationController.js b/src/controllers/locationController.js index 7bdaf2b..3174ad8 100644 --- a/src/controllers/locationController.js +++ b/src/controllers/locationController.js @@ -167,7 +167,12 @@ const pushLocationToAgheera = async ({ latitude, longitude, dni, tripId, measure longitude, vehicleId: licensePlate, licensePlate, - measurementTime + measurementTime, + metadata: { + source: 'location', + trip_id: tripId, + dni + } }); return { diff --git a/src/controllers/tripsController.js b/src/controllers/tripsController.js index d191aa8..7cf1328 100644 --- a/src/controllers/tripsController.js +++ b/src/controllers/tripsController.js @@ -284,7 +284,13 @@ const pushTripStatusPositionToAgheera = async ({ longitude: longitud, vehicleId: licensePlate, licensePlate, - measurementTime + measurementTime, + metadata: { + source: 'trip_status', + request_id: requestId, + flow, + trip_id: tripId + } }); appendTripStatusDebugLog({ diff --git a/src/services/agheeraPushClient.js b/src/services/agheeraPushClient.js index eae14ff..a06c6dc 100644 --- a/src/services/agheeraPushClient.js +++ b/src/services/agheeraPushClient.js @@ -1,4 +1,8 @@ +const fs = require('fs'); +const path = require('path'); + const DEFAULT_AGHEERA_PUSH_URL = 'https://push-test.agheera.com/Telematics/Positions'; +const DEFAULT_AGHEERA_PUSH_LOG_PATH = '/var/log/agheera_push.log'; let httpClientOverride = null; @@ -8,6 +12,41 @@ const getPushUrl = () => const getApiKey = () => 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 date = dateValue instanceof Date ? dateValue : new Date(dateValue); return date.toISOString().replace(/\.\d{3}Z$/, 'Z'); @@ -53,18 +92,9 @@ const pushPosition = async ({ longitude, vehicleId, 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 payload = buildPositionPayload({ latitude, @@ -74,23 +104,67 @@ const pushPosition = async ({ measurementTime }); - const response = await httpClient(url, { - method: 'POST', - headers: { - apiKey, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); + 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', + headers: { + apiKey, + 'Content-Type': 'application/json' + }, + 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() : ''; if (!response?.ok) { const error = new Error('Agheera push failed'); error.status = response?.status || null; error.body = responseBody; + await appendPushLog({ + metadata, + url, + payload, + success: false, + status: error.status, + responseBody, + error: error.message + }); throw error; } + await appendPushLog({ + metadata, + url, + payload, + success: true, + status: response.status, + responseBody + }); + return { status: response.status, body: responseBody @@ -107,6 +181,7 @@ const __resetHttpClientForTests = () => { module.exports = { DEFAULT_AGHEERA_PUSH_URL, + DEFAULT_AGHEERA_PUSH_LOG_PATH, pushPosition, __setHttpClientForTests, __resetHttpClientForTests diff --git a/test/agheera-push-client.log.test.js b/test/agheera-push-client.log.test.js new file mode 100644 index 0000000..2757a67 --- /dev/null +++ b/test/agheera-push-client.log.test.js @@ -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); +}); diff --git a/test/trips.status.integration.test.js b/test/trips.status.integration.test.js index 60ee057..227ab27 100644 --- a/test/trips.status.integration.test.js +++ b/test/trips.status.integration.test.js @@ -719,7 +719,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 +796,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 +862,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'; @@ -2795,7 +2795,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';