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 |
|---|---|---|---|
| EmailDispatcher | smtplib + TLS | Funcional | |
| 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¶
DispatchWorkerconsume jobs deasyncio.Queue- Llama a
dispatcher.send(notificacion) - Si falla y
attempts < max_retries: re-encola conjob.with_attempt() - Si falla y
attempts >= max_retries: actualiza estado aFALLIDA+ audit - Si exito: actualiza estado a
ENVIADA+ registrafecha_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.