mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
212 lines
6.9 KiB
Python
212 lines
6.9 KiB
Python
"""
|
||
Modelli Notifiche Multi-canale.
|
||
|
||
Struttura:
|
||
NotificationChannel → configurazione canale (Webhook, Email, Telegram, WhatsApp)
|
||
NotificationRule → regola evento → canale
|
||
NotificationLog → log tentativi di invio (con retry e circuit breaker)
|
||
|
||
Canali supportati:
|
||
webhook – POST HMAC-SHA256
|
||
email – SMTP
|
||
telegram – Bot API
|
||
whatsapp – Meta Cloud API v18
|
||
|
||
Sicurezza:
|
||
config_enc – configurazione sensibile cifrata AES-256-GCM
|
||
"""
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
Boolean,
|
||
DateTime,
|
||
Enum,
|
||
ForeignKey,
|
||
Index,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
func,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
||
from app.database import Base
|
||
|
||
ChannelType = Enum(
|
||
"webhook", "email", "telegram", "whatsapp",
|
||
name="notification_channel_type",
|
||
create_type=False,
|
||
)
|
||
|
||
NotificationStatus = Enum(
|
||
"pending", "sent", "failed", "skipped",
|
||
name="notification_status",
|
||
create_type=False,
|
||
)
|
||
|
||
|
||
class NotificationChannel(Base):
|
||
"""
|
||
Canale di notifica configurato da un tenant.
|
||
|
||
Il campo config_enc contiene la configurazione sensibile (API key,
|
||
token, SMTP password…) cifrata AES-256-GCM a livello applicativo.
|
||
"""
|
||
|
||
__tablename__ = "notification_channels"
|
||
|
||
id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||
)
|
||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
channel_type: Mapped[str] = mapped_column(ChannelType, nullable=False)
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
|
||
# Configurazione NON sensibile (JSON libero: url, chat_id, from_email…)
|
||
config: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||
|
||
# Configurazione sensibile cifrata (token, password…) – base64(GCM(json))
|
||
config_enc: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
|
||
# Circuit breaker
|
||
consecutive_failures: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
circuit_open_until: Mapped[datetime | None] = mapped_column(
|
||
DateTime(timezone=True), nullable=True
|
||
)
|
||
|
||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("users.id"), nullable=True
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||
)
|
||
|
||
# Relazioni
|
||
rules: Mapped[list["NotificationRule"]] = relationship(
|
||
"NotificationRule", back_populates="channel", cascade="all, delete-orphan"
|
||
)
|
||
logs: Mapped[list["NotificationLog"]] = relationship(
|
||
"NotificationLog", back_populates="channel", cascade="all, delete-orphan"
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_notif_channel_tenant", "tenant_id"),
|
||
)
|
||
|
||
def __repr__(self) -> str:
|
||
return f"<NotificationChannel {self.name!r} type={self.channel_type!r}>"
|
||
|
||
|
||
class NotificationRule(Base):
|
||
"""
|
||
Regola: evento PEChub → canale di notifica.
|
||
|
||
event_type: new_message | state_changed | anomaly | send_failed | ...
|
||
filter: JSONB con condizioni opzionali (mailbox_id, state, ecc.)
|
||
"""
|
||
|
||
__tablename__ = "notification_rules"
|
||
|
||
id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||
)
|
||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
channel_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True),
|
||
ForeignKey("notification_channels.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
)
|
||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||
filter: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
# Relazioni
|
||
channel: Mapped["NotificationChannel"] = relationship(
|
||
"NotificationChannel", back_populates="rules"
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_notif_rule_tenant", "tenant_id"),
|
||
Index("idx_notif_rule_channel", "channel_id"),
|
||
Index("idx_notif_rule_event", "event_type"),
|
||
)
|
||
|
||
def __repr__(self) -> str:
|
||
return f"<NotificationRule {self.name!r} event={self.event_type!r}>"
|
||
|
||
|
||
class NotificationLog(Base):
|
||
"""
|
||
Log di ogni tentativo di notifica.
|
||
|
||
Retry indipendente per canale: max 3 tentativi, backoff 5m → 30m → 2h.
|
||
"""
|
||
|
||
__tablename__ = "notification_log"
|
||
|
||
id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||
)
|
||
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
channel_id: Mapped[uuid.UUID] = mapped_column(
|
||
UUID(as_uuid=True),
|
||
ForeignKey("notification_channels.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
)
|
||
rule_id: Mapped[uuid.UUID | None] = mapped_column(
|
||
UUID(as_uuid=True),
|
||
ForeignKey("notification_rules.id", ondelete="SET NULL"),
|
||
nullable=True,
|
||
)
|
||
event_type: Mapped[str] = mapped_column(String(100), nullable=False)
|
||
event_payload: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||
|
||
status: Mapped[str] = mapped_column(NotificationStatus, nullable=False, default="pending")
|
||
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
max_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
||
next_retry_at: Mapped[datetime | None] = mapped_column(
|
||
DateTime(timezone=True), nullable=True
|
||
)
|
||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
http_status: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
|
||
sent_at: Mapped[datetime | None] = mapped_column(
|
||
DateTime(timezone=True), nullable=True
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
# Relazioni
|
||
channel: Mapped["NotificationChannel"] = relationship(
|
||
"NotificationChannel", back_populates="logs"
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_notif_log_tenant", "tenant_id"),
|
||
Index("idx_notif_log_channel", "channel_id"),
|
||
Index("idx_notif_log_status", "status", "next_retry_at"),
|
||
)
|
||
|
||
def __repr__(self) -> str:
|
||
return f"<NotificationLog channel={self.channel_id} status={self.status!r}>"
|