""" 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"" 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"" 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""