Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
360 lines
8.6 KiB
C#
360 lines
8.6 KiB
C#
namespace StellaOps.Notifier.Worker.Escalation;
|
|
|
|
/// <summary>
|
|
/// Service for managing on-call schedules.
|
|
/// </summary>
|
|
public interface IOnCallScheduleService
|
|
{
|
|
/// <summary>
|
|
/// Lists all on-call schedules for a tenant.
|
|
/// </summary>
|
|
Task<IReadOnlyList<OnCallSchedule>> ListSchedulesAsync(
|
|
string tenantId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets a specific on-call schedule.
|
|
/// </summary>
|
|
Task<OnCallSchedule?> GetScheduleAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Creates or updates an on-call schedule.
|
|
/// </summary>
|
|
Task<OnCallSchedule> UpsertScheduleAsync(
|
|
OnCallSchedule schedule,
|
|
string? actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Deletes an on-call schedule.
|
|
/// </summary>
|
|
Task<bool> DeleteScheduleAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
string? actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets the current on-call user(s) for a schedule.
|
|
/// </summary>
|
|
Task<IReadOnlyList<OnCallUser>> GetCurrentOnCallAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
DateTimeOffset? atTime = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets on-call coverage for a time range.
|
|
/// </summary>
|
|
Task<IReadOnlyList<OnCallShift>> GetCoverageAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
DateTimeOffset from,
|
|
DateTimeOffset to,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Creates an override for a schedule.
|
|
/// </summary>
|
|
Task<OnCallOverride> CreateOverrideAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
OnCallOverride @override,
|
|
string? actor,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Deletes an override.
|
|
/// </summary>
|
|
Task<bool> DeleteOverrideAsync(
|
|
string tenantId,
|
|
string scheduleId,
|
|
string overrideId,
|
|
string? actor,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An on-call schedule defining rotation coverage.
|
|
/// </summary>
|
|
public sealed record OnCallSchedule
|
|
{
|
|
/// <summary>
|
|
/// Unique schedule ID.
|
|
/// </summary>
|
|
public required string ScheduleId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Tenant ID.
|
|
/// </summary>
|
|
public required string TenantId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Display name.
|
|
/// </summary>
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Description.
|
|
/// </summary>
|
|
public string? Description { get; init; }
|
|
|
|
/// <summary>
|
|
/// Timezone for the schedule (IANA format).
|
|
/// </summary>
|
|
public required string Timezone { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether the schedule is enabled.
|
|
/// </summary>
|
|
public bool Enabled { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Rotation layers (combined to determine on-call).
|
|
/// </summary>
|
|
public required IReadOnlyList<RotationLayer> Layers { get; init; }
|
|
|
|
/// <summary>
|
|
/// Active overrides.
|
|
/// </summary>
|
|
public IReadOnlyList<OnCallOverride>? Overrides { get; init; }
|
|
|
|
/// <summary>
|
|
/// When created.
|
|
/// </summary>
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Who created.
|
|
/// </summary>
|
|
public string? CreatedBy { get; init; }
|
|
|
|
/// <summary>
|
|
/// When last updated.
|
|
/// </summary>
|
|
public DateTimeOffset UpdatedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Who last updated.
|
|
/// </summary>
|
|
public string? UpdatedBy { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A rotation layer in an on-call schedule.
|
|
/// </summary>
|
|
public sealed record RotationLayer
|
|
{
|
|
/// <summary>
|
|
/// Layer name.
|
|
/// </summary>
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Layer priority (lower = higher priority for conflicts).
|
|
/// </summary>
|
|
public int Priority { get; init; } = 100;
|
|
|
|
/// <summary>
|
|
/// Users in this rotation.
|
|
/// </summary>
|
|
public required IReadOnlyList<OnCallUser> Users { get; init; }
|
|
|
|
/// <summary>
|
|
/// Rotation type.
|
|
/// </summary>
|
|
public required RotationType Type { get; init; }
|
|
|
|
/// <summary>
|
|
/// Handoff time (when rotations change).
|
|
/// </summary>
|
|
public required TimeOnly HandoffTime { get; init; }
|
|
|
|
/// <summary>
|
|
/// Rotation interval.
|
|
/// </summary>
|
|
public required TimeSpan RotationInterval { get; init; }
|
|
|
|
/// <summary>
|
|
/// When this layer's rotation starts.
|
|
/// </summary>
|
|
public required DateTimeOffset RotationStart { get; init; }
|
|
|
|
/// <summary>
|
|
/// Days/times this layer is active (null = always).
|
|
/// </summary>
|
|
public IReadOnlyList<ScheduleRestriction>? Restrictions { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether this layer is enabled.
|
|
/// </summary>
|
|
public bool Enabled { get; init; } = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type of rotation.
|
|
/// </summary>
|
|
public enum RotationType
|
|
{
|
|
/// <summary>Daily rotation.</summary>
|
|
Daily,
|
|
|
|
/// <summary>Weekly rotation.</summary>
|
|
Weekly,
|
|
|
|
/// <summary>Custom interval rotation.</summary>
|
|
Custom
|
|
}
|
|
|
|
/// <summary>
|
|
/// A restriction on when a rotation layer is active.
|
|
/// </summary>
|
|
public sealed record ScheduleRestriction
|
|
{
|
|
/// <summary>
|
|
/// Type of restriction.
|
|
/// </summary>
|
|
public required RestrictionType Type { get; init; }
|
|
|
|
/// <summary>
|
|
/// Days of week (0=Sunday, 6=Saturday) for weekly restrictions.
|
|
/// </summary>
|
|
public IReadOnlyList<int>? DaysOfWeek { get; init; }
|
|
|
|
/// <summary>
|
|
/// Start time for the restriction.
|
|
/// </summary>
|
|
public TimeOnly? StartTime { get; init; }
|
|
|
|
/// <summary>
|
|
/// End time for the restriction.
|
|
/// </summary>
|
|
public TimeOnly? EndTime { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type of schedule restriction.
|
|
/// </summary>
|
|
public enum RestrictionType
|
|
{
|
|
/// <summary>Restrict to specific days of week.</summary>
|
|
DaysOfWeek,
|
|
|
|
/// <summary>Restrict to specific time range.</summary>
|
|
TimeOfDay,
|
|
|
|
/// <summary>Restrict to specific days and times.</summary>
|
|
DaysAndTime
|
|
}
|
|
|
|
/// <summary>
|
|
/// A user in an on-call schedule.
|
|
/// </summary>
|
|
public sealed record OnCallUser
|
|
{
|
|
/// <summary>
|
|
/// User ID.
|
|
/// </summary>
|
|
public required string UserId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Display name.
|
|
/// </summary>
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Email address.
|
|
/// </summary>
|
|
public string? Email { get; init; }
|
|
|
|
/// <summary>
|
|
/// Phone number.
|
|
/// </summary>
|
|
public string? Phone { get; init; }
|
|
|
|
/// <summary>
|
|
/// Preferred notification channel.
|
|
/// </summary>
|
|
public string? PreferredChannelId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Position in rotation order.
|
|
/// </summary>
|
|
public int Order { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// An override for an on-call schedule.
|
|
/// </summary>
|
|
public sealed record OnCallOverride
|
|
{
|
|
/// <summary>
|
|
/// Override ID.
|
|
/// </summary>
|
|
public required string OverrideId { get; init; }
|
|
|
|
/// <summary>
|
|
/// User taking over on-call.
|
|
/// </summary>
|
|
public required OnCallUser User { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the override starts.
|
|
/// </summary>
|
|
public required DateTimeOffset StartsAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the override ends.
|
|
/// </summary>
|
|
public required DateTimeOffset EndsAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reason for the override.
|
|
/// </summary>
|
|
public string? Reason { get; init; }
|
|
|
|
/// <summary>
|
|
/// Who created the override.
|
|
/// </summary>
|
|
public string? CreatedBy { get; init; }
|
|
|
|
/// <summary>
|
|
/// When created.
|
|
/// </summary>
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A computed on-call shift.
|
|
/// </summary>
|
|
public sealed record OnCallShift
|
|
{
|
|
/// <summary>
|
|
/// User on call.
|
|
/// </summary>
|
|
public required OnCallUser User { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the shift starts.
|
|
/// </summary>
|
|
public required DateTimeOffset StartsAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// When the shift ends.
|
|
/// </summary>
|
|
public required DateTimeOffset EndsAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Layer this shift comes from.
|
|
/// </summary>
|
|
public required string LayerName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether this is from an override.
|
|
/// </summary>
|
|
public bool IsOverride { get; init; }
|
|
}
|