mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
Implementazioni varie
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user