fastcheck/fastcheck_api/app/services/check_service.py
2026-04-16 12:05:29 -04:00

136 lines
4.6 KiB
Python

from __future__ import annotations
import datetime as dt
from typing import Any
from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorDatabase
from pymongo import ReturnDocument
from fastcheck_api.app.services.rabbitmq_service import rabbitmq_service
from fastcheck_api.app.utils.mongo import jsonable, to_object_id
class InsufficientCreditsError(Exception):
pass
class CheckService:
@staticmethod
async def create_single_check(
*,
db: AsyncIOMotorDatabase,
tenant_id: str,
user_id: str,
rut: str,
name: str | None,
) -> dict[str, Any]:
now = dt.datetime.now(dt.timezone.utc)
tenant_oid = to_object_id(tenant_id)
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:
raise InsufficientCreditsError("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, "supplierName": name},
"createdAt": now,
}
)
job_doc: dict[str, Any] = {
"tenantId": tenant_id,
"status": "processing",
"type": "single",
"createdBy": user_id,
"totalEvaluations": 1,
"completedEvaluations": 0,
"failedEvaluations": 0,
"supplierData": {"rut": rut, "name": name or "N/A"},
"createdAt": now,
"updatedAt": now,
}
insert = await db["evaluationjobs"].insert_one(job_doc)
job_id = str(insert.inserted_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})
if not stored:
raise RuntimeError("Failed to create job")
return jsonable(stored)
@staticmethod
async def list_jobs(*, db: AsyncIOMotorDatabase, tenant_id: str, page: int, limit: int) -> dict[str, Any]:
skip = (page - 1) * limit
cursor = (
db["evaluationjobs"]
.find({"tenantId": tenant_id})
.sort([("createdAt", -1)])
.skip(skip)
.limit(limit)
)
jobs = [jsonable(doc) async for doc in cursor]
total = await db["evaluationjobs"].count_documents({"tenantId": tenant_id})
pages = (total + limit - 1) // limit
return {"jobs": jobs, "total": total, "pages": pages}
@staticmethod
async def get_job(*, db: AsyncIOMotorDatabase, tenant_id: str, job_id: str) -> dict[str, Any] | None:
try:
oid = ObjectId(job_id)
except Exception:
return None
doc = await db["evaluationjobs"].find_one({"_id": oid, "tenantId": tenant_id})
return jsonable(doc) if doc else None
@staticmethod
async def list_results(
*, db: AsyncIOMotorDatabase, tenant_id: str, job_id: str, page: int, limit: int
) -> dict[str, Any]:
try:
oid = ObjectId(job_id)
except Exception:
raise ValueError("Invalid job id")
job = await db["evaluationjobs"].find_one({"_id": oid, "tenantId": tenant_id})
if not job:
raise LookupError("Job not found")
skip = (page - 1) * limit
cursor = (
db["evaluationresults"]
.find({"jobId": oid, "tenantId": tenant_id})
.sort([("createdAt", -1)])
.skip(skip)
.limit(limit)
)
results = [jsonable(doc) async for doc in cursor]
total = await db["evaluationresults"].count_documents({"jobId": oid, "tenantId": tenant_id})
pages = (total + limit - 1) // limit
return {"results": results, "total": total, "pages": pages}