vbox funzionanti

This commit is contained in:
2026-03-19 11:41:10 +01:00
parent 538d6a6bec
commit b7f7c1f7c0
32 changed files with 6043 additions and 262 deletions
+211
View File
@@ -0,0 +1,211 @@
"""
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 PecFlow → 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}>"