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.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, 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.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
|
||||||
|
|
@ -24,6 +24,7 @@ tags_metadata = [
|
||||||
{"name": "auth", "description": "Authentication and session management."},
|
{"name": "auth", "description": "Authentication and session management."},
|
||||||
{"name": "checks", "description": "Create checks (evaluations) and retrieve job/results."},
|
{"name": "checks", "description": "Create checks (evaluations) and retrieve job/results."},
|
||||||
{"name": "reports", "description": "Fetch saved reports from existing persistence."},
|
{"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."},
|
{"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(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(usage.router)
|
api_v1.include_router(usage.router)
|
||||||
app.include_router(api_v1)
|
app.include_router(api_v1)
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,18 @@ class LegacyService:
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
await self._client.aclose()
|
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(
|
resp = await self._client.post(
|
||||||
"rut/lookup",
|
"rut/lookup",
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
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()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user