Files
PecHub/backend/app/services/fascicolo_service.py
T

325 lines
11 KiB
Python

"""
Service per la gestione dei Fascicoli (fascicolazione pratiche).
"""
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError
from app.models.fascicolo import Fascicolo, FascicoloMessage
from app.models.message import Message
from app.models.user import User
from app.schemas.fascicolo import FascicoloCreate, FascicoloUpdate
class FascicoloService:
def __init__(self, db: AsyncSession):
self.db = db
# ─── CRUD Fascicolo ───────────────────────────────────────────────────────
async def list_fascicoli(
self,
tenant_id: uuid.UUID,
stato: str | None = None,
responsabile_id: uuid.UUID | None = None,
search: str | None = None,
) -> list[tuple[Fascicolo, int]]:
"""
Restituisce lista di (Fascicolo, message_count) con filtri opzionali.
"""
# Subquery per il conteggio messaggi
count_sub = (
select(
FascicoloMessage.fascicolo_id,
func.count(FascicoloMessage.message_id).label("cnt"),
)
.group_by(FascicoloMessage.fascicolo_id)
.subquery()
)
stmt = (
select(Fascicolo, func.coalesce(count_sub.c.cnt, 0).label("message_count"))
.outerjoin(count_sub, Fascicolo.id == count_sub.c.fascicolo_id)
.where(Fascicolo.tenant_id == tenant_id)
.order_by(Fascicolo.updated_at.desc())
)
if stato:
stmt = stmt.where(Fascicolo.stato == stato)
if responsabile_id:
stmt = stmt.where(Fascicolo.responsabile_id == responsabile_id)
if search:
pattern = f"%{search}%"
stmt = stmt.where(
Fascicolo.titolo.ilike(pattern)
| Fascicolo.numero_pratica.ilike(pattern)
| Fascicolo.categoria.ilike(pattern)
)
result = await self.db.execute(stmt)
return list(result.all())
async def get_fascicolo(
self, tenant_id: uuid.UUID, fascicolo_id: uuid.UUID
) -> tuple[Fascicolo, int]:
"""Restituisce (Fascicolo, message_count) o solleva NotFoundError."""
count_sub = (
select(
FascicoloMessage.fascicolo_id,
func.count(FascicoloMessage.message_id).label("cnt"),
)
.group_by(FascicoloMessage.fascicolo_id)
.subquery()
)
result = await self.db.execute(
select(Fascicolo, func.coalesce(count_sub.c.cnt, 0).label("message_count"))
.outerjoin(count_sub, Fascicolo.id == count_sub.c.fascicolo_id)
.where(
Fascicolo.id == fascicolo_id,
Fascicolo.tenant_id == tenant_id,
)
)
row = result.one_or_none()
if not row:
raise NotFoundError(f"Fascicolo {fascicolo_id} non trovato")
return row
async def create_fascicolo(
self,
tenant_id: uuid.UUID,
data: FascicoloCreate,
created_by: uuid.UUID,
) -> tuple[Fascicolo, int]:
fascicolo = Fascicolo(
id=uuid.uuid4(),
tenant_id=tenant_id,
titolo=data.titolo,
numero_pratica=data.numero_pratica,
stato=data.stato or "aperto",
categoria=data.categoria,
responsabile_id=data.responsabile_id,
scadenza=data.scadenza,
note=data.note,
created_by=created_by,
)
self.db.add(fascicolo)
await self.db.commit()
await self.db.refresh(fascicolo)
return fascicolo, 0
async def update_fascicolo(
self,
tenant_id: uuid.UUID,
fascicolo_id: uuid.UUID,
data: FascicoloUpdate,
current_user_id: uuid.UUID,
is_admin: bool,
) -> tuple[Fascicolo, int]:
fascicolo, count = await self.get_fascicolo(tenant_id, fascicolo_id)
# Solo admin o il creatore possono modificare
if not is_admin and fascicolo.created_by != current_user_id:
raise ForbiddenError("Solo il creatore o un amministratore puo' modificare questo fascicolo")
if data.titolo is not None:
fascicolo.titolo = data.titolo
if data.numero_pratica is not None:
fascicolo.numero_pratica = data.numero_pratica
if data.stato is not None:
fascicolo.stato = data.stato
if data.categoria is not None:
fascicolo.categoria = data.categoria
if data.responsabile_id is not None:
fascicolo.responsabile_id = data.responsabile_id
if data.scadenza is not None:
fascicolo.scadenza = data.scadenza
if data.note is not None:
fascicolo.note = data.note
fascicolo.updated_at = datetime.utcnow()
await self.db.commit()
await self.db.refresh(fascicolo)
return fascicolo, count
async def delete_fascicolo(
self,
tenant_id: uuid.UUID,
fascicolo_id: uuid.UUID,
) -> None:
fascicolo, _ = await self.get_fascicolo(tenant_id, fascicolo_id)
await self.db.delete(fascicolo)
await self.db.commit()
# ─── Messaggi nel fascicolo ───────────────────────────────────────────────
async def get_fascicolo_messages(
self,
tenant_id: uuid.UUID,
fascicolo_id: uuid.UUID,
) -> list[tuple[Message, datetime]]:
"""
Restituisce lista di (Message, added_at) ordinati per added_at desc.
"""
# Verifica fascicolo appartiene al tenant
await self.get_fascicolo(tenant_id, fascicolo_id)
result = await self.db.execute(
select(Message, FascicoloMessage.added_at)
.join(FascicoloMessage, Message.id == FascicoloMessage.message_id)
.where(
FascicoloMessage.fascicolo_id == fascicolo_id,
Message.tenant_id == tenant_id,
)
.order_by(FascicoloMessage.added_at.desc())
)
return list(result.all())
async def add_messages(
self,
tenant_id: uuid.UUID,
fascicolo_id: uuid.UUID,
message_ids: list[uuid.UUID],
added_by: uuid.UUID,
) -> int:
"""
Aggiunge messaggi al fascicolo. Ignora duplicati e messaggi non del tenant.
Restituisce il numero di messaggi aggiunti.
"""
await self.get_fascicolo(tenant_id, fascicolo_id)
# Verifica che i messaggi appartengano al tenant
msg_result = await self.db.execute(
select(Message.id).where(
Message.id.in_(message_ids),
Message.tenant_id == tenant_id,
)
)
valid_ids = list(msg_result.scalars().all())
if not valid_ids:
return 0
# Carica associazioni esistenti per evitare duplicati
existing_result = await self.db.execute(
select(FascicoloMessage.message_id).where(
FascicoloMessage.fascicolo_id == fascicolo_id,
FascicoloMessage.message_id.in_(valid_ids),
)
)
existing_ids = set(existing_result.scalars().all())
added = 0
for msg_id in valid_ids:
if msg_id not in existing_ids:
self.db.add(
FascicoloMessage(
fascicolo_id=fascicolo_id,
message_id=msg_id,
added_by=added_by,
)
)
added += 1
if added:
await self.db.commit()
return added
async def remove_messages(
self,
tenant_id: uuid.UUID,
fascicolo_id: uuid.UUID,
message_ids: list[uuid.UUID],
) -> int:
"""Rimuove messaggi dal fascicolo. Restituisce il numero rimosso."""
await self.get_fascicolo(tenant_id, fascicolo_id)
result = await self.db.execute(
delete(FascicoloMessage).where(
FascicoloMessage.fascicolo_id == fascicolo_id,
FascicoloMessage.message_id.in_(message_ids),
).returning(FascicoloMessage.message_id)
)
removed = len(result.fetchall())
if removed:
await self.db.commit()
return removed
# ─── Fascicoli di un messaggio ────────────────────────────────────────────
async def get_message_fascicoli(
self,
tenant_id: uuid.UUID,
message_id: uuid.UUID,
) -> list[Fascicolo]:
"""Restituisce i fascicoli a cui appartiene un messaggio."""
result = await self.db.execute(
select(Fascicolo)
.join(FascicoloMessage, Fascicolo.id == FascicoloMessage.fascicolo_id)
.where(
FascicoloMessage.message_id == message_id,
Fascicolo.tenant_id == tenant_id,
)
.order_by(Fascicolo.titolo)
)
return list(result.scalars().all())
# ─── Scadenzario fascicoli ────────────────────────────────────────────────
async def list_fascicoli_scadenzario(
self,
tenant_id: uuid.UUID,
days_ahead: int = 30,
include_overdue: bool = True,
) -> list[tuple[Fascicolo, int, bool]]:
"""
Restituisce lista di (Fascicolo, message_count, is_overdue) per lo scadenzario.
Filtra fascicoli con scadenza impostata nel range richiesto.
Ordinati: scaduti prima (ASC scadenza), poi futuri (ASC scadenza).
"""
now = datetime.now(timezone.utc)
future_limit = now + timedelta(days=days_ahead)
count_sub = (
select(
FascicoloMessage.fascicolo_id,
func.count(FascicoloMessage.message_id).label("cnt"),
)
.group_by(FascicoloMessage.fascicolo_id)
.subquery()
)
stmt = (
select(Fascicolo, func.coalesce(count_sub.c.cnt, 0).label("message_count"))
.outerjoin(count_sub, Fascicolo.id == count_sub.c.fascicolo_id)
.where(
Fascicolo.tenant_id == tenant_id,
Fascicolo.scadenza.is_not(None),
)
.order_by(Fascicolo.scadenza.asc())
)
if include_overdue:
# Scaduti (qualsiasi data passata) + futuri fino al limite
stmt = stmt.where(Fascicolo.scadenza <= future_limit)
else:
# Solo scadenze future entro il limite
stmt = stmt.where(
Fascicolo.scadenza > now,
Fascicolo.scadenza <= future_limit,
)
result = await self.db.execute(stmt)
rows = result.all()
return [
(fascicolo, int(cnt), fascicolo.scadenza < now if fascicolo.scadenza else False)
for fascicolo, cnt in rows
]