Skip to main content
Version: 1.0

5. Framework Packages

The Notification Framework ships as 6 versioned NuGet packages from the Shumoul.Saas.NotificationFramework repository, plus one small package (Shumoul.Notification.SignalR) consumed by the legacy hub host. This chapter documents each package's purpose, responsibilities, public API surface, and forbidden responsibilities.

5.1 Shumoul.Notification.Contracts

Purpose: the shared vocabulary every other package and the ERP host speak. Zero ERP references, zero EF Core references.

Responsibilities:

  • Enums: NotificationChannel, DevicePlatform, SaaSNotificationType, NotificationDeliveryStatus, NotificationDeliveryState, NotificationFailureType, RetryStrategy, NotificationDeadLetterStatus, SaaSNotificationScheduleStatus, NotificationReceiptStatus (namespace ...Contracts.Receipts), ProviderHealthState, campaign enums (CampaignType, CampaignStatus, CampaignExecutionStatus, RecurrenceType, DelayUnit, WorkflowStepTrigger, AudienceSourceType).
  • Cross-cutting DTOs: DispatchNotificationRequest, DirectCommunicationRequest, ChannelDeliveryResult, ChannelDispatchContext, NotificationCountersDto, NotificationHistoryFilterDto, TenantNotificationLogDto/TenantNotificationLogDetailsDto, UserNotificationDto.
  • Settings POCOs: SaaSNotificationSettings, PushSettings.
  • Constants: NotificationSignalRConstants (hub contract), NotificationEventKeys (well-known event key strings), NotificationPermissions (permission string catalog).
  • ErpCompat/ sub-tree — a deliberate compatibility layer. ErpCompat/Enums/*.cs and ErpCompat/DTOs/*.cs declare the same enum/DTO shapes but under namespace Shumoul.Domain.Enums.Notifications / namespace Shumoul.Application.DTOs.Notifications respectively, so existing ERP using statements keep working unchanged. Some ErpCompat types are renamed from their native counterpart — e.g. native NotificationDeliveryStatus is NotificationStatus in ErpCompat. Treat ErpCompat as "what the ERP actually imports" and native Contracts.* as "the cross-cutting SDK surface."

Public API: everything above is public; there is no internal-only surface in this package.

Forbidden responsibilities: must never reference Shumoul.Framework.Application, Shumoul.Domain, or any EF Core package — this was enforced as part of Level 3 Golden Reference certification (Gap 1, closed: the Shumoul.Framework.Application package reference was removed entirely from this project).

5.2 Shumoul.Notification.Abstractions

Purpose: the ports the ERP host must implement. No implementation logic lives here.

Responsibilities — one interface per aggregate root, deliberately not a "God Repository":

  • Repositories/: INotificationRepository, ITemplateRepository (+ TemplateData), IDeviceTokenRepository (+ DeviceTokenData), ICampaignRepository (+ CampaignStateData), IDeliveryPolicyRepository (+ DeliveryPolicyData), IDeliveryAttemptRepository (+ NewDeliveryAttemptData), IRetryRepository (+ DueRetryData, NewRetryData), IDeadLetterRepository (+ NewDeadLetterData).
  • Persistence/: INotificationPersistenceContextSaveChangesAsync, BeginTransactionAsync, CommitAsync, RollbackAsync. The only interface in this package that owns transaction control.
  • Core/ (zero ERP references): IUserDirectory, ICurrentUserContext, ITenantContext, INotificationClock, INotificationConfigurationProvider.
  • Adapters/ (contracts the ERP host must implement): IUserDirectoryAdapter, INotificationRepositoryAdapter (aggregates all 8 repository interfaces as a unit-of-work view), ICurrentUserAdapter, ITenantContextAdapter.
  • Providers/: INotificationChannelProvider (one implementation per NotificationChannel) and INotificationChannelProviderResolver.
  • Retry: INotificationRetryStrategy, IRetryStrategyProvider.

Public API: all interfaces above.

Forbidden responsibilities: must never contain a concrete class, must never reference EF Core or IRepositoryAsync directly, must never reference Shumoul.Framework.Application (Gap 1 closure, same as Contracts).

5.3 Shumoul.Notification.Core

Purpose: the actual dispatch engine, retry strategies, and provider resolution/failover logic.

Responsibilities:

  • Services/Dispatch/NotificationDispatchService.cs — the real dispatch engine (see Chapter 6 — Runtime Flow). Resolves event configuration, selects templates per channel/language, resolves an INotificationChannelProvider per channel via INotificationChannelProviderResolver, and hands off to it.
  • Providers/: per-channel implementations of INotificationChannelProviderEmailChannelProvider, SmsChannelProvider, PushChannelProvider, SignalRChannelProvider, InAppChannelProvider. (WhatsApp has no dedicated channel-provider class — it is handled inline by the ERP-owned WhatsAppNotificationSender, invoked directly by the dispatch path rather than through this resolver.)
  • RetryStrategies/: ImmediateRetryStrategy, FixedRetryStrategy, LinearRetryStrategy, ExponentialBackoffRetryStrategy, NoRetryStrategy, resolved via RetryStrategyProvider.
  • Notifications/: channel service implementations — FirebasePushNotificationService (multi-provider failover via INotificationProviderResolver/IProviderHealthMonitor), ScribanTemplateRenderer, EmailNotificationService, SmsNotificationService.
  • Provider health tracking: InMemoryProviderHealthMonitor — a 5-consecutive-failure circuit breaker with 30-minute auto-recovery, keyed by provider name + channel.

Public API: NotificationDispatchService, all channel providers, all retry strategies, RetryStrategyProvider, ScribanTemplateRenderer, EmailNotificationService, SmsNotificationService, FirebasePushNotificationService.

Forbidden responsibilities: must never contain ERP business rules (e.g. which purchase-invoice statuses trigger a notification) — that decision belongs to the ERP host, expressed only as calls into DispatchNotificationRequest.

Note on layered provider abstraction: Core actually has two composing abstraction layers, not one:

  1. Per-channel failover (INotificationProviderResolver / IProviderHealthMonitor) — priority-ordered, health-tracked provider implementations per channel (e.g. ZohoEmailProvider, FourJawalySmsProvider, FirebasePushProvider, MetaWhatsAppProvider), consumed internally by EmailNotificationService, SmsNotificationService, FirebasePushNotificationService.
  2. Unified channel dispatch (INotificationChannelProvider / INotificationChannelProviderResolver) — sits above layer 1. EmailChannelProvider, for example, wraps IEmailNotificationService rather than replacing it.

Both layers are intentional, permanent architecture (confirmed by ADR-002's Gap 3 closure note), not a migration-in-progress.

5.4 Shumoul.Notification.Persistence

Purpose: entity classes and EF Core configuration for every framework-owned table.

Responsibilities:

  • NotificationEntitypublic abstract class NotificationEntity : BaseEntity {}, an empty module-owned marker base class. Every framework entity inherits this instead of BaseEntity directly, to keep Shumoul.Framework.Domain.BaseEntity an implementation detail behind a module-owned seam (ADR-002, Gap 2 — accepted deviation: Shumoul.Framework.Domain remains a dependency of this project because BaseEntity/ICreationEntity/IAuditableEntity/ISoftDelete are needed).
  • Entities (all declare namespace Shumoul.Domain.Entities.Notifications for ERP compatibility): AppNotification, AppNotificationTemplate, DeviceToken, NotificationEventConfiguration, NotificationDeliveryPolicy, NotificationDeliveryAttempt, NotificationRetryQueue, NotificationDeadLetter, NotificationDeliveryReceipt, and 5 campaign entities (NotificationCampaign, NotificationCampaignAudience, NotificationCampaignExecution, NotificationCampaignStep, NotificationCampaignStepExecution).
  • NotificationModelBuilderContributor — applies EF configuration for AppNotification, AppNotificationTemplate, DeviceToken, NotificationDeadLetter, NotificationDeliveryAttempt, NotificationDeliveryPolicy, NotificationEventConfiguration, NotificationRetryQueue.

Known gap (documented, not hidden): this contributor does not configure NotificationCampaign* or NotificationDeliveryReceipt — those 6 tables are instead registered as DbSet<T> properties directly on the ERP's own ApplicationDbContext, and were created by an ERP-side migration (20260625113609_Add_NotificationDeliveryReceipt_Initial, whose name undersells its own scope — it actually creates all 6 campaign/receipt tables in one migration).

Public API: the entity classes themselves, NotificationEntity, NotificationModelBuilderContributor.

Forbidden responsibilities: must never reference IRepositoryAsync or the ERP's ApplicationDbContext directly — entities are plain EF Core POCOs; the ERP host's ApplicationDbContext and repository layer are what actually query them.

5.5 Shumoul.Notification.SignalR

Purpose: the small, reusable slice of real-time infrastructure that both a framework-native SignalR integration and the legacy NotificationHub in MultiTenancyApi can share.

Responsibilities:

  • NameIdentifierUserIdProvider : IUserIdProvider — resolves the SignalR-connected user's ID from the custom "NameIdentifier" JWT claim first, falling back to the standard ClaimTypes.NameIdentifier. This is what makes Clients.User(userId) resolve correctly for Shumoul's JWT shape.
  • Exposed via AddShumoulNotificationSignalR() — a DI extension method that registers the provider.

Public API: NameIdentifierUserIdProvider, AddShumoulNotificationSignalR().

Forbidden responsibilities: must never contain a Hub class itself, must never reference MultiTenancyApi-specific services (INotificationTopicSubscriberService, INotificationConnectionService) — see §4.4.1 for why the hub stays host-owned.

NotificationSignalRConstants (the canonical ClientMethod = "messages", TenantGroupPrefix = "GroupTenant-", TopicGroupPrefix = "Topic-" values) lives in Shumoul.Notification.Contracts, not in this package, since both the hub host and Angular/Flutter client documentation need to reference it without taking a SignalR package dependency.

5.6 ERP Host Integration (Shumoul.Saas.Api adapters)

The ERP host does not consume the framework's Abstractions interfaces by reimplementing storage from scratch — it adapts its existing IRepositoryAsync behind them:

Adapter (ERP-owned)Bridges
DeliveryPolicyRepositoryAdapter (Shumoul.Infrastructure\Adapters\Notification\)IRepositoryAsyncIDeliveryPolicyRepository/IDeliveryPolicyRepositoryAdapter
NotificationRetryProcessorJobAdapter, NotificationRetryExpireJobAdapter, NotificationDeadLetterCleanupJobAdapter (Shumoul.Infrastructure\Adapters\BackgroundJobs\)Background Jobs Framework IBackgroundJob → the framework's own retry/cleanup workers
WhatsAppNotificationSender (Shumoul.Infrastructure\Services\Notifications\)ERP-specific WhatsApp business templates → IWhatsAppService (from Shumoul.Saas.WhatsAppIntegration)

This mirrors the same two-tier adapter pattern used by the platform's Background Jobs Framework: generic runtime in a package, ERP-specific wiring in a small adapter class inside the host.