fastcheck_api fix

This commit is contained in:
valenti 2026-04-29 11:18:51 -04:00
parent 45269327c5
commit 20f37f37d3
11 changed files with 34043 additions and 36 deletions

View File

@ -38,6 +38,8 @@ 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,8 +1,12 @@
from __future__ import annotations from __future__ import annotations
import logging
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
@ -13,12 +17,43 @@ 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 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] = {
"sheriff": "provider_a",
"equifax": "provider_b",
"dequienes": "provider_c",
}
_MASK_PATTERN = re.compile("|".join(re.escape(k) for k in _MASK_REPLACEMENTS), re.IGNORECASE)
def _mask_text(value: str) -> str:
return _MASK_PATTERN.sub(lambda m: _MASK_REPLACEMENTS[m.group(0).lower()], value)
def _mask_json(value): # type: ignore[no-untyped-def]
if isinstance(value, str):
return _mask_text(value)
if isinstance(value, list):
return [_mask_json(v) for v in value]
if isinstance(value, dict):
out = {}
for k, v in value.items():
if isinstance(k, str) and k.lower() == "datasourcedesc":
continue
masked_key = _mask_text(k) if isinstance(k, str) else k
out[masked_key] = _mask_json(v)
return out
return value
@router.get( @router.get(
@ -50,12 +85,13 @@ async def create_check(
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
): ):
start = time.perf_counter() start = time.perf_counter()
clean_rut = sanitize_rut(payload.supplier.rut)
try: try:
job = await CheckService.create_single_check( job = await CheckService.create_single_check(
db=db, db=db,
tenant_id=user.tenant, tenant_id=user.tenant,
user_id=user.id, user_id=user.id,
rut=payload.supplier.rut, rut=clean_rut,
name=payload.supplier.name, name=payload.supplier.name,
) )
await AuditService.log_consulta( await AuditService.log_consulta(
@ -63,10 +99,10 @@ async def create_check(
tenant_id=user.tenant, tenant_id=user.tenant,
user_id=user.id, user_id=user.id,
consulta_type="individual", consulta_type="individual",
rut=payload.supplier.rut, rut=clean_rut,
endpoint="/api/v1/checks", endpoint="/api/v1/checks",
response_status=status.HTTP_201_CREATED, response_status=status.HTTP_201_CREATED,
request_data={"supplier": {"rut": payload.supplier.rut, "name": payload.supplier.name}}, request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}},
processing_time_ms=int((time.perf_counter() - start) * 1000), processing_time_ms=int((time.perf_counter() - start) * 1000),
credits_used=1, credits_used=1,
ip_address=request.client.host if request.client else None, ip_address=request.client.host if request.client else None,
@ -80,10 +116,10 @@ async def create_check(
tenant_id=user.tenant, tenant_id=user.tenant,
user_id=user.id, user_id=user.id,
consulta_type="individual", consulta_type="individual",
rut=payload.supplier.rut, rut=clean_rut,
endpoint="/api/v1/checks", endpoint="/api/v1/checks",
response_status=status.HTTP_402_PAYMENT_REQUIRED, response_status=status.HTTP_402_PAYMENT_REQUIRED,
request_data={"supplier": {"rut": payload.supplier.rut, "name": payload.supplier.name}}, request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}},
processing_time_ms=int((time.perf_counter() - start) * 1000), processing_time_ms=int((time.perf_counter() - start) * 1000),
credits_used=0, credits_used=0,
ip_address=request.client.host if request.client else None, ip_address=request.client.host if request.client else None,
@ -96,6 +132,176 @@ 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,
@ -127,7 +333,8 @@ async def get_check_results(
limit: int = Query(default=10, ge=1, le=100), limit: int = Query(default=10, ge=1, le=100),
): ):
try: try:
return await CheckService.list_results(db=db, tenant_id=user.tenant, job_id=job_id, page=page, limit=limit) payload = await CheckService.list_results(db=db, tenant_id=user.tenant, job_id=job_id, page=page, limit=limit)
return _mask_json(payload)
except LookupError: except LookupError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
except ValueError as e: except ValueError as e:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import re
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
@ -8,11 +9,38 @@ from motor.motor_asyncio import AsyncIOMotorDatabase
from fastcheck_api.app.api.dependencies import CurrentUser, require_permission from fastcheck_api.app.api.dependencies import CurrentUser, require_permission
from fastcheck_api.app.core.mongodb import get_db from fastcheck_api.app.core.mongodb import get_db
from fastcheck_api.app.schemas.reports import FastCheckReportOut from fastcheck_api.app.schemas.reports import FastCheckReportOut
from fastcheck_api.app.utils.mongo import jsonable from fastcheck_api.app.utils.mongo import jsonable, sanitize_rut
router = APIRouter(prefix="/reports", tags=["reports"]) router = APIRouter(prefix="/reports", tags=["reports"])
_MASK_REPLACEMENTS: dict[str, str] = {
"sheriff": "provider_a",
"equifax": "provider_b",
"dequienes": "provider_c",
}
_MASK_PATTERN = re.compile("|".join(re.escape(k) for k in _MASK_REPLACEMENTS), re.IGNORECASE)
def _mask_text(value: str) -> str:
return _MASK_PATTERN.sub(lambda m: _MASK_REPLACEMENTS[m.group(0).lower()], value)
def _mask_json(value): # type: ignore[no-untyped-def]
if isinstance(value, str):
return _mask_text(value)
if isinstance(value, list):
return [_mask_json(v) for v in value]
if isinstance(value, dict):
out = {}
for k, v in value.items():
if isinstance(k, str) and k.lower() == "datasourcedesc":
continue
masked_key = _mask_text(k) if isinstance(k, str) else k
out[masked_key] = _mask_json(v)
return out
return value
@router.get( @router.get(
"/fast-check/{rut}", "/fast-check/{rut}",
@ -25,6 +53,7 @@ async def get_fastcheck_report(
user: Annotated[CurrentUser, Depends(require_permission("evaluation:read"))], user: Annotated[CurrentUser, Depends(require_permission("evaluation:read"))],
db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)],
): ):
rut = sanitize_rut(rut)
doc = ( doc = (
await db["summaries"] await db["summaries"]
.find({"rut": rut, "tenantId": user.tenant, "summaryType": "fast-check"}) .find({"rut": rut, "tenantId": user.tenant, "summaryType": "fast-check"})
@ -34,4 +63,4 @@ async def get_fastcheck_report(
) )
if not doc: if not doc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Report not found")
return jsonable(doc[0]) return _mask_json(jsonable(doc[0]))

View File

@ -55,3 +55,8 @@ 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,6 +4,7 @@ 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:
@ -36,6 +37,8 @@ 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

@ -8,7 +8,7 @@ from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo import ReturnDocument from pymongo import ReturnDocument
from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service
from fastcheck_api.app.utils.mongo import jsonable, to_object_id from fastcheck_api.app.utils.mongo import jsonable, sanitize_rut, to_object_id
class InsufficientCreditsError(Exception): class InsufficientCreditsError(Exception):
@ -24,9 +24,11 @@ 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)
rut = sanitize_rut(rut)
tenant_after = await db["tenants"].find_one_and_update( tenant_after = await db["tenants"].find_one_and_update(
{"_id": tenant_oid, "creditBalance.availableCredits": {"$gte": 1}}, {"_id": tenant_oid, "creditBalance.availableCredits": {"$gte": 1}},
@ -75,6 +77,7 @@ 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}
) )

View File

@ -1,5 +1,8 @@
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
@ -13,11 +16,63 @@ 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}"},
@ -28,8 +83,16 @@ class LegacyService:
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}"},
@ -40,6 +103,13 @@ class LegacyService:
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

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import re
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@ -21,3 +22,10 @@ def jsonable(value: Any) -> Any:
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return [jsonable(v) for v in value] return [jsonable(v) for v in value]
return value return value
def sanitize_rut(value: str) -> str:
digits = "".join(re.findall(r"\d", value or ""))
if len(digits) <= 1:
return digits
return f"{digits[:-1]}-{digits[-1]}"

View File

@ -14,6 +14,7 @@ from fastcheck_api.app.core.config import settings
from fastcheck_api.app.core.logging import configure_logging from fastcheck_api.app.core.logging import configure_logging
from fastcheck_api.app.core.security import create_access_token from fastcheck_api.app.core.security import create_access_token
from fastcheck_api.app.services.legacy_service import legacy_service from fastcheck_api.app.services.legacy_service import legacy_service
from fastcheck_api.app.utils.mongo import sanitize_rut
logger = logging.getLogger("fastcheck.worker") logger = logging.getLogger("fastcheck.worker")
@ -48,7 +49,7 @@ async def _update_job_progress(db: Any, job_oid: ObjectId, tenant_id: str) -> No
async def _process_payload(db: Any, payload: dict[str, Any]) -> None: async def _process_payload(db: Any, payload: dict[str, Any]) -> None:
rut = str(payload.get("rut") or "") rut = sanitize_rut(str(payload.get("rut") or ""))
name = payload.get("name") name = payload.get("name")
tenant_id = str(payload.get("tenantId") or "") tenant_id = str(payload.get("tenantId") or "")
user_id = str(payload.get("userId") or "") user_id = str(payload.get("userId") or "")

File diff suppressed because it is too large Load Diff

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 from fastapi import FastAPI, HTTPException, Header, Request
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 from pydantic import BaseModel, ValidationError
from dotenv import load_dotenv from dotenv import load_dotenv
# Configure logging # Configure logging
@ -530,7 +530,25 @@ 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(payload: QueryRutRequest): async def query_rut(request: Request):
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,