namespace StellaOps.Notifier.Worker.Escalations; /// /// Manages on-call schedules and determines who is currently on-call. /// public interface IOnCallScheduleService { /// /// Gets a schedule by ID. /// Task GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default); /// /// Lists all schedules for a tenant. /// Task> ListAsync(string tenantId, CancellationToken cancellationToken = default); /// /// Creates or updates a schedule. /// Task UpsertAsync(OnCallSchedule schedule, CancellationToken cancellationToken = default); /// /// Deletes a schedule. /// Task DeleteAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default); /// /// Gets who is currently on-call for a schedule. /// Task GetCurrentOnCallAsync( string tenantId, string scheduleId, DateTimeOffset? asOf = null, CancellationToken cancellationToken = default); /// /// Gets on-call coverage for a time range. /// Task> GetCoverageAsync( string tenantId, string scheduleId, DateTimeOffset from, DateTimeOffset to, CancellationToken cancellationToken = default); /// /// Adds an override to a schedule. /// Task AddOverrideAsync( string tenantId, string scheduleId, OnCallOverride @override, CancellationToken cancellationToken = default); /// /// Removes an override from a schedule. /// Task RemoveOverrideAsync( string tenantId, string scheduleId, string overrideId, CancellationToken cancellationToken = default); } /// /// On-call schedule defining rotation of responders. /// public sealed record OnCallSchedule { /// /// Unique schedule identifier. /// public required string ScheduleId { get; init; } /// /// Tenant this schedule belongs to. /// public required string TenantId { get; init; } /// /// Human-readable name. /// public required string Name { get; init; } /// /// Description of the schedule. /// public string? Description { get; init; } /// /// Timezone for the schedule (IANA format). /// public string Timezone { get; init; } = "UTC"; /// /// Rotation layers (evaluated in order, first match wins). /// public required IReadOnlyList Layers { get; init; } /// /// Current overrides to the schedule. /// public IReadOnlyList Overrides { get; init; } = []; /// /// Fallback user if no one is on-call. /// public string? FallbackUserId { get; init; } /// /// When the schedule was created. /// public DateTimeOffset CreatedAt { get; init; } /// /// When the schedule was last updated. /// public DateTimeOffset UpdatedAt { get; init; } /// /// Whether the schedule is enabled. /// public bool Enabled { get; init; } = true; } /// /// A rotation layer within an on-call schedule. /// public sealed record RotationLayer { /// /// Layer name. /// public required string Name { get; init; } /// /// Rotation type. /// public required RotationType Type { get; init; } /// /// Users in the rotation (in order). /// public required IReadOnlyList Users { get; init; } /// /// When this rotation starts. /// public required DateTimeOffset StartTime { get; init; } /// /// Rotation interval (e.g., 1 week for weekly rotation). /// public required TimeSpan RotationInterval { get; init; } /// /// Handoff time of day (in schedule timezone). /// public TimeOnly HandoffTime { get; init; } = new(9, 0); /// /// Days of week this layer is active (empty = all days). /// public IReadOnlyList? ActiveDays { get; init; } /// /// Time restrictions (e.g., only active 9am-5pm). /// public OnCallTimeRestriction? TimeRestriction { get; init; } /// /// Layer priority (lower = higher priority). /// public int Priority { get; init; } } /// /// Type of rotation. /// public enum RotationType { /// /// Users rotate on a regular interval. /// Daily, /// /// Users rotate weekly. /// Weekly, /// /// Custom rotation interval. /// Custom } /// /// A user in an on-call rotation. /// public sealed record OnCallUser { /// /// User identifier. /// public required string UserId { get; init; } /// /// Display name. /// public string? DisplayName { get; init; } /// /// Email address. /// public string? Email { get; init; } /// /// Preferred notification channels. /// public IReadOnlyList PreferredChannels { get; init; } = []; /// /// Contact methods in priority order. /// public IReadOnlyList ContactMethods { get; init; } = []; } /// /// Contact method for a user. /// public sealed record ContactMethod { /// /// Contact type (email, sms, phone, slack, etc.). /// public required string Type { get; init; } /// /// Contact address/number. /// public required string Address { get; init; } /// /// Label for this contact method. /// public string? Label { get; init; } /// /// Whether this is verified. /// public bool Verified { get; init; } } /// /// Time restriction for a rotation layer. /// public sealed record OnCallTimeRestriction { /// /// Start time of active period. /// public required TimeOnly StartTime { get; init; } /// /// End time of active period. /// public required TimeOnly EndTime { get; init; } /// /// Whether the restriction spans midnight (e.g., 10pm-6am). /// public bool SpansMidnight => EndTime < StartTime; } /// /// Override to the normal on-call schedule. /// public sealed record OnCallOverride { /// /// Override identifier. /// public required string OverrideId { get; init; } /// /// User who will be on-call during this override. /// public required string UserId { get; init; } /// /// Display name of the override user. /// public string? UserDisplayName { get; init; } /// /// When the override starts. /// public required DateTimeOffset StartTime { get; init; } /// /// When the override ends. /// public required DateTimeOffset EndTime { get; init; } /// /// Reason for the override. /// public string? Reason { get; init; } /// /// Who created the override. /// public string? CreatedBy { get; init; } /// /// When the override was created. /// public DateTimeOffset CreatedAt { get; init; } } /// /// Result of on-call resolution. /// public sealed record OnCallResolution { /// /// Whether someone is on-call. /// public required bool HasOnCall { get; init; } /// /// The on-call user (if any). /// public OnCallUser? OnCallUser { get; init; } /// /// Which layer resolved the on-call. /// public string? ResolvedFromLayer { get; init; } /// /// Whether this is from an override. /// public bool IsOverride { get; init; } /// /// Override details if applicable. /// public OnCallOverride? Override { get; init; } /// /// Whether this is the fallback user. /// public bool IsFallback { get; init; } /// /// When the current on-call shift ends. /// public DateTimeOffset? ShiftEndsAt { get; init; } /// /// The time this resolution was calculated for. /// public DateTimeOffset AsOf { get; init; } public static OnCallResolution NoOneOnCall(DateTimeOffset asOf) => new() { HasOnCall = false, AsOf = asOf }; public static OnCallResolution FromUser(OnCallUser user, string layer, DateTimeOffset asOf, DateTimeOffset? shiftEnds = null) => new() { HasOnCall = true, OnCallUser = user, ResolvedFromLayer = layer, AsOf = asOf, ShiftEndsAt = shiftEnds }; public static OnCallResolution FromOverride(OnCallUser user, OnCallOverride @override, DateTimeOffset asOf) => new() { HasOnCall = true, OnCallUser = user, IsOverride = true, Override = @override, AsOf = asOf, ShiftEndsAt = @override.EndTime }; public static OnCallResolution FromFallback(OnCallUser user, DateTimeOffset asOf) => new() { HasOnCall = true, OnCallUser = user, IsFallback = true, AsOf = asOf }; } /// /// On-call coverage for a time period. /// public sealed record OnCallCoverage { /// /// Start of this coverage period. /// public required DateTimeOffset From { get; init; } /// /// End of this coverage period. /// public required DateTimeOffset To { get; init; } /// /// User on-call during this period. /// public required OnCallUser User { get; init; } /// /// Layer providing coverage. /// public string? Layer { get; init; } /// /// Whether this is from an override. /// public bool IsOverride { get; init; } }