""" 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=[], )