Spaces:
Paused
Paused
| """Módulo de scoring crediticio unificado.""" | |
| import logging | |
| from datetime import datetime | |
| from typing import List, Optional | |
| from app.reports.schemas import ( | |
| ScoreCrediticio, ScoreHistorial, | |
| PersonReport, CompanyReport, GroupReport | |
| ) | |
| logger = logging.getLogger(__name__) | |
| def compute_score(report: PersonReport, history_scores: List[ScoreHistorial] = None) -> ScoreCrediticio: | |
| """ | |
| Calcula el score crediticio (1-100) para una persona basado en su reporte. | |
| """ | |
| score = 60 # Base score | |
| factores = [] | |
| # 1. BCRA Situación Actual | |
| sit = report.financiero.bcra_situacion_actual | |
| if sit is None or sit <= 1: | |
| score += 40 | |
| factores.append("BCRA Situación Normal (Sit. 1 o sin deuda) +40pts") | |
| elif sit == 2: | |
| score += 28 | |
| factores.append("BCRA Situación 2 (Seguimiento especial) +28pts") | |
| elif sit == 3: | |
| score += 14 | |
| factores.append("BCRA Situación 3 (Con problemas) +14pts") | |
| elif sit == 4: | |
| score += 4 | |
| factores.append("BCRA Situación 4 (Alto riesgo de insolvencia) +4pts") | |
| else: | |
| # Sit 5 o 6 | |
| factores.append(f"BCRA Situación {sit} (Irrecuperable / Incoobrable) +0pts") | |
| # 2. Cheques Rechazados | |
| cant_cheques = len(report.financiero.cheques_rechazados) | |
| if cant_cheques > 0: | |
| deduccion = cant_cheques * 5 | |
| score -= deduccion | |
| factores.append(f"{cant_cheques} Cheques rechazados -{deduccion}pts") | |
| # 3. Causas Judiciales | |
| # Consideramos causas judiciales activas (que no tengan estado Terminado/Archivado) | |
| causas_activas = 0 | |
| for c in report.judicial.causas: | |
| estado = (c.estado or "").lower() | |
| if not any(x in estado for x in ["archiv", "termin", "finaliz", "cerrad", "resuelt"]): | |
| causas_activas += 1 | |
| if causas_activas > 0: | |
| deduccion = causas_activas * 4 | |
| score -= deduccion | |
| factores.append(f"{causas_activas} Causas judiciales activas -{deduccion}pts") | |
| # 4. Inhibiciones / Embargos | |
| # Usar solo patrimonial.inhibiciones que ya está consolidado (evitar duplicados con judicial) | |
| inhibiciones_totales = len(report.patrimonial.inhibiciones) | |
| if inhibiciones_totales > 0: | |
| deduccion = inhibiciones_totales * 10 | |
| score -= deduccion | |
| factores.append(f"{inhibiciones_totales} Inhibiciones/Embargos registrados -{deduccion}pts") | |
| # 5. AFIP Activo (Señal positiva) | |
| estado_afip = (report.fiscal.estado_afip or "").lower() | |
| if "activ" in estado_afip: | |
| score += 5 | |
| factores.append("AFIP Activo +5pts") | |
| # 6. Antigüedad laboral ANSES | |
| if report.previsional.tiene_aportes: | |
| alta_os = report.previsional.fecha_alta_obra_social | |
| es_antiguo = False | |
| if alta_os: | |
| try: | |
| # Intentamos parsear la fecha de alta. Formatos típicos: YYYY-MM-DD o DD/MM/YYYY | |
| fecha_alta = None | |
| for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d"): | |
| try: | |
| fecha_alta = datetime.strptime(alta_os, fmt) | |
| break | |
| except ValueError: | |
| continue | |
| if fecha_alta: | |
| anios = (datetime.now() - fecha_alta).days / 365.25 | |
| if anios >= 3.0: | |
| es_antiguo = True | |
| except Exception as e: | |
| logger.debug(f"Error parseando fecha alta obra social: {e}") | |
| # Fallback si no hay fecha pero tiene aportes al día | |
| if es_antiguo or (report.previsional.aportes_al_dia and not alta_os): | |
| score += 3 | |
| factores.append("Antigüedad laboral/Aportes > 3 años +3pts") | |
| # 7. Inmuebles registrados | |
| cant_inmuebles = len(report.patrimonial.inmuebles) | |
| if cant_inmuebles > 0: | |
| puntos_inm = min(6, cant_inmuebles * 2) | |
| score += puntos_inm | |
| factores.append(f"{cant_inmuebles} Inmuebles registrados +{puntos_inm}pts (máx +6)") | |
| # 8. Marcas INPI (solo para empresas; en personas siempre 0) | |
| cant_marcas = len(getattr(report, "marcas_inpi", [])) | |
| if cant_marcas > 0: | |
| puntos_marcas = min(4, cant_marcas * 2) | |
| score += puntos_marcas | |
| factores.append(f"{cant_marcas} Marcas INPI registradas +{puntos_marcas}pts (máx +4)") | |
| # Acotar el score final entre 1 y 100 | |
| score_final = max(1, min(100, score)) | |
| # Determinar nivel | |
| if score_final >= 80: | |
| nivel = "Excelente" | |
| elif score_final >= 60: | |
| nivel = "Bueno" | |
| elif score_final >= 40: | |
| nivel = "Regular" | |
| elif score_final >= 20: | |
| nivel = "Malo" | |
| else: | |
| nivel = "Crítico" | |
| return ScoreCrediticio( | |
| valor=score_final, | |
| nivel=nivel, | |
| historial=history_scores or [], | |
| factores=factores | |
| ) | |
| def compute_company_score(report: CompanyReport, history_scores: List[ScoreHistorial] = None) -> ScoreCrediticio: | |
| """ | |
| Calcula el score crediticio (0-1000) para una empresa/ persona jurídica. | |
| Replica la lógica del frontend: base 950, penalizaciones por BCRA, cheques y causas. | |
| """ | |
| score = 950 | |
| factores = [] | |
| # 1. BCRA Situación Actual | |
| sit = report.financiero.bcra_situacion_actual | |
| if sit is None or sit <= 1: | |
| factores.append("BCRA Situación Normal +0pts") | |
| elif sit == 2: | |
| score -= 150 | |
| factores.append("BCRA Situación 2 -150pts") | |
| elif sit == 3: | |
| score -= 300 | |
| factores.append("BCRA Situación 3 -300pts") | |
| elif sit == 4: | |
| score -= 500 | |
| factores.append("BCRA Situación 4 -500pts") | |
| elif sit >= 5: | |
| score -= 750 | |
| factores.append(f"BCRA Situación {sit} -750pts") | |
| # 2. Cheques Rechazados (máx -250) | |
| cant_cheques = len(report.financiero.cheques_rechazados) | |
| if cant_cheques > 0: | |
| deduccion = min(250, cant_cheques * 50) | |
| score -= deduccion | |
| factores.append(f"{cant_cheques} Cheques rechazados -{deduccion}pts (máx -250)") | |
| # 3. Causas Judiciales (máx -200) | |
| cant_causas = len(report.judicial.causas) | |
| if cant_causas > 0: | |
| deduccion = min(200, cant_causas * 40) | |
| score -= deduccion | |
| factores.append(f"{cant_causas} Causas judiciales -{deduccion}pts (máx -200)") | |
| # 4. AFIP Activo (bonus) | |
| estado_afip = (report.fiscal.estado_afip or "").lower() | |
| if "activ" in estado_afip: | |
| score += 20 | |
| factores.append("AFIP Activo +20pts") | |
| # 5. IGJ inscripto (bonus) | |
| if report.igj.numero_inscripcion: | |
| score += 10 | |
| factores.append("IGJ Inscripto +10pts") | |
| # Acotar entre 0 y 1000 | |
| score_final = max(0, min(1000, score)) | |
| # Determinar nivel | |
| if score_final >= 700: | |
| nivel = "Excelente" | |
| elif score_final >= 450: | |
| nivel = "Medio" | |
| else: | |
| nivel = "Crítico" | |
| return ScoreCrediticio( | |
| valor=score_final, | |
| nivel=nivel, | |
| historial=history_scores or [], | |
| factores=factores | |
| ) | |
| def compute_group_score(report: GroupReport, history_scores: List[ScoreHistorial] = None) -> ScoreCrediticio: | |
| """ | |
| Calcula el score crediticio (0-1000) para un grupo económico (titular + empresas + vehículos). | |
| Replica la lógica del frontend: base 950, penalizaciones por BCRA/riesgos, bonus por vehículos. | |
| """ | |
| score = 950 | |
| factores = [] | |
| # 1. BCRA del titular | |
| sit = report.target.financiero.bcra_situacion_actual | |
| if sit is None or sit <= 1: | |
| factores.append("BCRA Titular Normal +0pts") | |
| elif sit == 2: | |
| score -= 150 | |
| factores.append("BCRA Titular Sit. 2 -150pts") | |
| elif sit == 3: | |
| score -= 300 | |
| factores.append("BCRA Titular Sit. 3 -300pts") | |
| elif sit == 4: | |
| score -= 500 | |
| factores.append("BCRA Titular Sit. 4 -500pts") | |
| elif sit >= 5: | |
| score -= 750 | |
| factores.append(f"BCRA Titular Sit. {sit} -750pts") | |
| # 2. Riesgos consolidados (máx -300) | |
| total_riesgos = len(report.riesgo_consolidado) | |
| if total_riesgos > 0: | |
| deduccion = min(300, total_riesgos * 60) | |
| score -= deduccion | |
| factores.append(f"{total_riesgos} Riesgos consolidados -{deduccion}pts (máx -300)") | |
| # 3. Vehículos vinculados (bonus, máx +100) | |
| cant_vehiculos = len(report.vehiculos_vinculados) | |
| if cant_vehiculos > 0: | |
| bonus = min(100, cant_vehiculos * 25) | |
| score += bonus | |
| factores.append(f"{cant_vehiculos} Vehículos vinculados +{bonus}pts (máx +100)") | |
| # 4. Empresas vinculadas (bonus si están activas) | |
| empresas_activas = 0 | |
| for e in report.empresas_vinculadas: | |
| if "activ" in (e.fiscal.estado_afip or "").lower(): | |
| empresas_activas += 1 | |
| if empresas_activas > 0: | |
| bonus = min(50, empresas_activas * 25) | |
| score += bonus | |
| factores.append(f"{empresas_activas} Empresas activas +{bonus}pts (máx +50)") | |
| # Acotar entre 0 y 1000 | |
| score_final = max(0, min(1000, score)) | |
| # Determinar nivel | |
| if score_final >= 700: | |
| nivel = "Excelente" | |
| elif score_final >= 450: | |
| nivel = "Medio" | |
| else: | |
| nivel = "Crítico" | |
| return ScoreCrediticio( | |
| valor=score_final, | |
| nivel=nivel, | |
| historial=history_scores or [], | |
| factores=factores | |
| ) | |