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 conconsumos) con veredicto IA + OCR + override admin + auditoría de costo. - Tabla
auditorias_viajecon resumen ejecutivo IA +flags_agregados. AnalizarFotoConsumoJob→AnalizadorTicketService(Claude Sonnet 4.6 vision, tool-usedictaminar_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) ycoordenadas_ticket(EXIF de la foto). Sitios tienenlatitud/longitud(nullable). - Flag
lejos_del_sitio(>30 km del sitio del viaje) ya calculado enAuditorViajeService. - 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) — tablapoliticas_viaticoseditable 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
-
NoRechazadosScopeoculta los consumos rechazados (BLOQUEANTE para “apelable”).app/Models/Scopes/NoRechazadosScope.phpfiltraestatus != 'rechazado'en todas las queries por default. Es decir: cuando la IA marcarechazado, 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 deestatus). -
“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. -
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.
-
Validación de ubicación es solo punto-radio, no corredor.
lejos_del_sitiomide 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. -
coordenadasse 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),coordenadasregistra 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. -
Sitios con GPS nullable.
lejos_del_sitiosolo funciona si el sitio tienelatitud/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. -
No hay worker de queue en prod (RESUELTO 2026-05-28 — el premise era incorrecto. Sí hay worker: 8 procesos#371).queue:workvía supervisor (laravel-worker.conf) en la coladefault. El hook (ConsumosController::dispatchAnalisisIA) dispatcha adefault(sinonQueue), no aanalizar-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). -
Prompts hardcodeados. Los lineamientos hoy viven embebidos en
AnalizadorTicketService.php:109yResumidorAuditoriaService.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
rechazadoterminal. Que su veredicto negativo seaobservado(omarcado_ia) — “esta captura tiene una observación, revísala”. Solo un humano (admin/contabilidad) emite el estado finalrechazado/autorizado. Esto (a) hace la apelación natural, (b) elimina de raíz el problema delNoRechazadosScope, (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 systemdCERRADO 2026-05-28. Premise desactualizado: prod ya tiene 8 workersqueue:work(supervisorlaravel-worker.conf) en la coladefault, y el hook dispatcha adefault(no aanalizar-foto, literal inexistente en código). No requería construir nada. Detalle en bitácora deamadeus.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(norechazado);rechazado/autorizadoquedan para decisión humana. AjustarNoRechazadosScopepara que los observados sigan visibles al técnico y admin. ✅ DEPLOYADO 2026-05-28 (commit9ce3b6ben 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
85e0d16en 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 (commit8015dd5en 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
83b1964en main, migración corrida en prod:intentos_fallidosOK,max_intentos_revision=3; contadorintentos_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
coordenadasy 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)
- Semántica del rechazo →
observado. La IA nunca escriberechazadoterminal; emiteobservado. Solo un humano (admin/contabilidad) emite elrechazado/autorizadofinal. Se ajustaNoRechazadosScopepara que los observados sigan visibles. (Bloque A) - 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)
- 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)
- 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
- Las 5 decisiones de
amadeus-politicas-viaticos.mdquedaron 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
83b1964en main, migración corrida en prod, verificadointentos_fallidosOK +max_intentos_revision=3). Nuevo contadorintentos_fallidos(sube solo en observación IA, no se resetea al re-subir) con tope configurable (default 3). Al alcanzarlo, auto-ancla reusando #375 (setapelado_at→ cola admin de #376) y la policy bloquea re-subir. Badge “En revisión humana” en el listado. Notificación reusaConsumoRechazadoPorIA(sin stack nuevo). 5 tests (52 del módulo verdes); pint+build OK. Detalle en bitácora deamadeus.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é
/autorizacionesen la cola de revisión: apeladas al tope + badge “⚖️ Apelado” + motivo del técnico; rechazo-con-motivo reusandooverride_admin_*(dirección porestatus); al resolver (aprobar/rechazar) se notifica al técnico por push+email (eventoConsumoResueltoPorAdmin+ 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. Ramafeature/376-cola-revision, sin commitear (espera OK de Sergio). 35 rojos de inventario preexistentes (ajenos). Detalle en bitácora deamadeus.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_motivoenconsumos_analisis, no cambiaestatus(sigueobservado), 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. Policyapelarimpide doble apelación y apelar sobre override. 8 tests nuevos. Sergio autorizó commit+merge+deploy: commit85e0d16→ main →deploy.sh(migración + queue:restart). Verificado read-only: columnas + ruta OK, 0 apeladas. 35 rojos de inventario preexistentes (verificado enmain, ajenos). Detalle en bitácora deamadeus.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'(norechazado);analisis.estadointerno se queda enrechazado(blast radius mínimo). Migración de enum + migración de datos (decisión de Sergio: los rechazos IA de 191/192 vuelven aestatus=null). Arreglé de paso un bug latente delNoRechazadosScope(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: commit9ce3b6b→ main →deploy.shen 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 deamadeus.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:workvía supervisor en la coladefault, y el hook dispatcha adefault(el literalanalizar-fotono 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 deamadeus.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.mdqueda ready-to-build sin decisiones pendientes. #369 ya puede arrancar (Bloque 0b, tentativo 4-jun).