fastcheck_api fix

This commit is contained in:
valenti 2026-04-29 11:50:59 -04:00
parent 60732dc8cf
commit 0ac421da93
9 changed files with 27 additions and 313 deletions

View File

@ -38,8 +38,6 @@ async def login(payload: LoginRequest, db: Annotated[AsyncIOMotorDatabase, Depen
raise raise
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))
@router.get( @router.get(

View File

@ -1,12 +1,9 @@
from __future__ import annotations from __future__ import annotations
import logging
import re import re
import time import time
from typing import Annotated from typing import Annotated
import datetime as dt
from bson import ObjectId
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from motor.motor_asyncio import AsyncIOMotorDatabase from motor.motor_asyncio import AsyncIOMotorDatabase
@ -17,16 +14,13 @@ from fastcheck_api.app.schemas.checks import (
EvaluationJobOut, EvaluationJobOut,
ListJobsResponse, ListJobsResponse,
ListResultsResponse, ListResultsResponse,
LookupResponse,
) )
from fastcheck_api.app.services.audit_service import AuditService from fastcheck_api.app.services.audit_service import AuditService
from fastcheck_api.app.services.check_service import CheckService, InsufficientCreditsError from fastcheck_api.app.services.check_service import CheckService, InsufficientCreditsError
from fastcheck_api.app.services.legacy_service import legacy_service from fastcheck_api.app.utils.mongo import sanitize_rut
from fastcheck_api.app.utils.mongo import jsonable, sanitize_rut
router = APIRouter(prefix="/checks", tags=["checks"]) router = APIRouter(prefix="/checks", tags=["checks"])
logger = logging.getLogger("fastcheck.checks")
_MASK_REPLACEMENTS: dict[str, str] = { _MASK_REPLACEMENTS: dict[str, str] = {
"sheriff": "provider_a", "sheriff": "provider_a",
@ -132,176 +126,6 @@ async def create_check(
) )
@router.post(
"/lookup",
response_model=LookupResponse,
status_code=status.HTTP_201_CREATED,
summary="Lookup a RUT (sync)",
description="Create a check, run the legacy lookup immediately, persist the result, and return job+result in a single request.",
)
async def lookup(
request: Request,
payload: CreateCheckRequest,
user: Annotated[CurrentUser, Depends(require_permission("evaluation:create"))],
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
):
start = time.perf_counter()
clean_rut = sanitize_rut(payload.supplier.rut)
token = legacy_service.get_user_token(user_id=user.id)
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Legacy token missing or expired; login again",
)
try:
job = await CheckService.create_single_check(
db=db,
tenant_id=user.tenant,
user_id=user.id,
rut=clean_rut,
name=payload.supplier.name,
enqueue=False,
)
await AuditService.log_consulta(
db=db,
tenant_id=user.tenant,
user_id=user.id,
consulta_type="individual",
rut=clean_rut,
endpoint="/api/v1/checks/lookup",
response_status=status.HTTP_201_CREATED,
request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}},
processing_time_ms=int((time.perf_counter() - start) * 1000),
credits_used=1,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
metadata={"evaluationType": "single", "mode": "sync"},
)
except InsufficientCreditsError:
await AuditService.log_consulta(
db=db,
tenant_id=user.tenant,
user_id=user.id,
consulta_type="individual",
rut=clean_rut,
endpoint="/api/v1/checks/lookup",
response_status=status.HTTP_402_PAYMENT_REQUIRED,
request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}},
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", "mode": "sync"},
)
raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail="Insufficient credits")
job_id = str(job.get("_id") or "")
try:
job_oid = ObjectId(job_id)
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Invalid job id")
now = dt.datetime.now(dt.timezone.utc)
try:
result_data = await legacy_service.lookup_rut(rut=clean_rut, token=token)
ai_summary = result_data.get("aiAnalysis") if isinstance(result_data, dict) else None
if ai_summary and isinstance(ai_summary, str) and ai_summary.strip():
try:
await legacy_service.save_fastcheck_summary(rut=clean_rut, summary=ai_summary, token=token)
except Exception:
pass
insert = await db["evaluationresults"].insert_one(
{
"jobId": job_oid,
"tenantId": user.tenant,
"rut": clean_rut,
"name": payload.supplier.name,
"status": "success",
"data": result_data,
"createdAt": now,
"updatedAt": now,
}
)
await db["evaluationjobs"].update_one(
{"_id": job_oid, "tenantId": user.tenant},
{
"$set": {
"completedEvaluations": 1,
"failedEvaluations": 0,
"status": "completed",
"updatedAt": now,
}
},
)
stored_job = await db["evaluationjobs"].find_one({"_id": job_oid, "tenantId": user.tenant})
stored_result = await db["evaluationresults"].find_one({"_id": insert.inserted_id})
response_payload = {
"job": jsonable(stored_job) if stored_job else job,
"result": jsonable(stored_result)
if stored_result
else jsonable(
{
"jobId": str(job_oid),
"tenantId": user.tenant,
"rut": clean_rut,
"name": payload.supplier.name,
"status": "success",
"data": result_data,
"createdAt": now,
"updatedAt": now,
}
),
}
return _mask_json(response_payload)
except Exception as e:
request_id = getattr(request.state, "request_id", None)
logger.exception(
"lookup_failed",
extra={
"request_id": request_id,
"tenant_id": user.tenant,
"user_id": user.id,
"rut": clean_rut,
},
)
reason = str(e).replace("\n", " ").strip()
if not reason:
reason = e.__class__.__name__
if len(reason) > 500:
reason = reason[:500] + "..."
await db["evaluationresults"].insert_one(
{
"jobId": job_oid,
"tenantId": user.tenant,
"rut": clean_rut,
"name": payload.supplier.name,
"status": "failed",
"error": reason,
"createdAt": now,
"updatedAt": now,
}
)
await db["evaluationjobs"].update_one(
{"_id": job_oid, "tenantId": user.tenant},
{
"$set": {
"completedEvaluations": 0,
"failedEvaluations": 1,
"status": "failed",
"updatedAt": now,
}
},
)
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=reason)
@router.get( @router.get(
"/{job_id}", "/{job_id}",
response_model=EvaluationJobOut, response_model=EvaluationJobOut,

View File

@ -55,8 +55,3 @@ class ListResultsResponse(BaseModel):
results: list[EvaluationResultOut] results: list[EvaluationResultOut]
total: int = Field(description="Total results in this job.", examples=[1]) total: int = Field(description="Total results in this job.", examples=[1])
pages: int = Field(description="Total pages for the current limit.", examples=[1]) pages: int = Field(description="Total pages for the current limit.", examples=[1])
class LookupResponse(BaseModel):
job: EvaluationJobOut = Field(description="Created evaluation job.")
result: EvaluationResultOut = Field(description="Immediate evaluation result saved for this job.")

View File

@ -4,7 +4,6 @@ from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorDatabase from motor.motor_asyncio import AsyncIOMotorDatabase
from fastcheck_api.app.core.security import create_access_token, verify_password from fastcheck_api.app.core.security import create_access_token, verify_password
from fastcheck_api.app.services.legacy_service import legacy_service
class AuthService: class AuthService:
@ -37,8 +36,6 @@ class AuthService:
tenant_id=tenant_id_str, tenant_id=tenant_id_str,
) )
await legacy_service.login_user(user_id=str(user["_id"]), email=email.lower(), password=password)
return { return {
"token": token, "token": token,
"user": { "user": {

View File

@ -24,7 +24,6 @@ class CheckService:
user_id: str, user_id: str,
rut: str, rut: str,
name: str | None, name: str | None,
enqueue: bool = True,
) -> dict[str, Any]: ) -> dict[str, Any]:
now = dt.datetime.now(dt.timezone.utc) now = dt.datetime.now(dt.timezone.utc)
tenant_oid = to_object_id(tenant_id) tenant_oid = to_object_id(tenant_id)
@ -77,10 +76,9 @@ class CheckService:
insert = await db["evaluationjobs"].insert_one(job_doc) insert = await db["evaluationjobs"].insert_one(job_doc)
job_id = str(insert.inserted_id) job_id = str(insert.inserted_id)
if enqueue: await rabbitmq_service.publish_evaluation(
await rabbitmq_service.publish_evaluation( {"rut": rut, "name": name, "jobId": job_id, "tenantId": tenant_id, "userId": user_id}
{"rut": rut, "name": name, "jobId": job_id, "tenantId": tenant_id, "userId": user_id} )
)
stored = await db["evaluationjobs"].find_one({"_id": insert.inserted_id}) stored = await db["evaluationjobs"].find_one({"_id": insert.inserted_id})
if not stored: if not stored:

View File

@ -1,8 +1,5 @@
from __future__ import annotations from __future__ import annotations
import base64
import json
import time
from typing import Any from typing import Any
import httpx import httpx
@ -16,100 +13,33 @@ class LegacyService:
base_url=settings.legacy_base_url, base_url=settings.legacy_base_url,
timeout=settings.legacy_http_timeout_seconds, timeout=settings.legacy_http_timeout_seconds,
) )
self._user_tokens: dict[str, tuple[str, float | None]] = {}
async def close(self) -> None: async def close(self) -> None:
await self._client.aclose() await self._client.aclose()
def _extract_exp(self, token: str) -> float | None:
try:
parts = token.split(".")
if len(parts) < 2:
return None
payload_b64 = parts[1]
payload_b64 += "=" * (-len(payload_b64) % 4)
payload_raw = base64.urlsafe_b64decode(payload_b64.encode("utf-8"))
payload = json.loads(payload_raw.decode("utf-8"))
exp = payload.get("exp")
if isinstance(exp, (int, float)):
return float(exp)
except Exception:
return None
return None
def set_user_token(self, *, user_id: str, token: str) -> None:
exp = self._extract_exp(token)
self._user_tokens[user_id] = (token, exp)
def get_user_token(self, *, user_id: str) -> str | None:
entry = self._user_tokens.get(user_id)
if not entry:
return None
token, exp = entry
if exp is None:
return token
if (time.time() + 60) < exp:
return token
self._user_tokens.pop(user_id, None)
return None
async def login_user(self, *, user_id: str, email: str, password: str) -> str:
try:
resp = await self._client.post("auth/login", json={"email": email, "password": password})
resp.raise_for_status()
data = resp.json()
token = data.get("token") if isinstance(data, dict) else None
if not token or not isinstance(token, str):
raise RuntimeError("Legacy login did not return a token")
self.set_user_token(user_id=user_id, token=token)
return token
except httpx.HTTPStatusError as e:
body = (e.response.text or "").replace("\n", " ").strip()
if len(body) > 500:
body = body[:500] + "..."
raise RuntimeError(f"Legacy login failed ({e.response.status_code}): {body or 'no body'}") from e
except httpx.RequestError as e:
raise RuntimeError(f"Legacy login request error: {str(e)}") from e
async def lookup_rut(self, *, rut: str, token: str) -> dict[str, Any]: async def lookup_rut(self, *, rut: str, token: str) -> dict[str, Any]:
try: 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": False, "type": "masiva"}, )
) resp.raise_for_status()
resp.raise_for_status() data = resp.json()
data = resp.json() if not isinstance(data, dict):
if not isinstance(data, dict): raise RuntimeError("Unexpected legacy response")
raise RuntimeError("Unexpected legacy response") return data
return data
except httpx.HTTPStatusError as e:
body = (e.response.text or "").replace("\n", " ").strip()
if len(body) > 500:
body = body[:500] + "..."
raise RuntimeError(f"Legacy lookup failed ({e.response.status_code}): {body or 'no body'}") from e
except httpx.RequestError as e:
raise RuntimeError(f"Legacy lookup request error: {str(e)}") from e
async def save_fastcheck_summary(self, *, rut: str, summary: str, token: str) -> dict[str, Any]: async def save_fastcheck_summary(self, *, rut: str, summary: str, token: str) -> dict[str, Any]:
try: resp = await self._client.post(
resp = await self._client.post( f"rut/fast-check-summary/{rut}",
f"rut/fast-check-summary/{rut}", headers={"Authorization": f"Bearer {token}"},
headers={"Authorization": f"Bearer {token}"}, json={"summary": summary},
json={"summary": summary}, )
) resp.raise_for_status()
resp.raise_for_status() data = resp.json()
data = resp.json() if not isinstance(data, dict):
if not isinstance(data, dict): raise RuntimeError("Unexpected legacy response")
raise RuntimeError("Unexpected legacy response") return data
return data
except httpx.HTTPStatusError as e:
body = (e.response.text or "").replace("\n", " ").strip()
if len(body) > 500:
body = body[:500] + "..."
raise RuntimeError(f"Legacy save summary failed ({e.response.status_code}): {body or 'no body'}") from e
except httpx.RequestError as e:
raise RuntimeError(f"Legacy save summary request error: {str(e)}") from e
legacy_service = LegacyService() legacy_service = LegacyService()

View File

@ -18,10 +18,10 @@ from typing import Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests import requests
from fastapi import FastAPI, HTTPException, Header, Request from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, ValidationError from pydantic import BaseModel
from dotenv import load_dotenv from dotenv import load_dotenv
# Configure logging # Configure logging
@ -530,25 +530,7 @@ async def get_credit_score(rut: str):
return execute_sheriff_get(path) return execute_sheriff_get(path)
@app.post("/queryRut") @app.post("/queryRut")
async def query_rut(request: Request): async def query_rut(payload: QueryRutRequest):
raw = await request.body()
if not raw:
raise HTTPException(status_code=400, detail="Missing JSON body")
try:
parsed = json.loads(raw.decode("utf-8"))
except Exception:
snippet = raw[:500].decode("utf-8", errors="replace")
raise HTTPException(status_code=400, detail=f"Invalid JSON body: {snippet}")
if not isinstance(parsed, dict):
raise HTTPException(status_code=400, detail="Body must be a JSON object")
try:
payload = QueryRutRequest(**parsed)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
cargar_body = { cargar_body = {
"rut": payload.rut.strip(), "rut": payload.rut.strip(),
"isMonitoring": False, "isMonitoring": False,

View File

@ -228,8 +228,6 @@ export class SheriffService {
if (loadRutData == null) { if (loadRutData == null) {
console.error('loadRutData failed is null'); console.error('loadRutData failed is null');
await logEntry.save();
return logEntry.toObject();
} }
//must wait at least 30 seconds before starting the next //must wait at least 30 seconds before starting the next
await new Promise(resolve => setTimeout(resolve, parseInt(process.env.SHERIFF_API_DELAY || '30000'))); await new Promise(resolve => setTimeout(resolve, parseInt(process.env.SHERIFF_API_DELAY || '30000')));
@ -237,12 +235,6 @@ export class SheriffService {
const summaryData = await makeApiCall('summaryData', 'get', `/helper/${sanitizedRut}/summary?complete=true`); //added complete true const summaryData = await makeApiCall('summaryData', 'get', `/helper/${sanitizedRut}/summary?complete=true`); //added complete true
console.log(`[DEBUG] After summaryData call, sanitizedRut is still: ${sanitizedRut}`); console.log(`[DEBUG] After summaryData call, sanitizedRut is still: ${sanitizedRut}`);
if (summaryData == null) {
console.error('summaryData failed is null');
await logEntry.save();
return logEntry.toObject();
}
let societaryMeshExternal: any[] | null = null; let societaryMeshExternal: any[] | null = null;
if (summaryData.data.societaryMeshExternal && summaryData.data.societaryMeshExternal.length > 0) { if (summaryData.data.societaryMeshExternal && summaryData.data.societaryMeshExternal.length > 0) {

View File

@ -227,8 +227,6 @@ export class sheriffV2Service {
//console.log(`[DEBUG] loadRutData response RUT: ${loadRutData?.data?.rut || 'No RUT in response'}`); //console.log(`[DEBUG] loadRutData response RUT: ${loadRutData?.data?.rut || 'No RUT in response'}`);
if (summaryData == null) { if (summaryData == null) {
console.error('loadRutData failed is null'); console.error('loadRutData failed is null');
await logEntry.save();
return logEntry.toObject();
} }
if(!summaryData.data) { if(!summaryData.data) {
summaryData.data = {}; summaryData.data = {};