mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
241 lines
7.8 KiB
Python
241 lines
7.8 KiB
Python
"""
|
|
Service per la gestione della rubrica indirizzi PEC (Feature 6).
|
|
"""
|
|
|
|
import csv
|
|
import io
|
|
import uuid
|
|
|
|
from sqlalchemy import func, or_, select
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.exceptions import ConflictError, NotFoundError
|
|
from app.models.pec_contact import PecContact
|
|
from app.schemas.pec_contact import (
|
|
PecContactCreate,
|
|
PecContactImportResult,
|
|
PecContactUpdate,
|
|
)
|
|
|
|
|
|
class PecContactService:
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
async def list_contacts(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
q: str | None = None,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
) -> tuple[list[PecContact], int]:
|
|
query = select(PecContact).where(PecContact.tenant_id == tenant_id)
|
|
if q:
|
|
pattern = f"%{q.lower()}%"
|
|
query = query.where(
|
|
or_(
|
|
PecContact.email.ilike(pattern),
|
|
PecContact.name.ilike(pattern),
|
|
PecContact.organization.ilike(pattern),
|
|
)
|
|
)
|
|
query = query.order_by(PecContact.is_favorite.desc(), PecContact.name, PecContact.email)
|
|
|
|
count_q = select(func.count()).select_from(query.subquery())
|
|
total = (await self.db.execute(count_q)).scalar_one()
|
|
|
|
offset = (page - 1) * page_size
|
|
query = query.offset(offset).limit(page_size)
|
|
items = list((await self.db.execute(query)).scalars().all())
|
|
return items, total
|
|
|
|
async def get_contact(
|
|
self, tenant_id: uuid.UUID, contact_id: uuid.UUID
|
|
) -> PecContact:
|
|
result = await self.db.execute(
|
|
select(PecContact).where(
|
|
PecContact.id == contact_id,
|
|
PecContact.tenant_id == tenant_id,
|
|
)
|
|
)
|
|
contact = result.scalar_one_or_none()
|
|
if not contact:
|
|
raise NotFoundError(f"Contatto {contact_id} non trovato")
|
|
return contact
|
|
|
|
async def create_contact(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
data: PecContactCreate,
|
|
created_by: uuid.UUID | None = None,
|
|
) -> PecContact:
|
|
email = data.email.lower().strip()
|
|
# Verifica unicita'
|
|
existing = await self.db.execute(
|
|
select(PecContact).where(
|
|
PecContact.tenant_id == tenant_id,
|
|
PecContact.email == email,
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise ConflictError(f"Contatto '{email}' gia' esistente")
|
|
|
|
contact = PecContact(
|
|
tenant_id=tenant_id,
|
|
email=email,
|
|
name=data.name,
|
|
organization=data.organization,
|
|
notes=data.notes,
|
|
is_favorite=data.is_favorite,
|
|
auto_saved=False,
|
|
created_by=created_by,
|
|
)
|
|
self.db.add(contact)
|
|
await self.db.commit()
|
|
await self.db.refresh(contact)
|
|
return contact
|
|
|
|
async def update_contact(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
contact_id: uuid.UUID,
|
|
data: PecContactUpdate,
|
|
) -> PecContact:
|
|
contact = await self.get_contact(tenant_id, contact_id)
|
|
if data.name is not None:
|
|
contact.name = data.name
|
|
if data.organization is not None:
|
|
contact.organization = data.organization
|
|
if data.notes is not None:
|
|
contact.notes = data.notes
|
|
if data.is_favorite is not None:
|
|
contact.is_favorite = data.is_favorite
|
|
await self.db.commit()
|
|
await self.db.refresh(contact)
|
|
return contact
|
|
|
|
async def delete_contact(
|
|
self, tenant_id: uuid.UUID, contact_id: uuid.UUID
|
|
) -> None:
|
|
contact = await self.get_contact(tenant_id, contact_id)
|
|
await self.db.delete(contact)
|
|
await self.db.commit()
|
|
|
|
async def auto_save_sender(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
email: str,
|
|
) -> None:
|
|
"""
|
|
Salva automaticamente il mittente nella rubrica se non esiste ancora.
|
|
Operazione non bloccante: gli errori vengono ignorati silenziosamente.
|
|
|
|
Usata dal worker IMAP durante la sincronizzazione dei messaggi inbound.
|
|
"""
|
|
if not email:
|
|
return
|
|
email = email.lower().strip()
|
|
try:
|
|
# Upsert: inserisce solo se non esiste
|
|
stmt = insert(PecContact).values(
|
|
id=uuid.uuid4(),
|
|
tenant_id=tenant_id,
|
|
email=email,
|
|
auto_saved=True,
|
|
).on_conflict_do_nothing(
|
|
constraint="uq_pec_contact_email_tenant"
|
|
)
|
|
await self.db.execute(stmt)
|
|
await self.db.commit()
|
|
except Exception:
|
|
await self.db.rollback()
|
|
|
|
async def import_csv(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
csv_content: str,
|
|
created_by: uuid.UUID | None = None,
|
|
) -> PecContactImportResult:
|
|
"""
|
|
Importa contatti da un CSV con colonne: email, name, organization.
|
|
|
|
Aggiorna i record esistenti con name/organization se forniti.
|
|
"""
|
|
result = PecContactImportResult(created=0, updated=0, skipped=0)
|
|
reader = csv.DictReader(io.StringIO(csv_content))
|
|
|
|
for row_num, row in enumerate(reader, start=2):
|
|
email = row.get("email", "").strip().lower()
|
|
if not email or "@" not in email:
|
|
result.skipped += 1
|
|
result.errors.append(f"Riga {row_num}: email non valida '{email}'")
|
|
continue
|
|
|
|
name = row.get("name", "").strip() or None
|
|
organization = row.get("organization", "").strip() or None
|
|
|
|
try:
|
|
existing = await self.db.execute(
|
|
select(PecContact).where(
|
|
PecContact.tenant_id == tenant_id,
|
|
PecContact.email == email,
|
|
)
|
|
)
|
|
contact = existing.scalar_one_or_none()
|
|
|
|
if contact:
|
|
# Aggiorna solo se i campi erano vuoti (non sovrascrive dati manuali)
|
|
updated = False
|
|
if name and not contact.name:
|
|
contact.name = name
|
|
updated = True
|
|
if organization and not contact.organization:
|
|
contact.organization = organization
|
|
updated = True
|
|
if updated:
|
|
result.updated += 1
|
|
else:
|
|
result.skipped += 1
|
|
else:
|
|
contact = PecContact(
|
|
tenant_id=tenant_id,
|
|
email=email,
|
|
name=name,
|
|
organization=organization,
|
|
auto_saved=False,
|
|
created_by=created_by,
|
|
)
|
|
self.db.add(contact)
|
|
result.created += 1
|
|
except Exception as e:
|
|
result.errors.append(f"Riga {row_num} ({email}): {e}")
|
|
result.skipped += 1
|
|
|
|
await self.db.commit()
|
|
return result
|
|
|
|
async def search_for_autocomplete(
|
|
self,
|
|
tenant_id: uuid.UUID,
|
|
q: str,
|
|
limit: int = 10,
|
|
) -> list[PecContact]:
|
|
"""Ricerca veloce per autocomplete nel compose."""
|
|
if not q or len(q) < 2:
|
|
return []
|
|
pattern = f"%{q.lower()}%"
|
|
result = await self.db.execute(
|
|
select(PecContact)
|
|
.where(
|
|
PecContact.tenant_id == tenant_id,
|
|
or_(
|
|
PecContact.email.ilike(pattern),
|
|
PecContact.name.ilike(pattern),
|
|
)
|
|
)
|
|
.order_by(PecContact.is_favorite.desc(), PecContact.email)
|
|
.limit(limit)
|
|
)
|
|
return list(result.scalars().all())
|