fastcheck_api fix
This commit is contained in:
parent
45269327c5
commit
20f37f37d3
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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]))
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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,9 +77,10 @@ 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)
|
||||||
|
|
||||||
await rabbitmq_service.publish_evaluation(
|
if enqueue:
|
||||||
{"rut": rut, "name": name, "jobId": job_id, "tenantId": tenant_id, "userId": user_id}
|
await rabbitmq_service.publish_evaluation(
|
||||||
)
|
{"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:
|
||||||
|
|
|
||||||
|
|
@ -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,33 +16,100 @@ 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]:
|
||||||
resp = await self._client.post(
|
try:
|
||||||
"rut/lookup",
|
resp = await self._client.post(
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
"rut/lookup",
|
||||||
json={"rut": rut, "isMonitoring": False, "type": "masiva"},
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
)
|
json={"rut": rut, "isMonitoring": False, "type": "masiva"},
|
||||||
resp.raise_for_status()
|
)
|
||||||
data = resp.json()
|
resp.raise_for_status()
|
||||||
if not isinstance(data, dict):
|
data = resp.json()
|
||||||
raise RuntimeError("Unexpected legacy response")
|
if not isinstance(data, dict):
|
||||||
return data
|
raise RuntimeError("Unexpected legacy response")
|
||||||
|
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]:
|
||||||
resp = await self._client.post(
|
try:
|
||||||
f"rut/fast-check-summary/{rut}",
|
resp = await self._client.post(
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
f"rut/fast-check-summary/{rut}",
|
||||||
json={"summary": summary},
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
)
|
json={"summary": summary},
|
||||||
resp.raise_for_status()
|
)
|
||||||
data = resp.json()
|
resp.raise_for_status()
|
||||||
if not isinstance(data, dict):
|
data = resp.json()
|
||||||
raise RuntimeError("Unexpected legacy response")
|
if not isinstance(data, dict):
|
||||||
return data
|
raise RuntimeError("Unexpected legacy response")
|
||||||
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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]}"
|
||||||
|
|
|
||||||
|
|
@ -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 "")
|
||||||
|
|
|
||||||
33661
fastcheck_api/response_sample.json
Normal file
33661
fastcheck_api/response_sample.json
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user