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);