using System.Collections.Immutable;
using System.Globalization;
namespace StellaOps.Policy.Exceptions.Models;
///
/// Type of exception lifecycle event.
///
public enum ExceptionEventType
{
/// Exception was created (Proposed status).
Created,
/// Exception details were updated.
Updated,
/// Exception was approved by an approver.
Approved,
/// Exception was activated (Active status).
Activated,
/// Exception expiry was extended.
Extended,
/// Exception was explicitly revoked.
Revoked,
/// Exception expired automatically.
Expired,
/// Evidence was attached to exception.
EvidenceAttached,
/// Compensating control was added.
CompensatingControlAdded,
/// Exception was rejected before approval.
Rejected
}
///
/// Immutable event recording a state change in an exception's lifecycle.
///
///
/// Exception events form an append-only audit trail that can never be modified.
/// Each event captures:
/// - What happened (event type)
/// - Who did it (actor)
/// - When it happened (timestamp)
/// - What changed (before/after states)
/// - Why it happened (details)
///
public sealed record ExceptionEvent
{
///
/// Unique event identifier.
///
public required Guid EventId { get; init; }
///
/// Reference to the parent exception.
///
public required string ExceptionId { get; init; }
///
/// Sequence number within this exception's event stream.
///
public required int SequenceNumber { get; init; }
///
/// Type of event that occurred.
///
public required ExceptionEventType EventType { get; init; }
///
/// Identity of the actor who triggered this event.
/// May be a user ID, service account, or "system" for automated events.
///
public required string ActorId { get; init; }
///
/// When this event occurred.
///
public required DateTimeOffset OccurredAt { get; init; }
///
/// Status before this event (null for Created events).
///
public ExceptionStatus? PreviousStatus { get; init; }
///
/// Status after this event.
///
public required ExceptionStatus NewStatus { get; init; }
///
/// Version number after this event.
///
public required int NewVersion { get; init; }
///
/// Human-readable description of what happened.
///
public string? Description { get; init; }
///
/// Additional structured details about the event.
///
public ImmutableDictionary Details { get; init; } =
ImmutableDictionary.Empty;
///
/// IP address or client identifier of the actor (for audit).
///
public string? ClientInfo { get; init; }
///
/// Creates a "Created" event for a new exception.
///
public static ExceptionEvent ForCreated(
string exceptionId,
string actorId,
string? description = null,
string? clientInfo = null) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = 1,
EventType = ExceptionEventType.Created,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = null,
NewStatus = ExceptionStatus.Proposed,
NewVersion = 1,
Description = description ?? "Exception created",
ClientInfo = clientInfo
};
///
/// Creates an "Approved" event.
///
public static ExceptionEvent ForApproved(
string exceptionId,
int sequenceNumber,
string actorId,
int newVersion,
string? description = null,
string? clientInfo = null) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = sequenceNumber,
EventType = ExceptionEventType.Approved,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = ExceptionStatus.Proposed,
NewStatus = ExceptionStatus.Approved,
NewVersion = newVersion,
Description = description ?? $"Exception approved by {actorId}",
ClientInfo = clientInfo
};
///
/// Creates an "Activated" event.
///
public static ExceptionEvent ForActivated(
string exceptionId,
int sequenceNumber,
string actorId,
int newVersion,
ExceptionStatus previousStatus,
string? description = null,
string? clientInfo = null) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = sequenceNumber,
EventType = ExceptionEventType.Activated,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = previousStatus,
NewStatus = ExceptionStatus.Active,
NewVersion = newVersion,
Description = description ?? "Exception activated",
ClientInfo = clientInfo
};
///
/// Creates a "Revoked" event.
///
public static ExceptionEvent ForRevoked(
string exceptionId,
int sequenceNumber,
string actorId,
int newVersion,
ExceptionStatus previousStatus,
string reason,
string? clientInfo = null) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = sequenceNumber,
EventType = ExceptionEventType.Revoked,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = previousStatus,
NewStatus = ExceptionStatus.Revoked,
NewVersion = newVersion,
Description = $"Exception revoked: {reason}",
Details = ImmutableDictionary.Empty.Add("reason", reason),
ClientInfo = clientInfo
};
///
/// Creates an "Expired" event (typically from system).
///
public static ExceptionEvent ForExpired(
string exceptionId,
int sequenceNumber,
int newVersion) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = sequenceNumber,
EventType = ExceptionEventType.Expired,
ActorId = "system",
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = ExceptionStatus.Active,
NewStatus = ExceptionStatus.Expired,
NewVersion = newVersion,
Description = "Exception expired automatically"
};
///
/// Creates an "Extended" event.
///
public static ExceptionEvent ForExtended(
string exceptionId,
int sequenceNumber,
string actorId,
int newVersion,
DateTimeOffset previousExpiry,
DateTimeOffset newExpiry,
string? reason = null,
string? clientInfo = null) => new()
{
EventId = Guid.NewGuid(),
ExceptionId = exceptionId,
SequenceNumber = sequenceNumber,
EventType = ExceptionEventType.Extended,
ActorId = actorId,
OccurredAt = DateTimeOffset.UtcNow,
PreviousStatus = ExceptionStatus.Active,
NewStatus = ExceptionStatus.Active,
NewVersion = newVersion,
Description = reason ?? $"Exception extended from {previousExpiry:O} to {newExpiry:O}",
Details = ImmutableDictionary.Empty
.Add("previous_expiry", previousExpiry.ToString("O", CultureInfo.InvariantCulture))
.Add("new_expiry", newExpiry.ToString("O", CultureInfo.InvariantCulture)),
ClientInfo = clientInfo
};
}
///
/// Aggregated exception history for audit display.
///
public sealed record ExceptionHistory
{
///
/// The exception this history belongs to.
///
public required string ExceptionId { get; init; }
///
/// All events in chronological order.
///
public required ImmutableArray Events { get; init; }
///
/// Total number of events.
///
public int EventCount => Events.Length;
///
/// When the exception was first created.
///
public DateTimeOffset? FirstEventAt => Events.Length > 0 ? Events[0].OccurredAt : null;
///
/// When the last event occurred.
///
public DateTimeOffset? LastEventAt => Events.Length > 0 ? Events[^1].OccurredAt : null;
}