Files
git.stella-ops.org/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.Worker/Escalation/IOnCallScheduleService.cs
master e950474a77
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
up
2025-11-27 15:16:31 +02:00

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; }
}