From c5378f8723b1474a5d0f33b5015399068d7cced0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 19 Jun 2026 09:42:27 -0400 Subject: [PATCH] feat(sms): NotificationType.Sms + recipient phone + SmsConfiguration + repo iface (S1) --- .../Notifications/NotificationRecipient.cs | 63 ++++++++++++++++++- .../Notifications/SmsConfiguration.cs | 38 +++++++++++ .../Repositories/INotificationRepository.cs | 23 +++++++ .../Types/Enums/NotificationType.cs | 5 +- .../Types/EnumTests.cs | 2 +- 5 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmsConfiguration.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs index ff82a4ed..71ab9d89 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/NotificationRecipient.cs @@ -8,11 +8,13 @@ public class NotificationRecipient public int NotificationListId { get; set; } /// Gets or sets the display name of the recipient. public string Name { get; set; } - /// Gets or sets the recipient's email address. - public string EmailAddress { get; set; } + /// Gets or sets the recipient's email address, or null for non-email recipients. + public string? EmailAddress { get; set; } + /// Gets or sets the recipient's phone number (E.164), or null for non-SMS recipients. + public string? PhoneNumber { get; set; } /// - /// Initializes a new with the required fields. + /// Initializes a new with the required fields (email path). /// /// Display name of the recipient. /// Email address of the recipient. @@ -21,4 +23,59 @@ public class NotificationRecipient Name = name ?? throw new ArgumentNullException(nameof(name)); EmailAddress = emailAddress ?? throw new ArgumentNullException(nameof(emailAddress)); } + + /// + /// Creates an email recipient with the given name and email address; the phone number is left null. + /// + /// Display name of the recipient. + /// Email address of the recipient. + /// A new email . + 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); + } + + /// + /// Creates an SMS recipient with the given name and phone number; the email address is left null. + /// + /// Display name of the recipient. + /// Phone number (E.164) of the recipient. + /// A new SMS . + 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 + }; + } + + /// + /// Parameterless constructor used by EF Core materialization and the SMS factory, where the + /// contact field is assigned via property setters rather than constructor parameters. + /// + private NotificationRecipient() + { + Name = string.Empty; + } } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmsConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmsConfiguration.cs new file mode 100644 index 00000000..7db9f6ad --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmsConfiguration.cs @@ -0,0 +1,38 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications; + +public class SmsConfiguration +{ + /// Gets or sets the primary key. + public int Id { get; set; } + /// Gets or sets the Twilio Account SID. + public string AccountSid { get; set; } + /// Gets or sets the Twilio Auth Token (secret), or null when not applicable. + public string? AuthToken { get; set; } + /// Gets or sets the sender phone number (E.164) placed in the From field. + public string FromNumber { get; set; } + /// Gets or sets the Twilio Messaging Service SID used instead of a From number, or null. + public string? MessagingServiceSid { get; set; } + /// Gets or sets the Twilio REST API base URL, or null to use the provider default. + public string? ApiBaseUrl { get; set; } + /// Gets or sets the connection timeout in seconds. + public int ConnectionTimeoutSeconds { get; set; } + /// Gets or sets the maximum number of delivery retries before parking. + public int MaxRetries { get; set; } + /// Gets or sets the delay between retry attempts. + public TimeSpan RetryDelay { get; set; } + + /// + /// Initializes a new with required fields and sensible defaults + /// for the numeric and timeout fields. + /// + /// Twilio Account SID. + /// Sender phone number (E.164) for the From field. + public SmsConfiguration(string accountSid, string fromNumber) + { + AccountSid = accountSid ?? throw new ArgumentNullException(nameof(accountSid)); + FromNumber = fromNumber ?? throw new ArgumentNullException(nameof(fromNumber)); + ConnectionTimeoutSeconds = 30; + MaxRetries = 10; + RetryDelay = TimeSpan.FromMinutes(1); + } +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationRepository.cs index f95390b0..88c3aa0c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationRepository.cs @@ -101,6 +101,29 @@ public interface INotificationRepository /// A task representing the asynchronous operation. Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default); + // SmsConfiguration + /// Gets the SMS configuration, or null if none is configured. + /// Cancellation token. + /// The SMS configuration, or null if not found. + Task GetSmsConfigurationAsync(CancellationToken cancellationToken = default); + + /// Gets all SMS configurations. + /// Cancellation token. + /// A read-only list of SMS configurations. + Task> GetAllSmsConfigurationsAsync(CancellationToken cancellationToken = default); + + /// Adds a new SMS configuration. + /// The SMS configuration to add. + /// Cancellation token. + /// A task representing the asynchronous operation. + Task AddSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default); + + /// Updates an existing SMS configuration. + /// The SMS configuration to update. + /// Cancellation token. + /// A task representing the asynchronous operation. + Task UpdateSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default); + /// Saves pending changes to the repository. /// Cancellation token. /// The number of entities saved. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/NotificationType.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/NotificationType.cs index 432a1f01..4eb4d433 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/NotificationType.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/NotificationType.cs @@ -1,9 +1,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; /// -/// Delivery channel for a notification. Currently only email is supported. +/// Delivery channel for a notification. Email and SMS are supported. /// public enum NotificationType { - Email + Email, + Sms } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/EnumTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/EnumTests.cs index ded4e95b..2257f9da 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/EnumTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/EnumTests.cs @@ -13,7 +13,7 @@ public class EnumTests [InlineData(typeof(AlarmTriggerType), new[] { "ValueMatch", "RangeViolation", "RateOfChange", "HiLo", "Expression" })] [InlineData(typeof(ConnectionHealth), new[] { "Connected", "Disconnected", "Connecting", "Error" })] [InlineData(typeof(NotificationStatus), new[] { "Pending", "Retrying", "Delivered", "Parked", "Discarded" })] - [InlineData(typeof(NotificationType), new[] { "Email" })] + [InlineData(typeof(NotificationType), new[] { "Email", "Sms" })] public void Enum_ShouldHaveExpectedValues(Type enumType, string[] expectedNames) { var actualNames = Enum.GetNames(enumType);