Hub

electrosystems

amadeus-consumos-milestone

active high design-doc
Creado
2026-05-28
Actualizado
2026-05-28
Parent
amadeus

Amadeus — Milestone “Consumos bien capturados + IA por lineamientos” 🔥

Objetivo de negocio — que los técnicos suban bien los viáticos y que la IA audite conforme a lineamientos que nosotros podamos editar. Rechazo de IA nunca terminal (apelable), validación de ubicación, mapa de consumos y captura offline. Urgente por pedido de Sergio (2026-05-28).

TL;DR de lo que ya existe (NO se construye de cero)

El módulo de auditoría IA de consumos ya está en prod y se usó en los viajes 191/192 (4 pares, 80 consumos, $1.49 USD). Lo que hay hoy:

  • Tabla consumos_analisis (1:1 con consumos) con veredicto IA + OCR + override admin + auditoría de costo.
  • Tabla auditorias_viaje con resumen ejecutivo IA + flags_agregados.
  • AnalizarFotoConsumoJobAnalizadorTicketService (Claude Sonnet 4.6 vision, tool-use dictaminar_ticket).
  • AuditorViajeService + ResumidorAuditoriaService (síntesis por viaje).
  • 3 puertas de rechazo — foto inválida / no parece viático / monto no coincide.
  • Override admin (override_admin_id/override_motivo/override_at).
  • Notificación por rechazo — push + email técnico + email admin (ConsumoRechazadoPorIA → listener).
  • GPS capturado por dos vías — coordenadas (geolocalización del navegador al hacer submit) y coordenadas_ticket (EXIF de la foto). Sitios tienen latitud/longitud (nullable).
  • Flag lejos_del_sitio (>30 km del sitio del viaje) ya calculado en AuditorViajeService.
  • Dashboard admin /consumos/analisis (KPIs, costo vs cap, top razones, top usuarios).
  • Design doc de políticas/lineamientos (amadeus-politicas-viaticos.md, ready-to-build) — tabla politicas_viaticos editable en Nova + inyección en los 2 prompts. Esto es exactamente “lineamientos que podamos modificar”.

Lo que NO existe hoy: mapa interactivo embebido (solo links a Google Maps), captura offline / cola IndexedDB, captura de GPS al momento de la foto (hoy es al submit), coords de oficina/matriz, captura de ubicación de hospedaje, canal de apelación iniciado por el técnico, y — crítico — los rechazados se ocultan (ver friction #1).


🚩 Fricciones encontradas en el código

  1. NoRechazadosScope oculta los consumos rechazados (BLOQUEANTE para “apelable”). app/Models/Scopes/NoRechazadosScope.php filtra estatus != 'rechazado' en todas las queries por default. Es decir: cuando la IA marca rechazado, el consumo desaparece de los listados normales del técnico. Pedir que el rechazo sea “apelable y no definitivo” es incompatible con esconderlo. Hay que repensar este scope (o el significado de estatus).

  2. “Rechazado por IA” hoy SÍ es semánticamente terminal. La IA escribe directo consumos.estatus='rechazado'. No hay un estado intermedio “observado/marcado por IA, pendiente de revisión humana”. El único escape actual es (a) re-subir foto (resetea el análisis) o (b) override de admin. No hay acción de “apelar” iniciada por el técnico ni cola de revisión humana. La memoria del propio design doc (amadeus-validacion-fotos.md:269) ya difería “botón solicitar revisión humana” como segunda capa — es justo lo que Sergio pide ahora.

  3. Re-subir foto resetea sin límite. Hoy re-subir invalida el rechazo y re-dispara IA, sin tope. Esto es bueno como apelación pero permite que alguien re-suba 10 fotos negras distintas. Sergio dice explícitamente “aunque vuelvan a subir foto y siga inválida” — o sea, tras N reintentos fallidos debe haber una salida humana, no un loop infinito.

  4. Validación de ubicación es solo punto-radio, no corredor. lejos_del_sitio mide distancia recta consumo→sitio (>30 km = flag). No contempla que el técnico legítimamente consume en carretera rumbo al sitio (desde oficina o desde el hospedaje). Un radio simple genera falsos positivos en todo el trayecto. Además no hay coords de oficina/matriz en ningún lado (config/seeder), y no se captura la ubicación del hospedaje. Sin esos dos anclas, no se puede validar el corredor.

  5. coordenadas se captura al hacer submit, no al tomar la foto. Si un técnico toma la foto en el sitio (sin señal) y la sube horas después desde el hotel (con señal), coordenadas registra el hotel, no el sitio. Para offline + “preservar ubicación original” hay que capturar el GPS en el momento de la captura y arrastrarlo por la cola offline.

  6. Sitios con GPS nullable. lejos_del_sitio solo funciona si el sitio tiene latitud/longitud. Si muchos sitios están en null, la validación de ubicación es ciega. Hay que medir cobertura de GPS en sitios antes de prometer validación de ubicación confiable.

  7. No hay worker de queue en prod (#371). RESUELTO 2026-05-28 — el premise era incorrecto. Sí hay worker: 8 procesos queue:work vía supervisor (laravel-worker.conf) en la cola default. El hook (ConsumosController::dispatchAnalisisIA) dispatcha a default (sin onQueue), no a analizar-foto (ese literal no existe en código — vino del batch manual con --cola=analizar-foto). Por eso los jobs del batch “esperaron en vano”, pero el hook automático sí sería procesado. #371 cerrado; solo falta prender el flag (#249).

  8. Prompts hardcodeados. Los lineamientos hoy viven embebidos en AnalizadorTicketService.php:109 y ResumidorAuditoriaService.php:26. El design doc de políticas (#369) ya resuelve esto moviéndolos a BD editable — pero hasta que se construya, “modificar lineamientos” = editar código + deploy.


🤔 Pushback / lo que cuestiono de lo pedido

  • “Rechazado” como palabra es contraproducente. Recomiendo que la IA nunca escriba rechazado terminal. Que su veredicto negativo sea observado (o marcado_ia) — “esta captura tiene una observación, revísala”. Solo un humano (admin/contabilidad) emite el estado final rechazado/autorizado. Esto (a) hace la apelación natural, (b) elimina de raíz el problema del NoRechazadosScope, (c) baja la fricción emocional con el técnico en campo. Es un cambio de semántica + UI más que de motor IA. ¿De acuerdo con renombrar, o prefieres mantener “rechazado” + botón apelar encima?

  • Validación de ubicación: flag, nunca auto-rechazo. La ubicación tiene demasiados casos legítimos (carretera, desvío a comer, GPS impreciso bajo techo). Propongo que ubicación sea solo bandera para el auditor humano, jamás un rechazo automático. Coincide con tu filosofía de “no definitivo”.

  • Hospedaje: ¿cómo capturamos su ubicación? Tres opciones — (a) el técnico marca “aquí me hospedo” una vez por viaje (pin en mapa, 1 tap), (b) se infiere por clustering del GPS nocturno, (c) se ignora y solo validamos corredor oficina↔sitio. Recomiendo (a) — simple y explícito. (b) es frágil. ¿Cuál prefieres?

  • Offline es la pieza más cara y frágil — sugiero que sea su propia fase al final. El offline real (Service Worker + Background Sync API + IndexedDB) es poco confiable en iOS/Safari PWA. Una alternativa que cubre ~90% del caso con 1/4 del esfuerzo — “borrador local”: si no hay señal al subir, se guarda la foto + GPS + datos en IndexedDB y se sincroniza cuando el técnico reabre la app con señal (no Background Sync mágico en segundo plano). ¿Realmente capturan sin señal y sin reabrir la app después? Si el patrón real es “sin señal en el sitio, pero con señal en el hotel en la noche”, el borrador local basta. ¿Offline real o borrador local?

  • El mapa es lo más barato y de mayor impacto visual. Leaflet + OpenStreetMap (gratis, sin API key) embebido. Self-contained, no toca el motor IA. Buen candidato para una victoria temprana.

  • Conexión con #369 (políticas): lo que pides como “IA audita conforme a lineamientos que podamos modificar” ya está diseñado en amadeus-politicas-viaticos.md. No dupliquemos — ese design doc es el corazón de este milestone. Solo faltan sus 5 decisiones (D1-D5) para arrancar.


📦 Breakdown en pendientes chicos (orden de ataque recomendado)

Numerados globalmente. Agrupados por bloque; cada uno es entregable y verificable solo.

🎯 CUT 2-JUNIO — Bloque 0 + Bloque A

Lo que debe estar listo el 2026-06-02. Flujo “IA observa → técnico apela → humano resuelve” end-to-end.

Bloque 0 — Desbloqueos

  • #371 (ya existe) — Montar worker systemd CERRADO 2026-05-28. Premise desactualizado: prod ya tiene 8 workers queue:work (supervisor laravel-worker.conf) en la cola default, y el hook dispatcha a default (no a analizar-foto, literal inexistente en código). No requería construir nada. Detalle en bitácora de amadeus.md.
  • #249 (ya existe) 📅 2026-06-01 — Prender ANALISIS_HOOK_CONSUMOS_ENABLED=true. Desbloqueado (workers listos). Solo Sergio prende el flag.

Bloque A — Apelación / rechazo no terminal

  • #374 📅 2026-05-30 — Semántica del veredicto IA → la IA escribe observado (no rechazado); rechazado/autorizado quedan para decisión humana. Ajustar NoRechazadosScope para que los observados sigan visibles al técnico y admin. ✅ DEPLOYADO 2026-05-28 (commit 9ce3b6b en main, prod migrado, 59 IA-rechazados históricos reseteados a NULL, 83 tests verdes). (arquitectura, base del bloque — CERRADO)
  • #375 📅 2026-05-30 — Botón “Apelar” para el técnico en el detalle del consumo observado → cola de revisión humana, mensaje “Marcado por IA — no es definitivo, en revisión”. Notifica al admin. ✅ DEPLOYADO 2026-05-28 (commit 85e0d16 en main, prod migrado, ruta+columnas verificadas, 8 tests). (CERRADO)
  • #376 📅 2026-06-01 — Vista admin “Cola de revisión/apelaciones” — aprobar/rechazar-definitivo con motivo (reusar override_admin_*). Notifica al técnico el resultado (canal push+email ya existe). ✅ DEPLOYADO 2026-05-28 (commit 8015dd5 en main, sin migración, 6 tests). (CERRADO)
  • #377 📅 2026-06-02 — Tope de 3 re-subidas — tras 3 re-análisis IA fallidos consecutivos, el consumo se ancla a “requiere revisión humana” en vez de resetearse. Mensaje al técnico. ✅ DEPLOYADO 2026-05-28 (commit 83b1964 en main, migración corrida en prod: intentos_fallidos OK, max_intentos_revision=3; contador intentos_fallidos, auto-anclaje reusa #375/#376, policy bloquea re-subir, badge “En revisión humana”; 5 tests). Cierra el Bloque A completo (#374→#377).

Post 2-junio — secuencia recomendada

Bloque 0b — Lineamientos editables

  • #369 (ya existe) 📅 2026-06-04 — Resolver D1-D5 (recomendaciones arriba) + arrancar PR 1-4 de políticas. Base de “lineamientos modificables”.

Bloque C — Mapa de consumos (victoria visual, barato, no toca IA)

  • #380 📅 2026-06-06 — Pantalla mapa (Leaflet + OSM) con marcadores de consumos por viaje×usuario + popup/hover con detalle (thumb de foto, monto, concepto, estado IA, hora, comercio). Pintar sitio + oficina como referencias. Reusa coordenadas y los flags ya calculados.

Bloque B — Validación de ubicación (flag, no rechazo; sin hospedaje)

  • #378 📅 2026-06-09 — Coords de oficina/matriz en config + medir cobertura de GPS en sitios. (Hospedaje omitido — decisión 3.)
  • #379 📅 2026-06-10 — Validación de ubicación por corredor — válido si el consumo está cerca del sitio O de la oficina O sobre el segmento oficina↔sitio (distancia punto-a-segmento). Solo bandera, nunca auto-rechazo. Pasar anclas a la IA en el contexto (engancha con #369).

Bloque D — Captura offline (lo más caro — fase final)

  • #381 📅 2026-06-12 — Capturar GPS al momento de tomar la foto (no al submit) y persistirlo como ubicación original, arrastrándolo por toda la cola.
  • #382 📅 2026-06-16 — Subida offline = borrador local (decisión 4) — cola en IndexedDB; si no hay señal, guardar foto + GPS + datos y sincronizar al reabrir la app con señal. Preservar ubicación/timestamp originales. Notificar al técnico si tras sync queda observado.

✅ Decisiones tomadas (2026-05-28)

  1. Semántica del rechazo → observado. La IA nunca escribe rechazado terminal; emite observado. Solo un humano (admin/contabilidad) emite el rechazado/autorizado final. Se ajusta NoRechazadosScope para que los observados sigan visibles. (Bloque A)
  2. Tope de re-subidas = 3. Tras 3 re-análisis IA fallidos consecutivos, el consumo se ancla a “requiere revisión humana” (ya no resetea). (#377)
  3. Ubicación de hospedaje — OMITIDA por ahora. No se le pide al técnico marcarla (no abusar de su tiempo fuera de jornada). El corredor se valida solo oficina↔sitio + radio del sitio. Futuro: inferir hospedaje por GPS del celular + triangular con el GPS de los vehículos en Traccar (ver “Idea futura” abajo). (#378/#379 se simplifican)
  4. Offline = “borrador local”. Si no hay señal al subir, se guarda foto + GPS + datos en IndexedDB y se sincroniza al reabrir la app con señal (NO Background Sync real). Preserva ubicación/timestamp originales. (#382)

Idea futura — inferir hospedaje con Traccar

Electrosystems ya tiene los vehículos en Traccar (plataforma de rastreo GPS). En vez de pedirle al técnico que marque el hospedaje, se puede inferir cruzando — (a) clustering del GPS nocturno del celular del técnico + (b) posición del vehículo asignado al viaje en Traccar a esas horas. Donde el vehículo pernocta ≈ hospedaje. Esto da la tercera ancla del corredor sin fricción para el técnico. Requiere integración con la API de Traccar. Fuera del scope inicial — capturar como pendiente cuando se aborde el Bloque B.

✅ Decisiones D1-D5 de políticas — CERRADAS 2026-05-28

  1. Las 5 decisiones de amadeus-politicas-viaticos.md quedaron confirmadas por Sergio: D1 single-row con historial · D2 solo texto (sin JSON estructurado) · D3 cap diario a $20 USD · D4 sí prompt caching · D5 solo bandera, nunca bloqueo. #369 queda ready-to-build sin pendientes de decisión.

📅 Scoping del deadline (2026-06-02)

El milestone completo es ~10 días de trabajo; el 2-junio son ~3-4 días hábiles. No cabe completo. Cut acordado para el 2-junio = Bloque 0 (worker #371) + Bloque A completo (apelación #374/#375/#376/#377) + prender hook (#249). Eso entrega el flujo “IA observa → técnico apela → humano resuelve” funcionando end-to-end, que es el corazón del pedido. El resto (lineamientos editables #369, mapa #380, ubicación #378/#379, offline #381/#382) se secuencia después con sus propias fechas.

Archivos clave (referencia rápida)

  • Scope que oculta rechazados — app/Models/Scopes/NoRechazadosScope.php
  • Veredicto IA / puertas de rechazo — app/Jobs/AnalizarFotoConsumoJob.php:112-181
  • Prompt per-ticket — app/Services/Vision/AnalizadorTicketService.php:109-150
  • Flags de viaje (lejos_del_sitio, etc.) — app/Services/Vision/AuditorViajeService.php:151-286
  • Captura EXIF + GPS al submit — app/Http/Controllers/ConsumosController.php:128-182
  • Config — config/analisis.php (umbral_distancia_sitio_km, horario_laboral, ciudades_bbox)
  • Form de consumo (Vue) — resources/js/Components/Consumos/FormaConsumo.vue
  • Vista auditoría — resources/js/Pages/Admin/AuditoriaViaje.vue
  • Sitios GPS — database/migrations/2023_09_08_183722_crear_tabla_sitios.php (latitud/longitud nullable)
  • PWA — config/laravelpwa.php (configurada, sin offline upload)

Bitácora

2026-05-28 — análisis inicial del milestone

  • Pidió Sergio — modificar la auditoría automática de viáticos — rechazo apelable/no terminal, validación de ubicación (incluye corredor en carretera), pantalla de mapa con hover, captura offline preservando ubicación original, notificación de inválido; todo conforme a lineamientos editables. Análisis a fondo del código + fricciones + pushback + breakdown.
  • Hice — exploración completa del módulo consumos (subagente Explore) + lectura de los design docs existentes (validación-fotos, políticas) + verificación de 3 puntos en código (NoRechazadosScope oculta rechazados; sitios con GPS nullable; no hay coords de oficina ni captura de hospedaje). Documentado este design doc con 8 fricciones, 6 puntos de pushback, breakdown en 9 pendientes nuevos (#374-382) + 3 existentes (#369/#371/#249), y 6 decisiones pendientes.
  • Falta — respuestas de Sergio a las 6 decisiones; confirmar orden/fechas; entonces arrancar Bloque 0.

2026-05-28 — Sergio respondió decisiones + scoping del deadline

  • Decisiones: (1) sí, renombrar veredicto IA a observado; (2) tope de re-subidas = 3; (3) omitir captura de hospedaje (no abusar del tiempo del técnico fuera de jornada) — futuro: inferir con GPS celular + triangular con Traccar; (4) offline = borrador local; (6) orden a mi criterio, deadline 2-junio.
  • Pushback aceptado/registrado: el milestone completo (~10 días) no cabe en 5 días. Cut para 2-junio = Bloque 0 (worker #371) + Bloque A completo (apelación #374-377) + hook #249. Resto secuenciado post 2-junio (#369 lineamientos → #380 mapa → #378/#379 ubicación → #381/#382 offline).
  • Sigue abierto: D1-D5 de políticas (#369). Dejé recomendaciones por default para no bloquear.
  • Nueva idea futura capturada: inferir hospedaje cruzando GPS del celular con posición nocturna del vehículo en Traccar.

2026-05-28 (más tarde) — #377 implementado (tope de 3 re-subidas → ancla a revisión humana)

  • Cierra el Bloque A. DEPLOYADO 2026-05-28 (commit 83b1964 en main, migración corrida en prod, verificado intentos_fallidos OK + max_intentos_revision=3). Nuevo contador intentos_fallidos (sube solo en observación IA, no se resetea al re-subir) con tope configurable (default 3). Al alcanzarlo, auto-ancla reusando #375 (set apelado_at → cola admin de #376) y la policy bloquea re-subir. Badge “En revisión humana” en el listado. Notificación reusa ConsumoRechazadoPorIA (sin stack nuevo). 5 tests (52 del módulo verdes); pint+build OK. Detalle en bitácora de amadeus.md. Bloque A completo (#374→#377); queda #249 (prender el flag, solo Sergio) para que el flujo empiece a recibir consumos reales.

2026-05-28 (más tarde) — #376 DEPLOYADO (cola de revisión/apelaciones del admin)

  • Bloque A, tercera pieza. Evolucioné /autorizaciones en la cola de revisión: apeladas al tope + badge “⚖️ Apelado” + motivo del técnico; rechazo-con-motivo reusando override_admin_* (dirección por estatus); al resolver (aprobar/rechazar) se notifica al técnico por push+email (evento ConsumoResueltoPorAdmin + listener + ConsumoResueltoMail). Indicador inline del dashboard ahora distingue aprobado/rechazado/apelado. Sin migración. 6 tests nuevos (35 del módulo verdes); pint+build OK. Rama feature/376-cola-revision, sin commitear (espera OK de Sergio). 35 rojos de inventario preexistentes (ajenos). Detalle en bitácora de amadeus.md. Sigue #377 (tope de 3 re-subidas) para cerrar Bloque A.

2026-05-28 (más tarde) — #375 DEPLOYADO (botón Apelar → revisión humana)

  • Bloque A, segunda pieza. El técnico puede apelar una observación de la IA sin re-subir foto: registra apelado_at/apelacion_motivo en consumos_analisis, no cambia estatus (sigue observado), notifica al admin por email + push best-effort. Indicador ”⏳ En revisión humana” en el panel IA (técnico y admin). Re-subir foto limpia la apelación. Policy apelar impide doble apelación y apelar sobre override. 8 tests nuevos. Sergio autorizó commit+merge+deploy: commit 85e0d16 → main → deploy.sh (migración + queue:restart). Verificado read-only: columnas + ruta OK, 0 apeladas. 35 rojos de inventario preexistentes (verificado en main, ajenos). Detalle en bitácora de amadeus.md. Sigue #376.

2026-05-28 (más tarde) — #374 DEPLOYADO (IA observa, no rechaza)

  • Implementé y deployé el Bloque A base: la IA escribe consumos.estatus='observado' (no rechazado); analisis.estado interno se queda en rechazado (blast radius mínimo). Migración de enum + migración de datos (decisión de Sergio: los rechazos IA de 191/192 vuelven a estatus=null). Arreglé de paso un bug latente del NoRechazadosScope (orWhereNull sin agrupar). Copy user-facing suavizado (“En revisión” / “no es definitivo”). 83 tests verdes, pint + build OK. Sergio autorizó commit+push+merge+deploy: commit 9ce3b6b → main → deploy.sh en prod (2 migraciones + queue:restart). Verificado read-only: enum OK, estatus='rechazado'=0, 59 históricos reseteados a NULL (la estimación de 43 se quedó corta), forenses preservados. Detalle completo en bitácora de amadeus.md.

2026-05-28 (más tarde) — #371 cerrado (worker ya existía)

  • Arranqué el milestone por #371. Investigación read-only (repo + VM prod) reveló que el premise estaba desactualizado: prod ya tiene 8 workers queue:work vía supervisor en la cola default, y el hook dispatcha a default (el literal analizar-foto no existe en código — vino del batch manual). Presenté 3 opciones a Sergio (cola dedicada aislada / cerrar / workers oyen ambas); eligió cerrar — ya satisfecho. #249 desbloqueado (solo falta que Sergio prenda el flag). Friction #7 y Bloque 0 actualizados arriba. Detalle completo en bitácora de amadeus.md.

2026-05-28 (más tarde) — D1-D5 de políticas cerradas

  • Sergio confirmó las 5 recomendaciones tal cual: single-row con historial / solo texto / cap $20 USD / sí prompt caching / solo bandera nunca bloqueo. amadeus-politicas-viaticos.md queda ready-to-build sin decisiones pendientes. #369 ya puede arrancar (Bloque 0b, tentativo 4-jun).