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())
|
||||
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
Service per la gestione delle regole di smistamento automatico (Feature 2).
|
||||
|
||||
Il metodo evaluate_rules() viene chiamato dal worker dopo ogni messaggio inbound.
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.exceptions import NotFoundError
|
||||
from app.models.label import Label, MessageLabel
|
||||
from app.models.message import Message
|
||||
from app.models.routing_rule import RoutingRule, RoutingRuleAction, RoutingRuleCondition
|
||||
from app.schemas.routing_rule import RoutingRuleCreate, RoutingRuleUpdate
|
||||
|
||||
|
||||
class RoutingRuleService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
# ─── CRUD ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async def list_rules(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> tuple[list[RoutingRule], int]:
|
||||
query = (
|
||||
select(RoutingRule)
|
||||
.where(RoutingRule.tenant_id == tenant_id)
|
||||
.options(selectinload(RoutingRule.conditions), selectinload(RoutingRule.actions))
|
||||
.order_by(RoutingRule.priority)
|
||||
)
|
||||
count_q = select(func.count()).select_from(
|
||||
select(RoutingRule).where(RoutingRule.tenant_id == tenant_id).subquery()
|
||||
)
|
||||
total = (await self.db.execute(count_q)).scalar_one()
|
||||
items = list((await self.db.execute(query)).scalars().all())
|
||||
return items, total
|
||||
|
||||
async def get_rule(
|
||||
self, tenant_id: uuid.UUID, rule_id: uuid.UUID
|
||||
) -> RoutingRule:
|
||||
result = await self.db.execute(
|
||||
select(RoutingRule)
|
||||
.where(RoutingRule.id == rule_id, RoutingRule.tenant_id == tenant_id)
|
||||
.options(selectinload(RoutingRule.conditions), selectinload(RoutingRule.actions))
|
||||
)
|
||||
rule = result.scalar_one_or_none()
|
||||
if not rule:
|
||||
raise NotFoundError(f"Regola {rule_id} non trovata")
|
||||
return rule
|
||||
|
||||
async def create_rule(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
data: RoutingRuleCreate,
|
||||
created_by: uuid.UUID | None = None,
|
||||
) -> RoutingRule:
|
||||
rule = RoutingRule(
|
||||
tenant_id=tenant_id,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
is_active=data.is_active,
|
||||
priority=data.priority,
|
||||
stop_processing=data.stop_processing,
|
||||
created_by=created_by,
|
||||
)
|
||||
self.db.add(rule)
|
||||
await self.db.flush()
|
||||
|
||||
for cond in data.conditions:
|
||||
self.db.add(RoutingRuleCondition(
|
||||
rule_id=rule.id,
|
||||
field=cond.field,
|
||||
operator=cond.operator,
|
||||
value=cond.value,
|
||||
))
|
||||
for action in data.actions:
|
||||
self.db.add(RoutingRuleAction(
|
||||
rule_id=rule.id,
|
||||
action_type=action.action_type,
|
||||
action_value=action.action_value,
|
||||
))
|
||||
|
||||
await self.db.commit()
|
||||
return await self.get_rule(tenant_id, rule.id)
|
||||
|
||||
async def update_rule(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
rule_id: uuid.UUID,
|
||||
data: RoutingRuleUpdate,
|
||||
) -> RoutingRule:
|
||||
rule = await self.get_rule(tenant_id, rule_id)
|
||||
|
||||
if data.name is not None:
|
||||
rule.name = data.name
|
||||
if data.description is not None:
|
||||
rule.description = data.description
|
||||
if data.is_active is not None:
|
||||
rule.is_active = data.is_active
|
||||
if data.priority is not None:
|
||||
rule.priority = data.priority
|
||||
if data.stop_processing is not None:
|
||||
rule.stop_processing = data.stop_processing
|
||||
|
||||
# Se condizioni o azioni vengono aggiornate, le sostituisce completamente
|
||||
if data.conditions is not None:
|
||||
await self.db.execute(
|
||||
delete(RoutingRuleCondition).where(RoutingRuleCondition.rule_id == rule_id)
|
||||
)
|
||||
for cond in data.conditions:
|
||||
self.db.add(RoutingRuleCondition(
|
||||
rule_id=rule_id,
|
||||
field=cond.field,
|
||||
operator=cond.operator,
|
||||
value=cond.value,
|
||||
))
|
||||
|
||||
if data.actions is not None:
|
||||
await self.db.execute(
|
||||
delete(RoutingRuleAction).where(RoutingRuleAction.rule_id == rule_id)
|
||||
)
|
||||
for action in data.actions:
|
||||
self.db.add(RoutingRuleAction(
|
||||
rule_id=rule_id,
|
||||
action_type=action.action_type,
|
||||
action_value=action.action_value,
|
||||
))
|
||||
|
||||
await self.db.commit()
|
||||
return await self.get_rule(tenant_id, rule_id)
|
||||
|
||||
async def delete_rule(
|
||||
self, tenant_id: uuid.UUID, rule_id: uuid.UUID
|
||||
) -> None:
|
||||
rule = await self.get_rule(tenant_id, rule_id)
|
||||
await self.db.delete(rule)
|
||||
await self.db.commit()
|
||||
|
||||
# ─── Motore di valutazione ────────────────────────────────────────────────
|
||||
|
||||
async def evaluate_and_apply(
|
||||
self,
|
||||
message: Message,
|
||||
) -> int:
|
||||
"""
|
||||
Valuta le regole attive del tenant e applica le azioni su message.
|
||||
|
||||
Returns:
|
||||
Numero di regole che hanno prodotto match.
|
||||
"""
|
||||
# Carica regole attive ordinate per priority
|
||||
result = await self.db.execute(
|
||||
select(RoutingRule)
|
||||
.where(
|
||||
RoutingRule.tenant_id == message.tenant_id,
|
||||
RoutingRule.is_active == True, # noqa: E712
|
||||
)
|
||||
.options(selectinload(RoutingRule.conditions), selectinload(RoutingRule.actions))
|
||||
.order_by(RoutingRule.priority)
|
||||
)
|
||||
rules = list(result.scalars().all())
|
||||
|
||||
matched_count = 0
|
||||
for rule in rules:
|
||||
if await self._matches(message, rule.conditions):
|
||||
matched_count += 1
|
||||
await self._apply_actions(message, rule.actions)
|
||||
if rule.stop_processing:
|
||||
break
|
||||
|
||||
if matched_count > 0:
|
||||
await self.db.flush()
|
||||
|
||||
return matched_count
|
||||
|
||||
async def _matches(
|
||||
self,
|
||||
message: Message,
|
||||
conditions: list[RoutingRuleCondition],
|
||||
) -> bool:
|
||||
"""Restituisce True se tutte le condizioni (AND) sono soddisfatte."""
|
||||
if not conditions:
|
||||
# Una regola senza condizioni non fa mai match (comportamento sicuro)
|
||||
return False
|
||||
|
||||
for cond in conditions:
|
||||
field_value = self._get_field_value(message, cond.field)
|
||||
if not self._evaluate_condition(field_value, cond.operator, cond.value):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_field_value(self, message: Message, field: str) -> str:
|
||||
"""Estrae il valore del campo dal messaggio come stringa per il confronto."""
|
||||
if field == "from_address":
|
||||
return (message.from_address or "").lower()
|
||||
elif field == "to_address":
|
||||
return " ".join(message.to_addresses or []).lower()
|
||||
elif field == "subject":
|
||||
return (message.subject or "").lower()
|
||||
elif field == "mailbox_id":
|
||||
return str(message.mailbox_id)
|
||||
elif field == "pec_type":
|
||||
return message.pec_type or ""
|
||||
return ""
|
||||
|
||||
def _evaluate_condition(
|
||||
self, field_value: str, operator: str, value: str
|
||||
) -> bool:
|
||||
v = value.lower()
|
||||
fv = field_value.lower()
|
||||
if operator == "contains":
|
||||
return v in fv
|
||||
elif operator == "not_contains":
|
||||
return v not in fv
|
||||
elif operator == "equals":
|
||||
return fv == v
|
||||
elif operator == "starts_with":
|
||||
return fv.startswith(v)
|
||||
elif operator == "ends_with":
|
||||
return fv.endswith(v)
|
||||
elif operator == "regex":
|
||||
try:
|
||||
return bool(re.search(value, field_value, re.IGNORECASE))
|
||||
except re.error:
|
||||
return False
|
||||
return False
|
||||
|
||||
async def _apply_actions(
|
||||
self,
|
||||
message: Message,
|
||||
actions: list[RoutingRuleAction],
|
||||
) -> None:
|
||||
"""Esegue le azioni sulla regola che ha fatto match."""
|
||||
for action in actions:
|
||||
try:
|
||||
if action.action_type == "apply_label" and action.action_value:
|
||||
await self._action_apply_label(message, uuid.UUID(action.action_value))
|
||||
elif action.action_type == "mark_read":
|
||||
message.is_read = True
|
||||
elif action.action_type == "mark_starred":
|
||||
message.is_starred = True
|
||||
elif action.action_type == "notify_webhook" and action.action_value:
|
||||
await self._action_notify_webhook(message, action.action_value)
|
||||
except Exception:
|
||||
# Le azioni non devono interrompere il flusso principale
|
||||
pass
|
||||
|
||||
async def _action_apply_label(
|
||||
self, message: Message, label_id: uuid.UUID
|
||||
) -> None:
|
||||
"""Applica un'etichetta al messaggio (se non gia' presente)."""
|
||||
# Verifica che la label appartenga al tenant
|
||||
label = await self.db.execute(
|
||||
select(Label).where(
|
||||
Label.id == label_id,
|
||||
Label.tenant_id == message.tenant_id,
|
||||
)
|
||||
)
|
||||
if not label.scalar_one_or_none():
|
||||
return
|
||||
# Verifica che non sia gia' applicata
|
||||
existing = await self.db.execute(
|
||||
select(MessageLabel).where(
|
||||
MessageLabel.message_id == message.id,
|
||||
MessageLabel.label_id == label_id,
|
||||
)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
self.db.add(MessageLabel(message_id=message.id, label_id=label_id))
|
||||
|
||||
async def _action_notify_webhook(self, message: Message, url: str) -> None:
|
||||
"""Invia una notifica webhook per il messaggio."""
|
||||
import aiohttp
|
||||
import json
|
||||
payload = {
|
||||
"event": "routing_rule_match",
|
||||
"message_id": str(message.id),
|
||||
"subject": message.subject,
|
||||
"from_address": message.from_address,
|
||||
"pec_type": message.pec_type,
|
||||
}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=aiohttp.ClientTimeout(total=5),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -166,12 +166,16 @@ class SendService:
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
has_files = bool(attachments)
|
||||
|
||||
# Invio differito: il messaggio parte in stato 'draft' se programmato
|
||||
scheduled_at = getattr(data, "scheduled_at", None)
|
||||
is_scheduled = scheduled_at is not None and scheduled_at > now
|
||||
|
||||
message = Message(
|
||||
tenant_id=current_user.tenant_id,
|
||||
mailbox_id=data.mailbox_id,
|
||||
direction="outbound",
|
||||
pec_type="posta_certificata",
|
||||
state="queued",
|
||||
state="draft" if is_scheduled else "queued",
|
||||
subject=data.subject,
|
||||
from_address=mailbox.email_address,
|
||||
to_addresses=[str(a) for a in data.to_addresses],
|
||||
@@ -211,6 +215,7 @@ class SendService:
|
||||
max_attempts=5,
|
||||
created_by=current_user.id,
|
||||
queued_at=now,
|
||||
scheduled_at=scheduled_at if is_scheduled else None,
|
||||
)
|
||||
self.db.add(job)
|
||||
await self.db.flush()
|
||||
@@ -218,7 +223,15 @@ class SendService:
|
||||
# ── Enqueue job arq ───────────────────────────────────────────────────
|
||||
try:
|
||||
arq_pool = await _get_arq_pool()
|
||||
await arq_pool.enqueue_job("send_pec", str(job.id))
|
||||
if is_scheduled and scheduled_at:
|
||||
# Invio differito: defer_until = scheduled_at
|
||||
await arq_pool.enqueue_job(
|
||||
"send_pec",
|
||||
str(job.id),
|
||||
_defer_until=scheduled_at,
|
||||
)
|
||||
else:
|
||||
await arq_pool.enqueue_job("send_pec", str(job.id))
|
||||
except Exception as e:
|
||||
from app.core.logging import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Service per la gestione dei template messaggi (Feature 1).
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.exceptions import ConflictError, NotFoundError
|
||||
from app.models.template import MessageTemplate
|
||||
from app.schemas.template import TemplateCreate, TemplateUpdate
|
||||
|
||||
|
||||
class TemplateService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def list_templates(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
q: str | None = None,
|
||||
) -> tuple[list[MessageTemplate], int]:
|
||||
query = select(MessageTemplate).where(MessageTemplate.tenant_id == tenant_id)
|
||||
if q:
|
||||
query = query.where(MessageTemplate.name.ilike(f"%{q}%"))
|
||||
query = query.order_by(MessageTemplate.name)
|
||||
|
||||
count_q = select(func.count()).select_from(query.subquery())
|
||||
total = (await self.db.execute(count_q)).scalar_one()
|
||||
items = list((await self.db.execute(query)).scalars().all())
|
||||
return items, total
|
||||
|
||||
async def get_template(
|
||||
self, tenant_id: uuid.UUID, template_id: uuid.UUID
|
||||
) -> MessageTemplate:
|
||||
result = await self.db.execute(
|
||||
select(MessageTemplate).where(
|
||||
MessageTemplate.id == template_id,
|
||||
MessageTemplate.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
template = result.scalar_one_or_none()
|
||||
if not template:
|
||||
raise NotFoundError(f"Template {template_id} non trovato")
|
||||
return template
|
||||
|
||||
async def create_template(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
data: TemplateCreate,
|
||||
created_by: uuid.UUID | None = None,
|
||||
) -> MessageTemplate:
|
||||
# Verifica unicita' nome
|
||||
existing = await self.db.execute(
|
||||
select(MessageTemplate).where(
|
||||
MessageTemplate.tenant_id == tenant_id,
|
||||
MessageTemplate.name == data.name,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ConflictError(f"Template '{data.name}' gia' esistente")
|
||||
|
||||
template = MessageTemplate(
|
||||
tenant_id=tenant_id,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
subject=data.subject,
|
||||
body_text=data.body_text,
|
||||
body_html=data.body_html,
|
||||
created_by=created_by,
|
||||
)
|
||||
self.db.add(template)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(template)
|
||||
return template
|
||||
|
||||
async def update_template(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
template_id: uuid.UUID,
|
||||
data: TemplateUpdate,
|
||||
) -> MessageTemplate:
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
|
||||
if data.name is not None:
|
||||
existing = await self.db.execute(
|
||||
select(MessageTemplate).where(
|
||||
MessageTemplate.tenant_id == tenant_id,
|
||||
MessageTemplate.name == data.name,
|
||||
MessageTemplate.id != template_id,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise ConflictError(f"Template '{data.name}' gia' esistente")
|
||||
template.name = data.name
|
||||
|
||||
if data.description is not None:
|
||||
template.description = data.description
|
||||
if data.subject is not None:
|
||||
template.subject = data.subject
|
||||
if data.body_text is not None:
|
||||
template.body_text = data.body_text
|
||||
if data.body_html is not None:
|
||||
template.body_html = data.body_html
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(template)
|
||||
return template
|
||||
|
||||
async def delete_template(
|
||||
self, tenant_id: uuid.UUID, template_id: uuid.UUID
|
||||
) -> None:
|
||||
template = await self.get_template(tenant_id, template_id)
|
||||
await self.db.delete(template)
|
||||
await self.db.commit()
|
||||
Reference in New Issue
Block a user