feat(notifications): central SMS config + nullable recipient contact (S2)

Implement the central ConfigurationDatabase side of SMS notifications:

- NotificationConfiguration: EmailAddress now nullable (SMS-only recipients
  carry a PhoneNumber, no email); add PhoneNumber nvarchar(32); add
  SmsConfigurationConfiguration (AuthToken sized as the encrypted column,
  mirroring SmtpConfiguration.Credentials; timeout/retry mapped REQUIRED for
  ctor-default round-trip fidelity).
- ScadaBridgeDbContext: add SmsConfigurations DbSet, encrypt AuthToken at rest
  via EncryptedStringConverter, and cover SmsConfiguration in the schema-only
  secret-write guard.
- NotificationRepository: implement the four INotificationRepository SMS-config
  methods (resolves the 4x CS0535), mirroring the SMTP methods' stage-only /
  separate-SaveChangesAsync discipline.
- Migration AddSmsNotifications: idempotent (guarded) ALTER EmailAddress nullable,
  ADD PhoneNumber, CREATE SmsConfigurations; Down reverses cleanly (backfills
  NULL emails before restoring NOT NULL).
This commit is contained in:
Joseph Doherty
2026-06-19 09:57:55 -04:00
parent 095361b73f
commit b46691747c
6 changed files with 2143 additions and 2 deletions
@@ -0,0 +1,68 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
/// <summary>
/// SMS Notifications (S2): makes a notification recipient's contact path optional and adds
/// the central SMS-provider (Twilio) configuration table.
///
/// 1. <c>NotificationRecipients.EmailAddress</c> becomes nullable — an SMS-only recipient
/// carries a <c>PhoneNumber</c> and no email. (ALTER COLUMN, naturally idempotent.)
/// 2. <c>NotificationRecipients.PhoneNumber nvarchar(32) NULL</c> — E.164 phone for SMS
/// recipients (guarded ADD).
/// 3. <c>SmsConfigurations</c> table — central-only provider config; <c>AuthToken</c> is
/// stored encrypted at rest (EncryptedStringConverter → <c>nvarchar(max)</c>, mirroring
/// <c>SmtpConfigurations.Credentials</c>) (guarded CREATE TABLE).
///
/// Every DDL statement is guarded so the migration is safe to re-run against a
/// partially-migrated database (the repo's idempotent-migration convention; see
/// AddListAttributeElementType).
/// </summary>
public partial class AddSmsNotifications : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// ALTER COLUMN is naturally idempotent: re-running re-applies the same nullable shape.
migrationBuilder.Sql("ALTER TABLE [NotificationRecipients] ALTER COLUMN [EmailAddress] nvarchar(500) NULL;");
migrationBuilder.Sql(@"
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE Name='PhoneNumber' AND Object_ID=Object_ID('NotificationRecipients'))
ALTER TABLE [NotificationRecipients] ADD [PhoneNumber] nvarchar(32) NULL;");
migrationBuilder.Sql(@"
IF OBJECT_ID(N'dbo.SmsConfigurations', N'U') IS NULL
CREATE TABLE [SmsConfigurations] (
[Id] int NOT NULL IDENTITY(1, 1),
[AccountSid] nvarchar(100) NOT NULL,
[AuthToken] nvarchar(max) NULL,
[FromNumber] nvarchar(32) NOT NULL,
[MessagingServiceSid] nvarchar(100) NULL,
[ApiBaseUrl] nvarchar(500) NULL,
[ConnectionTimeoutSeconds] int NOT NULL,
[MaxRetries] int NOT NULL,
[RetryDelay] time NOT NULL,
CONSTRAINT [PK_SmsConfigurations] PRIMARY KEY ([Id])
);");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
IF OBJECT_ID(N'dbo.SmsConfigurations', N'U') IS NOT NULL
DROP TABLE [SmsConfigurations];");
migrationBuilder.Sql(@"
IF EXISTS (SELECT 1 FROM sys.columns WHERE Name='PhoneNumber' AND Object_ID=Object_ID('NotificationRecipients'))
ALTER TABLE [NotificationRecipients] DROP COLUMN [PhoneNumber];");
// Reversing to NOT NULL requires backfilling existing NULLs first, otherwise the
// ALTER fails. Mirrors EF's generated default ('') for the previously-required column.
migrationBuilder.Sql("UPDATE [NotificationRecipients] SET [EmailAddress] = '' WHERE [EmailAddress] IS NULL;");
migrationBuilder.Sql("ALTER TABLE [NotificationRecipients] ALTER COLUMN [EmailAddress] nvarchar(500) NOT NULL;");
}
}
}