up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

View File

@@ -0,0 +1,494 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Notify.Models;
/// <summary>
/// On-call schedule defining who is on-call at any given time.
/// </summary>
public sealed record NotifyOnCallSchedule
{
[JsonConstructor]
public NotifyOnCallSchedule(
string scheduleId,
string tenantId,
string name,
string timeZone,
ImmutableArray<NotifyOnCallLayer> layers,
ImmutableArray<NotifyOnCallOverride> overrides,
bool enabled = true,
string? description = null,
ImmutableDictionary<string, string>? metadata = null,
string? createdBy = null,
DateTimeOffset? createdAt = null,
string? updatedBy = null,
DateTimeOffset? updatedAt = null)
{
ScheduleId = NotifyValidation.EnsureNotNullOrWhiteSpace(scheduleId, nameof(scheduleId));
TenantId = NotifyValidation.EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId));
Name = NotifyValidation.EnsureNotNullOrWhiteSpace(name, nameof(name));
TimeZone = NotifyValidation.EnsureNotNullOrWhiteSpace(timeZone, nameof(timeZone));
Layers = layers.IsDefault ? ImmutableArray<NotifyOnCallLayer>.Empty : layers;
Overrides = overrides.IsDefault ? ImmutableArray<NotifyOnCallOverride>.Empty : overrides;
Enabled = enabled;
Description = NotifyValidation.TrimToNull(description);
Metadata = NotifyValidation.NormalizeStringDictionary(metadata);
CreatedBy = NotifyValidation.TrimToNull(createdBy);
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
UpdatedBy = NotifyValidation.TrimToNull(updatedBy);
UpdatedAt = NotifyValidation.EnsureUtc(updatedAt ?? CreatedAt);
}
public static NotifyOnCallSchedule Create(
string scheduleId,
string tenantId,
string name,
string timeZone,
IEnumerable<NotifyOnCallLayer>? layers = null,
IEnumerable<NotifyOnCallOverride>? overrides = null,
bool enabled = true,
string? description = null,
IEnumerable<KeyValuePair<string, string>>? metadata = null,
string? createdBy = null,
DateTimeOffset? createdAt = null,
string? updatedBy = null,
DateTimeOffset? updatedAt = null)
{
return new NotifyOnCallSchedule(
scheduleId,
tenantId,
name,
timeZone,
layers?.ToImmutableArray() ?? ImmutableArray<NotifyOnCallLayer>.Empty,
overrides?.ToImmutableArray() ?? ImmutableArray<NotifyOnCallOverride>.Empty,
enabled,
description,
ToImmutableDictionary(metadata),
createdBy,
createdAt,
updatedBy,
updatedAt);
}
public string ScheduleId { get; }
public string TenantId { get; }
public string Name { get; }
/// <summary>
/// IANA time zone for the schedule (e.g., "America/New_York").
/// </summary>
public string TimeZone { get; }
/// <summary>
/// Rotation layers that make up this schedule.
/// Multiple layers are combined to determine final on-call.
/// </summary>
public ImmutableArray<NotifyOnCallLayer> Layers { get; }
/// <summary>
/// Temporary overrides (e.g., vacation coverage).
/// </summary>
public ImmutableArray<NotifyOnCallOverride> Overrides { get; }
public bool Enabled { get; }
public string? Description { get; }
public ImmutableDictionary<string, string> Metadata { get; }
public string? CreatedBy { get; }
public DateTimeOffset CreatedAt { get; }
public string? UpdatedBy { get; }
public DateTimeOffset UpdatedAt { get; }
private static ImmutableDictionary<string, string>? ToImmutableDictionary(IEnumerable<KeyValuePair<string, string>>? pairs)
{
if (pairs is null)
{
return null;
}
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
foreach (var (key, value) in pairs)
{
builder[key] = value;
}
return builder.ToImmutable();
}
}
/// <summary>
/// A layer in an on-call schedule representing a rotation.
/// </summary>
public sealed record NotifyOnCallLayer
{
[JsonConstructor]
public NotifyOnCallLayer(
string layerId,
string name,
int priority,
NotifyRotationType rotationType,
TimeSpan rotationInterval,
DateTimeOffset rotationStartsAt,
ImmutableArray<NotifyOnCallParticipant> participants,
NotifyOnCallRestriction? restrictions = null)
{
LayerId = NotifyValidation.EnsureNotNullOrWhiteSpace(layerId, nameof(layerId));
Name = NotifyValidation.EnsureNotNullOrWhiteSpace(name, nameof(name));
Priority = priority;
RotationType = rotationType;
RotationInterval = rotationInterval > TimeSpan.Zero ? rotationInterval : TimeSpan.FromDays(7);
RotationStartsAt = NotifyValidation.EnsureUtc(rotationStartsAt);
Participants = participants.IsDefault ? ImmutableArray<NotifyOnCallParticipant>.Empty : participants;
Restrictions = restrictions;
}
public static NotifyOnCallLayer Create(
string layerId,
string name,
int priority,
NotifyRotationType rotationType,
TimeSpan rotationInterval,
DateTimeOffset rotationStartsAt,
IEnumerable<NotifyOnCallParticipant>? participants = null,
NotifyOnCallRestriction? restrictions = null)
{
return new NotifyOnCallLayer(
layerId,
name,
priority,
rotationType,
rotationInterval,
rotationStartsAt,
participants?.ToImmutableArray() ?? ImmutableArray<NotifyOnCallParticipant>.Empty,
restrictions);
}
public string LayerId { get; }
public string Name { get; }
/// <summary>
/// Higher priority layers take precedence when determining who is on-call.
/// </summary>
public int Priority { get; }
public NotifyRotationType RotationType { get; }
/// <summary>
/// Duration of each rotation (e.g., 1 week).
/// </summary>
public TimeSpan RotationInterval { get; }
/// <summary>
/// When the rotation schedule started.
/// </summary>
public DateTimeOffset RotationStartsAt { get; }
/// <summary>
/// Participants in the rotation.
/// </summary>
public ImmutableArray<NotifyOnCallParticipant> Participants { get; }
/// <summary>
/// Optional time restrictions for when this layer is active.
/// </summary>
public NotifyOnCallRestriction? Restrictions { get; }
}
/// <summary>
/// Participant in an on-call rotation.
/// </summary>
public sealed record NotifyOnCallParticipant
{
[JsonConstructor]
public NotifyOnCallParticipant(
string userId,
string? name = null,
string? email = null,
string? phone = null,
ImmutableArray<NotifyContactMethod> contactMethods = default)
{
UserId = NotifyValidation.EnsureNotNullOrWhiteSpace(userId, nameof(userId));
Name = NotifyValidation.TrimToNull(name);
Email = NotifyValidation.TrimToNull(email);
Phone = NotifyValidation.TrimToNull(phone);
ContactMethods = contactMethods.IsDefault ? ImmutableArray<NotifyContactMethod>.Empty : contactMethods;
}
public static NotifyOnCallParticipant Create(
string userId,
string? name = null,
string? email = null,
string? phone = null,
IEnumerable<NotifyContactMethod>? contactMethods = null)
{
return new NotifyOnCallParticipant(
userId,
name,
email,
phone,
contactMethods?.ToImmutableArray() ?? ImmutableArray<NotifyContactMethod>.Empty);
}
public string UserId { get; }
public string? Name { get; }
public string? Email { get; }
public string? Phone { get; }
public ImmutableArray<NotifyContactMethod> ContactMethods { get; }
}
/// <summary>
/// Contact method for a participant.
/// </summary>
public sealed record NotifyContactMethod
{
[JsonConstructor]
public NotifyContactMethod(
NotifyContactMethodType type,
string address,
int priority = 0,
bool enabled = true)
{
Type = type;
Address = NotifyValidation.EnsureNotNullOrWhiteSpace(address, nameof(address));
Priority = priority;
Enabled = enabled;
}
public NotifyContactMethodType Type { get; }
public string Address { get; }
public int Priority { get; }
public bool Enabled { get; }
}
/// <summary>
/// Type of contact method.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum NotifyContactMethodType
{
Email,
Sms,
Phone,
Slack,
Teams,
Webhook,
InAppInbox,
PagerDuty,
OpsGenie
}
/// <summary>
/// Type of rotation.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum NotifyRotationType
{
/// <summary>
/// Daily rotation.
/// </summary>
Daily,
/// <summary>
/// Weekly rotation.
/// </summary>
Weekly,
/// <summary>
/// Custom interval rotation.
/// </summary>
Custom
}
/// <summary>
/// Time restrictions for when an on-call layer is active.
/// </summary>
public sealed record NotifyOnCallRestriction
{
[JsonConstructor]
public NotifyOnCallRestriction(
NotifyRestrictionType type,
ImmutableArray<NotifyTimeRange> timeRanges)
{
Type = type;
TimeRanges = timeRanges.IsDefault ? ImmutableArray<NotifyTimeRange>.Empty : timeRanges;
}
public static NotifyOnCallRestriction Create(
NotifyRestrictionType type,
IEnumerable<NotifyTimeRange>? timeRanges = null)
{
return new NotifyOnCallRestriction(
type,
timeRanges?.ToImmutableArray() ?? ImmutableArray<NotifyTimeRange>.Empty);
}
public NotifyRestrictionType Type { get; }
public ImmutableArray<NotifyTimeRange> TimeRanges { get; }
}
/// <summary>
/// Type of restriction.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum NotifyRestrictionType
{
/// <summary>
/// Restrictions apply daily.
/// </summary>
DailyRestriction,
/// <summary>
/// Restrictions apply weekly on specific days.
/// </summary>
WeeklyRestriction
}
/// <summary>
/// A time range for restrictions.
/// </summary>
public sealed record NotifyTimeRange
{
[JsonConstructor]
public NotifyTimeRange(
DayOfWeek? dayOfWeek,
TimeOnly startTime,
TimeOnly endTime)
{
DayOfWeek = dayOfWeek;
StartTime = startTime;
EndTime = endTime;
}
/// <summary>
/// Day of week (null for daily restrictions).
/// </summary>
public DayOfWeek? DayOfWeek { get; }
public TimeOnly StartTime { get; }
public TimeOnly EndTime { get; }
}
/// <summary>
/// Temporary override for an on-call schedule.
/// </summary>
public sealed record NotifyOnCallOverride
{
[JsonConstructor]
public NotifyOnCallOverride(
string overrideId,
string userId,
DateTimeOffset startsAt,
DateTimeOffset endsAt,
string? reason = null,
string? createdBy = null,
DateTimeOffset? createdAt = null)
{
OverrideId = NotifyValidation.EnsureNotNullOrWhiteSpace(overrideId, nameof(overrideId));
UserId = NotifyValidation.EnsureNotNullOrWhiteSpace(userId, nameof(userId));
StartsAt = NotifyValidation.EnsureUtc(startsAt);
EndsAt = NotifyValidation.EnsureUtc(endsAt);
Reason = NotifyValidation.TrimToNull(reason);
CreatedBy = NotifyValidation.TrimToNull(createdBy);
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
if (EndsAt <= StartsAt)
{
throw new ArgumentException("EndsAt must be after StartsAt.", nameof(endsAt));
}
}
public static NotifyOnCallOverride Create(
string overrideId,
string userId,
DateTimeOffset startsAt,
DateTimeOffset endsAt,
string? reason = null,
string? createdBy = null,
DateTimeOffset? createdAt = null)
{
return new NotifyOnCallOverride(
overrideId,
userId,
startsAt,
endsAt,
reason,
createdBy,
createdAt);
}
public string OverrideId { get; }
/// <summary>
/// User who will be on-call during this override.
/// </summary>
public string UserId { get; }
public DateTimeOffset StartsAt { get; }
public DateTimeOffset EndsAt { get; }
public string? Reason { get; }
public string? CreatedBy { get; }
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// Checks if the override is active at the specified time.
/// </summary>
public bool IsActiveAt(DateTimeOffset timestamp)
=> timestamp >= StartsAt && timestamp < EndsAt;
}
/// <summary>
/// Result of resolving who is currently on-call.
/// </summary>
public sealed record NotifyOnCallResolution
{
public NotifyOnCallResolution(
string scheduleId,
DateTimeOffset evaluatedAt,
ImmutableArray<NotifyOnCallParticipant> onCallUsers,
string? sourceLayer = null,
string? sourceOverride = null)
{
ScheduleId = scheduleId;
EvaluatedAt = evaluatedAt;
OnCallUsers = onCallUsers.IsDefault ? ImmutableArray<NotifyOnCallParticipant>.Empty : onCallUsers;
SourceLayer = sourceLayer;
SourceOverride = sourceOverride;
}
public string ScheduleId { get; }
public DateTimeOffset EvaluatedAt { get; }
public ImmutableArray<NotifyOnCallParticipant> OnCallUsers { get; }
/// <summary>
/// The layer that provided the on-call user (if from rotation).
/// </summary>
public string? SourceLayer { get; }
/// <summary>
/// The override that provided the on-call user (if from override).
/// </summary>
public string? SourceOverride { get; }
}