3. Introduction
3.1 What is the Notification Framework?
The Shumoul Notification Framework is the platform-wide system responsible for composing, rendering, dispatching, retrying, and tracking every notification sent by Shumoul Cloud ERP — across six delivery channels: Email, SMS, WhatsApp, Push (mobile), SignalR (real-time web), and In-App.
It answers one question for every business event in the ERP ("a purchase invoice was posted", "a sales order was approved", "a subscription is about to expire"): who needs to know, on which channel, in which language, with what content, and what happens if delivery fails?
It is not a single project — it is a multi-repository framework:
| Concern | Where it lives |
|---|---|
| Channel-agnostic contracts, enums, DTOs | Shumoul.Notification.Contracts (NuGet package) |
| Interfaces / ports the ERP host must implement | Shumoul.Notification.Abstractions (NuGet package) |
| Dispatch engine, retry strategies, provider resolution | Shumoul.Notification.Core (NuGet package) |
| Entities and EF Core configuration | Shumoul.Notification.Persistence (NuGet package) |
| Real-time hub support (user-ID provider, constants) | Shumoul.Notification.SignalR (NuGet package) |
| ERP-specific entities, controllers, campaign engine, WhatsApp sender | Shumoul.Saas.Api (this repository) |
| Legacy SignalR hub + legacy CRUD controllers (pre-framework) | Shumoul.Saas.MultiTenancyApi |
3.2 Why was it built?
Before this framework existed, notification logic was scattered: a SignalR hub and a set of CRUD controllers
in the MultiTenancyApi host, ad-hoc IMailService/ISendSMSService calls sprinkled through ERP business
services, and no unified retry, dead-letter, or delivery-tracking story. Every new channel (WhatsApp, Push)
was bolted on separately, with its own guard conditions and its own failure handling.
The framework consolidates this into one dispatch pipeline with:
- A single request shape (
DispatchNotificationRequest) that every ERP business service uses regardless of channel. - One template system (event key + channel + language → rendered subject/body).
- One retry/backoff/dead-letter engine shared by every channel.
- One analytics surface answering "what got delivered, what failed, and why."
3.3 Problems it solves
- Channel fragmentation — Email, SMS, WhatsApp, Push, SignalR, and In-App previously had independent,
inconsistent failure handling. Now every channel flows through the same
INotificationChannelProviderdispatch contract and the same delivery-policy/retry/dead-letter engine. - No visibility into failures —
NotificationAnalyticsControllerandNotificationDeliveryReceiptControllergive operators a queryable view of delivery rates, failure reasons, and per-provider health. - No administrator control without a deploy —
NotificationEventConfigurationsControllerlets an administrator enable/disable a channel for a specific business event from the database, with zero code changes or redeploys. - Provider lock-in and outages — the provider-abstraction layer (§ Framework Packages) supports multiple providers per channel with automatic health-based failover.
- One-off/manual bulk sends —
NotificationCampaignControllerturns "send this to a segment of users, on a schedule, possibly multi-step" into a first-class, auditable entity instead of a one-off script.
3.4 Goals
- A single, typed dispatch contract for every ERP business service, regardless of destination channel.
- Database-driven configuration for which channels are active per event, per tenant, without a deployment.
- A durable retry/backoff/dead-letter pipeline shared by all channels.
- Real-time delivery to connected web clients via SignalR, with the same dispatch path as every other channel.
- First-class delivery visibility: attempts, receipts, analytics, and recent-failure surfaces.
- Reusable, host-independent packages (
Shumoul.Notification.*) that could in principle be consumed by a different ERP host, not just Shumoul's.
3.5 Architecture philosophy
The framework follows the platform's Framework First principle (see
docs/ARCHITECTURE/STANDARDS/ in this repository): generic, reusable notification infrastructure belongs in
the standalone Shumoul.Saas.NotificationFramework repository and is consumed as versioned NuGet packages.
The ERP host (Shumoul.Saas.Api) owns only what is genuinely ERP-specific: the campaign engine, the
WhatsApp business-template sender, and a small number of adapter classes that let the framework's
interfaces (IDeliveryPolicyRepository, INotificationRepository, etc.) run against the ERP's shared
IRepositoryAsync.
A deliberate, documented deviation exists: the framework's own entities (AppNotification,
NotificationDeliveryPolicy, NotificationRetryQueue, NotificationDeadLetter, campaign entities, etc.)
physically live in Shumoul.Notification.Persistence but declare namespace Shumoul.Domain.Entities.Notifications
(and DTOs declare namespace Shumoul.Application.DTOs.Notifications) so that existing ERP code can consume
them without an import-path change. This is recorded as ADR-002 in
docs/ARCHITECTURE/adr/ADR-002-notification-entity-base-class-deviation.md — an accepted, permanent
deviation, not a transitional hack.
3.6 Golden Reference status
As of this writing, the Notification Framework has completed a Golden Reference Module extraction
(2026-06-27 → 2026-06-29): all notification entities were moved out of Shumoul.Domain into the standalone
Shumoul.Notification.Persistence package, a unified INotificationChannelProvider dispatch layer was
introduced, and the ERP's own dispatch service was reduced to a thin pass-through adapter. Separately, every
Hangfire recurring job in the notification pipeline (retry processor, retry-expire, dead-letter cleanup,
campaign scheduler/executor/cleanup) was re-wrapped to run through the platform's Background Jobs
Framework (job IDs now carry a -pipeline suffix; the underlying business logic classes are unchanged).
This means: treat any older phase report under docs/NOTIFICATION_FRAMEWORK_PHASE*.md as historically
accurate but structurally superseded on the two points above. This documentation reflects the current,
post-extraction state; where an older report and current source disagree, this documentation follows source.