cosmoguard-bd/src/pif_compiler/api/routes/api_orders.py
2026-02-22 19:44:55 +01:00

440 lines
16 KiB
Python

from fastapi import APIRouter, HTTPException, BackgroundTasks, status
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from pif_compiler.classes.main_workflow import receive_order, process_order_pipeline, retry_order, trigger_pipeline, Project
from pif_compiler.functions.db_utils import db_connect, get_ordine_by_id, get_all_ordini, delete_ordine
from pif_compiler.functions.common_func import generate_project_source_pdfs, create_sources_zip
from pif_compiler.functions.common_log import get_logger
logger = get_logger()
router = APIRouter()
# ==================== REQUEST / RESPONSE MODELS ====================
class OrderIngredientInput(BaseModel):
inci: Optional[str] = None
cas: Optional[str] = None
percentage: float = 0.0
is_colorante: bool = False
skip_tox: bool = False
class OrderCreateRequest(BaseModel):
client_name: str = Field(..., description="Nome del cliente")
product_name: str = Field(..., description="Nome del prodotto cosmetico")
preset_esposizione: str = Field(..., description="Nome del preset di esposizione")
ingredients: List[OrderIngredientInput] = Field(..., description="Lista ingredienti")
class Config:
json_schema_extra = {
"example": {
"client_name": "Cosmetica Italia srl",
"product_name": "Crema 'Cremosa'",
"preset_esposizione": "Test Preset",
"ingredients": [
{"inci": "AQUA", "cas": "", "percentage": 90, "is_colorante": False, "skip_tox": True},
{"inci": None, "cas": "56-81-5", "percentage": 6, "is_colorante": False, "skip_tox": False},
{"inci": None, "cas": "9007-16-3", "percentage": 3, "is_colorante": False, "skip_tox": False},
{"inci": None, "cas": "JYY-807", "percentage": 1, "is_colorante": True, "skip_tox": False}
]
}
}
class OrderCreateResponse(BaseModel):
success: bool
id_ordine: Optional[int] = None
message: Optional[str] = None
error: Optional[str] = None
# ==================== ROUTES ====================
@router.post("/orders/create", response_model=OrderCreateResponse, tags=["Orders"])
async def create_order(request: OrderCreateRequest, background_tasks: BackgroundTasks):
"""
Crea un nuovo ordine e avvia l'elaborazione in background.
Il JSON viene salvato su MongoDB, il record su PostgreSQL (stato=RICEVUTO).
L'arricchimento degli ingredienti avviene in background.
"""
logger.info(f"Nuovo ordine ricevuto: cliente={request.client_name}, prodotto={request.product_name}")
try:
raw_json = request.model_dump()
id_ordine = receive_order(raw_json)
if id_ordine is None:
return OrderCreateResponse(
success=False,
error="Errore nel salvataggio dell'ordine"
)
# Avvia l'elaborazione in background
background_tasks.add_task(process_order_pipeline)
logger.info(f"Ordine {id_ordine} creato, elaborazione avviata in background")
return OrderCreateResponse(
success=True,
id_ordine=id_ordine,
message=f"Ordine {id_ordine} ricevuto. Elaborazione avviata in background."
)
except Exception as e:
logger.error(f"Errore creazione ordine: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.post("/orders/retry/{id_ordine}", response_model=OrderCreateResponse, tags=["Orders"])
async def retry_failed_order(id_ordine: int, background_tasks: BackgroundTasks):
"""
Resetta un ordine in stato ERRORE a RICEVUTO e rilancia la pipeline.
"""
logger.info(f"Retry ordine {id_ordine}")
try:
success = retry_order(id_ordine)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Ordine {id_ordine} non trovato o non in stato ERRORE"
)
background_tasks.add_task(process_order_pipeline)
return OrderCreateResponse(
success=True,
id_ordine=id_ordine,
message=f"Ordine {id_ordine} resettato. Rielaborazione avviata in background."
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Errore retry ordine {id_ordine}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.post("/orders/trigger-pipeline", response_model=OrderCreateResponse, tags=["Orders"])
async def manual_trigger_pipeline(background_tasks: BackgroundTasks):
"""
Lancia manualmente la pipeline di elaborazione.
Processa il prossimo ordine pendente (più vecchio con stato RICEVUTO).
"""
logger.info("Trigger manuale pipeline")
try:
background_tasks.add_task(trigger_pipeline)
return OrderCreateResponse(
success=True,
message="Pipeline avviata in background."
)
except Exception as e:
logger.error(f"Errore trigger pipeline: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.get("/orders/export/{id_ordine}", tags=["Orders"])
async def export_order_excel(id_ordine: int):
"""
Genera e scarica il file Excel per un ordine completato.
L'ordine deve avere un uuid_progetto associato (stato >= ARRICCHITO).
"""
logger.info(f"Export Excel per ordine {id_ordine}")
try:
# Recupera l'ordine dal DB
row = get_ordine_by_id(id_ordine)
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ordine {id_ordine} non trovato"
)
uuid_progetto = row[4] # uuid_progetto
if not uuid_progetto:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Ordine {id_ordine} non ha un progetto associato (elaborazione non completata?)"
)
# Recupera il progetto da MongoDB
from bson import ObjectId
collection = db_connect(collection_name='projects')
doc = collection.find_one({"_id": ObjectId(uuid_progetto)})
if not doc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Progetto {uuid_progetto} non trovato in MongoDB"
)
doc.pop("_id", None)
project = Project(**doc)
# Genera Excel
output_path = project.export_excel()
logger.info(f"Excel generato per ordine {id_ordine}: {output_path}")
return FileResponse(
path=output_path,
filename=f"progetto_{id_ordine}.xlsx",
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Errore export Excel ordine {id_ordine}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.get("/orders/export-sources/{id_ordine}", tags=["Orders"])
async def export_order_sources(id_ordine: int):
"""
Genera i PDF delle fonti per ogni ingrediente di un ordine e li restituisce in un archivio ZIP.
Include: PDF tossicologico del best_case (CAS_source.pdf) + PDF COSING (CAS_cosing.pdf).
"""
logger.info(f"Export fonti PDF per ordine {id_ordine}")
try:
# Recupera l'ordine dal DB
row = get_ordine_by_id(id_ordine)
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ordine {id_ordine} non trovato"
)
uuid_progetto = row[4]
if not uuid_progetto:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Ordine {id_ordine} non ha un progetto associato"
)
# Recupera il progetto da MongoDB
from bson import ObjectId
collection = db_connect(collection_name='projects')
doc = collection.find_one({"_id": ObjectId(uuid_progetto)})
if not doc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Progetto {uuid_progetto} non trovato in MongoDB"
)
doc.pop("_id", None)
project = Project(**doc)
# Genera i PDF delle fonti
pdf_paths = await generate_project_source_pdfs(project)
if not pdf_paths:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Nessuna fonte (tox/COSING) disponibile per questo progetto"
)
# Crea ZIP
import os
os.makedirs("exports", exist_ok=True)
zip_path = f"exports/fonti_ordine_{id_ordine}.zip"
create_sources_zip(pdf_paths, zip_path)
logger.info(f"ZIP fonti generato per ordine {id_ordine}: {zip_path} ({len(pdf_paths)} PDF)")
return FileResponse(
path=zip_path,
filename=f"fonti_ordine_{id_ordine}.zip",
media_type="application/zip"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Errore export fonti ordine {id_ordine}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.get("/orders/list", tags=["Orders"])
async def list_orders():
"""
Recupera la lista di tutti gli ordini con info cliente, compilatore e stato.
Per ciascun ordine recupera product_name dal documento MongoDB.
"""
logger.info("Richiesta lista ordini")
try:
rows = get_all_ordini()
orders = []
orders_col = db_connect(collection_name='orders')
for row in rows:
id_ordine, uuid_ordine, uuid_progetto, data_ordine, stato_ordine, note, \
nome_cliente, nome_compilatore, nome_stato = row
# Recupera product_name da MongoDB
product_name = None
if uuid_ordine:
try:
from bson import ObjectId
doc = orders_col.find_one({"_id": ObjectId(uuid_ordine)}, {"product_name": 1})
if doc:
product_name = doc.get("product_name")
except Exception:
pass
orders.append({
"id_ordine": id_ordine,
"uuid_ordine": uuid_ordine,
"uuid_progetto": uuid_progetto,
"data_ordine": data_ordine.isoformat() if data_ordine else None,
"stato_ordine": stato_ordine,
"note": note,
"nome_cliente": nome_cliente,
"nome_compilatore": nome_compilatore,
"nome_stato": nome_stato,
"product_name": product_name,
})
return {"success": True, "data": orders}
except Exception as e:
logger.error(f"Errore lista ordini: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.get("/orders/detail/{id_ordine}", tags=["Orders"])
async def get_order_detail(id_ordine: int):
"""
Recupera il dettaglio completo di un ordine: dati PostgreSQL + documento MongoDB.
Include product_name, preset, lista ingredienti dal JSON originale.
"""
logger.info(f"Dettaglio ordine {id_ordine}")
try:
row = get_ordine_by_id(id_ordine)
if not row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ordine {id_ordine} non trovato"
)
id_ord, id_cliente, id_compilatore, uuid_ordine, uuid_progetto, \
data_ordine, stato_ordine, note = row
# Recupera nomi da PostgreSQL
from pif_compiler.functions.db_utils import postgres_connect
conn = postgres_connect()
with conn.cursor() as cur:
nome_cliente = None
if id_cliente:
cur.execute("SELECT nome_cliente FROM clienti WHERE id_cliente = %s", (id_cliente,))
r = cur.fetchone()
nome_cliente = r[0] if r else None
nome_compilatore = None
if id_compilatore:
cur.execute("SELECT nome_compilatore FROM compilatori WHERE id_compilatore = %s", (id_compilatore,))
r = cur.fetchone()
nome_compilatore = r[0] if r else None
cur.execute("SELECT nome_stato FROM stati_ordini WHERE id_stato = %s", (stato_ordine,))
r = cur.fetchone()
nome_stato = r[0] if r else None
conn.close()
# Recupera dati dal documento MongoDB ordine
product_name = None
preset_esposizione = None
ingredients = []
if uuid_ordine:
from bson import ObjectId
orders_col = db_connect(collection_name='orders')
try:
doc = orders_col.find_one({"_id": ObjectId(uuid_ordine)})
if doc:
product_name = doc.get("product_name")
preset_esposizione = doc.get("preset_esposizione")
ingredients = doc.get("ingredients", [])
except Exception:
pass
return {
"success": True,
"order": {
"id_ordine": id_ord,
"uuid_ordine": uuid_ordine,
"uuid_progetto": uuid_progetto,
"data_ordine": data_ordine.isoformat() if data_ordine else None,
"stato_ordine": stato_ordine,
"nome_stato": nome_stato,
"note": note,
"nome_cliente": nome_cliente,
"nome_compilatore": nome_compilatore,
"product_name": product_name,
"preset_esposizione": preset_esposizione,
"ingredients": ingredients,
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Errore dettaglio ordine {id_ordine}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)
@router.delete("/orders/{id_ordine}", tags=["Orders"])
async def remove_order(id_ordine: int):
"""
Elimina un ordine e tutti i dati correlati (progetto, lineage, documenti MongoDB).
"""
logger.info(f"Eliminazione ordine {id_ordine}")
try:
success = delete_ordine(id_ordine)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Ordine {id_ordine} non trovato o errore nell'eliminazione"
)
return {"success": True, "message": f"Ordine {id_ordine} eliminato con successo"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Errore eliminazione ordine {id_ordine}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Errore interno: {str(e)}"
)