feat(notif): NotificationDetailRequest query for full notification detail
This commit is contained in:
@@ -76,6 +76,49 @@ public record DiscardNotificationResponse(
|
||||
bool Success,
|
||||
string? ErrorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Outbox UI -> Central: request for the full detail of a single notification
|
||||
/// (including Body and resolved recipients), for the report detail modal.
|
||||
/// </summary>
|
||||
public record NotificationDetailRequest(
|
||||
string CorrelationId,
|
||||
string NotificationId);
|
||||
|
||||
/// <summary>
|
||||
/// Central -> Outbox UI: full detail for one notification. On a repository fault or
|
||||
/// missing row, Success is false / Detail is null and ErrorMessage carries the cause.
|
||||
/// </summary>
|
||||
public record NotificationDetailResponse(
|
||||
string CorrelationId,
|
||||
bool Success,
|
||||
string? ErrorMessage,
|
||||
NotificationDetail? Detail);
|
||||
|
||||
/// <summary>
|
||||
/// Full notification detail for the report detail modal — everything in the grid's
|
||||
/// NotificationSummary plus Body, ResolvedTargets (recipients), TypeData, SourceScript,
|
||||
/// and the additional lifecycle timestamps.
|
||||
/// </summary>
|
||||
public record NotificationDetail(
|
||||
string NotificationId,
|
||||
string Type,
|
||||
string ListName,
|
||||
string Subject,
|
||||
string Body,
|
||||
string Status,
|
||||
int RetryCount,
|
||||
string? LastError,
|
||||
string? ResolvedTargets,
|
||||
string? TypeData,
|
||||
string SourceSiteId,
|
||||
string? SourceInstanceId,
|
||||
string? SourceScript,
|
||||
DateTimeOffset SiteEnqueuedAt,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset? LastAttemptAt,
|
||||
DateTimeOffset? NextAttemptAt,
|
||||
DateTimeOffset? DeliveredAt);
|
||||
|
||||
/// <summary>
|
||||
/// Outbox UI -> Central: request for the notification outbox KPI summary.
|
||||
/// </summary>
|
||||
|
||||
@@ -275,6 +275,13 @@ public class CommunicationService
|
||||
request, _options.QueryTimeout, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<NotificationDetailResponse> GetNotificationDetailAsync(
|
||||
NotificationDetailRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetNotificationOutbox().Ask<NotificationDetailResponse>(
|
||||
request, _options.QueryTimeout, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<NotificationKpiResponse> GetNotificationKpisAsync(
|
||||
NotificationKpiRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
|
||||
Receive<InternalMessages.PurgeComplete>(_ => { });
|
||||
Receive<NotificationOutboxQueryRequest>(HandleQuery);
|
||||
Receive<NotificationStatusQuery>(HandleStatusQuery);
|
||||
Receive<NotificationDetailRequest>(HandleDetailRequest);
|
||||
Receive<RetryNotificationRequest>(HandleRetry);
|
||||
Receive<DiscardNotificationRequest>(HandleDiscard);
|
||||
Receive<NotificationKpiRequest>(HandleKpiRequest);
|
||||
@@ -674,6 +675,59 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
|
||||
DeliveredAt: notification.DeliveredAt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a full-detail query for a single notification — backs the report detail
|
||||
/// modal, which needs the Body and resolved recipients that the grid summary omits.
|
||||
/// </summary>
|
||||
private void HandleDetailRequest(NotificationDetailRequest request)
|
||||
{
|
||||
var sender = Sender;
|
||||
|
||||
DetailAsync(request).PipeTo(
|
||||
sender,
|
||||
success: response => response,
|
||||
failure: ex => new NotificationDetailResponse(
|
||||
request.CorrelationId, Success: false,
|
||||
ErrorMessage: ex.GetBaseException().Message, Detail: null));
|
||||
}
|
||||
|
||||
private async Task<NotificationDetailResponse> DetailAsync(NotificationDetailRequest request)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var repository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>();
|
||||
var notification = await repository.GetByIdAsync(request.NotificationId);
|
||||
|
||||
if (notification is null)
|
||||
{
|
||||
return new NotificationDetailResponse(
|
||||
request.CorrelationId, Success: false,
|
||||
ErrorMessage: "notification not found", Detail: null);
|
||||
}
|
||||
|
||||
var detail = new NotificationDetail(
|
||||
notification.NotificationId,
|
||||
notification.Type.ToString(),
|
||||
notification.ListName,
|
||||
notification.Subject,
|
||||
notification.Body,
|
||||
notification.Status.ToString(),
|
||||
notification.RetryCount,
|
||||
notification.LastError,
|
||||
notification.ResolvedTargets,
|
||||
notification.TypeData,
|
||||
notification.SourceSiteId,
|
||||
notification.SourceInstanceId,
|
||||
notification.SourceScript,
|
||||
notification.SiteEnqueuedAt,
|
||||
notification.CreatedAt,
|
||||
notification.LastAttemptAt,
|
||||
notification.NextAttemptAt,
|
||||
notification.DeliveredAt);
|
||||
|
||||
return new NotificationDetailResponse(
|
||||
request.CorrelationId, Success: true, ErrorMessage: null, detail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a manual retry request. Only a <c>Parked</c> notification can be retried;
|
||||
/// it is reset to <c>Pending</c> with a cleared retry count, next-attempt time, and
|
||||
|
||||
@@ -306,6 +306,57 @@ public class NotificationOutboxActorQueryTests : TestKit
|
||||
Assert.Contains("not found", response.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailRequest_KnownId_ReturnsFullDetail_WithBodyAndResolvedTargets()
|
||||
{
|
||||
var row = MakeNotification(
|
||||
status: NotificationStatus.Delivered, retryCount: 2, lastError: "transient blip");
|
||||
row.Body = "Tank-7 has exceeded its high-level setpoint.";
|
||||
row.ResolvedTargets = "[\"ops@example.com\",\"oncall@example.com\"]";
|
||||
row.TypeData = "{\"priority\":\"high\"}";
|
||||
row.SourceScript = "HighLevelAlarm.csx";
|
||||
row.SiteEnqueuedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
row.DeliveredAt = DateTimeOffset.UtcNow;
|
||||
_repository.GetByIdAsync(row.NotificationId, Arg.Any<CancellationToken>()).Returns(row);
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(new NotificationDetailRequest("corr-d1", row.NotificationId), TestActor);
|
||||
|
||||
var response = ExpectMsg<NotificationDetailResponse>();
|
||||
Assert.Equal("corr-d1", response.CorrelationId);
|
||||
Assert.True(response.Success);
|
||||
Assert.Null(response.ErrorMessage);
|
||||
Assert.NotNull(response.Detail);
|
||||
var detail = response.Detail!;
|
||||
Assert.Equal(row.NotificationId, detail.NotificationId);
|
||||
Assert.Equal("Email", detail.Type);
|
||||
Assert.Equal("Delivered", detail.Status);
|
||||
Assert.Equal("Tank-7 has exceeded its high-level setpoint.", detail.Body);
|
||||
Assert.Equal("[\"ops@example.com\",\"oncall@example.com\"]", detail.ResolvedTargets);
|
||||
Assert.Equal("{\"priority\":\"high\"}", detail.TypeData);
|
||||
Assert.Equal("HighLevelAlarm.csx", detail.SourceScript);
|
||||
Assert.Equal("instance-42", detail.SourceInstanceId);
|
||||
Assert.Equal(2, detail.RetryCount);
|
||||
Assert.Equal("transient blip", detail.LastError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetailRequest_UnknownId_ReturnsNotFound()
|
||||
{
|
||||
_repository.GetByIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns((Notification?)null);
|
||||
var actor = CreateActor();
|
||||
|
||||
actor.Tell(new NotificationDetailRequest("corr-d2", "missing-id"), TestActor);
|
||||
|
||||
var response = ExpectMsg<NotificationDetailResponse>();
|
||||
Assert.Equal("corr-d2", response.CorrelationId);
|
||||
Assert.False(response.Success);
|
||||
Assert.Null(response.Detail);
|
||||
Assert.NotNull(response.ErrorMessage);
|
||||
Assert.Contains("not found", response.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KpiRequest_ComputesKpis_AndMapsSnapshot()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user