fastcheck_api fix
This commit is contained in:
parent
9bbfef66f9
commit
21ae30fcdf
132
SINGLE_EVALUATION_FLOW.md
Normal file
132
SINGLE_EVALUATION_FLOW.md
Normal 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. L’utente apre **Evaluación Individual** (`/evaluations/single`)
|
||||
2. Il frontend carica contesto sessione/tenant (token + crediti)
|
||||
3. L’utente 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, '')`
|
||||
- L’hyphen 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 l’utente è 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.
|
||||
219
fastcheck_api/app/api/v1/routers/rut.py
Normal file
219
fastcheck_api/app/api/v1/routers/rut.py
Normal 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},
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user