Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs
T
Joseph Doherty cd8e4872f6 fix(sms): code-review fixes — Admin-gate provider-config updates, guard secret-clear/data-loss, type-aware UI
Findings from the per-module code review of the SMS feature (code-reviews/):

- ManagementService (High): UpdateSmsConfig + UpdateSmtpConfig were Designer-gated
  while both /notifications/{sms,smtp} pages enforce RequireAdmin — a Designer
  blocked in the UI could still rotate a production credential via CLI. Moved both
  to the Administrator arm so the actor gate matches the UI.
- ManagementService (Medium): UpdateSmsConfig treated --auth-token "" as a value,
  silently clearing the stored Twilio token. Guard on IsNullOrWhiteSpace so empty ==
  omitted (SMTP Credentials keeps its null-only guard — empty is valid for no-auth).
- CentralUI (Medium): NotificationLists recipient badge rendered Name <Email>
  unconditionally, showing "Name <>" for SMS lists. Now type-aware (phone for SMS).
- ConfigurationDatabase (Medium): AddSmsNotifications.Down() backfilled NULL emails
  to '' — silent data loss for SMS-only recipients. Added a pre-drop guard that
  refuses rollback while such rows exist.
- NotificationOutbox (Low): SMS body truncation could split a surrogate pair at the
  cap boundary; back off one code unit to stay well-formed.
- Commons (Low): NotificationRecipient public ctor name-guard now matches the
  ForEmail factory (IsNullOrWhiteSpace). Documented SmsConfiguration.MaxRetries/
  RetryDelay as RESERVED (dispatcher reuses the shared SMTP-derived retry policy).
2026-06-19 15:02:02 -04:00

93 lines
3.7 KiB
C#

namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
public class NotificationRecipient
{
/// <summary>Gets or sets the database primary key.</summary>
public int Id { get; set; }
/// <summary>Gets or sets the id of the parent notification list.</summary>
public int NotificationListId { get; set; }
/// <summary>Gets or sets the display name of the recipient.</summary>
public string Name { get; set; }
/// <summary>Gets or sets the recipient's email address, or null for non-email recipients.</summary>
public string? EmailAddress { get; set; }
/// <summary>Gets or sets the recipient's phone number (E.164), or null for non-SMS recipients.</summary>
public string? PhoneNumber { get; set; }
/// <summary>
/// Initializes a new <see cref="NotificationRecipient"/> with the required fields (email path).
/// </summary>
/// <param name="name">Display name of the recipient.</param>
/// <param name="emailAddress">Email address of the recipient.</param>
public NotificationRecipient(string name, string emailAddress)
{
// Match the ForEmail factory's guard so the invariant ("a recipient always
// has a non-blank display name") holds regardless of construction path. EF
// materializes via the private parameterless ctor + property injection — an
// SMS-only recipient has a null EmailAddress — so this ctor is only reached
// by code that genuinely intends the email path.
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name must not be empty.", nameof(name));
}
Name = name;
EmailAddress = emailAddress ?? throw new ArgumentNullException(nameof(emailAddress));
}
/// <summary>
/// Creates an email recipient with the given name and email address; the phone number is left null.
/// </summary>
/// <param name="name">Display name of the recipient.</param>
/// <param name="emailAddress">Email address of the recipient.</param>
/// <returns>A new email <see cref="NotificationRecipient"/>.</returns>
public static NotificationRecipient ForEmail(string name, string emailAddress)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name must not be empty.", nameof(name));
}
if (emailAddress is null)
{
throw new ArgumentNullException(nameof(emailAddress));
}
return new NotificationRecipient(name, emailAddress);
}
/// <summary>
/// Creates an SMS recipient with the given name and phone number; the email address is left null.
/// </summary>
/// <param name="name">Display name of the recipient.</param>
/// <param name="phoneNumber">Phone number (E.164) of the recipient.</param>
/// <returns>A new SMS <see cref="NotificationRecipient"/>.</returns>
public static NotificationRecipient ForSms(string name, string phoneNumber)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name must not be empty.", nameof(name));
}
if (phoneNumber is null)
{
throw new ArgumentNullException(nameof(phoneNumber));
}
return new NotificationRecipient
{
Name = name,
PhoneNumber = phoneNumber
};
}
/// <summary>
/// Private parameterless constructor that backs the <see cref="ForSms"/> factory path,
/// where the contact field is assigned via property setters rather than constructor
/// parameters — without exposing a half-initialized public constructor to callers.
/// </summary>
private NotificationRecipient()
{
Name = string.Empty;
}
}