feat(sms): make FromNumber optional — support Twilio Messaging-Service-only configs (UI-Med-2)

Code-review finding UI-Med-2: the design doc + delivery adapter treat FromNumber and
MessagingServiceSid as either-or, but the entity ctor, EF schema, UI and CLI all hard-
required FromNumber — so a Messaging-Service-only Twilio config (a normal production
setup) could not be created. Bring the implementation into line with the spec:

- Commons: SmsConfiguration.FromNumber -> string? (ctor fromNumber optional);
  UpdateSmsConfigCommand.FromNumber -> string?.
- ConfigurationDatabase: FromNumber.IsRequired(false) + migration SmsFromNumberOptional
  (ALTER COLUMN nullable, idempotent; Down backfills '' — harmless, MsgSid keeps it
  deliverable) + regenerated model snapshot.
- Transport: SmsConfigDto.FromNumber -> string? (round-trips a Messaging-Service-only config).
- CentralUI: form validation requires AccountSid + at-least-one-of(FromNumber, MsgSid);
  nullable create/edit paths; From-number help text.
- CLI: --from-number no longer Required; BuildUpdateSmsConfigCommand validates the either-or.
- Adapter: From branch null-forgiving (guarded by the existing incomplete-config check).

Tests: ManagementActor MsgSid-only persists null FromNumber; CLI MsgSid-only builds +
neither-throws + contract (--from-number not Required); CentralUI MsgSid-only save.
This commit is contained in:
Joseph Doherty
2026-06-19 15:19:40 -04:00
parent a9393c8913
commit 33e1802e6d
14 changed files with 2133 additions and 28 deletions
@@ -73,8 +73,10 @@ public class SmsConfigurationConfiguration : IEntityTypeConfiguration<SmsConfigu
builder.Property(s => s.AuthToken)
.HasMaxLength(8000);
// Optional: a Twilio Messaging-Service-only config has no From number (a valid
// config carries a FromNumber and/or a MessagingServiceSid).
builder.Property(s => s.FromNumber)
.IsRequired()
.IsRequired(false)
.HasMaxLength(32);
builder.Property(s => s.MessagingServiceSid)
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
/// <summary>
/// SMS code-review (CentralUI-NNN / UI-Med-2): makes <c>SmsConfigurations.FromNumber</c>
/// optional. A Twilio Messaging-Service-only config supplies a MessagingServiceSid instead
/// of a From number; the design doc, the delivery adapter, and now the UI/CLI treat the two
/// as either-or, so the column must allow NULL.
///
/// ALTER COLUMN to NULL is naturally idempotent (re-running re-applies the same shape).
/// </summary>
public partial class SmsFromNumberOptional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER TABLE [SmsConfigurations] ALTER COLUMN [FromNumber] nvarchar(32) NULL;");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Reversing to NOT NULL requires backfilling NULLs first. Unlike the recipient
// EmailAddress backfill, '' is a harmless reversal here: a Messaging-Service-only
// config keeps its MessagingServiceSid and stays deliverable, so no data is lost.
migrationBuilder.Sql("UPDATE [SmsConfigurations] SET [FromNumber] = '' WHERE [FromNumber] IS NULL;");
migrationBuilder.Sql("ALTER TABLE [SmsConfigurations] ALTER COLUMN [FromNumber] nvarchar(32) NOT NULL;");
}
}
}
@@ -871,7 +871,6 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
.HasColumnType("int");
b.Property<string>("FromNumber")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");