From c5c85113793fa9f742969324a6edf6d339cbebc9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 19 Jun 2026 11:21:56 -0400 Subject: [PATCH] docs(sms): reconcile Component/CLAUDE/README docs for SMS notifications (S11) --- CLAUDE.md | 5 +- README.md | 4 +- .../Component-NotificationOutbox.md | 18 +++--- .../Component-NotificationService.md | 62 +++++++++++++++---- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7310ee06..58fb3828 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,8 +124,9 @@ Related repos cloned as sibling directories under `~/Desktop/` — referenced fo - Error classification: HTTP 5xx/408/429/connection errors = transient; other 4xx = permanent (returned to script). - Notification Service: SMTP with OAuth2 Client Credentials (Microsoft 365) or Basic Auth. BCC delivery, plain text. - Notification delivery is central-only: sites store-and-forward notifications to the central cluster (target = central, not SMTP); sites never talk to SMTP. Notification lists and SMTP config are no longer deployed to sites; recipient resolution happens at central, at delivery time. -- Notification lists carry a `Type` discriminator (`Email` now; `Teams` and others later). `Notify.To("list")` is type-agnostic; delivery is via per-type `INotificationDeliveryAdapter` (success/transient/permanent classification, same pattern as External System Gateway). +- Notification lists carry a `Type` discriminator (`Email` and `Sms`). `Notify.To("list")` is type-agnostic; delivery is via per-type `INotificationDeliveryAdapter` (Email via SMTP; Sms via Twilio REST — `SmsNotificationDeliveryAdapter`, no SDK, one POST per recipient, per-recipient rollup). List Type is fixed after creation. - `Notify.Send` is async — returns a `NotificationId` (GUID, idempotency key) status handle immediately. `Notify.Status(notificationId)` returns a status record (status, retry count, last error, key timestamps); answered site-locally as `Forwarding` while still in the site S&F buffer, otherwise round-trips to central. +- SMS delivery adapter (T9/T10, 2026-06-19): `SmsNotificationDeliveryAdapter` — Twilio REST, no SDK, HTTP Basic auth (`AccountSid:AuthToken`), one `POST .../Messages.json` per recipient. `SmsConfiguration` entity (`AccountSid`, `AuthToken` encrypted via Data Protection, `FromNumber`/`MessagingServiceSid`, `ApiBaseUrl`, timeout/retry) stored centrally; managed Admin-only via CLI `notification sms list|update` + Central UI `/notifications/sms`. Central-only (never deployed to sites). Per-recipient rollup: all-accepted → Delivered; any-transient → retry/park; mix → delivered-to-good + note; all-permanent → Park. `SmsConfiguration` (Auth Token in SecretsBlock) travels in Transport bundles via `--sms-configs`. - Inbound API: `POST /api/{methodName}`, `X-API-Key` header, flat JSON, extended type system (Object, List). ### Templates & Deployment @@ -203,7 +204,7 @@ Related repos cloned as sibling directories under `~/Desktop/` — referenced fo - Stuck = `Pending`/`Retrying` older than a configurable age threshold (default 10 min) — display-only (KPI count + row badge), no escalation/alerting. - Headline KPI tiles surface on the Health dashboard; a new Central UI Notification Outbox page offers a queryable list with Retry/Discard actions on parked notifications. - Site Call Audit KPIs are central-computed point-in-time from the `SiteCalls` table (global + per-site), mirroring the Notification Outbox KPI shape; tiles surface on the Health dashboard alongside a queryable Central UI Site Calls page with Retry/Discard on parked rows. -- KPI History & Trends (#26, M6): a reusable central KPI-history backbone — supersedes the prior "point-in-time only, no time-series store" stance — backed by a tall/EAV `KpiSample` table in central MS SQL (no new infra). A `KpiHistoryRecorderActor` cluster singleton (`kpi-history-recorder`, **not readiness-gated**, best-effort with per-source isolation) samples every minute by enumerating DI-registered `IKpiSampleSource`s (each lives with its owner, registered via `TryAddEnumerable`, reusing existing KPI/aggregator reads); daily purge after `RetentionDays` (default 90). Querying is `IKpiHistoryRepository.GetRawSeriesAsync` → `KpiSeriesBucketer` (last-value-per-bucket) → scoped dual-ctor `KpiHistoryQueryService` → a reusable **custom-SVG** `KpiTrendChart` (no third-party charting lib). Trends ship on four surfaces: Notification Outbox, Site Calls, Audit Log pages + a per-site Health-dashboard panel. `KpiHistoryOptions` (`ScadaBridge:KpiHistory`): SampleInterval 60s, RetentionDays 90, PurgeInterval 1d, DefaultMaxSeriesPoints 200; validated. M6's T9 (Teams + other non-Email delivery adapters) and T10 (`NotificationType` enum values + Central UI list "Type" selector) are deferred to the next major version. +- KPI History & Trends (#26, M6): a reusable central KPI-history backbone — supersedes the prior "point-in-time only, no time-series store" stance — backed by a tall/EAV `KpiSample` table in central MS SQL (no new infra). A `KpiHistoryRecorderActor` cluster singleton (`kpi-history-recorder`, **not readiness-gated**, best-effort with per-source isolation) samples every minute by enumerating DI-registered `IKpiSampleSource`s (each lives with its owner, registered via `TryAddEnumerable`, reusing existing KPI/aggregator reads); daily purge after `RetentionDays` (default 90). Querying is `IKpiHistoryRepository.GetRawSeriesAsync` → `KpiSeriesBucketer` (last-value-per-bucket) → scoped dual-ctor `KpiHistoryQueryService` → a reusable **custom-SVG** `KpiTrendChart` (no third-party charting lib). Trends ship on four surfaces: Notification Outbox, Site Calls, Audit Log pages + a per-site Health-dashboard panel. `KpiHistoryOptions` (`ScadaBridge:KpiHistory`): SampleInterval 60s, RetentionDays 90, PurgeInterval 1d, DefaultMaxSeriesPoints 200; validated. M6's T9 and T10 were originally deferred. **T9/T10 delivered 2026-06-19** as an SMS (Twilio) adapter — Teams was evaluated and dropped (outbound-only backends cannot send 1:1 chat bodies via Graph without a Bot Framework inbound endpoint; SMS is inherently per-person and outbound-only). `NotificationType.Sms` added; `SmsNotificationDeliveryAdapter` (Twilio REST, no SDK) registered alongside Email; Central UI Type selector (adapter-gated) + SMS recipient input; CLI `--type sms --phones` on list create/update; `notification sms list|update` commands. ### Code Organization - Entity classes are persistence-ignorant POCOs in Commons; EF mappings in Configuration Database. diff --git a/README.md b/README.md index afaa0267..d5c4f041 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository is the full **implementation** project for ScadaBridge — the C | Central Database | MS SQL Server, Entity Framework Core | | Site Storage | SQLite (deployed configs, S&F buffer, event logs) | | Authentication | Direct LDAP/AD bind (LDAPS/StartTLS), JWT sessions | -| Notifications | Delivered from the central cluster (SMTP, OAuth2/Microsoft 365); store-and-forwarded from sites | +| Notifications | Delivered from the central cluster (Email via SMTP/OAuth2-M365; SMS via Twilio REST); store-and-forwarded from sites | | Hosting | Windows Server, Windows Service | | Cluster | Akka.NET Cluster (active/standby, keep-oldest SBR) | | Logging | Serilog (structured) | @@ -83,7 +83,7 @@ Both stacks share the infrastructure services in [`infra/`](infra/) (MS SQL, LDA | 5 | Central–Site Communication | [docs/requirements/Component-Communication.md](docs/requirements/Component-Communication.md) | Dual transport: Akka.NET ClusterClient (command/control) + gRPC server-streaming (real-time data). 9 message patterns with per-pattern timeouts, SiteStreamGrpcServer/Client, application-level correlation IDs, transport heartbeat config, gRPC keepalive, message ordering, connection failure behavior. The gRPC stream additively carries the read-only native alarm mirror (computed + native OPC UA / MxAccess) via the enriched `AlarmStateUpdate`. | | 6 | Store-and-Forward Engine | [docs/requirements/Component-StoreAndForward.md](docs/requirements/Component-StoreAndForward.md) | Buffering (transient failures only), fixed-interval retry, parking, async best-effort replication, SQLite persistence at sites. | | 7 | External System Gateway | [docs/requirements/Component-ExternalSystemGateway.md](docs/requirements/Component-ExternalSystemGateway.md) | HTTP/REST + JSON, API key/Basic Auth, per-system timeout, dual call modes (Call/CachedCall), transient/permanent error classification, dedicated blocking I/O dispatcher, ADO.NET connection pooling. | -| 8 | Notification Service | [docs/requirements/Component-NotificationService.md](docs/requirements/Component-NotificationService.md) | Central-only — manages typed notification-list and SMTP definitions, supplies per-type delivery adapters (SMTP with OAuth2 (M365) or Basic Auth, BCC, plain text); delivery performed by the Notification Outbox. | +| 8 | Notification Service | [docs/requirements/Component-NotificationService.md](docs/requirements/Component-NotificationService.md) | Central-only — manages typed notification-list, SMTP, and SMS definitions; supplies per-type delivery adapters (Email via SMTP with OAuth2 (M365) or Basic Auth, BCC, plain text; SMS via Twilio REST, per-recipient, outbound-only); delivery performed by the Notification Outbox. | | 9 | Central UI | [docs/requirements/Component-CentralUI.md](docs/requirements/Component-CentralUI.md) | Blazor Server with SignalR real-time push, load balancer failover with JWT, all management workflows. Custom-content modal host (`DialogService.ShowAsync`) with focus-trap/restore; dark-mode CSS-variable token layer (`[data-bs-theme="dark"]` overriding `ZB.MOM.WW.Theme` tokens in `site.css`, `localStorage`-persisted, SSR no-flash); reusable presentational components `OffsetPager`, `KeysetPager`, and `DateTimeRangeFilter` adopted across report/audit pages. | | 10 | Security & Auth | [docs/requirements/Component-Security.md](docs/requirements/Component-Security.md) | Direct LDAP bind (LDAPS/StartTLS), JWT sessions (HMAC-SHA256, 15-min refresh, 30-min idle), role-based authorization (incl. the `Operator`/`Verifier` two-person secured-write roles + policies), site-scoped permissions. | | 11 | Health Monitoring | [docs/requirements/Component-HealthMonitoring.md](docs/requirements/Component-HealthMonitoring.md) | 30s report interval, 60s offline threshold, monotonic sequence numbers, raw error counts, tag resolution counts, dead letter monitoring. | diff --git a/docs/requirements/Component-NotificationOutbox.md b/docs/requirements/Component-NotificationOutbox.md index 9e476bed..8cc443ab 100644 --- a/docs/requirements/Component-NotificationOutbox.md +++ b/docs/requirements/Component-NotificationOutbox.md @@ -65,12 +65,12 @@ The site forwards only `(listName, subject, body)` plus provenance — recipient ## The `Notifications` Table -The table is type-agnostic so it can record any notification type the system supports — email today, Microsoft Teams and others later. One row per notification. +The table is type-agnostic so it can record any notification type the system supports — `Email` and `Sms` today; others can be added by registering a new `INotificationDeliveryAdapter`. One row per notification. | Field | Notes | |---|---| | `NotificationId` | GUID, primary key. Generated at the **site**; used as the idempotency key. | -| `Type` | `Email` / `Teams` / … discriminator. | +| `Type` | `Email` / `Sms` / … discriminator. Stamped at ingest from the target list's `Type` (default `Email` if the list is not found — delivery parks on "list not found" regardless). | | `ListName` | Target notification list. | | `Subject`, `Body` | Plain-text content. | | `TypeData` | JSON — extensibility hook for future per-type fields. | @@ -99,7 +99,7 @@ The Notification Outbox and the central [`Site Call Audit`](Component-SiteCallAu ### Retry Policy -Delivery retry reuses the central SMTP configuration's max-retry-count and fixed retry interval. The interval is fixed (no exponential backoff), consistent with the existing fixed-interval store-and-forward convention. +Delivery retry reuses the central SMTP configuration's max-retry-count and fixed retry interval for Email notifications. SMS notifications reuse the `SmsConfiguration`'s retry settings. The interval is fixed (no exponential backoff), consistent with the existing fixed-interval store-and-forward convention. ### Retention @@ -133,10 +133,12 @@ The operational `Notifications` table remains the **source of truth** for the di A delivery adapter implementing `INotificationDeliveryAdapter` is registered per `Type`. Each `Deliver(...)` call returns one of `success | transient failure | permanent failure`, mirroring the External System Gateway error-classification pattern. -- **Email adapter — implemented now.** The existing SMTP composition/send logic, relocated to the central cluster. -- **Teams and other adapters — future.** The `Type` discriminator and the adapter interface are the seam; no Teams code exists in this design. Teams auth and targeting (Incoming Webhooks vs Graph API) is a separate design conversation. +- **Email adapter.** The existing SMTP composition/send logic, relocated to the central cluster. Sends a single BCC email to all list recipients. +- **SMS adapter (Twilio REST).** Added in T9/T10 (2026-06-19). Sends one Twilio REST request per recipient phone number. Per-recipient results are rolled up: all-accepted → `Success`; any-transient → retry/park; mix of accepted + permanent-bad → `Success` with bad numbers noted in `LastError`; all-permanent → `Permanent` (Park). See [Component-NotificationService.md](Component-NotificationService.md), SMS Delivery Adapter. -Delivery adapters are provided by the Notification Service, which manages notification-list and SMTP definitions and supplies the stateless per-type "deliver one notification" implementations. +The outbox dispatches by looking up the adapter registered for the notification's `Type`. If no adapter is registered for a given `Type`, the notification is parked with a "no adapter" error — the seam is open for future delivery channels. + +Delivery adapters are provided by the Notification Service, which manages notification-list, SMTP, and SMS definitions and supplies the stateless per-type "deliver one notification" implementations. The SMS `SmsConfiguration` (including encrypted Auth Token) travels in Transport bundles alongside SMTP config. ## Active/Standby Behavior @@ -177,7 +179,7 @@ Delivery max-retry-count and retry interval are not part of `NotificationOutboxO ## Dependencies -- **Notification Service**: Provides notification-list and SMTP definitions, and the per-type delivery adapters the outbox invokes. +- **Notification Service**: Provides notification-list, SMTP, and SMS definitions, and the per-type delivery adapters the outbox invokes (Email + Twilio SMS). - **Configuration Database**: Hosts the `Notifications` table; provides the entity POCO, repository, and EF migration for outbox persistence. - **Central–Site Communication**: Carries inbound notification submissions and acks between sites and central. - **Audit Log (#23)**: The dispatcher direct-writes `Notification.Attempt` and `Notification.Terminal` rows to the central `AuditLog` via `ICentralAuditWriter` (insert-if-not-exists on `EventId`); the site-emitted `Notification.Enqueued` row arrives via the standard audit telemetry channel. See [Component-AuditLog.md](Component-AuditLog.md), Central direct-write (central-originated events). @@ -187,7 +189,7 @@ Delivery max-retry-count and retry interval are not part of `NotificationOutboxO ## Interactions - **Site Store-and-Forward Engine**: Forwards notifications to central via Central–Site Communication; the outbox ingests them and acks once persisted. -- **Notification Service**: Supplies delivery adapters and resolves notification lists at delivery time. +- **Notification Service**: Supplies delivery adapters (Email + SMS) and resolves notification lists at delivery time. - **Central UI**: Queries the `Notifications` table for the Notification Outbox page and issues operator Retry/Discard actions on parked notifications. - **Health Monitoring**: Polls the outbox for KPI tiles on the health dashboard. - **KPI History (#26)**: Emits `IKpiSampleSource` (`NotificationOutboxKpiSampleSource`, Global + per-Site + per-Node) consumed by the KpiHistory recorder (#26), reusing the existing `Compute…KpisAsync` reads; the resulting `queueDepth` / `stuckCount` / `parkedCount` / `deliveredLastInterval` / `oldestPendingAgeSeconds` series render as trends on the Notification Outbox page via `KpiTrendChart`. See [Component-KpiHistory.md](Component-KpiHistory.md). diff --git a/docs/requirements/Component-NotificationService.md b/docs/requirements/Component-NotificationService.md index 6dbb1b40..2b79c105 100644 --- a/docs/requirements/Component-NotificationService.md +++ b/docs/requirements/Component-NotificationService.md @@ -2,7 +2,7 @@ ## Purpose -The Notification Service is the central component that manages notification-list and SMTP definitions and provides the per-type delivery adapters used to send notifications. It manages notification-list and SMTP definitions, and supplies the stateless "deliver one notification" adapter implementations that the Notification Outbox invokes at delivery time. +The Notification Service is the central component that manages notification-list, SMTP, and SMS definitions and provides the per-type delivery adapters used to send notifications. It manages notification-list and delivery-channel definitions, and supplies the stateless "deliver one notification" adapter implementations that the Notification Outbox invokes at delivery time. The Notification Service no longer delivers notifications from sites. Notification delivery has been inverted: a site script's notification is store-and-forwarded to the central cluster, and the central **Notification Outbox** owns dispatch and delivery, calling an `INotificationDeliveryAdapter` supplied by this component. See [`Component-NotificationOutbox.md`](Component-NotificationOutbox.md). @@ -13,25 +13,27 @@ Central cluster only. The Notification Service manages definitions in the centra ## Responsibilities ### Definitions (Central) -- Store notification lists in the configuration database: list name, list **type**, and type-specific targets (e.g. recipients for an `Email` list). +- Store notification lists in the configuration database: list name, list **type**, and type-specific targets (e.g. recipients for an `Email` list, phone numbers for an `Sms` list). - Store email server configuration (SMTP settings). -- Managed by users with the Designer role. -- Notification lists and SMTP configuration are **not deployed to sites** — they exist centrally only. There is no deploy-to-sites artifact and no local SQLite copy. +- Store SMS provider configuration (`SmsConfiguration`: Twilio credentials and endpoint settings). +- Managed by users with the Designer role (notification lists) and Admin role (SMTP and SMS configuration). +- Notification lists, SMTP configuration, and SMS configuration are **not deployed to sites** — they exist centrally only. There is no deploy-to-sites artifact and no local SQLite copy. ### Delivery Adapters (Central) - Provide a delivery adapter implementing `INotificationDeliveryAdapter` for each notification `Type`. - Each adapter is a stateless "deliver one notification" implementation: it composes and sends a single notification and classifies the outcome. - The **Email adapter** is the relocated SMTP composition and send logic — formerly run at sites, it now runs on the central cluster. -- Resolve a notification list name to its concrete targets (e.g. recipient addresses) at delivery time, on behalf of the Notification Outbox. +- The **SMS adapter** delivers notifications via Twilio REST to a list's phone-number recipients — see SMS Delivery Adapter below. +- Resolve a notification list name to its concrete targets (e.g. recipient addresses or phone numbers) at delivery time, on behalf of the Notification Outbox. ## Notification List Definition Each notification list includes: - **Name**: Unique identifier (e.g., "Maintenance-Team", "Shift-Supervisors"). -- **Type**: The notification type — `Email` (implemented now); `Teams` and other types are planned for the future. `Notify.To("list")` works transparently for any type — the calling script does not care about the type. -- **Type-specific targets**: The targets appropriate to the list type. For an `Email` list, one or more recipient entries, each with: - - Recipient name. - - Email address. +- **Type**: The notification channel — `Email` or `Sms`. `Notify.To("list")` works transparently for any type — the calling script does not care about the type. The type is chosen at list creation and is **fixed** — it cannot be changed on update (prevents email/phone recipient mismatch within a list). +- **Type-specific targets**: The targets appropriate to the list type: + - **Email list** — one or more recipient entries, each with a recipient name and email address. + - **SMS list** — one or more recipient entries, each with a recipient name and E.164 phone number. Lists are defined and stored centrally only. **Recipient resolution happens at central, at delivery time** — a site forwards only `(listName, subject, body)` plus provenance; the Notification Outbox asks the Notification Service to resolve the list when it dispatches the notification. @@ -50,6 +52,20 @@ The SMTP configuration is defined centrally and used by the central Email delive - **Max concurrent connections**: Maximum simultaneous SMTP connections from the central cluster (default: 5). - **Retry settings**: Max retry count, fixed time between retries. The Notification Outbox reuses these for delivery retry of transient failures. +## SMS Configuration + +The `SmsConfiguration` entity is defined centrally and used by the central SMS delivery adapter. It mirrors `SmtpConfiguration` in structure and is not deployed to sites. It is managed by Admin-role users via the CLI (`notification sms list|update`) and the Central UI `/notifications/sms` page. It includes: + +- **Account SID**: Twilio Account SID (plaintext; also appears in the API URL path). +- **Auth Token**: Twilio Auth Token, **encrypted at rest** via ASP.NET Data Protection (`EncryptedStringConverter`). The Auth Token is never returned from the list command — the listing reports it only as a `hasAuthToken` presence flag. +- **From number**: Sender phone number in E.164 format (e.g., `+15551234567`). Used unless a Messaging Service SID is specified. +- **Messaging Service SID** (optional): Twilio Messaging Service SID. When present, Twilio uses it for sender selection; overrides the From number. +- **API base URL** (optional): Override for the Twilio REST API base URL (default: `https://api.twilio.com`). Allows pointing at a test/stub handler or a regional endpoint. +- **Connection timeout**: Maximum time to wait for a Twilio API response. +- **Max retries** / **Retry delay**: Delivery retry settings, mirroring the SMTP retry model. + +The `SmsConfiguration` entity travels in Transport bundles — the Auth Token rides the encrypted `SecretsBlock` (keyed by Account SID), consistent with how SMTP credentials are bundled. + ## Script API ```csharp @@ -65,14 +81,34 @@ NotificationStatus status = Notify.Status(id); ## Notification Delivery Behavior -Delivery is performed centrally by the Notification Outbox, which calls the `INotificationDeliveryAdapter` registered for the notification's `Type`. The behavior below describes the Email adapter. +Delivery is performed centrally by the Notification Outbox, which calls the `INotificationDeliveryAdapter` registered for the notification's `Type`. ### Recipient Handling (Email) - A single email is sent per notification, with all list recipients in **BCC**. The from address is placed in the To field. - Recipients do not see each other's email addresses. - No per-recipient deduplication — if the same email address appears in multiple lists and a script sends to both, they receive multiple emails. -### Error Classification +## SMS Delivery Adapter + +The `SmsNotificationDeliveryAdapter` delivers notifications to an SMS list via the Twilio REST API (no Twilio SDK — uses `IHttpClientFactory` with a named `"Twilio"` `HttpClient` and HTTP Basic auth with `AccountSid:AuthToken`). It is outbound-only; true per-recipient delivery confirmation requires a status-callback webhook (out of scope for v1). "Accepted by Twilio" is treated as delivered, consistent with how the Email adapter treats "accepted by SMTP." + +### Message Format +- SMS has no subject line. The message body is composed as `Subject` + newline + `Body` (whichever are present), plain text, truncated to a configurable cap (`SmsOptions.MaxMessageLength`, default 1600 — the Twilio maximum) with an ellipsis when over. Twilio segments at 160 GSM-7 characters and bills per segment. + +### Per-Recipient Delivery (no BCC equivalent) +SMS has no BCC mechanism. The adapter sends one Twilio request per recipient and classifies each: + +- **All accepted** → `Success`; `ResolvedTargets` is snapshotted with the accepted numbers. +- **Any transient failure** → `Transient` (the whole notification retries at the fixed interval, then Parks after max-retries). Numbers already accepted on a prior attempt are re-texted on retry — the same "re-send to all" characteristic the Email adapter already has with BCC. (v1 does not track per-recipient state; that is a documented future enhancement.) +- **No transient failures, mix of accepted + permanent-bad** → `Success` to the good numbers; permanently-bad numbers are recorded in `LastError`. The notification is not parked if anything got through. +- **All permanent / no recipients / no SMS config / list-not-found** → `Permanent` (Park). + +### Error Classification (SMS) +A small `SmsErrorClassifier` mirrors the External System Gateway pattern: +- **Transient**: HTTP 5xx, 408, 429; `HttpRequestException`, timeout (non-caller-cancel). +- **Permanent**: other 4xx (including 401/403 bad credentials, 400 invalid/unsubscribed number). + +### Error Classification (Email) Each `Deliver(...)` call returns one of `success | transient failure | permanent failure`, consistent with the External System Gateway pattern. There is **no synchronous permanent-failure return to the script** — `Send()` returns immediately, before any delivery is attempted. - **Transient failures** (connection refused, timeout, SMTP 4xx temporary errors): The Notification Outbox moves the row to `Retrying` and schedules another attempt per the SMTP configuration's retry settings. @@ -81,11 +117,11 @@ Each `Deliver(...)` call returns one of `success | transient failure | permanent - A script observes failures only by calling `Notify.Status(id)` and seeing a `Parked` status — not as a synchronous exception. ### No Rate Limiting -- No application-level rate limiting. If the SMTP server enforces sending limits (e.g., Microsoft 365 throttling), those manifest as transient failures and are retried naturally by the Notification Outbox. +- No application-level rate limiting. If the delivery endpoint enforces sending limits (e.g., Microsoft 365 throttling or Twilio rate limits), those manifest as transient failures and are retried naturally by the Notification Outbox. ## Dependencies -- **Configuration Database (MS SQL)**: Stores notification list definitions (name, type, type-specific targets) and SMTP config. +- **Configuration Database (MS SQL)**: Stores notification list definitions (name, type, type-specific targets), SMTP configuration, and `SmsConfiguration`. - **Notification Outbox**: Invokes the delivery adapters supplied by this component and asks it to resolve notification lists at delivery time. - **Security & Auth**: Designer role manages notification lists. - **Configuration Database (via IAuditService)**: Notification list changes are audit logged.