fastcheck_api fix
This commit is contained in:
parent
21ae30fcdf
commit
f52c920b98
|
|
@ -1,38 +1,174 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
||||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||||
from pymongo import ReturnDocument
|
from pymongo import ReturnDocument
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from fastcheck_api.app.api.dependencies import CurrentUser, require_permission
|
from fastcheck_api.app.api.dependencies import CurrentUser, require_permission
|
||||||
from fastcheck_api.app.core.mongodb import get_db
|
from fastcheck_api.app.core.mongodb import get_db
|
||||||
|
from fastcheck_api.app.services.dequienes_native_service import dequienes_native_service
|
||||||
from fastcheck_api.app.services.audit_service import AuditService
|
from fastcheck_api.app.services.audit_service import AuditService
|
||||||
from fastcheck_api.app.services.legacy_service import legacy_service
|
from fastcheck_api.app.services.risk_calculation_service import RiskCalculationService
|
||||||
from fastcheck_api.app.utils.mongo import sanitize_rut, to_object_id
|
from fastcheck_api.app.services.sheriff_v2_native_service import sheriff_v2_native_service
|
||||||
|
from fastcheck_api.app.utils.mongo import jsonable, sanitize_rut, to_object_id
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/rut", tags=["rut"])
|
router = APIRouter(prefix="/rut", tags=["rut"])
|
||||||
|
logger = logging.getLogger("fastcheck.rut")
|
||||||
_bearer = HTTPBearer(auto_error=False)
|
|
||||||
|
|
||||||
|
|
||||||
class RutLookupRequest(BaseModel):
|
class RutLookupRequest(BaseModel):
|
||||||
rut: str = Field(examples=["12345678-9"])
|
rut: str = Field(examples=["12345678-9"])
|
||||||
isMonitoring: bool = Field(default=False, description="Compatibility flag with legacy backend.")
|
isMonitoring: bool = Field(default=False)
|
||||||
|
isRefreshing: bool = Field(default=False)
|
||||||
|
isPep: bool = Field(default=False)
|
||||||
|
type: str = Field(default="individual")
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list(value: Any) -> list[Any]:
|
||||||
|
return value if isinstance(value, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_get(dct: Any, *path: str, default: Any = None) -> Any:
|
||||||
|
cur = dct
|
||||||
|
for key in path:
|
||||||
|
if not isinstance(cur, dict):
|
||||||
|
return default
|
||||||
|
cur = cur.get(key)
|
||||||
|
return default if cur is None else cur
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_socios_from_diario_oficial(rut: str, diario_oficial: Any, limit: int = 200) -> list[dict[str, Any]]:
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for item in _as_list(diario_oficial):
|
||||||
|
for socio in _as_list(_safe_get(item, "socios", default=[])):
|
||||||
|
socio_rut = sanitize_rut(str(_safe_get(socio, "rut", default="") or ""))
|
||||||
|
if not socio_rut or socio_rut == rut:
|
||||||
|
continue
|
||||||
|
if socio_rut in seen:
|
||||||
|
continue
|
||||||
|
seen.add(socio_rut)
|
||||||
|
name = str(_safe_get(socio, "nombre", default="") or _safe_get(socio, "name", default="") or "").strip()
|
||||||
|
out.append({"name": name or "N/A", "rut": socio_rut, "personType": None, "isNaturalPerson": None})
|
||||||
|
if len(out) >= limit:
|
||||||
|
return out
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_is_pep_from_compliance(compliance: Any) -> bool:
|
||||||
|
pep = _as_list(_safe_get(compliance, "pepChile", "coincidencias", default=[]))
|
||||||
|
fam = _as_list(_safe_get(compliance, "familiaresPep", "coincidencias", default=[]))
|
||||||
|
pub = _as_list(_safe_get(compliance, "funcionariosPublicos", "coincidencias", default=[]))
|
||||||
|
return bool(pep or fam or pub)
|
||||||
|
|
||||||
|
|
||||||
|
def _simple_risk_assessment(filtered_details: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
compliance = filtered_details.get("compliance") or {}
|
||||||
|
is_pep = _compute_is_pep_from_compliance(compliance)
|
||||||
|
has_judicial = bool(filtered_details.get("civilCasesData") or filtered_details.get("laboralCasesData") or filtered_details.get("cobranzaCasesData"))
|
||||||
|
has_labor_collection = bool(filtered_details.get("moraPrevisionalCasesData") or filtered_details.get("multaLaboralCasesData"))
|
||||||
|
semaphore = "bajo"
|
||||||
|
if is_pep:
|
||||||
|
semaphore = "critico"
|
||||||
|
elif has_judicial or has_labor_collection:
|
||||||
|
semaphore = "alto"
|
||||||
|
return {
|
||||||
|
"riskSummary": {"semaphore": semaphore},
|
||||||
|
"allRules": {
|
||||||
|
"complianceRules": [],
|
||||||
|
"legalRules": [],
|
||||||
|
"capitalHumanoRules": [],
|
||||||
|
"financieroTributarioRules": [],
|
||||||
|
},
|
||||||
|
"summaryDocumentMD": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_is_pep_from_risk(risk_assessment: dict[str, Any] | None) -> bool:
|
||||||
|
if not isinstance(risk_assessment, dict):
|
||||||
|
return False
|
||||||
|
rules = _safe_get(risk_assessment, "allRules", "complianceRules", default=[])
|
||||||
|
for r in _as_list(rules):
|
||||||
|
label = str(_safe_get(r, "label", default="") or "")
|
||||||
|
if label in {"PEP Chile", "Familiares PEP"} and bool(_safe_get(r, "detected", default=False)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_PROV_REPLACEMENTS: dict[str, str] = {
|
||||||
|
"sheriff": "sProv",
|
||||||
|
"dequienes": "dProv",
|
||||||
|
}
|
||||||
|
_PROV_PATTERN = re.compile("|".join(re.escape(k) for k in _PROV_REPLACEMENTS), re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_providers_text(value: str) -> str:
|
||||||
|
return _PROV_PATTERN.sub(lambda m: _PROV_REPLACEMENTS[m.group(0).lower()], value)
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_providers_json(value: Any) -> Any:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return _mask_providers_text(value)
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_mask_providers_json(v) for v in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
out: dict[Any, Any] = {}
|
||||||
|
for k, v in value.items():
|
||||||
|
masked_key = _mask_providers_text(k) if isinstance(k, str) else k
|
||||||
|
out[masked_key] = _mask_providers_json(v)
|
||||||
|
return out
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_date(value: str | None) -> str | None:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
raw = str(value).strip()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
if re.match(r"^\d{4}-\d{2}-\d{2}$", raw):
|
||||||
|
return raw
|
||||||
|
m = re.match(r"^(\d{2})-(\d{2})-(\d{4})$", raw)
|
||||||
|
if m:
|
||||||
|
dd, mm, yyyy = m.group(1), m.group(2), m.group(3)
|
||||||
|
return f"{yyyy}-{mm}-{dd}"
|
||||||
|
m = re.match(r"^(\d{2})/(\d{2})/(\d{4})$", raw)
|
||||||
|
if m:
|
||||||
|
dd, mm, yyyy = m.group(1), m.group(2), m.group(3)
|
||||||
|
return f"{yyyy}-{mm}-{dd}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_cache_info_json(value: Any) -> Any:
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_strip_cache_info_json(v) for v in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
out: dict[Any, Any] = {}
|
||||||
|
for k, v in value.items():
|
||||||
|
if k == "_cache_info":
|
||||||
|
continue
|
||||||
|
out[k] = _strip_cache_info_json(v)
|
||||||
|
return out
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_result_payload(payload: Any) -> Any:
|
||||||
|
return _mask_providers_json(_strip_cache_info_json(payload))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/lookup",
|
"/lookup",
|
||||||
summary="Lookup RUT (single evaluation)",
|
summary="Lookup RUT",
|
||||||
description=(
|
description=(
|
||||||
"Runs a single evaluation for a RUT using the legacy backend and persists the result in MongoDB.\n\n"
|
"Runs a native RUT lookup using Sheriff V2 APIs and persists the result in MongoDB (same persistence model as the legacy Node implementation)."
|
||||||
"If a successful result for the same RUT already exists for the tenant, returns it without consuming credits."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def lookup_rut(
|
async def lookup_rut(
|
||||||
|
|
@ -40,32 +176,33 @@ async def lookup_rut(
|
||||||
payload: RutLookupRequest,
|
payload: RutLookupRequest,
|
||||||
user: Annotated[CurrentUser, Depends(require_permission("rut:lookup"))],
|
user: Annotated[CurrentUser, Depends(require_permission("rut:lookup"))],
|
||||||
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
|
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
|
||||||
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)] = None,
|
|
||||||
):
|
):
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
rut = sanitize_rut(payload.rut)
|
rut = sanitize_rut(payload.rut)
|
||||||
|
|
||||||
token = credentials.credentials if credentials else None
|
refresh = bool(payload.isMonitoring or payload.isRefreshing)
|
||||||
if not token:
|
now = dt.datetime.now(dt.timezone.utc)
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="No token provided")
|
|
||||||
|
|
||||||
cached = (
|
if refresh:
|
||||||
await db["evaluationresults"]
|
await db["results"].update_many(
|
||||||
.find({"tenantId": user.tenant, "rut": rut, "status": "success"})
|
{"rut": rut, "tenantId": user.tenant, "archived": {"$ne": True}},
|
||||||
.sort([("createdAt", -1)])
|
{"$set": {"archived": True, "archivedAt": now, "updatedAt": now}},
|
||||||
.limit(1)
|
)
|
||||||
.to_list(length=1)
|
|
||||||
|
existing = await db["results"].find_one(
|
||||||
|
{"rut": rut, "tenantId": user.tenant, "archived": {"$ne": True}},
|
||||||
|
sort=[("createdAt", -1)],
|
||||||
)
|
)
|
||||||
if cached and isinstance(cached[0], dict) and isinstance(cached[0].get("data"), dict):
|
if existing and not refresh:
|
||||||
await AuditService.log_consulta(
|
await AuditService.log_consulta(
|
||||||
db=db,
|
db=db,
|
||||||
tenant_id=user.tenant,
|
tenant_id=user.tenant,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
consulta_type="individual",
|
consulta_type="individual",
|
||||||
rut=rut,
|
rut=rut,
|
||||||
endpoint="/api/v1/rut/lookup",
|
endpoint="/client-api-v1/rut/lookup",
|
||||||
response_status=status.HTTP_200_OK,
|
response_status=status.HTTP_200_OK,
|
||||||
request_data={"rut": rut, "isMonitoring": payload.isMonitoring},
|
request_data=jsonable(payload.model_dump()),
|
||||||
response_data={"source": "cache"},
|
response_data={"source": "cache"},
|
||||||
processing_time_ms=int((time.perf_counter() - start) * 1000),
|
processing_time_ms=int((time.perf_counter() - start) * 1000),
|
||||||
credits_used=0,
|
credits_used=0,
|
||||||
|
|
@ -73,147 +210,231 @@ async def lookup_rut(
|
||||||
user_agent=request.headers.get("user-agent"),
|
user_agent=request.headers.get("user-agent"),
|
||||||
metadata={"source": "cache"},
|
metadata={"source": "cache"},
|
||||||
)
|
)
|
||||||
return {**cached[0]["data"], "isFromCache": True}
|
return _sanitize_result_payload(jsonable(existing))
|
||||||
|
|
||||||
tenant_oid = to_object_id(user.tenant)
|
should_deduct = (not payload.isPep) and (payload.isRefreshing or not refresh)
|
||||||
now = dt.datetime.now(dt.timezone.utc)
|
if should_deduct:
|
||||||
|
tenant_oid = to_object_id(user.tenant)
|
||||||
tenant_after = await db["tenants"].find_one_and_update(
|
tenant_after = await db["tenants"].find_one_and_update(
|
||||||
{"_id": tenant_oid, "creditBalance.availableCredits": {"$gte": 1}},
|
{"_id": tenant_oid, "creditBalance.availableCredits": {"$gte": 1}},
|
||||||
{
|
{
|
||||||
"$inc": {
|
"$inc": {
|
||||||
"creditBalance.availableCredits": -1,
|
"creditBalance.availableCredits": -1,
|
||||||
"creditBalance.totalCreditsUsed": 1,
|
"creditBalance.totalCreditsUsed": 1,
|
||||||
|
},
|
||||||
|
"$set": {
|
||||||
|
"creditBalance.lastCreditOperation": now,
|
||||||
|
"updatedAt": now,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"$set": {
|
return_document=ReturnDocument.AFTER,
|
||||||
"creditBalance.lastCreditOperation": now,
|
|
||||||
"updatedAt": now,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_document=ReturnDocument.AFTER,
|
|
||||||
)
|
|
||||||
if not tenant_after:
|
|
||||||
await AuditService.log_consulta(
|
|
||||||
db=db,
|
|
||||||
tenant_id=user.tenant,
|
|
||||||
user_id=user.id,
|
|
||||||
consulta_type="individual",
|
|
||||||
rut=rut,
|
|
||||||
endpoint="/api/v1/rut/lookup",
|
|
||||||
response_status=status.HTTP_402_PAYMENT_REQUIRED,
|
|
||||||
request_data={"rut": rut, "isMonitoring": payload.isMonitoring},
|
|
||||||
processing_time_ms=int((time.perf_counter() - start) * 1000),
|
|
||||||
credits_used=0,
|
|
||||||
ip_address=request.client.host if request.client else None,
|
|
||||||
user_agent=request.headers.get("user-agent"),
|
|
||||||
metadata={"reason": "insufficient_credits"},
|
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail="Insufficient credits")
|
if not tenant_after:
|
||||||
|
await AuditService.log_consulta(
|
||||||
|
db=db,
|
||||||
|
tenant_id=user.tenant,
|
||||||
|
user_id=user.id,
|
||||||
|
consulta_type="individual",
|
||||||
|
rut=rut,
|
||||||
|
endpoint="/client-api-v1/rut/lookup",
|
||||||
|
response_status=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
|
request_data=jsonable(payload.model_dump()),
|
||||||
|
processing_time_ms=int((time.perf_counter() - start) * 1000),
|
||||||
|
credits_used=0,
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
metadata={"reason": "insufficient_credits"},
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
||||||
|
detail={"message": "Insufficient credits", "availableCredits": int((tenant_after or {}).get("creditBalance", {}).get("availableCredits", 0))},
|
||||||
|
)
|
||||||
|
|
||||||
new_balance = int((tenant_after.get("creditBalance") or {}).get("availableCredits") or 0)
|
new_balance = int((tenant_after.get("creditBalance") or {}).get("availableCredits") or 0)
|
||||||
await db["creditoperations"].insert_one(
|
await db["creditoperations"].insert_one(
|
||||||
{
|
{
|
||||||
"tenantId": tenant_oid,
|
"tenantId": tenant_oid,
|
||||||
"userId": to_object_id(user.id),
|
"userId": to_object_id(user.id),
|
||||||
"operationType": "evaluation",
|
"operationType": "evaluation",
|
||||||
"creditsChanged": -1,
|
"creditsChanged": -1,
|
||||||
"balanceAfter": new_balance,
|
"balanceAfter": new_balance,
|
||||||
"description": f"Valutazione singola per {rut}",
|
"description": f"Valutazione singola per {rut}",
|
||||||
"metadata": {"evaluationType": "single", "supplierRut": rut},
|
"metadata": {"evaluationType": "single", "supplierRut": rut, "source": "fastcheck_api"},
|
||||||
"createdAt": now,
|
"createdAt": now,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
job_doc: dict[str, Any] = {
|
|
||||||
"tenantId": user.tenant,
|
|
||||||
"status": "processing",
|
|
||||||
"type": "single",
|
|
||||||
"createdBy": user.id,
|
|
||||||
"totalEvaluations": 1,
|
|
||||||
"completedEvaluations": 0,
|
|
||||||
"failedEvaluations": 0,
|
|
||||||
"supplierData": {"rut": rut, "name": "N/A"},
|
|
||||||
"createdAt": now,
|
|
||||||
"updatedAt": now,
|
|
||||||
}
|
|
||||||
job_insert = await db["evaluationjobs"].insert_one(job_doc)
|
|
||||||
job_id = str(job_insert.inserted_id)
|
|
||||||
|
|
||||||
response_status = status.HTTP_200_OK
|
response_status = status.HTTP_200_OK
|
||||||
error_message: str | None = None
|
error_message: str | None = None
|
||||||
result_data: dict[str, Any] | None = None
|
|
||||||
try:
|
try:
|
||||||
result_data = await legacy_service.lookup_rut(
|
sheriff_payload = await sheriff_v2_native_service.query_rut(rut=rut, is_monitoring=payload.isMonitoring)
|
||||||
rut=rut,
|
masked_sheriff_payload = _mask_providers_json(sheriff_payload)
|
||||||
token=token,
|
|
||||||
is_monitoring=payload.isMonitoring,
|
|
||||||
evaluation_type="individual",
|
|
||||||
)
|
|
||||||
ai_summary = result_data.get("aiAnalysis")
|
|
||||||
if isinstance(ai_summary, str) and ai_summary.strip():
|
|
||||||
try:
|
|
||||||
await legacy_service.save_fastcheck_summary(rut=rut, summary=ai_summary, token=token)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await db["evaluationresults"].insert_one(
|
resumen_data = _safe_get(masked_sheriff_payload, "resumen", "data", default={})
|
||||||
{
|
data_sii = _safe_get(resumen_data, "identificacion", default={})
|
||||||
"jobId": job_insert.inserted_id,
|
credit_scoring = _safe_get(masked_sheriff_payload, "creditScore", "data", default={})
|
||||||
"tenantId": user.tenant,
|
compliance = _safe_get(masked_sheriff_payload, "compliance", "data", default={})
|
||||||
"rut": rut,
|
|
||||||
"name": None,
|
filtered_details: dict[str, Any] = {
|
||||||
"status": "success",
|
"rut": _safe_get(resumen_data, "rut", default=rut),
|
||||||
"data": result_data,
|
"razonSocial": _safe_get(data_sii, "razonSocial", default=None),
|
||||||
"createdAt": now,
|
"presentaActividades": _safe_get(data_sii, "presentaActividades", default=None),
|
||||||
"updatedAt": now,
|
"inicioActividades": _safe_get(data_sii, "inicioActividades", default=None),
|
||||||
}
|
"autorizadoMonedaExtranjera": _safe_get(data_sii, "autorizadoMonedaExtranjera", default=None),
|
||||||
|
"empresaMenor": _safe_get(data_sii, "empresaMenor", default=None),
|
||||||
|
"observaciones": _safe_get(data_sii, "observaciones", default=None),
|
||||||
|
"notas": _safe_get(data_sii, "notas", default=None),
|
||||||
|
"situacionActual": _safe_get(data_sii, "situacionActual", default=None),
|
||||||
|
"fetchedAt": now.date().isoformat(),
|
||||||
|
"creditScoringData": credit_scoring,
|
||||||
|
"compliance": compliance,
|
||||||
|
"boletinComercialSummary": _safe_get(masked_sheriff_payload, "resumen", "boletinComercial", default={}),
|
||||||
|
"boletinLaboralSummary": _safe_get(resumen_data, "boletinLaboral", default={}),
|
||||||
|
"boletinLaboralLastUpdate": _safe_get(resumen_data, "boletinLaboral", "ultimaActualizacion", default=None),
|
||||||
|
"vehiclesSummary": _safe_get(resumen_data, "bienes", "vehiculos", default={}),
|
||||||
|
"propertiesSummary": _safe_get(resumen_data, "bienes", "propiedades", default={}),
|
||||||
|
"deudaBancariaSummary": _safe_get(masked_sheriff_payload, "resumen", "deudaBancaria", default={}),
|
||||||
|
"mallaSocietariaData": _safe_get(masked_sheriff_payload, "legal", "mallaSocietaria", "data", default={}),
|
||||||
|
"officialDiaryData": _safe_get(masked_sheriff_payload, "legal", "mallaSocietaria", "data", "diarioOficial", default=[]),
|
||||||
|
"civilCasesData": _safe_get(masked_sheriff_payload, "judicial", "civil", "data", default={}),
|
||||||
|
"laboralCasesData": _safe_get(masked_sheriff_payload, "judicial", "laboral", "data", default={}),
|
||||||
|
"cobranzaCasesData": _safe_get(masked_sheriff_payload, "judicial", "cobranza", "data", default={}),
|
||||||
|
"moraPrevisionalCasesData": _safe_get(masked_sheriff_payload, "cobranzaLaboral", "moraPrevisional", "data", default={}),
|
||||||
|
"multaLaboralCasesData": _safe_get(masked_sheriff_payload, "cobranzaLaboral", "multaLaboral", "data", default={}),
|
||||||
|
}
|
||||||
|
|
||||||
|
compliance_person_type = "juridical" if bool(_safe_get(resumen_data, "esPersonaJuridica", default=False)) else "natural"
|
||||||
|
filtered_details["compliancePersonType"] = compliance_person_type
|
||||||
|
filtered_details["complianceSelectedRowsPepChile"] = _as_list(_safe_get(compliance, "pepChile", "coincidencias", default=[]))
|
||||||
|
filtered_details["complianceSelectedRowsPublicOfficial"] = _as_list(
|
||||||
|
_safe_get(compliance, "funcionariosPublicos", "coincidencias", default=[])
|
||||||
)
|
)
|
||||||
await db["evaluationjobs"].update_one(
|
filtered_details["complianceSelectedRowsPepChileFamily"] = _as_list(_safe_get(compliance, "familiaresPep", "coincidencias", default=[]))
|
||||||
{"_id": job_insert.inserted_id, "tenantId": user.tenant},
|
filtered_details["complianceSelectedRowsPenal"] = _as_list(_safe_get(compliance, "penal", "coincidencias", default=[]))
|
||||||
{"$set": {"status": "completed", "completedEvaluations": 1, "updatedAt": now}},
|
filtered_details["complianceSelectedlistasInternacionales"] = _as_list(
|
||||||
|
_safe_get(compliance, "listasInternacionales", "coincidencias", default=[])
|
||||||
)
|
)
|
||||||
return {**result_data, "jobId": job_id, "isFromCache": False}
|
|
||||||
except HTTPException as e:
|
company_general_info: dict[str, Any] = {
|
||||||
raise e
|
"rapresentanteLegal": "N/D",
|
||||||
except Exception as e:
|
"rapresentanteLegalRut": "N/D",
|
||||||
response_status = status.HTTP_502_BAD_GATEWAY
|
"inicioActividades": filtered_details.get("inicioActividades") or "",
|
||||||
error_message = str(e)
|
"fechaDeConstitucion": "",
|
||||||
await db["evaluationresults"].insert_one(
|
"empresaMenor": filtered_details.get("empresaMenor") or "",
|
||||||
{
|
"razonSocial": filtered_details.get("razonSocial") or "",
|
||||||
"jobId": job_insert.inserted_id,
|
"socios": [],
|
||||||
"tenantId": user.tenant,
|
"administradores": [],
|
||||||
"rut": rut,
|
"rangoVentas": None,
|
||||||
"name": None,
|
"tamanoEmpresa": None,
|
||||||
"status": "failed",
|
"iconImpresa": None,
|
||||||
"error": error_message,
|
"cantidadTrabajadores": None,
|
||||||
"createdAt": now,
|
}
|
||||||
"updatedAt": now,
|
|
||||||
}
|
diario = filtered_details.get("officialDiaryData")
|
||||||
|
company_general_info["socios"] = _extract_socios_from_diario_oficial(rut, diario)
|
||||||
|
|
||||||
|
if not payload.isPep:
|
||||||
|
try:
|
||||||
|
deq_relationships = await dequienes_native_service.relationships(
|
||||||
|
rut=rut,
|
||||||
|
distance=2,
|
||||||
|
relationship_direction="BOTH",
|
||||||
|
one_path_per_node=False,
|
||||||
|
)
|
||||||
|
filtered_details["dequienesRelationships"] = _mask_providers_json(_strip_cache_info_json(deq_relationships))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Dequienes relationships failed")
|
||||||
|
try:
|
||||||
|
deq_legal = await dequienes_native_service.legal_events(rut=rut)
|
||||||
|
filtered_details["dequienesLegalEvents"] = _mask_providers_json(_strip_cache_info_json(deq_legal))
|
||||||
|
raw_date = deq_legal.get("constitution_date") if isinstance(deq_legal, dict) else None
|
||||||
|
normalized = _normalize_date(raw_date if isinstance(raw_date, str) else None)
|
||||||
|
if normalized and not company_general_info.get("fechaDeConstitucion"):
|
||||||
|
company_general_info["fechaDeConstitucion"] = normalized
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Dequienes legal events failed")
|
||||||
|
|
||||||
|
if not company_general_info.get("fechaDeConstitucion"):
|
||||||
|
company_general_info["fechaDeConstitucion"] = "No hay informacion"
|
||||||
|
|
||||||
|
risk_assessment = await RiskCalculationService.calculate_risk(
|
||||||
|
db,
|
||||||
|
tenant_id=user.tenant,
|
||||||
|
rut=rut,
|
||||||
|
sheriff_v2_data=masked_sheriff_payload,
|
||||||
|
filtered_details=filtered_details,
|
||||||
|
company_general_info=company_general_info,
|
||||||
|
is_pep_only=payload.isPep,
|
||||||
)
|
)
|
||||||
await db["evaluationjobs"].update_one(
|
is_pep = _compute_is_pep_from_risk(risk_assessment) or _compute_is_pep_from_compliance(compliance)
|
||||||
{"_id": job_insert.inserted_id, "tenantId": user.tenant},
|
|
||||||
{"$set": {"status": "completed", "failedEvaluations": 1, "updatedAt": now}},
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=response_status, detail="Legacy lookup failed")
|
|
||||||
finally:
|
|
||||||
processing_ms = int((time.perf_counter() - start) * 1000)
|
processing_ms = int((time.perf_counter() - start) * 1000)
|
||||||
audit_response: dict[str, Any] | None = None
|
result_doc: dict[str, Any] = {
|
||||||
if result_data is not None:
|
"rut": rut,
|
||||||
audit_response = {"jobId": job_id}
|
"tenantId": user.tenant,
|
||||||
|
"queryType": "derived_socio_pep" if payload.isPep else "primary",
|
||||||
|
"sheriffLogData": {
|
||||||
|
"rut": rut,
|
||||||
|
"tenantId": user.tenant,
|
||||||
|
"fetchedAt": now,
|
||||||
|
"companyGeneralInfo": company_general_info if not payload.isPep else None,
|
||||||
|
"filteredDetails": filtered_details,
|
||||||
|
"rawSheriffV2": masked_sheriff_payload,
|
||||||
|
},
|
||||||
|
"details": filtered_details,
|
||||||
|
"companyGeneralInfo": None if payload.isPep else company_general_info,
|
||||||
|
"riskAssessment": risk_assessment,
|
||||||
|
"isPep": bool(is_pep),
|
||||||
|
"userEmail": user.email,
|
||||||
|
"status": 200,
|
||||||
|
"type": payload.type,
|
||||||
|
"processingTime": f"{processing_ms}ms",
|
||||||
|
"creditsConsumed": 1 if should_deduct else 0,
|
||||||
|
"archived": False,
|
||||||
|
"createdAt": now,
|
||||||
|
"updatedAt": now,
|
||||||
|
}
|
||||||
|
|
||||||
|
insert = await db["results"].insert_one(result_doc)
|
||||||
|
stored = await db["results"].find_one({"_id": insert.inserted_id})
|
||||||
|
response_payload = jsonable(stored) if stored else jsonable({**result_doc, "_id": str(insert.inserted_id)})
|
||||||
|
response_payload = _sanitize_result_payload(response_payload)
|
||||||
|
|
||||||
await AuditService.log_consulta(
|
await AuditService.log_consulta(
|
||||||
db=db,
|
db=db,
|
||||||
tenant_id=user.tenant,
|
tenant_id=user.tenant,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
consulta_type="individual",
|
consulta_type="individual",
|
||||||
rut=rut,
|
rut=rut,
|
||||||
endpoint="/api/v1/rut/lookup",
|
endpoint="/client-api-v1/rut/lookup",
|
||||||
response_status=response_status,
|
response_status=response_status,
|
||||||
request_data={"rut": rut, "isMonitoring": payload.isMonitoring},
|
request_data=jsonable(payload.model_dump()),
|
||||||
response_data=audit_response,
|
response_data={"resultId": response_payload.get("_id")},
|
||||||
error_message=error_message,
|
|
||||||
processing_time_ms=processing_ms,
|
processing_time_ms=processing_ms,
|
||||||
credits_used=1,
|
credits_used=1 if should_deduct else 0,
|
||||||
ip_address=request.client.host if request.client else None,
|
ip_address=request.client.host if request.client else None,
|
||||||
user_agent=request.headers.get("user-agent"),
|
user_agent=request.headers.get("user-agent"),
|
||||||
metadata={"evaluationType": "single", "jobId": job_id},
|
metadata={"evaluationType": "single", "source": "fastcheck_api"},
|
||||||
)
|
)
|
||||||
|
return response_payload
|
||||||
|
except Exception as e:
|
||||||
|
response_status = status.HTTP_502_BAD_GATEWAY
|
||||||
|
error_message = str(e)
|
||||||
|
logger.exception(f"Native lookup failed error={error_message[:500]}")
|
||||||
|
await AuditService.log_consulta(
|
||||||
|
db=db,
|
||||||
|
tenant_id=user.tenant,
|
||||||
|
user_id=user.id,
|
||||||
|
consulta_type="individual",
|
||||||
|
rut=rut,
|
||||||
|
endpoint="/client-api-v1/rut/lookup",
|
||||||
|
response_status=response_status,
|
||||||
|
request_data=jsonable(payload.model_dump()),
|
||||||
|
error_message=error_message[:2000],
|
||||||
|
processing_time_ms=int((time.perf_counter() - start) * 1000),
|
||||||
|
credits_used=1 if should_deduct else 0,
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
metadata={"evaluationType": "single", "source": "fastcheck_api"},
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=response_status, detail={"message": "Native lookup failed", "error": error_message[:2000]})
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,19 @@ class Settings(BaseSettings):
|
||||||
legacy_base_url: str = Field(default="http://localhost:4040/api", validation_alias="LEGACY_BASE_URL")
|
legacy_base_url: str = Field(default="http://localhost:4040/api", validation_alias="LEGACY_BASE_URL")
|
||||||
legacy_http_timeout_seconds: int = Field(default=30, validation_alias="LEGACY_HTTP_TIMEOUT_SECONDS")
|
legacy_http_timeout_seconds: int = Field(default=30, validation_alias="LEGACY_HTTP_TIMEOUT_SECONDS")
|
||||||
|
|
||||||
|
sheriff_v2_base_url: str | None = Field(default=None, validation_alias="SHERIFF_V2_BASE_URL")
|
||||||
|
sheriff_v2_access_key: str | None = Field(default=None, validation_alias="SHERIFF_V2_ACCESS_KEY")
|
||||||
|
sheriff_v2_access_secret: str | None = Field(default=None, validation_alias="SHERIFF_V2_ACCESS_SECRET")
|
||||||
|
sheriff_v2_client_identifier: str = Field(
|
||||||
|
default="SheriffSecureClient-v1",
|
||||||
|
validation_alias="SHERIFF_V2_CLIENT_IDENTIFIER",
|
||||||
|
)
|
||||||
|
sheriff_v2_token_ttl_seconds: int = Field(default=600, validation_alias="SHERIFF_V2_TOKEN_TTL_SECONDS")
|
||||||
|
|
||||||
|
dequienes_base_url: str = Field(default="http://127.0.0.1:8023", validation_alias="DEQUIENES_BASE_URL")
|
||||||
|
dequienes_api_key: str | None = Field(default=None, validation_alias="DEQUIENES_API_KEY")
|
||||||
|
dequienes_http_timeout_seconds: int = Field(default=30, validation_alias="DEQUIENES_HTTP_TIMEOUT_SECONDS")
|
||||||
|
|
||||||
rabbitmq_enabled: bool = Field(default=True, validation_alias="RABBITMQ_ENABLED")
|
rabbitmq_enabled: bool = Field(default=True, validation_alias="RABBITMQ_ENABLED")
|
||||||
rabbitmq_queue: str = Field(default="evaluation_queue", validation_alias="RABBITMQ_QUEUE")
|
rabbitmq_queue: str = Field(default="evaluation_queue", validation_alias="RABBITMQ_QUEUE")
|
||||||
rabbitmq_url: str | None = Field(default=None, validation_alias="RABBITMQ_URL")
|
rabbitmq_url: str | None = Field(default=None, validation_alias="RABBITMQ_URL")
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ from fastapi import APIRouter
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
|
||||||
from fastcheck_api.app.api.v1.routers import auth, checks, reports, rut, usage
|
#from fastcheck_api.app.api.v1.routers import auth, checks, reports, rut, usage
|
||||||
|
from fastcheck_api.app.api.v1.routers import auth, checks, rut, usage
|
||||||
from fastcheck_api.app.core.logging import configure_logging
|
from fastcheck_api.app.core.logging import configure_logging
|
||||||
from fastcheck_api.app.core.mongodb import lifespan_db
|
from fastcheck_api.app.core.mongodb import lifespan_db
|
||||||
from fastcheck_api.app.services.legacy_service import legacy_service
|
from fastcheck_api.app.services.legacy_service import legacy_service
|
||||||
|
from fastcheck_api.app.services.dequienes_native_service import dequienes_native_service
|
||||||
|
from fastcheck_api.app.services.sheriff_v2_native_service import sheriff_v2_native_service
|
||||||
from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service
|
from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service
|
||||||
from fastcheck_api.app.middleware.request_logging import RequestLoggingMiddleware
|
from fastcheck_api.app.middleware.request_logging import RequestLoggingMiddleware
|
||||||
from fastcheck_api.app.middleware.tenant_context import TenantContextMiddleware
|
from fastcheck_api.app.middleware.tenant_context import TenantContextMiddleware
|
||||||
|
|
@ -46,6 +49,8 @@ async def lifespan(app: FastAPI):
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("RabbitMQ shutdown failed")
|
logger.exception("RabbitMQ shutdown failed")
|
||||||
await legacy_service.close()
|
await legacy_service.close()
|
||||||
|
await sheriff_v2_native_service.close()
|
||||||
|
await dequienes_native_service.aclose()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
|
@ -156,7 +161,7 @@ async def prefixed_openapi():
|
||||||
|
|
||||||
api_v1.include_router(auth.router)
|
api_v1.include_router(auth.router)
|
||||||
api_v1.include_router(checks.router)
|
api_v1.include_router(checks.router)
|
||||||
api_v1.include_router(reports.router)
|
#api_v1.include_router(reports.router)
|
||||||
api_v1.include_router(rut.router)
|
api_v1.include_router(rut.router)
|
||||||
api_v1.include_router(usage.router)
|
api_v1.include_router(usage.router)
|
||||||
app.include_router(api_v1)
|
app.include_router(api_v1)
|
||||||
|
|
|
||||||
76
fastcheck_api/app/services/dequienes_native_service.py
Normal file
76
fastcheck_api/app/services/dequienes_native_service.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from fastcheck_api.app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_rut_for_dequienes(rut: str) -> str:
|
||||||
|
rut_no_dots = (rut or "").replace(".", "").strip()
|
||||||
|
parts = rut_no_dots.split("-", 1)
|
||||||
|
numeric = re.sub(r"\D+", "", parts[0])
|
||||||
|
numeric = re.sub(r"^0+", "", numeric) or "0"
|
||||||
|
return numeric
|
||||||
|
|
||||||
|
|
||||||
|
class DequienesNativeService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._client: httpx.AsyncClient | None = None
|
||||||
|
|
||||||
|
def _get_client(self) -> httpx.AsyncClient:
|
||||||
|
if self._client is None:
|
||||||
|
self._client = httpx.AsyncClient(
|
||||||
|
base_url=settings.dequienes_base_url.rstrip("/"),
|
||||||
|
timeout=httpx.Timeout(settings.dequienes_http_timeout_seconds),
|
||||||
|
)
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
if self._client is not None:
|
||||||
|
await self._client.aclose()
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
headers: dict[str, str] = {"accept": "application/json"}
|
||||||
|
api_key = (settings.dequienes_api_key or "").strip()
|
||||||
|
if api_key:
|
||||||
|
headers["x-api-key"] = api_key
|
||||||
|
return headers
|
||||||
|
|
||||||
|
async def relationships(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
rut: str,
|
||||||
|
distance: int = 2,
|
||||||
|
relationship_direction: str = "BOTH",
|
||||||
|
one_path_per_node: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
rut_sanitized = _sanitize_rut_for_dequienes(rut)
|
||||||
|
client = self._get_client()
|
||||||
|
resp = await client.get(
|
||||||
|
f"/relationships/{rut_sanitized}",
|
||||||
|
params={
|
||||||
|
"distance": int(distance),
|
||||||
|
"relationship_direction": relationship_direction,
|
||||||
|
"one_path_per_node": "true" if one_path_per_node else "false",
|
||||||
|
},
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return data if isinstance(data, dict) else {"raw": data}
|
||||||
|
|
||||||
|
async def legal_events(self, *, rut: str) -> dict[str, Any]:
|
||||||
|
rut_sanitized = _sanitize_rut_for_dequienes(rut)
|
||||||
|
client = self._get_client()
|
||||||
|
resp = await client.get(f"/api/legal-events/{rut_sanitized}", headers=self._headers())
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
return data if isinstance(data, dict) else {"raw": data}
|
||||||
|
|
||||||
|
|
||||||
|
dequienes_native_service = DequienesNativeService()
|
||||||
|
|
||||||
697
fastcheck_api/app/services/risk_calculation_service.py
Normal file
697
fastcheck_api/app/services/risk_calculation_service.py
Normal file
|
|
@ -0,0 +1,697 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||||
|
|
||||||
|
from fastcheck_api.app.utils.mongo import sanitize_rut
|
||||||
|
|
||||||
|
|
||||||
|
def _as_dict(value: Any) -> dict[str, Any]:
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list(value: Any) -> list[Any]:
|
||||||
|
return value if isinstance(value, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_get(value: Any, *path: str, default: Any = None) -> Any:
|
||||||
|
cur = value
|
||||||
|
for key in path:
|
||||||
|
if not isinstance(cur, dict):
|
||||||
|
return default
|
||||||
|
cur = cur.get(key)
|
||||||
|
return default if cur is None else cur
|
||||||
|
|
||||||
|
|
||||||
|
def _risk_from_impact(detected: bool, impacto: str) -> str:
|
||||||
|
if not detected:
|
||||||
|
return "bajo"
|
||||||
|
if impacto == "severo":
|
||||||
|
return "critico"
|
||||||
|
if impacto == "significativo":
|
||||||
|
return "alto"
|
||||||
|
return "bajo"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text(value: str) -> str:
|
||||||
|
value = (value or "").strip()
|
||||||
|
value = value.replace("Á", "A").replace("É", "E").replace("Í", "I").replace("Ó", "O").replace("Ú", "U")
|
||||||
|
value = value.replace("á", "a").replace("é", "e").replace("í", "i").replace("ó", "o").replace("ú", "u")
|
||||||
|
value = re.sub(r"[^\w\s]", " ", value, flags=re.UNICODE)
|
||||||
|
value = re.sub(r"\s+", " ", value, flags=re.UNICODE).strip()
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _rut_variants(rut: str) -> list[str]:
|
||||||
|
rut = sanitize_rut(rut)
|
||||||
|
clean = rut.replace(".", "").replace("-", "").upper()
|
||||||
|
if len(clean) < 2:
|
||||||
|
return [rut]
|
||||||
|
dotted = f"{clean[:-1]:0>9}"
|
||||||
|
dotted = re.sub(r"^(\d{2})(\d{3})(\d{3})$", r"\1.\2.\3", dotted[:-1]) + "-" + clean[-1]
|
||||||
|
with_dash = f"{clean[:-1]}-{clean[-1]}"
|
||||||
|
return list(dict.fromkeys([rut, with_dash, clean, dotted]))
|
||||||
|
|
||||||
|
|
||||||
|
class RiskCalculationService:
|
||||||
|
@staticmethod
|
||||||
|
async def _load_equifax_normalized(
|
||||||
|
db: AsyncIOMotorDatabase, *, tenant_id: str, rut: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
rut = sanitize_rut(rut)
|
||||||
|
base = rut.replace(".", "").replace("-", "").upper()
|
||||||
|
rut_with_dash = f"{base[:-1]}-{base[-1]}" if len(base) > 1 else rut
|
||||||
|
rut_without_dash = base
|
||||||
|
query: dict[str, Any] = {"tenantId": tenant_id, "$or": [{"rut": rut}, {"rut": rut_with_dash}, {"rut": rut_without_dash}]}
|
||||||
|
doc = await db["equifax-responses"].find_one(query, sort=[("createdAt", -1)])
|
||||||
|
if not doc or not isinstance(doc, dict):
|
||||||
|
return None
|
||||||
|
normalized = doc.get("normalizedData")
|
||||||
|
return normalized if isinstance(normalized, dict) else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _load_antiunion_cases(db: AsyncIOMotorDatabase, *, rut: str) -> list[dict[str, Any]]:
|
||||||
|
variants = _rut_variants(rut)
|
||||||
|
ors: list[dict[str, Any]] = []
|
||||||
|
for v in variants:
|
||||||
|
ors.append({"rut": {"$regex": f"^{re.escape(v)}$", "$options": "i"}})
|
||||||
|
cursor = db["antiunioncases"].find({"$or": ors}).sort([("createdAt", -1)]).limit(200)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
async for doc in cursor:
|
||||||
|
if isinstance(doc, dict):
|
||||||
|
out.append(doc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _load_ley_records(db: AsyncIOMotorDatabase, *, collection: str, rut: str) -> list[dict[str, Any]]:
|
||||||
|
clean = rut.replace(".", "").upper()
|
||||||
|
cursor = db[collection].find({"rut": clean}).sort([("createdAt", -1)]).limit(200)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
async for doc in cursor:
|
||||||
|
if isinstance(doc, dict):
|
||||||
|
out.append(doc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _load_listas_propias(db: AsyncIOMotorDatabase, *, collection: str, tenant_id: str, rut: str) -> list[dict[str, Any]]:
|
||||||
|
clean = rut.replace(".", "").upper()
|
||||||
|
cursor = db[collection].find({"rut": clean, "tenant": tenant_id}).sort([("createdAt", -1)]).limit(200)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
async for doc in cursor:
|
||||||
|
if isinstance(doc, dict):
|
||||||
|
out.append(doc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _find_snifa_by_company_name(
|
||||||
|
db: AsyncIOMotorDatabase,
|
||||||
|
*,
|
||||||
|
razon_social: str,
|
||||||
|
field_flag: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
name = _normalize_text(razon_social)
|
||||||
|
if not name:
|
||||||
|
return []
|
||||||
|
tokens = [t for t in name.split(" ") if len(t) >= 3]
|
||||||
|
tokens = tokens[:6]
|
||||||
|
if not tokens:
|
||||||
|
return []
|
||||||
|
regex = ".*".join(re.escape(t) for t in tokens)
|
||||||
|
query = {
|
||||||
|
field_flag: {"$regex": r"^SI$", "$options": "i"},
|
||||||
|
"razonSocial": {"$regex": regex, "$options": "i"},
|
||||||
|
}
|
||||||
|
cursor = db["snifasancionatorios"].find(query).sort([("createdAt", -1)]).limit(200)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
async for doc in cursor:
|
||||||
|
if isinstance(doc, dict):
|
||||||
|
out.append(doc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _evaluate_compliance_rules(log_entry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
compliance_rules: list[dict[str, Any]] = []
|
||||||
|
summary = _as_dict(_safe_get(log_entry, "summaryData", "data", default={}))
|
||||||
|
v2 = _as_dict(_safe_get(log_entry, "filteredDetails", "sheriffV2Data", default={}))
|
||||||
|
compliance_data = _as_dict(_safe_get(v2, "compliance", "data", default={}))
|
||||||
|
compliance_local = _as_dict(_safe_get(log_entry, "filteredDetails", "compliance", default={}))
|
||||||
|
|
||||||
|
ley21121_records = _as_list(summary.get("ley21121Records") or [])
|
||||||
|
ley21121_detected = bool(summary.get("ley21121Detected"))
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Condenas Ley 21.121",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": ley21121_detected,
|
||||||
|
"risk": _risk_from_impact(ley21121_detected, "severo"),
|
||||||
|
"score": len(ley21121_records),
|
||||||
|
"details": ley21121_records,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ley20393_records = _as_list(summary.get("ley20393Records") or [])
|
||||||
|
ley20393_detected = bool(summary.get("ley20393Detected"))
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Condenas Ley 20.393",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": ley20393_detected,
|
||||||
|
"risk": _risk_from_impact(ley20393_detected, "severo"),
|
||||||
|
"score": len(ley20393_records),
|
||||||
|
"details": ley20393_records,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
listas_int = _as_list(_safe_get(compliance_data, "listasInternacionales", "coincidencias", default=[]))
|
||||||
|
detected = len(listas_int) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Listas Internacionales",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "severo"),
|
||||||
|
"score": len(listas_int),
|
||||||
|
"details": listas_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sanciones = _as_list(_safe_get(summary, "snifaSancionesRecords", default=[]))
|
||||||
|
detected = len(sanciones) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Sanciones Medioambientales",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(sanciones),
|
||||||
|
"details": sanciones,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
procesos = _as_list(_safe_get(summary, "snifaProcesoRecords", default=[]))
|
||||||
|
detected = len(procesos) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Proceso Sanciones Medioambientales",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(procesos),
|
||||||
|
"details": procesos,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
lpaltos = _as_list(summary.get("lpaltosRecords") or [])
|
||||||
|
lpaltos_detected = bool(summary.get("lpaltosDetected"))
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Listas Propias Alto Impacto",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": lpaltos_detected,
|
||||||
|
"risk": _risk_from_impact(lpaltos_detected, "severo"),
|
||||||
|
"score": len(lpaltos),
|
||||||
|
"details": lpaltos,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
lpmedios = _as_list(summary.get("lpmediosRecords") or [])
|
||||||
|
lpmedios_detected = bool(summary.get("lpmediosDetected"))
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Listas Propias Mediano Impacto",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": lpmedios_detected,
|
||||||
|
"risk": _risk_from_impact(lpmedios_detected, "significativo"),
|
||||||
|
"score": len(lpmedios),
|
||||||
|
"details": lpmedios,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
noticias = _as_list(_safe_get(compliance_local, "noticias", "coincidencias", default=[]))
|
||||||
|
detected = len(noticias) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Reputación Pública y Mediática",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "severo"),
|
||||||
|
"score": len(noticias),
|
||||||
|
"details": noticias,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pep = _as_list(_safe_get(compliance_data, "pepChile", "coincidencias", default=[]))
|
||||||
|
detected = len(pep) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "PEP Chile",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(pep),
|
||||||
|
"details": pep,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
penales = _as_list(_safe_get(compliance_data, "penal", "coincidencias", default=[]))
|
||||||
|
filtered_penales: list[Any] = []
|
||||||
|
for d in penales:
|
||||||
|
posture = str(_safe_get(d, "postura", default="") or "").lower()
|
||||||
|
if posture and posture not in {"denunciante", "querellante"}:
|
||||||
|
filtered_penales.append(d)
|
||||||
|
detected = len(filtered_penales) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Causas Penales",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "severo"),
|
||||||
|
"score": len(filtered_penales),
|
||||||
|
"details": filtered_penales,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
familiares = _as_list(_safe_get(compliance_data, "familiaresPep", "coincidencias", default=[]))
|
||||||
|
detected = len(familiares) > 0
|
||||||
|
compliance_rules.append(
|
||||||
|
{
|
||||||
|
"label": "Familiares PEP",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(familiares),
|
||||||
|
"details": familiares,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return compliance_rules
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _evaluate_capital_humano_rules(log_entry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
antiunion = _as_list(_safe_get(log_entry, "antiunionCases", default=[]))
|
||||||
|
detected = len(antiunion) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Condenas por Prácticas Antisindicales",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "severo"),
|
||||||
|
"score": len(antiunion),
|
||||||
|
"details": antiunion,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
compliance_person_type = str(_safe_get(log_entry, "filteredDetails", "compliancePersonType", default="") or "").lower()
|
||||||
|
equifax = _as_dict(_safe_get(log_entry, "equifaxData", default={}))
|
||||||
|
bolab = _as_list(_safe_get(equifax, "allData", "commercialData", "credit", "debtsSummary", "bolab", "commercialBolab", default=[]))
|
||||||
|
deuda_previsional = []
|
||||||
|
if compliance_person_type == "juridical":
|
||||||
|
for entry in bolab:
|
||||||
|
it = str(_safe_get(entry, "injuryType", default="") or _safe_get(entry, "injurytype", default="") or "").upper()
|
||||||
|
if it != "M":
|
||||||
|
deuda_previsional.append(entry)
|
||||||
|
detected = len(deuda_previsional) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Deuda Previsional Publicada",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(deuda_previsional),
|
||||||
|
"details": deuda_previsional,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bolab_j = _as_list(_safe_get(log_entry, "filteredDetails", "bolabTypePersonaJuridica", default=[]))
|
||||||
|
bolab_n = _as_list(_safe_get(log_entry, "filteredDetails", "bolabTypePersonaNatural", default=[]))
|
||||||
|
multas = []
|
||||||
|
for entry in [*bolab_j, *bolab_n]:
|
||||||
|
it = str(_safe_get(entry, "injuryType", default="") or _safe_get(entry, "injurytype", default="") or "").upper()
|
||||||
|
if it == "M":
|
||||||
|
multas.append(entry)
|
||||||
|
detected = len(multas) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Multas Laborales",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(multas),
|
||||||
|
"details": multas,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
v2 = _as_dict(_safe_get(log_entry, "filteredDetails", "sheriffV2Data", default={}))
|
||||||
|
mora_casos = _as_list(_safe_get(v2, "cobranzaLaboral", "moraPrevisional", "data", "casos", default=[]))
|
||||||
|
detected = len(mora_casos) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Deuda Previsional Presunta",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(mora_casos),
|
||||||
|
"details": mora_casos,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_cases(value: Any) -> list[Any]:
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
if isinstance(value, dict):
|
||||||
|
data = value.get("data")
|
||||||
|
if isinstance(data, dict):
|
||||||
|
cases = data.get("casos")
|
||||||
|
if isinstance(cases, list):
|
||||||
|
return cases
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _evaluate_legal_rules(log_entry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
v2 = _as_dict(_safe_get(log_entry, "filteredDetails", "sheriffV2Data", default={}))
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
casos_quiebra = int(_safe_get(v2, "resumen", "data", "judicial", "casosQuiebra", default=0) or 0)
|
||||||
|
detected = casos_quiebra > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Quiebra Judicial",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "severo"),
|
||||||
|
"score": casos_quiebra,
|
||||||
|
"details": ["Existe quiebra judicial"] if detected else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
equifax = _as_dict(_safe_get(log_entry, "equifaxData", default={}))
|
||||||
|
boletin_concursal = _as_list(
|
||||||
|
_safe_get(
|
||||||
|
equifax,
|
||||||
|
"allData",
|
||||||
|
"commercialData",
|
||||||
|
"credit",
|
||||||
|
"boletinConcursal",
|
||||||
|
"detailBoletinConcursal",
|
||||||
|
"commercialDetailBoletinConcursal",
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
detected = len(boletin_concursal) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Boletin Concursal",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(boletin_concursal),
|
||||||
|
"details": boletin_concursal,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
civil_cases = RiskCalculationService._extract_cases(_safe_get(v2, "judicial", "civil", default={}))
|
||||||
|
detected = len(civil_cases) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Causas Civiles",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(civil_cases),
|
||||||
|
"details": civil_cases,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
laboral_cases = RiskCalculationService._extract_cases(_safe_get(v2, "judicial", "laboral", default={}))
|
||||||
|
detected = len(laboral_cases) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Causas Laborales",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(laboral_cases),
|
||||||
|
"details": laboral_cases,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cobranza_cases = RiskCalculationService._extract_cases(_safe_get(v2, "judicial", "cobranza", default={}))
|
||||||
|
detected = len(cobranza_cases) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Causas de Cobranza Laboral",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(cobranza_cases),
|
||||||
|
"details": cobranza_cases,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _evaluate_financiero_tributario_rules(log_entry: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
v2 = _as_dict(_safe_get(log_entry, "filteredDetails", "sheriffV2Data", default={}))
|
||||||
|
ident = _as_dict(_safe_get(v2, "resumen", "data", "identificacion", default={}))
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
observaciones = str(_safe_get(ident, "observaciones", default="") or "")
|
||||||
|
termino = "término de giro" in observaciones.lower()
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Término de Giro",
|
||||||
|
"impacto": "severo",
|
||||||
|
"detected": termino,
|
||||||
|
"risk": _risk_from_impact(termino, "severo"),
|
||||||
|
"score": 1 if termino else 0,
|
||||||
|
"details": observaciones if termino else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
equifax = _as_dict(_safe_get(log_entry, "filteredDetails", "equifaxData", default={}))
|
||||||
|
icom = _as_list(_safe_get(equifax, "allData", "commercialData", "credit", "debtsSummary", "icom", "commercialIcom", default=[]))
|
||||||
|
if not icom:
|
||||||
|
icom = _as_list(_safe_get(equifax, "protestosMorosidadesPersonaNaturalList", default=[]))
|
||||||
|
detected = len(icom) > 0
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Protestos y Morosidades",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": len(icom),
|
||||||
|
"details": icom,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
inicio_actividades = _safe_get(ident, "inicioActividades", default=None)
|
||||||
|
actividad = _as_list(_safe_get(ident, "actividadEconomicaVigente", default=[]))
|
||||||
|
missing_inicio = inicio_actividades is None
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Inicio de Actividades",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": missing_inicio,
|
||||||
|
"risk": _risk_from_impact(missing_inicio, "significativo"),
|
||||||
|
"score": 1 if missing_inicio else 0,
|
||||||
|
"details": actividad if not missing_inicio else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
situacion = str(_safe_get(ident, "situacionActual", default="") or "")
|
||||||
|
detected = bool(situacion) and not re.search(r"No se encuentra", situacion, flags=re.IGNORECASE)
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"label": "Contribuyente de difícil fiscalización",
|
||||||
|
"impacto": "significativo",
|
||||||
|
"detected": detected,
|
||||||
|
"risk": _risk_from_impact(detected, "significativo"),
|
||||||
|
"score": 1 if detected else 0,
|
||||||
|
"details": [{"situacionActual": situacion}] if detected else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _risk_summary(
|
||||||
|
compliance_rules: list[dict[str, Any]],
|
||||||
|
legal_rules: list[dict[str, Any]],
|
||||||
|
capital_humano_rules: list[dict[str, Any]],
|
||||||
|
financiero_tributario_rules: list[dict[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
def _count_detected(rules: list[dict[str, Any]], impacto: str) -> int:
|
||||||
|
return sum(1 for r in rules if r.get("impacto") == impacto and bool(r.get("detected")))
|
||||||
|
|
||||||
|
total_severo = _count_detected(compliance_rules, "severo") + _count_detected(legal_rules, "severo") + _count_detected(capital_humano_rules, "severo") + _count_detected(financiero_tributario_rules, "severo")
|
||||||
|
total_significativo = _count_detected(compliance_rules, "significativo") + _count_detected(legal_rules, "significativo") + _count_detected(capital_humano_rules, "significativo") + _count_detected(financiero_tributario_rules, "significativo")
|
||||||
|
total_detected = (
|
||||||
|
sum(1 for r in compliance_rules if r.get("detected"))
|
||||||
|
+ sum(1 for r in legal_rules if r.get("detected"))
|
||||||
|
+ sum(1 for r in capital_humano_rules if r.get("detected"))
|
||||||
|
+ sum(1 for r in financiero_tributario_rules if r.get("detected"))
|
||||||
|
)
|
||||||
|
total_parameters = len(compliance_rules) + len(legal_rules) + len(capital_humano_rules) + len(financiero_tributario_rules)
|
||||||
|
|
||||||
|
semaphore = "green"
|
||||||
|
if total_severo > 0:
|
||||||
|
semaphore = "red"
|
||||||
|
elif total_significativo > 0.7 * total_parameters:
|
||||||
|
semaphore = "red"
|
||||||
|
elif total_significativo > 0.5 * total_parameters:
|
||||||
|
semaphore = "orange"
|
||||||
|
elif total_significativo > 0:
|
||||||
|
semaphore = "yellow"
|
||||||
|
|
||||||
|
icon = "🟢"
|
||||||
|
desc = "Riego Bajo"
|
||||||
|
if semaphore == "red":
|
||||||
|
icon = "🔴"
|
||||||
|
desc = "Riesgo Crítico"
|
||||||
|
elif semaphore == "orange":
|
||||||
|
icon = "🟠"
|
||||||
|
desc = "Riesgo Alto"
|
||||||
|
elif semaphore == "yellow":
|
||||||
|
icon = "🟡"
|
||||||
|
desc = "Riesgo Medio"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"totalSevero": total_severo,
|
||||||
|
"totalSignificativo": total_significativo,
|
||||||
|
"totalDetected": total_detected,
|
||||||
|
"totalParameters": total_parameters,
|
||||||
|
"semaphore": semaphore,
|
||||||
|
"semaphoreIcon": icon,
|
||||||
|
"riskLevelDescription": desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_doc_markdown(rut: str, razon_social: str | None, risk_summary: dict[str, Any], all_rules: dict[str, Any]) -> str:
|
||||||
|
company = razon_social or "N/A"
|
||||||
|
lines = [
|
||||||
|
f"# Evaluación de Riesgo\n",
|
||||||
|
f"**RUT**: {rut}\n",
|
||||||
|
f"**Razón Social**: {company}\n",
|
||||||
|
f"**Semáforo**: {risk_summary.get('semaphoreIcon')} ({risk_summary.get('riskLevelDescription')})\n",
|
||||||
|
"\n",
|
||||||
|
"## Resumen\n",
|
||||||
|
f"- Total parámetros: {risk_summary.get('totalParameters')}\n",
|
||||||
|
f"- Detectados: {risk_summary.get('totalDetected')}\n",
|
||||||
|
f"- Severos detectados: {risk_summary.get('totalSevero')}\n",
|
||||||
|
f"- Significativos detectados: {risk_summary.get('totalSignificativo')}\n",
|
||||||
|
"\n",
|
||||||
|
"## Detalle de Reglas\n",
|
||||||
|
]
|
||||||
|
for section_key, title in [
|
||||||
|
("complianceRules", "Compliance"),
|
||||||
|
("legalRules", "Legal"),
|
||||||
|
("capitalHumanoRules", "Capital Humano"),
|
||||||
|
("financieroTributarioRules", "Financiero / Tributario"),
|
||||||
|
]:
|
||||||
|
rules = _as_list(_safe_get(all_rules, section_key, default=[]))
|
||||||
|
lines.append(f"### {title}\n")
|
||||||
|
lines.append("| Parámetro | Impacto | Riesgo | Detectado |\n")
|
||||||
|
lines.append("|---|---|---|---|\n")
|
||||||
|
for r in rules:
|
||||||
|
label = str(_safe_get(r, "label", default="") or "")
|
||||||
|
impacto = str(_safe_get(r, "impacto", default="") or "")
|
||||||
|
risk = str(_safe_get(r, "risk", default="") or "")
|
||||||
|
detected = "SI" if bool(_safe_get(r, "detected", default=False)) else "NO"
|
||||||
|
lines.append(f"| {label} | {impacto} | {risk} | {detected} |\n")
|
||||||
|
lines.append("\n")
|
||||||
|
return "".join(lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def calculate_risk(
|
||||||
|
db: AsyncIOMotorDatabase,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
rut: str,
|
||||||
|
sheriff_v2_data: dict[str, Any],
|
||||||
|
filtered_details: dict[str, Any],
|
||||||
|
company_general_info: dict[str, Any] | None,
|
||||||
|
is_pep_only: bool,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
if is_pep_only:
|
||||||
|
return None
|
||||||
|
|
||||||
|
clean_rut = sanitize_rut(rut)
|
||||||
|
|
||||||
|
ley21121 = await RiskCalculationService._load_ley_records(db, collection="ley21121s", rut=clean_rut)
|
||||||
|
ley20393 = await RiskCalculationService._load_ley_records(db, collection="ley20393s", rut=clean_rut)
|
||||||
|
lpaltos = await RiskCalculationService._load_listas_propias(db, collection="lpaltos", tenant_id=tenant_id, rut=clean_rut)
|
||||||
|
lpmedios = await RiskCalculationService._load_listas_propias(db, collection="lpmedios", tenant_id=tenant_id, rut=clean_rut)
|
||||||
|
antiunion = await RiskCalculationService._load_antiunion_cases(db, rut=clean_rut)
|
||||||
|
equifax = await RiskCalculationService._load_equifax_normalized(db, tenant_id=tenant_id, rut=clean_rut)
|
||||||
|
|
||||||
|
razon_social = str(filtered_details.get("razonSocial") or "")
|
||||||
|
snifa_sanciones = await RiskCalculationService._find_snifa_by_company_name(
|
||||||
|
db, razon_social=razon_social, field_flag="fastCheckSanciones"
|
||||||
|
)
|
||||||
|
snifa_procesos = await RiskCalculationService._find_snifa_by_company_name(
|
||||||
|
db, razon_social=razon_social, field_flag="fastCheckProcesoSancionatorio"
|
||||||
|
)
|
||||||
|
|
||||||
|
summary_data = {
|
||||||
|
"ley21121Detected": len(ley21121) > 0,
|
||||||
|
"ley21121Records": ley21121,
|
||||||
|
"ley20393Detected": len(ley20393) > 0,
|
||||||
|
"ley20393Records": ley20393,
|
||||||
|
"lpaltosDetected": len(lpaltos) > 0,
|
||||||
|
"lpaltosRecords": lpaltos,
|
||||||
|
"lpmediosDetected": len(lpmedios) > 0,
|
||||||
|
"lpmediosRecords": lpmedios,
|
||||||
|
"snifaSancionesRecords": snifa_sanciones,
|
||||||
|
"snifaProcesoRecords": snifa_procesos,
|
||||||
|
}
|
||||||
|
|
||||||
|
log_entry: dict[str, Any] = {
|
||||||
|
"rut": clean_rut,
|
||||||
|
"tenantId": tenant_id,
|
||||||
|
"summaryData": {"data": summary_data},
|
||||||
|
"filteredDetails": {**filtered_details, "sheriffV2Data": sheriff_v2_data},
|
||||||
|
"companyGeneralInfo": company_general_info or {},
|
||||||
|
"antiunionCases": antiunion,
|
||||||
|
"equifaxData": equifax or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if equifax:
|
||||||
|
log_entry["filteredDetails"]["equifaxData"] = equifax
|
||||||
|
if "bolabTypePersonaJuridica" in equifax:
|
||||||
|
log_entry["filteredDetails"]["bolabTypePersonaJuridica"] = _as_list(equifax.get("bolabTypePersonaJuridica"))
|
||||||
|
if "bolabTypePersonaNatural" in equifax:
|
||||||
|
log_entry["filteredDetails"]["bolabTypePersonaNatural"] = _as_list(equifax.get("bolabTypePersonaNatural"))
|
||||||
|
if "protestosMorosidadesPersonaNaturalList" in equifax:
|
||||||
|
log_entry["filteredDetails"]["protestosMorosidadesPersonaNaturalList"] = _as_list(equifax.get("protestosMorosidadesPersonaNaturalList"))
|
||||||
|
|
||||||
|
compliance_rules = RiskCalculationService._evaluate_compliance_rules(log_entry)
|
||||||
|
legal_rules = RiskCalculationService._evaluate_legal_rules(log_entry)
|
||||||
|
capital_rules = RiskCalculationService._evaluate_capital_humano_rules(log_entry)
|
||||||
|
financiero_rules = RiskCalculationService._evaluate_financiero_tributario_rules(log_entry)
|
||||||
|
|
||||||
|
all_rules = {
|
||||||
|
"complianceRules": compliance_rules,
|
||||||
|
"legalRules": legal_rules,
|
||||||
|
"capitalHumanoRules": capital_rules,
|
||||||
|
"financieroTributarioRules": financiero_rules,
|
||||||
|
}
|
||||||
|
risk_summary = RiskCalculationService._risk_summary(compliance_rules, legal_rules, capital_rules, financiero_rules)
|
||||||
|
summary_md = RiskCalculationService._build_doc_markdown(clean_rut, razon_social or None, risk_summary, all_rules)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"riskSummary": risk_summary,
|
||||||
|
"allRules": all_rules,
|
||||||
|
"summaryDocumentMD": summary_md,
|
||||||
|
"financialRisk": None,
|
||||||
|
}
|
||||||
|
|
||||||
144
fastcheck_api/app/services/sheriff_v2_native_service.py
Normal file
144
fastcheck_api/app/services/sheriff_v2_native_service.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from fastcheck_api.app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class SheriffV2NativeService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._client = httpx.AsyncClient(timeout=60)
|
||||||
|
self._token: str | None = None
|
||||||
|
self._token_expires_at: float = 0.0
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
await self._client.aclose()
|
||||||
|
|
||||||
|
def _base_url(self) -> str:
|
||||||
|
base_url = (settings.sheriff_v2_base_url or "").strip()
|
||||||
|
if not base_url:
|
||||||
|
raise RuntimeError("Missing SHERIFF_V2_BASE_URL")
|
||||||
|
return base_url.rstrip("/")
|
||||||
|
|
||||||
|
def _client_identifier(self) -> str:
|
||||||
|
return (settings.sheriff_v2_client_identifier or "SheriffSecureClient-v1").strip()
|
||||||
|
|
||||||
|
def _credentials(self) -> tuple[str, str]:
|
||||||
|
access_key = (settings.sheriff_v2_access_key or "").strip()
|
||||||
|
access_secret = (settings.sheriff_v2_access_secret or "").strip()
|
||||||
|
if not access_key or not access_secret:
|
||||||
|
raise RuntimeError("Missing shf V2 access credentials")
|
||||||
|
return access_key, access_secret
|
||||||
|
|
||||||
|
async def _get_token(self) -> str:
|
||||||
|
now = time.time()
|
||||||
|
if self._token and now < (self._token_expires_at - 30):
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
base_url = self._base_url()
|
||||||
|
access_key, access_secret = self._credentials()
|
||||||
|
url = f"{base_url}/api/clients/v2/apiCredentials/getToken"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"accept": "application/json",
|
||||||
|
"x-client-identifier": self._client_identifier(),
|
||||||
|
}
|
||||||
|
resp = await self._client.post(url, headers=headers, json={"accessKey": access_key, "accessSecret": access_secret})
|
||||||
|
resp.raise_for_status()
|
||||||
|
payload = resp.json()
|
||||||
|
token: str | None = None
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
raw = payload.get("data") or payload.get("token") or payload.get("access_token") or payload.get("accessToken")
|
||||||
|
if isinstance(raw, str):
|
||||||
|
token = raw
|
||||||
|
elif isinstance(raw, dict):
|
||||||
|
inner = raw.get("token") or raw.get("access_token") or raw.get("accessToken")
|
||||||
|
if isinstance(inner, str):
|
||||||
|
token = inner
|
||||||
|
token = (token or "").strip()
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError("shf token response invalid")
|
||||||
|
|
||||||
|
ttl = int(settings.sheriff_v2_token_ttl_seconds or 600)
|
||||||
|
self._token = token
|
||||||
|
self._token_expires_at = now + ttl
|
||||||
|
return token
|
||||||
|
|
||||||
|
async def _request(self, method: str, path: str, *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
base_url = self._base_url()
|
||||||
|
url = f"{base_url}{path}"
|
||||||
|
token = await self._get_token()
|
||||||
|
headers = {
|
||||||
|
"accept": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"x-client-identifier": self._client_identifier(),
|
||||||
|
}
|
||||||
|
resp = await self._client.request(method, url, headers=headers, params=params, json=json)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
self._token = None
|
||||||
|
token = await self._get_token()
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
resp = await self._client.request(method, url, headers=headers, params=params, json=json)
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
body = (resp.text or "")[:2000]
|
||||||
|
raise RuntimeError(f"shf {resp.status_code} for {method} {path}: {body}")
|
||||||
|
data = resp.json()
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise RuntimeError("Unexpected shf response")
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def query_rut(self, *, rut: str, is_monitoring: bool = False) -> dict[str, Any]:
|
||||||
|
rut = (rut or "").strip()
|
||||||
|
if not rut:
|
||||||
|
raise ValueError("Missing rut")
|
||||||
|
|
||||||
|
cargar = await self._request(
|
||||||
|
"POST",
|
||||||
|
"/api/clients/v2/helper/cargarRut",
|
||||||
|
json={"rut": rut, "isMonitoring": bool(is_monitoring), "includeEquifax": False},
|
||||||
|
)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
resumen = await self._request(
|
||||||
|
"GET",
|
||||||
|
"/api/clients/v2/helper/resumen",
|
||||||
|
params={"rut": rut, "complete": "True"},
|
||||||
|
)
|
||||||
|
|
||||||
|
result: dict[str, Any] = {"success": True, "cargarRut": cargar, "resumen": resumen}
|
||||||
|
|
||||||
|
ok = True
|
||||||
|
if isinstance(cargar, dict) and ("success" in cargar) and (not bool(cargar.get("success"))):
|
||||||
|
ok = False
|
||||||
|
if isinstance(resumen, dict) and ("success" in resumen) and (not bool(resumen.get("success"))):
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
judicial_civil, judicial_cobranza, judicial_laboral, multa_laboral, mora_previsional, compliance, malla_societaria, credit_score = await asyncio.gather(
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/judicial/{rut}/civil"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/judicial/{rut}/cobranza"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/judicial/{rut}/laboral"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/cobranzaLaboral/{rut}/multaLaboral"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/cobranzaLaboral/{rut}/moraPrevisional"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/compliance/{rut}"),
|
||||||
|
self._request("GET", f"/api/clients/v2/helper/legal/{rut}/mallaSocietaria"),
|
||||||
|
self._request("GET", f"/api/clients/v2/creditScore/{rut}"),
|
||||||
|
)
|
||||||
|
result.update(
|
||||||
|
{
|
||||||
|
"judicial": {"civil": judicial_civil, "cobranza": judicial_cobranza, "laboral": judicial_laboral},
|
||||||
|
"cobranzaLaboral": {"multaLaboral": multa_laboral, "moraPrevisional": mora_previsional},
|
||||||
|
"compliance": compliance,
|
||||||
|
"legal": {"mallaSocietaria": malla_societaria},
|
||||||
|
"creditScore": credit_score,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
sheriff_v2_native_service = SheriffV2NativeService()
|
||||||
|
|
@ -25,7 +25,23 @@ def jsonable(value: Any) -> Any:
|
||||||
|
|
||||||
|
|
||||||
def sanitize_rut(value: str) -> str:
|
def sanitize_rut(value: str) -> str:
|
||||||
digits = "".join(re.findall(r"\d", value or ""))
|
raw = (value or "").strip().replace(".", "").replace(" ", "").upper()
|
||||||
if len(digits) <= 1:
|
if not raw:
|
||||||
return digits
|
return ""
|
||||||
return f"{digits[:-1]}-{digits[-1]}"
|
|
||||||
|
if "-" in raw:
|
||||||
|
base, dv = raw.split("-", 1)
|
||||||
|
base_digits = "".join(re.findall(r"\d", base))
|
||||||
|
dv_clean = "".join(re.findall(r"[0-9K]", dv))[:1]
|
||||||
|
if not base_digits:
|
||||||
|
return raw
|
||||||
|
base_digits = base_digits.lstrip("0") or "0"
|
||||||
|
return f"{base_digits}-{dv_clean}" if dv_clean else base_digits
|
||||||
|
|
||||||
|
cleaned = "".join(re.findall(r"[0-9K]", raw))
|
||||||
|
if len(cleaned) <= 1:
|
||||||
|
return cleaned
|
||||||
|
base_digits = "".join(re.findall(r"\d", cleaned[:-1]))
|
||||||
|
dv = cleaned[-1]
|
||||||
|
base_digits = base_digits.lstrip("0") or "0"
|
||||||
|
return f"{base_digits}-{dv}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user