fastcheck_api fix

This commit is contained in:
valenti 2026-04-29 17:17:39 -04:00
parent 9bbfef66f9
commit 21ae30fcdf
4 changed files with 363 additions and 3 deletions

132
SINGLE_EVALUATION_FLOW.md Normal file
View File

@ -0,0 +1,132 @@
# SINGLE_EVALUATION_FLOW
## Scopo
Descrive il flusso frontend della funzionalità **Evaluación Individual** (route: `/evaluations/single`) e il passaggio alla pagina risultati **FastCheck** (route: `/fast-check-ex`), includendo le chiamate API effettuate dal frontend.
## Componenti coinvolti
- **UI form valutazione**: `src/pages/evaluations/SingleEvaluation.tsx`
- **Pagina risultati**: `src/pages/FastCheckEX.tsx`
- **Client HTTP**: `src/services/api.ts` (`apiClient`)
- **Auth**: `src/contexts/AuthContext.tsx` + `src/services/authService.ts`
- **Tenant/Crediti**: `src/contexts/TenantContext.tsx`
## Base URL API
Il frontend usa:
- `globalThis.__APP_ENV__?.VITE_API_BASE_URL`
- fallback: `https://duxiter.azurianlab.com/api`
Tutte le chiamate sotto sono relative a questa base URL.
## Flusso ad alto livello
1. Lutente apre **Evaluación Individual** (`/evaluations/single`)
2. Il frontend carica contesto sessione/tenant (token + crediti)
3. Lutente inserisce e conferma il **RUT** (validazione lato client + accettazione termini)
4. Alla submit:
- controlla se esiste già un risultato in DB
- se esiste: reindirizza direttamente a **FastCheck** con `rut` in querystring
- se non esiste: avvia `lookup` e poi reindirizza a **FastCheck**
5. In **FastCheck** vengono caricati i dettagli del risultato e i dati “accessori” (AI analysis, liste, note, ecc.)
## Normalizzazione RUT (frontend)
- Durante digitazione il RUT viene formattato.
- Prima delle API, il RUT viene “sanitizzato” togliendo i punti: `formattedRut.replace(/\./g, '')`
- Lhyphen rimane (es. `12.345.678-9``12345678-9`)
## Diagramma (sequenza)
```mermaid
sequenceDiagram
autonumber
actor U as Utente
participant SE as FE: /evaluations/single (SingleEvaluation.tsx)
participant AC as FE: AuthContext/AuthService
participant TC as FE: TenantContext
participant API as Backend API (VITE_API_BASE_URL)
participant FC as FE: /fast-check-ex (FastCheckEX.tsx)
U->>SE: Apre pagina
SE->>AC: Ripristina sessione (token in localStorage)
AC->>API: GET /auth/me
SE->>TC: Carica tenant/crediti
TC->>API: GET /tenant (Bearer token)
U->>SE: Inserisce RUT + accetta termini
U->>SE: Submit
SE->>API: GET /rut/results/rut/{rutSanitized}
alt Esiste già un risultato
SE-->>FC: navigate("/fast-check-ex?rut={rutSanitized}")
else Non esiste (404)
SE->>API: POST /rut/lookup { rut: rutSanitized, isMonitoring: false }
SE-->>FC: navigate("/fast-check-ex?rut={rutSanitized}")
end
FC->>API: GET /rut/results/rut/{rutSanitized} (opzionale ?risk=true)
opt Se non c'è rut o serve "latest"
FC->>API: GET /rut/results/latest
FC->>API: GET /rut/results/rut/{latestRut}
end
opt Funzioni accessorie in FastCheck
FC->>API: GET /rut/ai-analysis/{rut} (opzionale ?regenerate=true)
FC->>API: POST /rut/fast-check-summary/{rut} { summary }
FC->>API: GET /lpalto/search/rut/{rut}
FC->>API: GET /lpmedio/search/rut/{rut}
FC->>API: GET /antiunion-cases/rut/{rut}
FC->>API: GET /user-notes/rut/{rut}
FC->>API: POST /user-notes { rut, note, tags }
FC->>API: PUT /user-notes/{noteId} { note, tags }
FC->>API: DELETE /user-notes/{noteId}
FC->>API: PUT /rut/results/rut/{rut}/update-pep-status { ... }
end
```
## Chiamate API per “Evaluación Individual” (pagina `/evaluations/single`)
Chiamate direttamente collegate al submit:
1. **Controllo esistenza risultato**
- `GET /rut/results/rut/{rut}`
- Se risponde con dati: considerato “già esistente” → redirect a FastCheck
- Se 404: procede con `lookup`
2. **Avvio valutazione**
- `POST /rut/lookup`
- Body: `{ "rut": "{rut}", "isMonitoring": false }`
- In seguito redirect a FastCheck
## Chiamate “di contesto” (sessione/tenant)
Queste possono avvenire mentre lutente è su `/evaluations/single`:
- `GET /auth/me` (ripristino sessione)
- `GET /tenant` (crediti/tenant; header `Authorization: Bearer <token>`)
## Caricamento risultati (pagina `/fast-check-ex`)
FastCheck usa tipicamente questi endpoint per mostrare il risultato della valutazione:
- `GET /rut/results/rut/{rut}` (opzionale `?risk=true`)
- `GET /rut/results/{resultId}` (opzionale `?risk=true`)
- `GET /rut/results/latest`
In base alle feature attive in UI, possono aggiungersi:
- `GET /rut/ai-analysis/{rut}` (opzionale `?regenerate=true`)
- `POST /rut/fast-check-summary/{rut}`
- `GET /lpalto/search/rut/{rut}`
- `GET /lpmedio/search/rut/{rut}`
- `GET /antiunion-cases/rut/{rut}`
- Note utente:
- `GET /user-notes/rut/{rut}`
- `POST /user-notes`
- `PUT /user-notes/{noteId}`
- `DELETE /user-notes/{noteId}`
- Aggiornamento stato PEP:
- `PUT /rut/results/rut/{rut}/update-pep-status`
## Stati e UX (sintesi)
- Submit bloccato se:
- RUT non valido
- termini non accettati
- loading in corso
- Se crediti tenant = 0: viene mostrato un dialog informativo.
- In caso di errore non-404 nel check esistenza: viene loggato ma il flusso continua con `lookup`.
## Note operative
- Il redirect a `/fast-check-ex` è parte integrante del flusso: la pagina “Evaluación Individual” funge da avvio/trigger, mentre la visualizzazione del risultato avviene in FastCheck.

View File

@ -0,0 +1,219 @@
from __future__ import annotations
import datetime as dt
import time
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo import ReturnDocument
from pydantic import BaseModel, Field
from fastcheck_api.app.api.dependencies import CurrentUser, require_permission
from fastcheck_api.app.core.mongodb import get_db
from fastcheck_api.app.services.audit_service import AuditService
from fastcheck_api.app.services.legacy_service import legacy_service
from fastcheck_api.app.utils.mongo import sanitize_rut, to_object_id
router = APIRouter(prefix="/rut", tags=["rut"])
_bearer = HTTPBearer(auto_error=False)
class RutLookupRequest(BaseModel):
rut: str = Field(examples=["12345678-9"])
isMonitoring: bool = Field(default=False, description="Compatibility flag with legacy backend.")
@router.post(
"/lookup",
summary="Lookup RUT (single evaluation)",
description=(
"Runs a single evaluation for a RUT using the legacy backend and persists the result in MongoDB.\n\n"
"If a successful result for the same RUT already exists for the tenant, returns it without consuming credits."
),
)
async def lookup_rut(
request: Request,
payload: RutLookupRequest,
user: Annotated[CurrentUser, Depends(require_permission("rut:lookup"))],
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer)] = None,
):
start = time.perf_counter()
rut = sanitize_rut(payload.rut)
token = credentials.credentials if credentials else None
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="No token provided")
cached = (
await db["evaluationresults"]
.find({"tenantId": user.tenant, "rut": rut, "status": "success"})
.sort([("createdAt", -1)])
.limit(1)
.to_list(length=1)
)
if cached and isinstance(cached[0], dict) and isinstance(cached[0].get("data"), dict):
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_200_OK,
request_data={"rut": rut, "isMonitoring": payload.isMonitoring},
response_data={"source": "cache"},
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={"source": "cache"},
)
return {**cached[0]["data"], "isFromCache": True}
tenant_oid = to_object_id(user.tenant)
now = dt.datetime.now(dt.timezone.utc)
tenant_after = await db["tenants"].find_one_and_update(
{"_id": tenant_oid, "creditBalance.availableCredits": {"$gte": 1}},
{
"$inc": {
"creditBalance.availableCredits": -1,
"creditBalance.totalCreditsUsed": 1,
},
"$set": {
"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")
new_balance = int((tenant_after.get("creditBalance") or {}).get("availableCredits") or 0)
await db["creditoperations"].insert_one(
{
"tenantId": tenant_oid,
"userId": to_object_id(user.id),
"operationType": "evaluation",
"creditsChanged": -1,
"balanceAfter": new_balance,
"description": f"Valutazione singola per {rut}",
"metadata": {"evaluationType": "single", "supplierRut": rut},
"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
error_message: str | None = None
result_data: dict[str, Any] | None = None
try:
result_data = await legacy_service.lookup_rut(
rut=rut,
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(
{
"jobId": job_insert.inserted_id,
"tenantId": user.tenant,
"rut": rut,
"name": None,
"status": "success",
"data": result_data,
"createdAt": now,
"updatedAt": now,
}
)
await db["evaluationjobs"].update_one(
{"_id": job_insert.inserted_id, "tenantId": user.tenant},
{"$set": {"status": "completed", "completedEvaluations": 1, "updatedAt": now}},
)
return {**result_data, "jobId": job_id, "isFromCache": False}
except HTTPException as e:
raise e
except Exception as e:
response_status = status.HTTP_502_BAD_GATEWAY
error_message = str(e)
await db["evaluationresults"].insert_one(
{
"jobId": job_insert.inserted_id,
"tenantId": user.tenant,
"rut": rut,
"name": None,
"status": "failed",
"error": error_message,
"createdAt": now,
"updatedAt": now,
}
)
await db["evaluationjobs"].update_one(
{"_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)
audit_response: dict[str, Any] | None = None
if result_data is not None:
audit_response = {"jobId": job_id}
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=response_status,
request_data={"rut": rut, "isMonitoring": payload.isMonitoring},
response_data=audit_response,
error_message=error_message,
processing_time_ms=processing_ms,
credits_used=1,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
metadata={"evaluationType": "single", "jobId": job_id},
)

View File

@ -8,7 +8,7 @@ from fastapi import APIRouter
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from fastcheck_api.app.api.v1.routers import auth, checks, reports, usage
from fastcheck_api.app.api.v1.routers import auth, checks, reports, rut, usage
from fastcheck_api.app.core.logging import configure_logging
from fastcheck_api.app.core.mongodb import lifespan_db
from fastcheck_api.app.services.legacy_service import legacy_service
@ -24,6 +24,7 @@ tags_metadata = [
{"name": "auth", "description": "Authentication and session management."},
{"name": "checks", "description": "Create checks (evaluations) and retrieve job/results."},
{"name": "reports", "description": "Fetch saved reports from existing persistence."},
{"name": "rut", "description": "Legacy-compatible RUT lookup endpoints."},
{"name": "usage", "description": "Usage and credit balance information."},
]
@ -156,5 +157,6 @@ async def prefixed_openapi():
api_v1.include_router(auth.router)
api_v1.include_router(checks.router)
api_v1.include_router(reports.router)
api_v1.include_router(rut.router)
api_v1.include_router(usage.router)
app.include_router(api_v1)

View File

@ -17,11 +17,18 @@ class LegacyService:
async def close(self) -> None:
await self._client.aclose()
async def lookup_rut(self, *, rut: str, token: str) -> dict[str, Any]:
async def lookup_rut(
self,
*,
rut: str,
token: str,
is_monitoring: bool = False,
evaluation_type: str = "individual",
) -> dict[str, Any]:
resp = await self._client.post(
"rut/lookup",
headers={"Authorization": f"Bearer {token}"},
json={"rut": rut, "isMonitoring": False, "type": "masiva"},
json={"rut": rut, "isMonitoring": is_monitoring, "type": evaluation_type},
)
resp.raise_for_status()
data = resp.json()