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
| Requirement | Notes |
|---|---|
| SQL Server | Notification tables live in the ERP's ApplicationDbContext (per-tenant) and SharedDbContext (TenantNotificationLog) |
| Hangfire | Backs every recurring job in §16.3 — now wrapped by the platform's Background Jobs Framework |
| Firebase project + service account | Required only if PushSettings.EnableSending = true |
| SMTP account | Required for the Email channel |
| Meta WhatsApp Business account + Cloud API access | Required for the WhatsApp channel and inbound delivery receipts |
| SMS gateway account | Required 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 ID | Schedule |
|---|---|
notification-retry-processor-pipeline | Every 2 minutes |
notification-retry-expire-pipeline | Hourly |
notification-deadletter-cleanup-pipeline | Daily, 02:00 UTC |
campaign-scheduler-pipeline | Every 1 minute |
campaign-workflow-executor-pipeline | Every 2 minutes |
campaign-cleanup-pipeline | Daily, 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.