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.