From 397a62677f376e122e1c99ad2b8e0133550a424a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 19 May 2026 00:48:48 -0400 Subject: [PATCH] feat(notification-outbox): add Notification entity --- .../Entities/Notifications/Notification.cs | 48 +++++++++++++++++++ .../Entities/NotificationEntityTests.cs | 33 +++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/ScadaLink.Commons/Entities/Notifications/Notification.cs create mode 100644 tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs diff --git a/src/ScadaLink.Commons/Entities/Notifications/Notification.cs b/src/ScadaLink.Commons/Entities/Notifications/Notification.cs new file mode 100644 index 0000000..ab94f56 --- /dev/null +++ b/src/ScadaLink.Commons/Entities/Notifications/Notification.cs @@ -0,0 +1,48 @@ +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Entities.Notifications; + +/// +/// A single notification queued in the central outbox. Created at a site (where the +/// GUID is generated) and forwarded to the central cluster +/// for delivery, retry, and audit. The lifecycle is tracked by . +/// +public class Notification +{ + /// GUID primary key, generated at the originating site. + public string NotificationId { get; set; } + public NotificationType Type { get; set; } + public string ListName { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + + /// JSON extensibility hook for channel-specific payload data. + public string? TypeData { get; set; } + public NotificationStatus Status { get; set; } = NotificationStatus.Pending; + public int RetryCount { get; set; } + public string? LastError { get; set; } + + /// Resolved delivery targets snapshotted at delivery time, for audit. + public string? ResolvedTargets { get; set; } + public string SourceSiteId { get; set; } + public string? SourceInstanceId { get; set; } + public string? SourceScript { get; set; } + public DateTimeOffset SiteEnqueuedAt { get; set; } + + /// Central ingest time. + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? LastAttemptAt { get; set; } + public DateTimeOffset? NextAttemptAt { get; set; } + public DateTimeOffset? DeliveredAt { get; set; } + + public Notification(string notificationId, NotificationType type, string listName, + string subject, string body, string sourceSiteId) + { + NotificationId = notificationId ?? throw new ArgumentNullException(nameof(notificationId)); + Type = type; + ListName = listName ?? throw new ArgumentNullException(nameof(listName)); + Subject = subject ?? throw new ArgumentNullException(nameof(subject)); + Body = body ?? throw new ArgumentNullException(nameof(body)); + SourceSiteId = sourceSiteId ?? throw new ArgumentNullException(nameof(sourceSiteId)); + } +} diff --git a/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs b/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs new file mode 100644 index 0000000..75bc2c5 --- /dev/null +++ b/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs @@ -0,0 +1,33 @@ +using ScadaLink.Commons.Entities.Notifications; +using ScadaLink.Commons.Types.Enums; + +namespace ScadaLink.Commons.Tests.Entities; + +/// +/// Verifies the outbox entity's constructor defaults +/// and null-argument guards on required reference-type parameters. +/// +public class NotificationEntityTests +{ + [Fact] + public void Constructor_SetsDefaults() + { + var n = new Notification("id-1", NotificationType.Email, "ops-team", "subj", "body", "SiteA"); + Assert.Equal(NotificationStatus.Pending, n.Status); + Assert.Equal(0, n.RetryCount); + Assert.Equal("id-1", n.NotificationId); + Assert.Equal(NotificationType.Email, n.Type); + Assert.Equal("ops-team", n.ListName); + Assert.Equal("SiteA", n.SourceSiteId); + } + + [Fact] + public void Constructor_NullListName_Throws() + => Assert.Throws( + () => new Notification("id", NotificationType.Email, null!, "s", "b", "SiteA")); + + [Fact] + public void Constructor_NullNotificationId_Throws() + => Assert.Throws( + () => new Notification(null!, NotificationType.Email, "list", "s", "b", "SiteA")); +}