> For the complete documentation index, see [llms.txt](https://docs.digit.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digit.org/health/design/architecture/low-level-design/services/health-services/health-notification.md).

# Health Notification

## Overview

Health Notification Service is the **messaging brain** of a health campaign — it decides *who should be told what, when, and over which channel*. It listens to things happening elsewhere in the platform (stock moving between facilities, a referral being raised, a household being served during distribution) and turns each of those into a human-readable message.

It does two jobs:

* **Push notifications (immediate)** — when a stock transfer or a referral happens, it sends an in-app push alert (via the **egov-notification-push** service → FCM) to the right facility's field staff.
* **SMS reminders (scheduled)** — when a beneficiary household is served, it schedules one or more SMS messages (e.g. a follow-up reminder 2 or 3 days later) and a daily job sends them out.

Along the way, it **looks up the recipient's details**, **picks the correct localised template** (English/French, etc.), **fills in the blanks** (names, quantities, dates), and **encrypts personal data** (phone numbers) before storing it.

In short: *"something happened — figure out who to message, in their language, and send it now or later."*

## Business Flow

* **During the campaign (runtime)**, two kinds of triggers arrive on Kafka:
  * A **stock movement** (issued/returned, accepted/rejected) or a **health-facility referral** → an **immediate push** alert to the facility staff who need to act (e.g. "stock is on its way, please confirm receipt").
  * A **distribution task** was completed successfully (a beneficiary was administered a commodity) → one or more **scheduled SMS** follow-ups to the beneficiary household.
* **Which messages fire, to whom, and in what language** is not hard-coded — it is read from the **MDMS** campaign configuration (`HCM-NOTIFICATION-CONFIG`). Programme owners switch event types on/off, set recipient roles, set template codes, and set how many hours/days after the event each SMS should go out.
* **Personal data is protected** — beneficiary phone numbers and message context are encrypted before being stored, and only decrypted at send time.
* **A daily scheduler** wakes up, finds every SMS that is now due, builds the final localised text, and hands it to the SMS gateway.

## API Details

This service is **driven almost entirely by Kafka events**, not by public REST calls. There is no beneficiary-facing or client-facing API. The HTTP endpoints that do exist are **operational/diagnostic only**.

**REST entry points (operational):**

<table><thead><tr><th width="251.05078125">Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td><code>POST /health-notification-service/notification/v1/_cache/refresh</code></td><td>Reloads the MDMS notification config and localisation templates into the in-memory cache without a restart (<code>CacheController</code>).</td></tr><tr><td><code>POST /health-notification-service/test/v1/stock/_notify</code></td><td>Test-only — replays a raw stock Kafka payload through the push flow. Active only under the <code>hns-local</code> profile (<code>TestNotificationController</code>); not enabled in deployed environments.</td></tr></tbody></table>

**Kafka entry points (the real work):**

<table><thead><tr><th width="214.68359375">Topic listened to</th><th width="179.640625">Consumer</th><th>What it does</th></tr></thead><tbody><tr><td><code>save-stock-topic</code>, <code>update-stock-topic</code></td><td><code>NotificationConsumer</code></td><td>Stock movement → routes by schema to <code>StockNotificationAdapter</code> → <strong>immediate push</strong>.</td></tr><tr><td><code>save-hfreferral-topic</code>, <code>update-hfreferral-topic</code></td><td><code>NotificationConsumer</code></td><td>Referral → <code>HFReferralNotificationAdapter</code> → <strong>immediate push</strong> (only on create).</td></tr><tr><td><code>save-project-task-topic</code></td><td><code>ProjectTaskConsumer</code></td><td>A completed distribution task → <code>PostDistributionService</code> → <strong>schedules SMS</strong> rows.</td></tr><tr><td><code>save-final-sms-message</code> (internal)</td><td><code>SaveFinalSmsMessageConsumer</code></td><td>The fully-built SMS message → marks the notification <strong>SENT</strong> in the DB.</td></tr></tbody></table>

**Topics it publishes to:** `egov.core.notification.push` (to egov-notification-push / FCM), `save-final-sms-message` (its own internal hand-off), `save-scheduled-notification-topic-health` / `update-scheduled-notification-topic-health` (to egov-persister + indexer).

> A `kafka.tenant.id.pattern` prefix (e.g. `ba-`) lets the same listeners work in both single-tenant and central-instance (multi-state) deployments.

#### Kafka topics

<table><thead><tr><th width="287.68359375">Topic</th><th width="117.0078125">Dir</th><th>Purpose</th></tr></thead><tbody><tr><td><code>save-project-task-topic</code></td><td>in</td><td>Project task events that trigger notifications</td></tr><tr><td><code>save-stock-topic</code></td><td>in</td><td>Stock create events (notification triggers)</td></tr><tr><td><code>update-stock-topic</code></td><td>in</td><td>Stock update events (notification triggers)</td></tr><tr><td><code>save-hfreferral-topic</code></td><td>in</td><td>HF-referral create events (notification triggers)</td></tr><tr><td><code>update-hfreferral-topic</code></td><td>in</td><td>HF-referral update events (notification triggers)</td></tr><tr><td><code>save-final-sms-message</code></td><td>in/out</td><td>Queue + self-consume the final SMS to dispatch</td></tr><tr><td><code>save-scheduled-notification-topic-health</code></td><td>out</td><td>Persist a scheduled notification</td></tr><tr><td><code>update-scheduled-notification-topic-health</code></td><td>out</td><td>Update a scheduled notification</td></tr><tr><td><code>egov.core.notification.sms</code></td><td>out</td><td>Outbound SMS</td></tr><tr><td><code>egov.core.notification.push</code></td><td>out</td><td>Outbound push</td></tr></tbody></table>

### Dependencies

* **MDMS (`egov-mdms-service`)** — the source of truth for which notifications are enabled, recipient roles, template codes, locales, and SMS delay timing (modules `HCM-NOTIFICATION-CONFIG`, `HCM-PROJECT-TYPES`).
* **Localisation (`egov-localization`)** — supplies the message templates per locale; the service fills in placeholders.
* **egov-notification-push** — the downstream service that resolves device tokens from a facility id and sends the actual **push (FCM)** notification.
* **SMS gateway** — final SMS messages are emitted on the SMS topic (`egov.core.notification.sms`) for delivery.
* **Project / Project-Facility** — looks up project type and beneficiary type, and resolves a `projectFacilityId` to a real `facilityId`.
* **Household & Individual** — resolves the beneficiary's name and phone number for scheduled SMS.
* **Facility** — resolves facility names used in push message placeholders.
* **enc-client (egov-enc-service)** — encrypts/decrypts the PII (mobile number, message context) on the scheduled-notification path.
* **health-services-common / -models** — shared clients, models and utilities.
* **Kafka** — every trigger and every hand-off is a Kafka message.
* **PostgreSQL + Flyway** — stores the `scheduled_notification` table (migration applied on boot).
* **egov-persister** (deployed via the `configs/` repo) — actually writes the `save/update-scheduled-notification` events to Postgres.
* **egov-indexer → Elasticsearch** — indexes the same events for dashboards/audit.
* **Redis** — caching used by the shared repository layer.

## Process Flow

There are two distinct flows. The **push** flow is immediate; the **SMS** flow is deferred — events are first stored as scheduled rows, then a daily job sends them.

<figure><img src="https://2077406040-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FI0KFbSBTySIQ7TCOKnJF%2Fuploads%2F88cCwngCFSjTydWAHxVy%2Fimage.png?alt=media&amp;token=e98ca726-a793-4659-a325-b3ed32fc2275" alt=""><figcaption></figcaption></figure>

## Failure / Retry Handling

* **One bad record doesn't sink the batch.** Consumers and the processor handle each record/task independently — a failure is logged and counted, the rest continue (`processAndSendBatch`, `processDistributionTasks`, `dispatchBatch`).
* **Scheduled SMS have explicit status tracking.** Each `scheduled_notification` row moves `PENDING → IN_PROGRESS → SENT` (or `FAILED`), with an `attempts` counter, `lastAttemptAt`, and a truncated `errorMessage`. Rows are marked `IN_PROGRESS` before dispatch, so the next scheduler run won't pick them up twice.
* **Errors are classified as recoverable vs non-recoverable.** Transient problems (timeouts, send failures) are flagged recoverable; bad data / missing template is non-recoverable — useful when triaging the `FAILED` rows.
* **Missing template = graceful fallback (push) / hard fail (SMS).** On the push path, if the localisation template is missing, the service still sends with a fallback body; on the SMS path, a missing template marks the notification `FAILED`.
* **No automatic retry loop today.** The scheduler processes the rows it finds in a run; failed rows are not automatically re-attempted on a back-off — they stay `FAILED` and need operational follow-up. The scheduler caps how many rows it fetches per run (default 10,000) to avoid memory blow-ups on a large backlog; the remainder is picked up next run.
* **Skips are silent by design.** No mobile number, SMS disabled in MDMS, event type not enabled, unknown schema, or unresolvable facility → the event is logged and skipped, not errored. Check the logs when an expected notification "didn't fire".
* **Config drift is a classic trap.** If the MDMS notification config or the deployed persister config for the scheduled-notification topics is missing/stale in an environment, events are accepted, but nothing is sent / nothing lands in Postgres.

{% hint style="info" %}

### HCM v2.1: Health Notification Service — New Service Introduction

* **New to the release line**: The health-notification-service (together with `egov-notification-push`) ships for the first time in HCM v2.1.
* **In-app push alerts for stock and referrals**: Stock movements (issued/returned, and accepted/rejected outcomes) and new health-facility referrals generate push notifications to the right facility staff, routed by role.
* **Scheduled SMS to beneficiaries post-distribution**: When a household is served, follow-up SMS (e.g., day-2/day-3 reminders) are scheduled based on delay timing from MDMS, with a daily job handling delivery.
* **Config-driven behaviour**: Which events fire, to whom, in which language, with which template, and after what delay — all driven by MDMS campaign config, so programme owners can tune notifications without code changes.
* **PII encrypted at rest**: Beneficiary phone numbers and message context are encrypted (via enc-client) before storage and decrypted only at send time.
* **Localised, multi-locale templates**: Messages are built from localisation templates per locale (e.g., `en_NG`, `fr_NG`) with placeholders filled at send time.
* **Multi-tenant / central-instance aware**: Kafka listeners and persister topics support both single-tenant and central-instance (multi-state) deployments via tenant-id topic prefixing.
* **Operational cache refresh**: A `_cache/refresh` endpoint reloads MDMS and localisation data without requiring a restart.

<br>
{% endhint %}

## Known Risks / Limitations

* **No automatic retry / dead-letter for failed sends.** A `FAILED` scheduled SMS is not automatically re-attempted; it needs manual/operational intervention. There is no back-off queue.
* **Heavy dependence on MDMS + localisation correctness.** A missing or mis-keyed MDMS event entry or template silently suppresses notifications (logged-and-skipped). This is the most common "why didn't it send?" cause.
* **Recipient resolution can be brittle.** Push recipients are resolved from stock sender/receiver facility IDs (with a legacy `primaryRole` fallback); SMS recipients depend on the household/individual having a usable phone number — no phone means a silent skip.
* **SMS delay is day-granular.** MDMS `delayHours` is converted to whole days (`delayHours / 24`), and the scheduler runs on a daily cron — sub-day timing is effectively rounded.
* **Push path is push-only.** `NotificationProcessorService` currently only sends the `PUSH` channel in the immediate flow; an SMS channel on that path is logged as unsupported.
* **Timezone is configuration-critical.** All "today"/scheduling date math uses `notification.timezone`; a wrong value shifts which SMS are considered "due".
* **HFReferral notifications are create-only.** Referral updates do not trigger notifications by design.
* **DAMAGED stock entries produce no push** currently (no event-type mapping).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.digit.org/health/design/architecture/low-level-design/services/health-services/health-notification.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
