# Consulta y firma eCMR desde la app `GET /api/trips/:id/ecmr` Enviar `Authorization: Bearer ` con la sesión de la app. La respuesta contiene únicamente documentos activos con PDF generado de los tramos cuyo DNI coincide con el conductor autenticado. ```json { "success": true, "data": [ { "id_documento": 7, "n_proveedor": 2, "numero": "ECMR-123", "version": 1, "estado": "emitido", "fecha_creacion": "2026-09-16 10:00:00", "document_url": "https:///adm/pages/logic/ecmr/documento.php?t=", "firma_carga_completada": false, "firma_descarga_completada": false, "firma_carga_url": "https:///adm/pages/logic/ecmr/firmar.php?t=", "firma_descarga_url": "https:///adm/pages/logic/ecmr/firmar.php?t=", "firma_expira_en_segundos": 900 } ] } ``` ## Integración React / React Native En el detalle del viaje, consultar esta ruta y mostrar: - «Ver eCMR»: abrir `document_url` mediante el visor PDF de la app. - «Firmar carga»: abrir `firma_carga_url` en el navegador seguro o WebView con soporte HTTPS y WebCrypto. Si es `null`, mostrar «Carga firmada». - «Firmar descarga»: abrir `firma_descarga_url` de la misma manera. Si es `null`, mostrar «Descarga firmada». Los enlaces son independientes por documento y rol. Después de volver de la pantalla de firma, consultar de nuevo la ruta para refrescar el estado. Los campos `firma_*_completada` son booleanos; no inferir las firmas a partir del estado general del viaje. Las URLs de firma duran 15 minutos desde su emisión. Volver a consultar cuando caduquen. Cada consulta crea invitaciones temporales para los roles pendientes: no usar sondeo periódico ni precarga masiva. La respuesta lleva `no-store`. No persistir ni registrar enlaces, y no enviar el Bearer de la app a las URLs PHP. El token del enlace es la credencial temporal, no un parámetro `skip_otp`. La pantalla PHP valida el acceso emitido por Node, comprueba que el conductor continúa asignado al tramo y omite el código de seis cifras. El DNI procede de la sesión y no se puede cambiar. Se mantienen nombre, consentimiento y firma criptográfica. Las invitaciones de correo/WhatsApp conservan su OTP. Tras firmar carga desde la app, se consulta la app para acceder a la firma de descarga; no se envía un correo o WhatsApp automático en este canal. Si `data` está vacío, mostrar «Este viaje todavía no tiene eCMR disponible». El PDF se entrega como `attachment`; abrirlo en un navegador puede descargarlo. Errores: 401 sin sesión/DNI, 404 identificador inválido o viaje inexistente, 403 conductor no asignado, 500 error de base de datos/invitaciones. No mostrar los errores como ausencia de documentos. Un enlace PHP caducado o cuyo tramo ya no está asignado devuelve 403 y pide volver a abrirlo desde la app. ## Requisitos - Node y PHP deben usar las mismas tablas `ecmr_documentos`, `ecmr_invitaciones`, `ecmr_firmas` y `c_viajes_proveedor`. - Desplegar también `firma_app.php`, los cambios de `firma_api.php`, `firmar.php`, `common.php` y las traducciones ES/EN del ERP. - `public_url` debe apuntar al ERP actualizado y ser accesible mediante HTTPS. - No requiere nuevas columnas ni migraciones para los enlaces de firma. - No se ha facilitado el código React; queda pendiente conectar sus botones. ## Validación - 40 pruebas Node: consulta eCMR, autorización, enlaces por rol, rollback y regresión de listado, viaje activo y puntos intermedios. - 27 comprobaciones PHP: DNI, caducidad, asignación, OTP y traducciones. - Prueba del JavaScript de firma para sesiones app y enlaces con OTP. - Consultas SQL reales e invitación temporal verificadas en desarrollo mediante una transacción revertida. No se ha firmado ningún documento real. ## QR del PDF DeCA / eCMR `GET /api/trips/:id/ecmr/:documentId/qr` Usar el mismo `Authorization: Bearer ` y el `id_documento` obtenido en la consulta eCMR del viaje. Se especifica el documento porque un viaje puede tener varios tramos y versiones. ```json { "success": true, "data": { "id_documento": 7, "n_proveedor": 2, "numero": "ECMR-123", "version": 1, "document_url": "https:///adm/pages/logic/ecmr/documento.php?t=", "qr_data_url": "data:image/png;base64,..." } } ``` La imagen PNG es de 320 × 320 píxeles y codifica exactamente `document_url`, la URL del PDF usada por el QR del documento. No codifica un enlace de firma. En React Native puede mostrarse con: ```jsx ``` Solo devuelve documentos activos con PDF generado y del tramo asignado al DNI de la sesión. Responde `401` sin sesión/DNI y `404` si los identificadores no son válidos, el documento no existe, pertenece a otro viaje/conductor, está sustituido o no tiene PDF generado. El `404` evita revelar documentos ajenos. Los fallos de BD o generación del QR devuelven `500` con un mensaje genérico. La respuesta lleva `Cache-Control: no-store`. Esta consulta no crea invitaciones, no modifica la BD ni envía mensajes; se puede usar para actualizar la imagen sin renovar enlaces de firma. Al sustituirse una versión, volver a consultar los documentos del viaje y utilizar el nuevo identificador. El QR contiene un enlace público al PDF: no registrarlo en logs ni analítica. Despliegue: instalar las dependencias del lockfile (`npm ci --omit=dev`) y reiniciar el proceso Node según el procedimiento habitual. Se añade `qrcode` 1.5.4. No requiere migraciones de BD ni cambios en PHP. Pruebas: `node --test test/trips.ecmr.integration.test.js` cubre autenticación, identificadores, filtrado por tramo/conductor, ausencia de documentos, PNG real, URL codificada, errores de BD/generador y regresión del flujo de firma.