Skip to main content
Version: 1.0

16. Deployment

This chapter is adapted from the platform's existing operational runbooks (docs/NOTIFICATION_FRAMEWORK_PHASE441_DEPLOYMENT_RUNBOOK.md, docs/NOTIFICATION_FRAMEWORK_PHASE441_SERVER_CHECKLIST.md), corrected against verified current source where they disagree (see the callout in §16.4).

16.1 Requirements

RequirementNotes
SQL ServerNotification tables live in the ERP's ApplicationDbContext (per-tenant) and SharedDbContext (TenantNotificationLog)
HangfireBacks every recurring job in §16.3 — now wrapped by the platform's Background Jobs Framework
Firebase project + service accountRequired only if PushSettings.EnableSending = true
SMTP accountRequired for the Email channel
Meta WhatsApp Business account + Cloud API accessRequired for the WhatsApp channel and inbound delivery receipts
SMS gateway accountRequired for the SMS channel (see §15.5 — config keys are environment-specific)

16.2 Services

The Notification Framework has no standalone process of its own — it runs entirely inside the ERP API host process (Shumoul.Api) plus the MultiTenancyApi host (for the legacy SignalR hub and legacy controllers). There is nothing to deploy separately beyond the ERP API and MultiTenancyApi hosts themselves.

16.3 Background jobs

Registered in Shumoul.Infrastructure\Extensions\ApplicationBuilderExtensions.cs, all now routed through the Background Jobs Framework pipeline (see Chapter 12 and §9.9 for the campaign jobs):

Job IDSchedule
notification-retry-processor-pipelineEvery 2 minutes
notification-retry-expire-pipelineHourly
notification-deadletter-cleanup-pipelineDaily, 02:00 UTC
campaign-scheduler-pipelineEvery 1 minute
campaign-workflow-executor-pipelineEvery 2 minutes
campaign-cleanup-pipelineDaily, 03:00 UTC

Confirm all six appear as recurring jobs in the Hangfire dashboard after any deployment.

16.4 Pre-deployment checklist

[ ] git status — clean working tree on main
[ ] dotnet build — 0 errors, 0 new warnings
[ ] Confirm current package versions (Shumoul.Notification.*, Shumoul.Framework.*, Shumoul.Framework.MultiTenancy.*)
against C:\MultiTenancy / local-packages — do not assume the versions in an older phase report are current
[ ] Check for pending EF Core migrations: `dotnet ef migrations list --project Shumoul.Infrastructure --startup-project Shumoul.Api --context ApplicationDbContext`
(do not assume "zero migrations" as an older report may state — later phases, including the campaign
engine's WorkerToken column, did add migrations)
[ ] Backup/snapshot taken if environment policy requires it
[ ] Test tenant identified for post-deploy smoke tests

16.5 Post-deployment quick checks

# 1. Health check
curl -s https://<your-api>/api/health

# 2. Provider health/statistics (requires auth token)
curl -s -H "Authorization: Bearer <token>" \
https://<your-api>/api/v1/NotificationAnalytics/Providers

# 3. Analytics summary
curl -s -H "Authorization: Bearer <token>" \
"https://<your-api>/api/v1/NotificationAnalytics/Summary?dateFrom=2026-06-01&dateTo=2026-06-30"

Expected health check route: /api/health (confirmed in current source at Shumoul.Infrastructure\Extensions\ServiceCollectionExtensions.cs) — an older phase risk note mentioning /healthz as a placeholder is superseded; use /api/health.

16.6 Database verification

-- Confirm the applied migration baseline is current — do not assume a specific migration ID from an
-- older report; list the actual latest applied migration instead:
SELECT TOP 1 MigrationId FROM [__EFMigrationsHistory] ORDER BY MigrationId DESC;

-- Delivery health snapshot (last 24 hours) — use the VERIFIED enum values (see §16.4 correction below)
SELECT ResultState, COUNT(*) AS Count
FROM NotificationDeliveryAttempts WITH (NOLOCK)
WHERE StartedOn >= DATEADD(DAY, -1, GETUTCDATE())
GROUP BY ResultState;

16.4 Known inaccuracy in the existing server checklist

docs/NOTIFICATION_FRAMEWORK_PHASE441_SERVER_CHECKLIST.md documents ResultState as 1=Succeeded 2=Failed 3=Retrying 4=Expired. This does not match the actual NotificationDeliveryState enum (verified in source, see §12.1):

Pending=0, Processing=1, Succeeded=2, RetryScheduled=3, Retrying=4, Failed=5, DeadLetter=6, Cancelled=7, Expired=8

Use the enum above when interpreting ResultState values in any query or dashboard — do not carry the older checklist's mapping forward.

16.7 Smoke test sequence

Trigger one notification per channel and verify each lands with ResultState = 2 (Succeeded, per the corrected enum above) in NotificationDeliveryAttempts:

[ ] Email → event triggered → email received → ResultState = 2
[ ] SMS → event triggered → SMS received → ResultState = 2
[ ] WhatsApp → template sent → message received → ResultState = 2 (and a NotificationDeliveryReceipt row
appears shortly after, once Meta's delivery webhook fires)
[ ] Push → event triggered → push received → ResultState = 2

After each send, re-check GET /api/v1/NotificationAnalytics/Providers and confirm totalSucceeded incremented for the corresponding provider. See Chapter 17 — Testing Guide for a fuller failure/retry/dead-letter simulation procedure.

16.8 Regression quick-checks

[ ] Every analytics endpoint returns 200: Summary, Channels, Events, Sla, RetryQueue, DeadLetters, Trends,
RecentFailures, Failures/TopReasons, Providers
[ ] Hangfire dashboard lists all 6 pipeline jobs from §16.3 as recurring
[ ] No stuck NotificationRetryQueue rows in State=Retrying older than 2 hours (the retry-expire job's
orphan-repair threshold — see §12.4)

16.9 Security quick-checks

[ ] Logs contain no AccessToken, PhoneNumberId, SMTP password, or rendered WhatsApp parameter values
[ ] GET /NotificationAnalytics/Providers response body contains no credential fields
[ ] Every notification endpoint returns 401 without a Bearer token, and 403 with a token lacking the
required permission (see Chapter 14)
[ ] WhatsAppCloudApi.SignatureValidationEnabled is true in production

16.10 Rollback

Whether a given deployment is code-only or includes a migration depends on what changed — check §16.4 pre-deployment checklist for pending migrations before assuming a code-only rollback is sufficient. For a code-only change:

1. Deploy the previous build artifact from the pipeline.
2. GET /api/health → confirm Healthy.
3. Re-run the smoke test sequence (§16.7) for at least one channel to confirm baseline behavior.
4. Open an investigation ticket before re-attempting the deployment.

If the deployment included a migration, follow the platform's general migration rollback guidance in .claude/rules/migrations.md — never run dotnet ef database update to roll back without explicit user instruction per CLAUDE.md §7.