Saltar a contenido

Contrato de normalización

Todo normalizador escribe el mismo objeto, sin importar la fuente. Este contrato es el verdadero entregable del eslabón de ingesta: aguas abajo nada debe saber de dónde vino un registro.

Campos de identidad

Campo Notas
fuente_id FK a fuentes
id_nativo ID en la fuente de origen. Junto con fuente_id es la clave de dedup
url_canonica Enlace para el reporte
hash Hash del contenido normalizado, para detectar modificaciones
capturado_en Timestamp de ingesta

Campos descriptivos

titulo, texto_crudo, organismo, pais, idioma, tipo_aviso, moneda, valor_estimado.

Campos de fecha

Campo Notas
fecha_publicacion_utc Normalizada
fecha_publicacion_original String tal como vino
fecha_limite_utc Nullable
fecha_limite_original String tal como vino

Zonas horarias

Los portales publican fecha local sin zona horaria. Guardar el string original además del UTC: un cierre "a las 10:00" en Ginebra no es lo mismo que en Santo Domingo, y esa hora decide si ofertamos o no.

Campos de decisión

Los que realmente importan. Ver Capacidad y consecuencias.

Campo Tipo Por qué
categoria_adquisicion enum bienes / obras / servicios_no_consultoria / servicios_consultoria. El filtro más potente y es gratis
metodo varchar ICB, RFB, QCBS, CQS, SSS, RFQ…
escalon enum Ver escalera de precursores
id_proyecto_padre FK nullable El campo que más predice si ganamos. Es el join entre carriles
exige_fabricante_o_representante bool nullable Descarte duro HD-FAB
umbral_facturacion numeric nullable Descarte duro HD-FACT
garantia_oferta numeric nullable Descarte duro HD-GAR
elegibilidad_pais varchar[] nullable Descarte duro HD-ELEG
dias_ventana int nullable fecha_limite - fecha_publicacion
incumbente_detectado varchar nullable Fabricante nombrado en el pliego
codigos jsonb {cpv, unspsc, psc, naics}
contacto jsonb Nombre, correo, teléfono cuando la fuente lo trae

Los campos de decisión pueden ser null en la ingesta

Varios solo se conocen después de extraer el pliego. Es correcto que queden null en la primera pasada y se completen en la etapa de extracción. Lo que no es correcto es tratar null como "no aplica" en los filtros duros: null significa desconocido y el ítem sigue vivo.

Normalizadores como funciones puras

def normalizar(payload: dict, fuente: Fuente) -> RegistroNormalizado:
    ...

Entra un payload, sale un registro. Sin efectos secundarios, sin acceso a red, sin acceso a base. Esto los hace testeables.

Regla no negociable

Cada normalizador tiene un test contra un payload congelado guardado en el repo.

Es el único modo de fallo real de un normalizador: que la fuente cambie un campo sin avisar. El test corre en CI y falla cuando la forma cambia. Sin esto, nos enteramos porque el reporte salió vacío tres semanas.

Payloads congelados en tests/fixtures/<fuente>/<fecha>.json. Cuando una fuente cambia, se agrega un fixture nuevo, no se reemplaza el viejo: el histórico guardado en crudo sigue teniendo la forma antigua y el normalizador debe seguir manejándola.