El terreno no siempre tiene cobertura

Sótanos, salas técnicas, azoteas: un técnico debe poder fichar, tomar notas y fotos sin conexión. La aplicación móvil, en Flutter, escribe cada acción en una base de datos local, en una cola de envío (outbox), y la sincroniza por lotes en cuanto vuelve la red. La base local sirve para trabajar sin conexión; el servidor sigue siendo la fuente de verdad.

Los eventos sincronizados cubren toda la intervención:

  • llegada, inicio, pausa, reanudación y fin del trabajo;
  • notas y mediciones estructuradas;
  • piezas utilizadas o no disponibles;
  • fotos y firma del cliente;
  • cliente ausente, acceso bloqueado, peligro, nueva visita necesaria, puesta en seguridad.

Un identificador por evento

Cada evento recibe en el teléfono un clientEventId (UUID v4), que nunca se regenera al reintentar. Repetir exactamente el mismo evento no tiene ningún efecto, incluso después del cierre de la intervención: la respuesta simplemente lo señala como duplicado. Reutilizar el mismo identificador con otro contenido se rechaza (409).

Así, la aplicación puede reenviar un lote completo tras perder una respuesta de red, sin riesgo de duplicados. Un lote contiene como máximo 100 eventos y es transaccional: si uno de ellos no es válido, no se crea ninguno. El servidor también controla la secuencia de fichajes; una pausa sin inicio o dos inicios seguidos se rechazan. Una pieza declarada solo descuenta el stock del vehículo una vez, aunque el envío se repita.

Archivos verificados antes de aceptarse

Antes de enviar una foto o una firma, la aplicación declara una intención de envío: tipo MIME, tamaño y huella SHA-256. El servidor responde con una URL que lleva un token aleatorio, válido durante quince minutos y de un solo uso.

Al recibir el archivo, comprueba el token, el tamaño, la huella y la firma binaria real del fichero. Solo un archivo validado, que pertenezca a la misma organización y a la misma intervención, puede después citarse en un evento de campo. Los archivos se guardan en un almacenamiento de objetos (S3 o compatible, como MinIO), no en PostgreSQL.

Un flujo en tiempo real que se reanuda donde se quedó

Las pantallas de planificación y la aplicación reciben los cambios mediante un flujo SSE autenticado y aislado por organización. Tras un corte, el cliente se reanuda desde el último evento recibido gracias a la cabecera Last-Event-ID; los eventos se conservan siete días por defecto. Un técnico solo recibe los de las intervenciones que tiene asignadas.

Las notificaciones push (Firebase Cloud Messaging) son deliberadamente genéricas: solo transportan identificadores, y la aplicación siempre vuelve a cargar los datos de negocio a través de la API autenticada.

Lo que cambia la idempotencia

Sitúa la complejidad en el lugar adecuado. La aplicación no necesita saber si un envío anterior llegó: ante la duda, lo reenvía. El servidor garantiza que un mismo hecho solo se registra una vez.