220 lines
8.1 KiB
Python
220 lines
8.1 KiB
Python
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},
|
|
)
|