crowdata / app /reports /builders.py
YOSOYYONOSOYOTRO's picture
Upload folder using huggingface_hub (part 2)
4223796 verified
Raw
History Blame Contribute Delete
26.2 kB
"""
Report Builders — Transform raw scraper data into structured report models.
Each builder handles a specific report type.
"""
import logging
from typing import Any, Optional
from datetime import datetime, timezone
from app.reports.schemas import (
ScoreHistorial, ScoreCrediticio,
PersonReport, CompanyReport, Identificacion, Contacto, Domicilio,
DatosFiscales, DatosFinancieros, DatosSocietarios, DatosPatrimoniales,
DatosJudiciales, PublicacionBO, BcraHistorial, ChequeRechazado,
CausaJudicial, Vehiculo, InfraccionTransito, MarcaINPI, Vinculo,
DatosPrevisionales, DatosAcademicos, TituloAcademico, IndicadorRiesgo,
Inmueble, Inhibicion, DatosRegistroCivil, GroupReport, VehicleReport,
PropertyReport, ReportMeta, ActividadFiscal, CompanyIdentificacion, RedSocial,
DatosIGJ, ViasSalud, ContratoEstado, MatriculaProfesional,
DatosBilleterasVirtuales, BilleteraVirtual, TimelineEvent, DeudorAlimentario, Monotributo
)
from app.reports.schemas import (
CompanyReport, VehicleReport, PropertyReport, GroupReport
)
logger = logging.getLogger(__name__)
# ─── Helper Functions ───
def _safe_get(data: dict, key: str, default=None):
"""Obtiene valor de dict de forma segura."""
if not isinstance(data, dict):
return default
return data.get(key, default)
def _safe_list(data: dict, key: str) -> list:
"""Obtiene lista de forma segura."""
val = _safe_get(data, key)
return val if isinstance(val, list) else []
def _safe_dict(data: dict, key: str) -> dict:
val = _safe_get(data, key)
return val if isinstance(val, dict) else {}
# ─── Builders ───
def build_identificacion(data: dict, cuit: str) -> Identificacion:
"""Construye sección de identificación."""
return Identificacion(
cuit=cuit,
tipo_clave=_safe_get(data, "tipo_clave"),
tipo_documento=_safe_get(data, "tipo_documento"),
numero_documento=_safe_get(data, "numero_documento"),
nombres=_safe_get(data, "nombres"),
apellido=_safe_get(data, "apellido"),
nombres_completo=_safe_get(data, "nombres_completo"),
fecha_nacimiento=_safe_get(data, "fecha_nacimiento"),
fecha_fallecimiento=_safe_get(data, "fecha_fallecimiento"),
sexo=_safe_get(data, "sexo"),
)
def build_contacto(data: dict) -> Contacto:
"""Construye sección de contacto."""
# Intentar desde redes sociales
redes = _safe_list(data.get("redes_sociales", {}), "redes_sociales") if isinstance(data.get("redes_sociales"), dict) else _safe_list(data, "redes_sociales")
emails = []
phones = []
for r in redes:
if isinstance(r, dict):
if r.get("tipo") == "email":
emails.append(r.get("valor", ""))
elif r.get("tipo") == "telefono":
phones.append(r.get("valor", ""))
return Contacto(
emails=emails,
telefonos=phones,
redes_sociales=_safe_list(data, "redes_sociales"),
)
def build_domicilios(data: dict) -> list[Domicilio]:
"""Construye lista de domicilios."""
domicilios_raw = _safe_list(data, "domicilios")
domicilios = []
for d in domicilios_raw:
if isinstance(d, dict):
domicilios.append(Domicilio(
calle=_safe_get(d, "calle"),
numero=_safe_get(d, "numero"),
piso=_safe_get(d, "piso"),
depto=_safe_get(d, "depto"),
localidad=_safe_get(d, "localidad"),
provincia=_safe_get(d, "provincia"),
codigo_postal=_safe_get(d, "codigo_postal"),
tipo=_safe_get(d, "tipo"),
))
return domicilios
def build_datos_fiscales(arca_data: dict, cuit: str, actividades: list[ActividadFiscal] = None) -> DatosFiscales:
"""Construye datos fiscales desde ARCA/AFIP."""
if not isinstance(arca_data, dict):
arca_data = {}
domicilios = build_domicilios(arca_data)
domicilio_fiscal_raw = _safe_get(arca_data, "domicilio_fiscal")
domicilio_fiscal = Domicilio(**domicilio_fiscal_raw) if isinstance(domicilio_fiscal_raw, dict) and domicilio_fiscal_raw else None
if domicilio_fiscal and not domicilios:
domicilios = [domicilio_fiscal]
monotributo_raw = _safe_get(arca_data, "monotributo")
monotributo_obj = Monotributo(**monotributo_raw) if isinstance(monotributo_raw, dict) else None
return DatosFiscales(
cuit=arca_data.get("cuit", cuit),
tipo_clave=_safe_get(arca_data, "tipo_clave"),
tipo_documento=_safe_get(arca_data, "tipo_documento"),
numero_documento=_safe_get(arca_data, "numero_documento"),
nombres=_safe_get(arca_data, "nombres"),
apellido=_safe_get(arca_data, "apellido"),
nombres_completo=_safe_get(arca_data, "nombres_completo"),
condicion_iva=_safe_get(arca_data, "condicion_iva"),
estado_afip=_safe_get(arca_data, "estado_afip"),
fecha_inicio_actividad=_safe_get(arca_data, "fecha_inicio_actividad"),
fecha_nacimiento=_safe_get(arca_data, "fecha_nacimiento"),
fecha_contrato_social=_safe_get(arca_data, "fecha_contrato_social"),
fecha_fallecimiento=_safe_get(arca_data, "fecha_fallecimiento"),
forma_juridica=_safe_get(arca_data, "forma_juridica"),
mes_cierre=_safe_get(arca_data, "mes_cierre"),
periodo_actividad_principal=_safe_get(arca_data, "periodo_actividad_principal"),
actividades=actividades or [],
domicilios=domicilios,
domicilio_fiscal=domicilio_fiscal,
monotributo=monotributo_obj,
)
def build_datos_financieros(bcra_data: dict, historial_financiero_anterior: dict = None) -> DatosFinancieros:
"""Construye datos financieros desde BCRA + fallback histórico."""
if not isinstance(bcra_data, dict):
bcra_data = {}
deudas = []
for ent in _safe_list(bcra_data, "entidades"):
if isinstance(ent, dict):
deudas.append(BcraHistorial(
entidad=ent.get("entidad"),
situacion=ent.get("situacion"),
fecha=ent.get("fecha"),
monto=ent.get("monto"),
tipo=ent.get("tipo"),
))
cheques = []
for ch in _safe_list(bcra_data, "cheques_rechazados"):
if isinstance(ch, dict):
cheques.append(ChequeRechazado(
fecha=ch.get("fecha"),
banco=ch.get("banco"),
sucursal=ch.get("sucursal"),
monto=ch.get("monto"),
motivo=ch.get("motivo"),
))
# Fallback histórico si BCRA falló
if not _safe_get(bcra_data, "entidades") and historial_financiero_anterior:
hfa = historial_financiero_anterior
situacion = hfa.get("bcra_situacion_actual")
if situacion:
bcra_data = {
"bcra_situacion_actual": situacion,
"entidades": hfa.get("entidades", []),
"cheques_rechazados": hfa.get("cheques_rechazados", []),
}
situacion = _safe_get(bcra_data, "bcra_situacion_actual")
if situacion is None:
situacion = 1 # Normal por defecto
return DatosFinancieros(
bcra_situacion_actual=situacion,
bcra_situacion_descripcion=SITUACIONES_BCRA.get(situacion, "Desconocida"),
bcra_historial=_safe_list(bcra_data, "entidades"),
bcra_total_deuda_miles=_safe_get(bcra_data, "bcra_total_deuda_miles", 0.0),
bcra_dias_atraso_max=_safe_get(bcra_data, "bcra_dias_atraso_max", 0),
cheques_rechazados=_safe_list(bcra_data, "cheques_rechazados"),
tiene_deuda=_safe_get(bcra_data, "tiene_deuda", False),
)
def build_datos_societarios(igj_data: dict, inpi_data: dict, compras_data: dict) -> DatosSocietarios:
"""Construye datos societarios."""
return DatosSocietarios(
igj=_safe_dict(igj_data, "igj"),
inpi=_safe_dict(inpi_data, "inpi"),
compras_estatales=_safe_dict(compras_data, "compras_estatales"),
)
def build_datos_patrimoniales(
arba_auto_data: dict, arba_catastro_data: dict,
dnrpa_data: dict, infracciones_data: dict,
carto_arba_data: dict, sinais_data: dict
) -> DatosPatrimoniales:
"""Construye datos patrimoniales."""
inmuebles = []
for inm in _safe_list(arba_catastro_data, "inmuebles"):
if isinstance(inm, dict):
inmuebles.append(Inmueble(
partida=inm.get("partida"),
nomenclatura=inm.get("nomenclatura"),
direccion=inm.get("direccion"),
superficie=inm.get("superficie"),
valuacion=inm.get("valuacion"),
tipo=inm.get("tipo"),
))
vehiculos = []
for v in _safe_list(dnrpa_data, "vehiculos"):
if isinstance(v, dict):
vehiculos.append(Vehiculo(
dominio=v.get("dominio"),
marca=v.get("marca"),
modelo=v.get("modelo"),
anio=v.get("anio"),
tipo=v.get("tipo"),
registro=v.get("registro"),
titular=v.get("titular"),
estado=v.get("estado"),
))
infracciones = []
for inf in _safe_list(infracciones_data, "infracciones"):
if isinstance(inf, dict):
infracciones.append(InfraccionTransito(
fecha=inf.get("fecha"),
tipo=inf.get("tipo"),
jurisdiccion=inf.get("jurisdiccion"),
monto=inf.get("monto"),
estado=inf.get("estado"),
))
return DatosPatrimoniales(
inmuebles=inmuebles,
vehiculos=vehiculos,
infracciones_transito=infracciones,
)
def build_datos_judiciales(
pj_data: dict, pj_prov_data: dict,
bo_data: dict, bo_prov_data: dict,
inhibiciones_data: dict, juba_data: dict,
siscop_data: dict
) -> DatosJudiciales:
"""Construye datos judiciales."""
causas = []
for c in _safe_list(pj_data, "causas"):
if isinstance(c, dict):
causas.append(CausaJudicial(
numero=c.get("numero"),
fuero=c.get("fuero"),
caratula=c.get("caratula"),
estado=c.get("estado"),
fecha=c.get("fecha"),
rol=c.get("rol"),
))
publicaciones = []
for p in _safe_list(bo_data, "publicaciones"):
if isinstance(p, dict):
publicaciones.append(PublicacionBO(
fecha=p.get("fecha"),
seccion=p.get("seccion"),
texto=p.get("texto"),
url=p.get("url"),
))
inhibiciones = []
for inh in _safe_list(inhibiciones_data, "inhibiciones"):
if isinstance(inh, dict):
inhibiciones.append(Inhibicion(
numero=inh.get("numero"),
juzgado=inh.get("juzgado"),
fecha=inh.get("fecha"),
monto=inh.get("monto"),
estado=inh.get("estado"),
))
return DatosJudiciales(
causas=causas,
publicaciones_bo=publicaciones,
inhibiciones=inhibiciones,
)
def build_vinculos(igj_data: dict, domicilios: list[Domicilio]) -> list[Vinculo]:
"""Detecta vínculos por domicilio compartido (IGJ)."""
vinculos = []
if not igj_data or not domicilios:
return vinculos
doms_persona = set()
for d in domicilios:
parts = [d.calle, d.numero, d.localidad]
normalized = " ".join(p.lower().strip() for p in parts if p).strip()
if normalized and len(normalized) > 5:
doms_persona.add(normalized)
for socio in igj_data.get("socios_directivos", []):
if not isinstance(socio, dict):
continue
dom_socio = socio.get("domicilio", "")
if dom_socio:
dom_norm = " ".join(dom_socio.lower().split()[:5])
for dom_p in doms_persona:
if dom_norm and dom_p and (dom_norm in dom_p or dom_p in dom_norm):
vinculos.append(Vinculo(
cuit=socio.get("cuil"),
nombre=socio.get("nombre", ""),
tipo="Domiciliario",
detalle=f"Domicilio compartido: {dom_socio}"
))
break
return vinculos
def build_riesgo_indicadores(data: dict, all_results: dict) -> list[IndicadorRiesgo]:
"""Construye indicadores de riesgo."""
indicadores = []
# BCRA
financiero = _safe_dict(data, "financiero")
situacion = financiero.get("bcra_situacion_actual")
if situacion and situacion > 1:
indicadores.append(IndicadorRiesgo(
codigo="BCRA_SITUACION",
nivel="Alto" if situacion >= 4 else "Medio",
descripcion=f"Situación BCRA: {SITUACIONES_BCRA.get(situacion, 'Desconocida')}",
detalle=f"Situación actual: {situacion}",
))
# Judicial
judicial = _safe_dict(data, "judicial")
causas = _safe_list(judicial, "causas")
if len(causas) > 3:
indicadores.append(IndicadorRiesgo(
codigo="JUDICIAL_CAUSAS_MULTIPLES",
nivel="Medio",
descripcion="Múltiples causas judiciales",
detalle=f"Se detectaron {len(causas)} causas judiciales",
))
# Inhibiciones
inhibiciones = _safe_list(judicial, "inhibiciones")
if inhibiciones:
indicadores.append(IndicadorRiesgo(
codigo="INHIBICIONES",
nivel="Alto",
descripcion="Inhibiciones registradas",
detalle=f"{len(inhibiciones)} inhibición(es) activa(s)",
))
# Deudores alimentarios
deudores = _safe_list(data.get("deudores_alimentarios", {}), "deudores")
if deudores:
indicadores.append(IndicadorRiesgo(
codigo="DEUDOR_ALIMENTARIO",
nivel="Alto",
descripcion="Registro de deudor alimentario moroso",
detalle="Aparece en RDAM",
))
# BCRA cheques rechazados
cheques = _safe_list(financiero, "cheques_rechazados")
if cheques:
indicadores.append(IndicadorRiesgo(
codigo="CHEQUES_RECHAZADOS",
nivel="Medio",
descripcion="Cheques rechazados registrados",
detalle=f"{len(cheques)} cheque(s) rechazado(s)",
))
return indicadores
def build_person_report(
cuit: str,
data: dict,
results: dict,
force_refresh: bool = False,
) -> PersonReport:
"""Construye reporte completo de persona."""
# Datos base
arca_data = _safe_dict(results.get("arca_afip", {}), "data") if "arca_afip" in results else _safe_dict(data, "fiscal")
bcra_data = _safe_dict(results.get("bcra", {}), "data") if "bcra" in results else _safe_dict(data, "financiero")
igj_data = _safe_dict(results.get("igj", {}), "data") if "igj" in results else _safe_dict(data, "societario", {}).get("igj", {})
inpi_data = _safe_dict(results.get("inpi", {}), "data") if "inpi" in results else _safe_dict(data, "societario", {}).get("inpi", {})
compras_data = _safe_dict(results.get("compras_estatales", {}), "data") if "compras_estatales" in results else _safe_dict(data, "societario", {}).get("compras_estatales", {})
# Identificación
identificacion = build_identificacion(arca_data, cuit)
contacto = build_contacto(_safe_dict(data, "contacto"))
domicilios = build_domicilios(arca_data)
# Secciones principales
fiscal = build_datos_fiscales(arca_data, cuit)
financiero = build_datos_financieros(bcra_data, _safe_dict(data, "financiero"))
societario = build_datos_societarios(igj_data, inpi_data, compras_data)
patrimonial = build_datos_patrimoniales(
_safe_dict(data, "arba_automotores"),
_safe_dict(data, "arba_catastro"),
_safe_dict(data, "dnrpa"),
_safe_dict(data, "infracciones"),
_safe_dict(data, "carto_arba"),
_safe_dict(data, "sinai"),
)
judicial = build_datos_judiciales(
_safe_dict(data, "poder_judicial"),
_safe_dict(data, "poder_judicial_provincial"),
_safe_dict(data, "boletin_oficial"),
_safe_dict(data, "boletines_provinciales"),
_safe_dict(data, "inhibiciones"),
_safe_dict(data, "juba"),
_safe_dict(data, "siscop"),
)
previsional = DatosPrevisionales(
anses=_safe_dict(data, "anses"),
monotributo_historial=_safe_dict(data, "monotributo_historial"),
)
academico = DatosAcademicos(
sgarhu=_safe_dict(data, "sgarhu"),
titulos=[],
)
registro_civil = DatosRegistroCivil(
siscop=_safe_dict(data, "siscop"),
defunciones=[],
)
billeteras = DatosBilleterasVirtuales(
billeteras=_safe_list(data.get("billeteras_virtuales", {}), "billeteras"),
)
compras_estatales = ContratoEstado(**_safe_dict(data, "compras_estatales")) if data.get("compras_estatales") else None
# Marcas INPI
marcas_inpi = []
for m in _safe_list(inpi_data, "marcas"):
if isinstance(m, dict):
marcas_inpi.append(MarcaINPI(
numero=m.get("numero"),
denominacion=m.get("denominacion"),
clase=m.get("clase"),
estado=m.get("estado"),
titular=m.get("titular"),
fecha_presentacion=m.get("fecha_presentacion"),
))
# Vínculos
domicilios_objs = [Domicilio(**d) if isinstance(d, dict) else d for d in _safe_list(arca_data, "domicilios")]
vinculos = build_vinculos(igj_data, domicilios_objs)
# Riesgo
riesgo = build_riesgo_indicadores(data, _safe_dict(data, "resultados"))
# Timeline BO
timeline = []
for t in _safe_list(data.get("timeline_boa", {}), "eventos"):
if isinstance(t, dict):
timeline.append(TimelineEvent(
fecha=t.get("fecha"),
tipo=t.get("tipo"),
descripcion=t.get("descripcion"),
fuente=t.get("fuente"),
))
# Deudores alimentarios
deudores = []
for d in _safe_list(data.get("deudores_alimentarios", {}), "deudores"):
if isinstance(d, dict):
deudores.append(DeudorAlimentario(
dni=d.get("dni"),
nombre=d.get("nombre"),
estado=d.get("estado"),
detalle=d.get("detalle"),
))
# Monotributo historial
monotributo = None
mh = _safe_dict(data, "monotributo_historial")
if mh:
monotributo = Monotributo(
historial_categorias=_safe_list(mh, "historial_categorias"),
categoria_actual=mh.get("categoria_actual"),
es_monotributista=mh.get("es_monotributista", False),
nota=mh.get("nota"),
)
# Registro conductores
licencia = None
rc = _safe_dict(data, "registro_conductores")
if rc:
licencia = {
"numero": rc.get("numero"),
"categoria": rc.get("categoria"),
"vencimiento": rc.get("vencimiento"),
}
# Score
score = compute_score(PersonReport(
meta=ReportMeta(report_id="", generated_at=datetime.now(timezone.utc), sources=[]),
identificacion=identificacion,
contacto=contacto,
domicilios=domicilios,
fiscal=fiscal,
financiero=financiero,
societario=societario,
patrimonial=patrimonial,
judicial=judicial,
previsional=previsional,
academico=academico,
registro_civil=registro_civil,
billeteras_virtuales=billeteras,
compras_estatales=compras_estatales,
marcas_inpi=marcas_inpi,
timeline=timeline,
deudores_alimentarios=deudores,
monotributo_historial=monotributo,
licencia_conducir=licencia,
vinculos=[],
riesgo=riesgo,
))
return PersonReport(
meta=ReportMeta(
report_id=uuid.uuid4().hex[:12],
generated_at=datetime.now(timezone.utc),
sources=[s for s in ["arca_afip", "bcra", "igj", "inpi", "bcra", "dnrpa", "poder_judicial",
"renaper", "anses", "cnv", "uif", "boletin_oficial", "boletines_provinciales",
"anses", "sinai", "inpi", "juba", "inhibiciones", "poder_judicial_provincial",
"siscop", "billeteras_virtuales", "ruido", "sgarhu", "contratar",
"colegios_profesionales", "renaper_facial", "archive_org", "padron_electoral",
"compras_estatales", "cnv"] if s in results],
),
score=score,
identificacion=identificacion,
contacto=contacto,
domicilios=domicilios,
fiscal=fiscal,
financiero=financiero,
societario=societario,
patrimonial=patrimonial,
judicial=judicial,
previsional=previsional,
academico=academico,
registro_civil=registro_civil,
billeteras_virtuales=billeteras,
compras_estatales=compras_estatales,
marcas_inpi=marcas_inpi,
timeline=timeline,
deudores_alimentarios=deudores,
monotributo_historial=monotributo,
registro_conductores=licencia,
vinculos=vinculos,
riesgo=riesgo,
)
def build_company_report(cuit: str, data: dict, results: dict) -> CompanyReport:
"""Construye reporte de empresa."""
# Similar a build_person_report pero para empresa
# ... implementación simplificada
return CompanyReport(
meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=[]),
score=compute_company_score(CompanyReport(
meta=ReportMeta(report_id="", generated_at=datetime.now(timezone.utc), sources=[]),
identificacion=CompanyIdentificacion(cuit=cuit, razon_social=""),
fiscal=DatosFiscales(cuit=cuit),
financiero=DatosFinancieros(bcra_situacion_actual=1, bcra_situacion_descripcion="Normal"),
societario=DatosSocietarios(),
judicial=DatosJudiciales(),
patrimonial=DatosPatrimoniales(),
billeteras_virtuales=DatosBilleterasVirtuales(),
riesgo=[],
)),
identificacion=CompanyIdentificacion(cuit=cuit, razon_social=""),
fiscal=DatosFiscales(cuit=cuit),
financiero=DatosFinancieros(bcra_situacion_actual=1, bcra_situacion_descripcion="Normal"),
societario=DatosSocietarios(),
judicial=DatosJudiciales(),
patrimonial=DatosPatrimoniales(),
billeteras_virtuales=DatosBilleterasVirtuales(),
riesgo=[],
)
def build_vehicle_report(dominio: str, data: dict, results: dict) -> VehicleReport:
"""Construye reporte de vehículo."""
dnrpa = _safe_dict(data, "dnrpa")
infracciones = _safe_dict(data, "infracciones")
siniestros = _safe_dict(data, "siniestros")
sinais = _safe_dict(data, "sinais")
vehiculo = None
for v in _safe_list(dnrpa, "vehiculos"):
if isinstance(v, dict) and v.get("dominio") == dominio:
vehiculo = Vehiculo(
dominio=v.get("dominio"),
marca=v.get("marca"),
modelo=v.get("modelo"),
anio=v.get("anio"),
tipo=v.get("tipo"),
registro=v.get("registro"),
titular=v.get("titular"),
estado=v.get("estado"),
)
break
infracciones_list = []
for inf in _safe_list(infracciones, "infracciones"):
if isinstance(inf, dict):
infracciones_list.append(InfraccionTransito(
fecha=inf.get("fecha"),
tipo=inf.get("tipo"),
jurisdiccion=inf.get("jurisdiccion"),
monto=inf.get("monto"),
estado=inf.get("estado"),
))
return VehicleReport(
meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
vehiculo=vehiculo,
infracciones=infracciones_list,
siniestros=[],
riesgo=[],
)
def build_property_report(calle: str, numero: str, localidad: str, provincia: str, data: dict, results: dict) -> PropertyReport:
"""Construye reporte de propiedad."""
arba_catastro = _safe_dict(data, "arba_catastro")
carto_arba = _safe_dict(data, "carto_arba")
bo_data = _safe_dict(data, "boletin_oficial")
bo_prov = _safe_dict(data, "boletin_oficial_pba")
inmuebles = []
for inm in _safe_list(arba_catastro, "inmuebles"):
if isinstance(inm, dict):
inmuebles.append(Inmueble(
partida=inm.get("partida"),
nomenclatura=inm.get("nomenclatura"),
direccion=inm.get("direccion"),
superficie=inm.get("superficie"),
valuacion=inm.get("valuacion"),
tipo=inm.get("tipo"),
))
return PropertyReport(
meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
direccion=f"{calle} {numero}, {localidad}, {provincia}",
inmuebles=inmuebles,
publicaciones_bo=[],
riesgo=[],
)
def build_group_report(cuit: str, data: dict, results: dict) -> GroupReport:
"""Construye reporte de grupo económico."""
persona = build_person_report(cuit, data, results)
# TODO: Empresas vinculadas, vehículos, inmuebles
return GroupReport(
meta=ReportMeta(report_id=uuid.uuid4().hex[:12], generated_at=datetime.now(timezone.utc), sources=list(results.keys())),
score_grupo=compute_group_score(PersonReport(...), [], [], []), # Simplificado
persona=persona,
empresas=[],
vehiculos=[],
inmuebles=[],
)