271 lines
9.3 KiB
Python
271 lines
9.3 KiB
Python
"""
|
|
Service per la gestione dei Fascicoli (fascicolazione pratiche).
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
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())
|