fix(sms): repair S1 build breaks — null-filter EmailAddress projections + SiteNotificationRepository SMS stubs

S1 made NotificationRecipient.EmailAddress nullable + added SmsConfiguration
and four INotificationRepository SMS methods, breaking compilation beyond the
intentionally-deferred central NotificationRepository.

Fix 1 (CS8620/CS8604 nullable EmailAddress projections, email-only paths):
- NotificationOutbox EmailNotificationDeliveryAdapter: filter non-null emails
- DeploymentManager ArtifactDeploymentService: filter non-null emails
- Transport EntitySerializer: filter non-null emails into NotificationRecipientDto

Fix 2 (CS0535): stub the four SMS-config methods on SiteRuntime
SiteNotificationRepository (central-only — NotSupportedException, matching the
existing 'Managed via artifact deployment from Central' write-path pattern).

Doc nits: reword NotificationRecipient private ctor and SmsConfiguration.AuthToken
comments.

The central ConfigurationDatabase.NotificationRepository compile break is left
as-is (S2 implements those four methods).
This commit is contained in:
Joseph Doherty
2026-06-19 09:51:07 -04:00
parent c5378f8723
commit 095361b73f
6 changed files with 35 additions and 8 deletions
@@ -71,8 +71,9 @@ public class NotificationRecipient
} }
/// <summary> /// <summary>
/// Parameterless constructor used by EF Core materialization and the SMS factory, where the /// Private parameterless constructor that backs the <see cref="ForSms"/> factory path,
/// contact field is assigned via property setters rather than constructor parameters. /// where the contact field is assigned via property setters rather than constructor
/// parameters — without exposing a half-initialized public constructor to callers.
/// </summary> /// </summary>
private NotificationRecipient() private NotificationRecipient()
{ {
@@ -6,7 +6,10 @@ public class SmsConfiguration
public int Id { get; set; } public int Id { get; set; }
/// <summary>Gets or sets the Twilio Account SID.</summary> /// <summary>Gets or sets the Twilio Account SID.</summary>
public string AccountSid { get; set; } public string AccountSid { get; set; }
/// <summary>Gets or sets the Twilio Auth Token (secret), or null when not applicable.</summary> /// <summary>
/// Gets or sets the Twilio Auth Token (secret). Stored encrypted; null only transiently
/// during configuration, never a valid production value.
/// </summary>
public string? AuthToken { get; set; } public string? AuthToken { get; set; }
/// <summary>Gets or sets the sender phone number (E.164) placed in the From field.</summary> /// <summary>Gets or sets the sender phone number (E.164) placed in the From field.</summary>
public string FromNumber { get; set; } public string FromNumber { get; set; }
@@ -179,7 +179,7 @@ public class ArtifactDeploymentService
// Map notification lists // Map notification lists
var notificationListArtifacts = notificationLists.Select(nl => var notificationListArtifacts = notificationLists.Select(nl =>
new NotificationListArtifact(nl.Name, nl.Recipients.Select(r => r.EmailAddress).ToList())).ToList(); new NotificationListArtifact(nl.Name, nl.Recipients.Where(r => r.EmailAddress is not null).Select(r => r.EmailAddress!).ToList())).ToList();
// Map SMTP configurations — use Host as the artifact name (matches SQLite PK on site) // Map SMTP configurations — use Host as the artifact name (matches SQLite PK on site)
var smtpArtifacts = smtpConfigurations.Select(smtp => var smtpArtifacts = smtpConfigurations.Select(smtp =>
@@ -106,7 +106,10 @@ public sealed class EmailNotificationDeliveryAdapter : INotificationDeliveryAdap
return DeliveryOutcome.Permanent(addressError); return DeliveryOutcome.Permanent(addressError);
} }
var recipientAddresses = recipients.Select(r => r.EmailAddress).ToList(); var recipientAddresses = recipients
.Where(r => r.EmailAddress is not null)
.Select(r => r.EmailAddress!)
.ToList();
try try
{ {
@@ -193,6 +193,24 @@ public class SiteNotificationRepository : INotificationRepository
public Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default) public Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central"); => throw new NotSupportedException("Managed via artifact deployment from Central");
// ── SmsConfiguration (central-only — never deployed to or served from the site) ──
/// <inheritdoc />
public Task<SmsConfiguration?> GetSmsConfigurationAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central");
/// <inheritdoc />
public Task<IReadOnlyList<SmsConfiguration>> GetAllSmsConfigurationsAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central");
/// <inheritdoc />
public Task AddSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central");
/// <inheritdoc />
public Task UpdateSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central");
/// <inheritdoc /> /// <inheritdoc />
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException("Managed via artifact deployment from Central"); => throw new NotSupportedException("Managed via artifact deployment from Central");
@@ -138,9 +138,11 @@ public sealed class EntitySerializer
NotificationLists: aggregate.NotificationLists.Select(nl => new NotificationListDto( NotificationLists: aggregate.NotificationLists.Select(nl => new NotificationListDto(
Name: nl.Name, Name: nl.Name,
Type: nl.Type, Type: nl.Type,
Recipients: nl.Recipients.Select(r => new NotificationRecipientDto( Recipients: nl.Recipients
Name: r.Name, .Where(r => r.EmailAddress is not null)
EmailAddress: r.EmailAddress)).ToList())).ToList(), .Select(r => new NotificationRecipientDto(
Name: r.Name,
EmailAddress: r.EmailAddress!)).ToList())).ToList(),
SmtpConfigs: aggregate.SmtpConfigurations.Select(smtp => SmtpConfigs: aggregate.SmtpConfigurations.Select(smtp =>
{ {
SecretsBlock? secrets = null; SecretsBlock? secrets = null;