Alert Processor — Reference
Complete reference for the alert pipeline: how each channel works, how recipients are resolved, the full content dict contract, all edge cases, and the channel request dataclasses.
Pipeline overview
POST /api/user/alerts
│
├── AlertsPostHandler.handle()
│ ├── Validate unitId, createdOn, recipients
│ └── Build AlertsRequestData dataclass
│
└── AlertsProcessor.process_alerts()
├── get_content() ← maps alertType → content class → builds content dict
├── get_recipient_contacts() ← resolves email addresses, phone numbers, FCM tokens
└── send_to_all_channel() ← builds channel DTOs and dispatches:
├── EmailChannel → SMTP email (with optional HTML template)
├── MessageChannel → SMS gateway
├── WhatsAppChannel → WhatsApp gateway
├── FirebaseChannel → FCM push notifications
└── WebChannel → Azure Web PubSub (real-time web)
All existing alert types
alertType | Content class |
|---|---|
unit_threshold | UnitThresholdAlertContent |
energy_daily_threshold | UnitThresholdAlertContent |
hourly_threshold | HourlyThresholdAlertContent |
monthly_threshold | MonthlyThresholdAlertContent |
energy_monthly_threshold | MonthlyThresholdAlertContent |
device_status | DeviceStatusAlertContent |
level_exceed | LevelAlertContent |
stable_flow_alert | StableFlowAlertContent |
flow_rate_above_the_range | FlowRateAlertContent |
flow_rate_below_the_range | FlowRateAlertContent |
flow_rate_back_in_range | FlowRateAlertContent |
quality_threshold | QualityThresholdAlertContent |
energy_threshold | EnergyAlertContent |
no_flow_n_days | NoFlowAlertContent |
no_level_variation_n_days | NoLevelVariationAlertContent |
water_balance_leakage | WaterBalanceLeakageAlertContent |
Content dict contract
The content class build() method returns a plain dict. All channels consume this dict — it must contain at minimum subject and body. All other keys are available as Jinja2 variables in the email HTML template.
{
'subject': str, # email subject, SMS/WhatsApp header, FCM title
'body': str, # main message text used by every channel
'industry_name': str, # available to email template
'unit_name': str, # available to email template
'alert_type': str, # used by EmailChannel to select the template
# ... any additional keys your template needs
}
Content classes must not call the database or dispatch channels. They only build and return a dict.
Channel dispatch — how each channel works
EmailChannel — SMTP
File: app/services/alerts/channels/email_channel.py
Renders the Jinja2 HTML template using the full content dict as template variables, then sends via SMTP to all emailIds in the resolved recipient list.
EmailRequest(
bodyHtml = render_template('alert/my_alert.html', **content),
subject = content['subject'],
notification_type = content['alert_type'],
)
If no custom template exists for the alert_type, falls back to alert/default.html.
SMTP secrets (Azure Key Vault): SMTP-HOST, SMTP-PORT, SMTP-USERNAME, SMTP-PASSWORD
MessageChannel — SMS
File: app/services/alerts/channels/message_channel.py
Sends plain text only to all phoneNo values in the recipient list.
SMSRequest(
notification_type = content['alert_type'],
subject = content['body'], # field is named 'subject' for historical reasons; carries the SMS body
)
SMS gateway secrets: SMS-API-ENDPOINT, SMS-USER-ID, SMS-PASSWORD, MSG-TYPE, SMS-VERSION, SMS-FORMAT
WhatsAppChannel — WhatsApp gateway
File: app/services/alerts/channels/whatsapp_channel.py
WhatsappRequest(
notification_type = content['alert_type'],
title = content['subject'],
body = content['body'],
)
Uses the same phoneNo list as SMS.
FirebaseChannel — FCM push notifications
File: app/services/alerts/channels/firebase_channel.py
MobilePushRequest(
notification_type = content['alert_type'],
title = content['subject'],
body = content['body'],
)
FCM tokens are not in the POST body — they are batch-fetched from Cosmos DB via DatabaseSupporter.get_fcm_ids(industryId, user_ids), reading the fcmTokens array on each user document.
Firebase credentials: FIREBASE-CREDENTIALS secret (service account JSON dict)
WebChannel — Azure Web PubSub
File: app/services/alerts/channels/web_channel.py
WebPushRequest(
notification_type = content['alert_type'],
title = content['subject'],
body = content['body'],
)
Publishes to the V2_{industryId} group on Azure Web PubSub. Does not use the recipient list — always broadcasts to all connected web clients for the industry.
Web PubSub secrets: WEBPUB-ENDPOINT, HUB-NAME
Recipient resolution
get_recipient_contacts() maps the recipients[] array from the POST body to actual contact details:
| Contact type | Source |
|---|---|
emailIds | Fetched from each recipient's user document in the users Cosmos container |
phoneNo | Fetched from each recipient's user document |
| FCM tokens | Batch-fetched via DatabaseSupporter.get_fcm_ids(industryId, user_ids) |
| Web PubSub group | Always V2_{industryId} — no per-recipient resolution |
Channel request dataclasses reference
All five DTOs live in app/data_class/alert/channel_request_args.py:
| Dataclass | Fields | Channel |
|---|---|---|
EmailRequest | bodyHtml, subject, notification_type | SMTP email |
SMSRequest | notification_type, subject (carries body text) | SMS gateway |
WhatsappRequest | notification_type, title, body | WhatsApp gateway |
MobilePushRequest | notification_type, title, body | FCM push |
WebPushRequest | notification_type, title, body | Azure Web PubSub |
Edge cases
recipients is empty or absent
All five channel sets will be empty. The processor returns 200 with success: [] and failed: []. Your content builder is never invoked — the pipeline stops before get_content() when recipients is empty.
Unknown alertType — None from get_content()
If your elif branch is not reached, get_content() returns None. The processor returns "Alert type not implemented" and stops — nothing is dispatched. Verify the string in your constant exactly matches what the caller sends.
A channel has no recipients — skipped, not errored
If a user has no emailIds or no phoneNo, that channel's recipient set is empty and the channel is silently skipped. Partial dispatch is normal.
FCM tokens vs email/phone
Email and phone come from recipients[] → resolved from Cosmos DB user documents. FCM tokens are not in the POST body — batch-fetched separately.
Web PubSub broadcasts to the whole industry
The web channel ignores the recipient list. All connected web clients for the industry receive the event regardless of who the alert was addressed to.
createdOn format
Must be YYYY-MM-DDTHH:MM:SSZ. Any other format returns 400. Your content builder receives this pre-validated.
alerts_config controls which alert types each user receives
Even if you dispatch an alert, the alerts_config container gates delivery per user. If a user does not have your alertType in their permissions.customUsers[userId].permissions list, they will not receive it. When adding a new alert type, ensure the relevant users are configured in alerts_config.
Email HTML template
Templates live at app/templates/alert/. All content dict keys are available as Jinja2 variables.
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; font-size: 13px; color: #333; }
.alert-box { border-left: 4px solid #e53e3e; padding: 12px 16px; background: #fff5f5; }
</style>
</head>
<body>
<h2>{{ subject }}</h2>
<div class="alert-box">
<p>{{ industry_name }} — {{ unit_name }}</p>
<p>{{ body }}</p>
</div>
</body>
</html>
If your alert type has no custom template, alert/default.html is used automatically.