Saltar a contenido

Autor: Iker Acevedo

Fecha creacion: 2026-10-18
Estado: desarrollo


Modelos de dominio y datos

PedidosInter.MotivoNovedadInter (campo nuevo)

Único campo que esta lambda escribe en PedidosInter (con $set, nunca reemplazando el documento).

"REHUSADO / SE NEGO A RECIBIR NOTIFICACION - https://www.google.com/maps?q=10.868765635,-74.78554886"
  • Es texto para mostrar; no se parsea. Sin geo queda solo el motivo.
  • Se conserva cuando la guía sale de novedad (es lo último que reportó Inter).
  • La colección PedidosInter mezcla transportadoras y tiene más de 100 campos planos; por eso el detalle no va ahí.
  • Numeropreenvio es NumberLong; ws-inter usa la guía como texto.

Colección SeguimientoNovedadesInter

Un documento por guía. El nombre evita confundirse con NovedadesLogigho, que son novedades financieras.

{
  "Guia": "240063616541",                // texto, como lo usa ws-inter
  "Numeropreenvio": 240063616541,        // NumberLong: clave de búsqueda (índice único)
  "PedidoId": ObjectId("…"), "Idtienda": "…", "Tienda": "…",
  "EstadoSeguimiento": "Activo",         // Activo | Cerrado (salió de novedad: no se consulta más)
  "Prioridad": "Alta",                   // Alta | Normal
  "HuellaPedido": "Intento de entrega|No logramos hacer la entrega",   // Estado|Descripcion Asociada al solicitar
  "Solicitud": {
    "Estado": "Respondida",              // Enviada | Respondida | Fallida (sin callback o error del proveedor) | NoEncontrada (encontrado:false)
    "Ref": "…", "JobId": "…", "FechaEnvio": ISODate, "FechaRespuesta": ISODate,
    "Intentos": 0,                       // envíos seguidos sin respuesta
    "UltimoError": null
  },
  "UltimoMotivo": {
    "Motivo": "REHUSADO / SE NEGO A RECIBIR NOTIFICACION",
    "EstadoInter": "En Proceso de Devolución",
    "Fecha": ISODate("2026-10-17T17:18:59Z"),       // UTC
    "FechaRaw": "17/10/2026 12:18:59 p. m.",        // tal como la muestra Inter
    "Ciudad": "MALAMBO\\ATLA\\COL", "Mensajero": "…", "Observacion": "Descargado desde APP",
    "Geo": { "Latitud": 10.868765635, "Longitud": -74.78554886, "Url": "https://www.google.com/maps?q=…" }
  },
  "HuellaMotivos": "a3f1…",              // hash de TODOS los motivos: si no cambia, el motivo no cambió
  "Historial": [ { "Motivo": { … }, "Huella": "…", "DetectadoEn": ISODate } ],   // solo cambios, tope 50
  "UltimaConsultaEn": ISODate,           // consultado_en de ws-inter: ignora callbacks viejos
  "UltimaRespuesta": { … },              // solo guia, consultado_en, encontrado, estado_actual, ultimo_motivo, flujo, motivos (+ job_id, ref, status); sin datos personales; se sobrescribe
  "ConsultasTotales": 3, "ConsultasSinCambio": 2,
  "FechaCreacion": ISODate, "FechaActualizacion": ISODate, "FechaCierre": null
}

Todas las fechas son BSON Date en UTC.

Índices (comandos en el runbook, sección A2)

Índice Para qué
{ Numeropreenvio: 1 } único Una guía = un seguimiento; evita duplicados por envíos simultáneos
{ "Solicitud.Ref": 1 } Encontrar el seguimiento por la referencia que devuelve ws-inter
{ EstadoSeguimiento: 1 } Cerrar los seguimientos activos que salieron de novedad

Escrituras

Todas son parciales ($set, $inc, $push, $setOnInsert): el envío (Step) y el callback (webhook) pueden tocar el mismo seguimiento casi a la vez sin pisarse. Nunca se reemplaza el documento.

Clases (Dominio/)

Clase Responsabilidad
ReglaSeleccionNovedad ¿La guía está en novedad? ¿Qué prioridad tiene?
PlanificadorEnvios Decide Enviar / Saltar / MarcarFallida (tabla del caso de uso)
ExtractorMotivoNovedad Del JSON de ws-inter saca motivo, geo, huella y texto
SeguimientoNovedad Documento de la colección anterior
PedidoInterNovedad Los 6 campos de PedidosInter que se leen

Campos nuevos de SeguimientoNovedadesInter (historia 027, spec 012)

Los escribe solo la función Distancias, con $set parcial de estos campos (cuatro en la v1 de la historia; desde la v8 son cinco con DireccionMensajero) (nunca reescribe el documento: el callback de ws-inter puede estar tocando el mismo seguimiento). Reglas en Reglas de la distancia. Observado: Dominio/Entidades/Distancias.cs, DistanciasRepositorio.cs.

{
  "DireccionPedido": {                      // geocodificación de la dirección del pedido (una por guía)
    "HuellaDireccion": "sha256…",           // regla + dirección + ciudad + departamento normalizados
    "Latitud": 4.6, "Longitud": -74.08,     // ejemplo inventado
    "TipoUbicacion": "ROOFTOP",             // ROOFTOP | RANGE_INTERPOLATED | GEOMETRIC_CENTER | APPROXIMATE
    "CoincidenciaParcial": false,
    "Localidades": ["Bogotá"],              // opcional: locality / administrative_area_level_2 del resultado
    "ConfirmacionDireccion": "Confirmada",  // opcional: Confirmada | NoConfirmada | ausente
    "DireccionGoogle": "…",                 // v4: formatted_address de la consulta principal (máx. 300). Dirección: dato personal
    "MotivoValidacion": null,               // v4: Aproximada | CiudadDistinta | SinCiudad | NoEncontrada | NoConfirmada | DistanciaInverosimil (v5); null = aceptada
    "DireccionGoogleConfirmacion": "…",     // v4: formatted_address de la segunda geocodificación (solo rojos)
    "DistanciaConfirmacionMetros": 12,      // v4: Haversine entre las dos geocodificaciones
    "Estado": "Encontrada",                 // Encontrada | NoEncontrada | Error
    "GeocodificadaEn": ISODate
  },
  "Distancias": [                           // una por intento; tope de 50, igual que Historial
    {
      "HuellaGeo": "4.610000,-74.070000",   // "lat,lng" a 6 decimales: llave de la caché de rutas
      "FechaIntento": ISODate,
      "LatitudNovedad": 4.61, "LongitudNovedad": -74.07,
      "Metros": 640,                        // v6: MÍNIMO entre a pie y carro (los que existan)
      "MetrosLineaRecta": 410,
      "MetrosCaminando": 640, "MetrosCarro": 880,   // v6, opcionales; MetrosCarro = el menor entre ida y vuelta
      "ModoMinimoEvaluado": true,           // v6, opcional: ya se evaluó el mínimo (evita quedar pendiente para siempre)
      "Modo": "WALK",                       // WALK | DRIVE: de dónde salió el mínimo (v5/v6; las guardadas por la v4 dicen DRIVE)
      "Color": "rojo",                      // verde | amarillo | rojo | null
      "Estado": "Calculada",                // Calculada | SinGeo | DireccionNoEncontrada | BajaConfianza (también > 50 km, con metros) | Error
      "UrlRuta": "https://www.google.com/maps/dir/?api=1&origin=…&destination=…&travelmode=walking",   // walking o driving según Modo
      "CalculadaEn": ISODate
    }
  ],
  "DistanciaUltima": { /* copia de la entrada que corresponde a UltimoMotivo */ },
  "HuellaDistancias": "<HuellaMotivos>|v4|walk1",  // versión de la regla (geocodificación|ruta) con la que se calculó
  "DireccionMensajero": {                   // v8: geocodificación INVERSA del punto del último intento (una vez por guía y por punto)
    "Texto": "…",                           // formatted_address (máx. 300); ausente si SinResultado o Error. Dato personal
    "HuellaGeo": "4.610000,-74.070000",     // lat,lng a 6 decimales del punto geocodificado
    "FechaCalculo": ISODate,
    "Estado": "Calculada"                   // Calculada | SinResultado | Error
  }
}
Campo Significado
DireccionPedido Resultado de geocodificar PedidosInter.Direccion + Ciudad. Es dato personal derivado: no se loguea ni sale en los Excel. Solo se repite si cambia HuellaDireccion
DireccionPedido.Localidades Opcional (BsonIgnoreIfNull). Con ellas se valida que la ciudad del resultado sea la del pedido
DireccionPedido.ConfirmacionDireccion Opcional. Resultado de la segunda geocodificación, solo para guías con ruta roja. No se repite mientras la dirección no cambie
Distancias[] Un resultado por intento (cada entrada de Historial más el último motivo). Sin coordenada: Estado = SinGeo
DistanciaUltima Copia de la distancia del intento que corresponde a UltimoMotivo; es lo que lee la vista (columna) y el reporte 1
HuellaDistancias "{HuellaMotivos}\|v4\|walk1". Si es distinta de esa expresión, la guía es pendiente y se recalcula. Si no se pudo completar (Google falló, falta el pedido) no se fija y se reintenta
DireccionMensajero v8. Dirección en texto del punto del último intento. No sube ninguna versión: es pendiente aparte (ReglaDireccionMensajero.Necesita). Se escribe con un $set de un solo campo. SinResultado no se reintenta con la misma HuellaGeo; Error sí. Nunca se loguea
Distancias[].MetrosCaminando / MetrosCarro / ModoMinimoEvaluado v6. Metros es el mínimo de los dos modos; Modo dice cuál ganó (empate: WALK)

origin del enlace es la dirección del pedido; destination, el punto del mensajero. Valores con InvariantCulture (punto decimal).

Observado: Dominio/Entidades/Distancias.cs (DireccionPedido, DireccionMensajero, DistanciaIntento) y Dominio/Constantes/Constantes.cs (EstadoDireccionMensajero, MotivoValidacionDireccion, ModoRuta).

Colección ReportesNovedadesInter (nueva)

La escribe solo la función Reportes: un documento por corrida. De la última Exitosa sale la frontera de «Nuevas».

{
  "_id": ObjectId("…"),
  "FechaInicio": ISODate, "FechaFin": ISODate,
  "Origen": "programado",                 // programado | slack
  "UsuarioSlack": "U0000EJEMPLO",         // solo si Origen = slack (id inventado)
  "Estado": "Exitosa",                    // Exitosa | Fallida
  "FilasReporte1": { "Nuevas": 3, "Acumulado": 40 },
  "FilasReporte2": { "Nuevas": 7, "Acumulado": 120 },
  "Error": null,                          // causa corta si Fallida
  "BaseNuevas": "normal"                  // v6, opcional: normal | completo | desde AAAA-MM-DD (qué base usó «Nuevas»)
}

Colección CandadosNovedadesInter (nueva)

Candado entre corridas de Distancias: { "_id": "distancias", "Dueno": "<id de la corrida>", "ExpiraEn": ISODate }. Se toma con un upsert atómico y se libera al terminar; vence a los 15 min por si la función muere sin liberarlo.

Índices nuevos (mejora M-256)

Los crea Iker en prod (en PreProd puede la IA). Comandos y orden en el runbook.

Colección Índice Para qué
CandadosNovedadesInter { ExpiraEn: 1 } con expireAfterSeconds: 0 TTL: limpia candados vencidos (el código ya comprueba el vencimiento, así que funciona aunque tarde)
ReportesNovedadesInter { Estado: 1, FechaInicio: -1 } Hallar la última corrida Exitosa
SeguimientoNovedadesInter { HuellaMotivos: 1 } Condicional: solo si el explain lo pide. Hoy la selección de pendientes lee una proyección mínima y compara en memoria

Clases nuevas (Dominio/)

Clase Responsabilidad
ReglaColorDistancia Metros → verde / amarillo / rojo (250 y 500)
ReglaConfianzaGeocodificacion ROOFTOP/RANGE_INTERPOLATED + ciudad igual a la del pedido
ReglaConfirmacionDireccion Segunda geocodificación a ≤ 150 m
ValidadorCoordenada, Haversine Defensa de coordenadas y línea recta de referencia
SanitizadorDireccion, VersionReglaGeocodificacion, TextoGeografico Limpieza de la dirección, versión de la regla (v3), huellas y enlaces
NormalizadorTexto, ListaMotivosExcluidos Clave de comparación de motivos y los 8 excluidos
FiltroReporte1, FiltroReporte2 Qué guías entran a cada reporte
DireccionPedido, DistanciaIntento, CorridaReporte Entidades de las secciones anteriores

Datos que solo lee Reportes (v8)

Fuente Campos Uso
AuditoriaCambioEntregaInter (colección ajena, solo lectura) NumeroGuia (texto de 12 dígitos; índice único ux_numeroGuia, observado en prod 2026-11-07: 9 379 documentos) Reporte 2: descartar toda guía con registro (cambio a oficina o paso por oficina). Consulta $in por lotes de 1 000, proyección de ese único campo, solo para las candidatas
SeguimientoNovedadesInter.UltimaRespuesta.gestion_app[] Solo orden y s3_key (proyección de inclusión; no se lee la respuesta completa) Columnas «Imagen novedad N» (hasta 10; claves gestion-app/<guía>/<fecha>/<guía>-<n>.jpg)
SeguimientoNovedadesInter.UltimaRespuesta estado_actual y motivos[].fecha «Estado Inter» y «Días desde la primera novedad»
PedidosInter IntentosEntrega, Estado (Estado Logigho), Direccion, FechaCarga (texto yyyy-MM-dd HH:mm:ss en hora de Bogotá) Filtros y columnas de los reportes

FilaReporte (record en Distancias.cs) junta el seguimiento con esos datos: IntentosEntrega, EstadoLogigho, DireccionPedido, FechaCreacionPedidoUtc, PrimeraNovedadUtc, EstadoActualInter e ImagenesS3. Su propiedad EstadoInter es UltimaRespuesta.estado_actual y, si viene vacío, UltimoMotivo.EstadoInter.

Clases nuevas de la v4 a la v8 (Dominio/)

Clase Responsabilidad
ReglaDistanciaMinima Mínimo entre a pie y carro; cuándo pedir el carro (a pie > 250 m); cuándo una guía necesita evaluar el mínimo
ReglaDireccionMensajero Cuándo falta la dirección en texto del mensajero (marca independiente de HuellaDistancias)
VersionReglaRuta walk1: entra solo en HuellaDistancias
ClasificadorAgenciaOficina Reporte 1: separa «agencia u oficina» de «entrega a dirección» (v7)
FiltroEstadoReporte1, ListaEstadosInter Filtro de estado del reporte 1 (v8) y lista de Estados Inter del reporte 2
FiltroReporte1PorRevisar Hoja «Por revisar (más de 50 km)»
FirmaEvidencia, EnlacesEvidencia Firma HMAC-SHA256 y validación de los enlaces de imágenes (ADR-005)
ArgumentoReporte, BaseNuevas, ResumenAutomaticoReporte Argumento del comando (completo, desde AAAA-MM-DD) y resumen del mensaje de Slack