feat: close autonomous operations for blocked products
This commit is contained in:
+68
-3
@@ -1,15 +1,18 @@
|
||||
"""Authenticated version-one Hub routes."""
|
||||
import hashlib, json, logging
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
import csv
|
||||
import io
|
||||
from sqlalchemy import select
|
||||
from .auth import authenticate, bootstrap
|
||||
from .classification import ENGINE_VERSION, Classifier
|
||||
from .config import settings
|
||||
from .datasets.service import DatasetService
|
||||
from .jobs import claim_next_job, complete_job, create_job, fail_job
|
||||
from .blocked_products import list_blockers, resolve_blocker, resolve_product_blockers, upsert_blocker
|
||||
from .context_keys import validate_entry
|
||||
from .models import FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Job
|
||||
from .models import BlockedProduct, FiscalContextEntry, ClassificationCache, ClassificationRequest, ClassificationResult, Dataset, Job
|
||||
|
||||
logger = logging.getLogger("gti_fiscal_hub")
|
||||
|
||||
@@ -182,6 +185,63 @@ def router(session_factory, bootstrap_token):
|
||||
}
|
||||
finally: session.close()
|
||||
|
||||
def _blocked_view(row):
|
||||
return {
|
||||
"id": row.id, "tenant_id": row.tenant_id, "company": row.company, "product_code": row.product_code,
|
||||
"gtin": row.gtin, "description": row.description, "blocker_type": row.blocker_type, "field": row.field,
|
||||
"reason_code": row.reason_code, "evidence_summary": row.evidence_summary, "first_seen_at": row.first_seen_at.isoformat() if row.first_seen_at else None,
|
||||
"last_seen_at": row.last_seen_at.isoformat() if row.last_seen_at else None, "retry_count": row.retry_count,
|
||||
"next_retry_at": row.next_retry_at.isoformat() if row.next_retry_at else None, "resolved_at": row.resolved_at.isoformat() if row.resolved_at else None,
|
||||
"resolution_source": row.resolution_source,
|
||||
}
|
||||
|
||||
@routes.get("/blockers")
|
||||
def blockers_list(blocker_type: str | None = None, field: str | None = None, identity=Depends(principal)):
|
||||
session, agent = identity
|
||||
try:
|
||||
return {"blockers": [_blocked_view(row) for row in list_blockers(session, agent.tenant_id, blocker_type=blocker_type, field=field)]}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@routes.get("/blockers/export.csv")
|
||||
def blockers_export(identity=Depends(principal)):
|
||||
session, agent = identity
|
||||
try:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["id", "company", "product_code", "gtin", "blocker_type", "field", "reason_code", "retry_count", "first_seen_at", "last_seen_at"])
|
||||
for row in list_blockers(session, agent.tenant_id):
|
||||
writer.writerow([row.id, row.company, row.product_code, row.gtin or "", row.blocker_type, row.field, row.reason_code, row.retry_count, row.first_seen_at.isoformat() if row.first_seen_at else "", row.last_seen_at.isoformat() if row.last_seen_at else ""])
|
||||
return Response(output.getvalue(), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=blocked-products.csv"})
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@routes.post("/blockers/reprocess")
|
||||
def blockers_reprocess(payload: dict, identity=Depends(principal)):
|
||||
session, agent = identity
|
||||
try:
|
||||
rows = list_blockers(session, agent.tenant_id, blocker_type=payload.get("blocker_type"), field=payload.get("field"))
|
||||
codes = [row.product_code for row in rows if row.product_code]
|
||||
if not codes:
|
||||
return {"created": False, "reason": "NO_DUE_BLOCKERS"}
|
||||
job = create_job(session, agent, "REPROCESS_PRODUCTS", {"codigos": codes, "reason": "BLOCKER_REPROCESS"})
|
||||
return {"created": True, "job_id": job.id, "products": len(codes)}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@routes.post("/blockers/{blocker_id}/resolve")
|
||||
def blockers_resolve(blocker_id: int, payload: dict, identity=Depends(principal)):
|
||||
session, agent = identity
|
||||
try:
|
||||
row = session.get(BlockedProduct, blocker_id)
|
||||
if row is None or row.tenant_id != agent.tenant_id:
|
||||
raise HTTPException(404, "blocker not found")
|
||||
if not resolve_blocker(session, blocker_id, payload.get("resolution_source", "OPERATOR")):
|
||||
raise HTTPException(404, "blocker not found")
|
||||
return {"resolved": True, "blocker_id": blocker_id}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def _job_view(job):
|
||||
return {
|
||||
"job_id": job.id, "agent_id": job.agent_id, "kind": job.kind, "status": job.status,
|
||||
@@ -231,7 +291,12 @@ def router(session_factory, bootstrap_token):
|
||||
try:
|
||||
job = session.get(Job, job_id)
|
||||
if job is None or job.agent_id != agent.id: raise HTTPException(404, "job not found")
|
||||
if not complete_job(session, job, payload.get("result_summary", {})): raise HTTPException(409, "job is not RUNNING")
|
||||
result_summary = payload.get("result_summary", {})
|
||||
for blocker in result_summary.get("blocked_products", []):
|
||||
upsert_blocker(session, agent.tenant_id, blocker)
|
||||
for product_code in result_summary.get("resolved_products", []):
|
||||
resolve_product_blockers(session, agent.tenant_id, product_code)
|
||||
if not complete_job(session, job, result_summary): raise HTTPException(409, "job is not RUNNING")
|
||||
logger.info("job completed job_id=%s agent_id=%s result=%s", job_id, agent.id, payload.get("result_summary", {}))
|
||||
return _job_view(session.get(Job, job_id))
|
||||
finally: session.close()
|
||||
|
||||
Reference in New Issue
Block a user