Saltar a contenido

Dispatch Multicanal

GexCom implementa despacho de notificaciones via el Strategy Pattern: cada canal tiene su propio dispatcher que implementa INotificacionDispatcher.

Arquitectura de Dispatch

graph TB
    UC[DispatchNotificacionUseCase] --> REG[DispatcherRegistry]
    REG --> |canal=EMAIL| ED[EmailDispatcher<br/>SMTP / smtplib]
    REG --> |canal=WHATSAPP| WD[WhatsAppDispatcher<br/>Meta API / urllib]
    REG --> |canal desconocido| ERR[Failure: dispatcher no encontrado]

    subgraph Worker["DispatchWorker (Background)"]
        Q[asyncio.Queue<br/>DispatchJob items]
        L[Loop asincrono<br/>retry policy]
        Q --> L
        L --> |reintento| Q
    end

    UC --> Q
    L --> ED
    L --> WD
    L --> |exito| UPD1[update ENVIADA + audit]
    L --> |fallo max_retries| UPD2[update FALLIDA + audit]

    subgraph Events["EventBus (Domain Events)"]
        EB[EventBus]
        E1[NotificacionCreadaEvent]
        E2[EstadoCambiadoEvent]
        EB --> E1
        EB --> E2
    end

    UC --> EB

INotificacionDispatcher Protocol

class INotificacionDispatcher(Protocol):
    def send(self, notificacion: Notificacion) -> Result[None, str]: ...
    def supports_canal(self, canal: CanalNotificacion) -> bool: ...

Ambos dispatchers implementan este Protocol (duck typing, sin herencia).

DispatchJob (frozen dataclass)

@dataclass(frozen=True)
class DispatchJob:
    notificacion: Notificacion
    max_retries: int = 3
    attempts: int = 0

    def with_attempt(self) -> DispatchJob:
        return DispatchJob(
            notificacion=self.notificacion,
            max_retries=self.max_retries,
            attempts=self.attempts + 1,
        )

Inmutable — cada reintento crea una nueva instancia con with_attempt().

Canales Soportados

Canal Dispatcher Implementacion Estado
EMAIL EmailDispatcher smtplib + TLS Funcional
WHATSAPP WhatsAppDispatcher Meta Graph API Funcional
PRESENCIAL Manual (sin auto-dispatch) Funcional
TELEFONO Manual (sin auto-dispatch) Funcional
CORREO_FISICO Manual (sin auto-dispatch) Funcional

Politica de Reintentos

  1. DispatchWorker consume jobs de asyncio.Queue
  2. Llama a dispatcher.send(notificacion)
  3. Si falla y attempts < max_retries: re-encola con job.with_attempt()
  4. Si falla y attempts >= max_retries: actualiza estado a FALLIDA + audit
  5. Si exito: actualiza estado a ENVIADA + registra fecha_envio + audit

Domain Events

El EventBus in-process permite reaccionar a cambios de estado sin acoplar capas:

# Publicar evento
event_bus.publish(EstadoCambiadoEvent(
    notificacion_id=notif.id,
    estado_anterior=EstadoNotificacion.EN_PROCESO,
    estado_nuevo=EstadoNotificacion.ENVIADA,
))

# Suscribir handler
event_bus.subscribe(EstadoCambiadoEvent, mi_handler)

Backlog P03

El EventBus esta implementado pero sin consumidores activos en produccion (item EVENTBUS-INTEG en backlog P03). Los eventos se publican pero ningún handler esta suscrito todavia.