shoc-backend/SeaHaven.DataServices/Models/NotificationFeedReadModels.cs
Alexandre Brandizzi 14c8e46dd0 feat(notifications): SEV response-window alerts and breach acknowledgement
Reactive/Emergency work orders with a SEV 1-5 level are timed from their
creation against the SEV Respond deadline (2/4/8/24/72 hours, one backend
table). From 50% they are at risk: a dismissable High row in the "SLA at
Risk" section and an entry in the feed's slaAtRisk set with the server
clock (start, deadline, percent) for the banner and toast. From 100% they
are a Critical acknowledge row that only acknowledging removes.

POST /api/notifications/sla/{id}/acknowledge records who and when as a
work-order audit entry ("SLA breach acknowledged by <name>"), scoped to the
caller's feed audience: 404 outside it, 409 before the deadline, 204 when
recorded or already recorded. A later severity change is a new breach.
2026-09-25 11:16:21 -03:00

101 lines
4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace SeaHaven.DataServices.Models;
/// <summary>
/// Server-derived feed scope. <see cref="AccountId"/> null is org-wide;
/// <see cref="DispatcherId"/> null means every dispatcher's work orders.
/// </summary>
public sealed record NotificationFeedScope(int? AccountId, string? DispatcherId);
public sealed class UnassignedWorkOrdersSummary
{
public int Count { get; init; }
public DateTime? LatestCreatedAt { get; init; }
}
public sealed class NotificationWorkOrderCandidate
{
public int Id { get; init; }
public string? Number { get; init; }
public DateTime ScheduledDate { get; init; }
}
/// <summary>A bounded slice of candidates plus the unbounded total they were taken from.</summary>
public sealed class NotificationCandidatePage
{
public int Total { get; init; }
public IReadOnlyList<NotificationWorkOrderCandidate> Items { get; init; } = Array.Empty<NotificationWorkOrderCandidate>();
}
/// <summary>
/// Server-derived scope for items addressed to one person: <see cref="UserId"/> is the signed-in
/// user from claims and <see cref="AccountId"/> null is org-wide.
/// </summary>
public sealed record NotificationPersonalScope(int? AccountId, string UserId);
/// <summary>An open work order assigned to the user, with when and by whom it was last assigned.</summary>
public sealed class NotificationAssignmentCandidate
{
public int WorkOrderId { get; init; }
public string? Number { get; init; }
public DateTime AssignedAt { get; init; }
public string? AssignedBy { get; init; }
}
public sealed class NotificationCommentCandidate
{
public int CommentId { get; init; }
public int WorkOrderId { get; init; }
public string? WorkOrderNumber { get; init; }
public string? AuthorFirstName { get; init; }
public string? AuthorLastName { get; init; }
/// <summary>Free-text author for comments with no user (email sync, vendor portal).</summary>
public string? Commenter { get; init; }
public string? Text { get; init; }
public DateTime CreatedAt { get; init; }
}
public sealed class NotificationUpliftDecisionCandidate
{
public int UpliftId { get; init; }
public int WorkOrderId { get; init; }
public string? WorkOrderNumber { get; init; }
public string Status { get; init; } = "";
public decimal Amount { get; init; }
public string? DecisionNote { get; init; }
public DateTime DecidedAt { get; init; }
}
/// <summary>
/// Per SEV level, the latest work-order creation time that has reached a response-window threshold.
/// The business service computes them; the query only compares creation times.
/// </summary>
public sealed record NotificationSlaCutoffs(DateTime Sev1, DateTime Sev2, DateTime Sev3, DateTime Sev4, DateTime Sev5);
/// <summary>
/// The work-order audit entry that records an SLA breach acknowledgement: who (UserId), when (CreatedAt)
/// and the SEV level whose deadline was missed (NewValue). It is also what keeps the breach out of the feed.
/// </summary>
public static class SlaBreachAcknowledgementAudit
{
public const string FieldName = "SlaBreachAcknowledged";
}
/// <summary>An open Reactive/Emergency work order with a SEV 1–5 level and a creation time.</summary>
public sealed class NotificationSlaCandidate
{
public int Id { get; init; }
public string? Number { get; init; }
public string Severity { get; init; } = "";
public DateTime CreatedAt { get; init; }
/// <summary>A breach of the work order's current severity has already been acknowledged.</summary>
public bool BreachAcknowledged { get; init; }
}
public sealed class NotificationSlaCandidates
{
/// <summary>Every work order past half its window but not past the whole window.</summary>
public IReadOnlyList<NotificationSlaCandidate> AtRisk { get; init; } = Array.Empty<NotificationSlaCandidate>();
/// <summary>Unacknowledged breaches: the newest slice, plus the unbounded total.</summary>
public IReadOnlyList<NotificationSlaCandidate> Breached { get; init; } = Array.Empty<NotificationSlaCandidate>();
public int BreachedTotal { get; init; }
}