From 5497ffd181df83bdbc0628e223bd4fd779030967 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 6 Sep 2026 10:59:49 -0400 Subject: [PATCH] feat(notification-outbox): additive NotificationId query filter for ?correlationId= deep links Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- .../Notification/NotificationOutboxQueries.cs | 8 ++++- .../Notifications/NotificationOutboxFilter.cs | 7 +++- .../NotificationOutboxRepository.cs | 6 ++++ .../NotificationOutboxActor.cs | 3 +- ...icationOutboxRepositoryIntegrationTests.cs | 28 +++++++++++++++ .../NotificationOutboxActorQueryTests.cs | 34 +++++++++++++++++++ 6 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Notification/NotificationOutboxQueries.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Notification/NotificationOutboxQueries.cs index 45803fa3..7c25b2f0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Notification/NotificationOutboxQueries.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Notification/NotificationOutboxQueries.cs @@ -6,6 +6,11 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification; /// Outbox UI -> Central: paginated, filtered query over the notification outbox. /// All filter fields are optional; restricts results to stuck notifications. /// +/// +/// Exact match on a single notification — the ?correlationId= deep link from the +/// Flow Debugger / Audit Log. Additive; null means "no filter" (legacy senders +/// compile unchanged). +/// public record NotificationOutboxQueryRequest( string CorrelationId, string? StatusFilter, @@ -18,7 +23,8 @@ public record NotificationOutboxQueryRequest( DateTimeOffset? To, int PageNumber, int PageSize, - string? SourceNodeFilter = null); + string? SourceNodeFilter = null, + string? NotificationIdFilter = null); /// /// A single notification row summarised for outbox UI display. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Notifications/NotificationOutboxFilter.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Notifications/NotificationOutboxFilter.cs index 2d27ab42..307bd732 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Notifications/NotificationOutboxFilter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Notifications/NotificationOutboxFilter.cs @@ -23,6 +23,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications; /// "central-a", "site-plant-a-node-a"). Exact match; null /// means "do not constrain". /// +/// +/// Exact match on a single NotificationId — the ?correlationId= deep link +/// from the Flow Debugger / Audit Log. null means "do not constrain". +/// public record NotificationOutboxFilter( NotificationStatus? Status = null, NotificationType? Type = null, @@ -33,4 +37,5 @@ public record NotificationOutboxFilter( DateTimeOffset? StuckCutoff = null, DateTimeOffset? From = null, DateTimeOffset? To = null, - string? SourceNode = null); + string? SourceNode = null, + string? NotificationId = null); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs index f005c08e..b3f5af4a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs @@ -280,6 +280,12 @@ VALUES query = query.Where(n => n.Type == type); } + // Exact match on one notification — the ?correlationId= deep link. + if (!string.IsNullOrEmpty(filter.NotificationId)) + { + query = query.Where(n => n.NotificationId == filter.NotificationId); + } + if (!string.IsNullOrEmpty(filter.SourceSiteId)) { query = query.Where(n => n.SourceSiteId == filter.SourceSiteId); diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs index f478aef3..412053b5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs @@ -940,7 +940,8 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers StuckCutoff: request.StuckOnly ? StuckCutoff(now) : null, From: request.From, To: request.To, - SourceNode: request.SourceNodeFilter); + SourceNode: request.SourceNodeFilter, + NotificationId: request.NotificationIdFilter); using var scope = _serviceProvider.CreateScope(); var repository = scope.ServiceProvider.GetRequiredService(); diff --git a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs index 93a483eb..8587b73c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations; @@ -101,6 +102,33 @@ public class NotificationOutboxRepositoryIntegrationTests : IClassFixture(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(((IReadOnlyList)Array.Empty(), 0)); + var actor = CreateActor(); + + actor.Tell( + new NotificationOutboxQueryRequest( + CorrelationId: "corr-nid", + StatusFilter: null, + TypeFilter: null, + SourceSiteFilter: null, + ListNameFilter: null, + StuckOnly: false, + SubjectKeyword: null, + From: null, + To: null, + PageNumber: 1, + PageSize: 50, + NotificationIdFilter: "abc"), + TestActor); + + var response = ExpectMsg(); + Assert.True(response.Success); + + // The ?correlationId= deep link from the Flow Debugger / Audit Log rides through + // to the repository as an exact-match NotificationId constraint. + _repository.Received(1).QueryAsync( + Arg.Is(f => f.NotificationId == "abc"), + 1, 50, Arg.Any()); + } + [Fact] public void Query_WithStatusFilterString_ParsesToEnumOnFilter() {