Files
git.stella-ops.org/src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/Endpoints/QuietHoursEndpoints.cs
StellaOps Bot ef6e4b2067
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
2025-11-27 21:45:32 +02:00

352 lines
13 KiB
C#

using Microsoft.AspNetCore.Mvc;
using StellaOps.Notifier.Worker.Correlation;
namespace StellaOps.Notifier.WebService.Endpoints;
/// <summary>
/// API endpoints for quiet hours calendar management.
/// </summary>
public static class QuietHoursEndpoints
{
/// <summary>
/// Maps quiet hours endpoints.
/// </summary>
public static IEndpointRouteBuilder MapQuietHoursEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v2/quiet-hours")
.WithTags("QuietHours")
.WithOpenApi();
group.MapGet("/calendars", ListCalendarsAsync)
.WithName("ListQuietHoursCalendars")
.WithSummary("List all quiet hours calendars")
.WithDescription("Returns all quiet hours calendars for the tenant.");
group.MapGet("/calendars/{calendarId}", GetCalendarAsync)
.WithName("GetQuietHoursCalendar")
.WithSummary("Get a quiet hours calendar")
.WithDescription("Returns a specific quiet hours calendar by ID.");
group.MapPost("/calendars", CreateCalendarAsync)
.WithName("CreateQuietHoursCalendar")
.WithSummary("Create a quiet hours calendar")
.WithDescription("Creates a new quiet hours calendar with schedules.");
group.MapPut("/calendars/{calendarId}", UpdateCalendarAsync)
.WithName("UpdateQuietHoursCalendar")
.WithSummary("Update a quiet hours calendar")
.WithDescription("Updates an existing quiet hours calendar.");
group.MapDelete("/calendars/{calendarId}", DeleteCalendarAsync)
.WithName("DeleteQuietHoursCalendar")
.WithSummary("Delete a quiet hours calendar")
.WithDescription("Deletes a quiet hours calendar.");
group.MapPost("/evaluate", EvaluateAsync)
.WithName("EvaluateQuietHours")
.WithSummary("Evaluate quiet hours")
.WithDescription("Checks if quiet hours are currently active for an event kind.");
return app;
}
private static async Task<IResult> ListCalendarsAsync(
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "X-Tenant-Id header is required." });
}
var calendars = await calendarService.ListCalendarsAsync(tenantId, cancellationToken);
return Results.Ok(calendars.Select(MapToApiResponse));
}
private static async Task<IResult> GetCalendarAsync(
string calendarId,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "X-Tenant-Id header is required." });
}
var calendar = await calendarService.GetCalendarAsync(tenantId, calendarId, cancellationToken);
if (calendar is null)
{
return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." });
}
return Results.Ok(MapToApiResponse(calendar));
}
private static async Task<IResult> CreateCalendarAsync(
[FromBody] QuietHoursCalendarApiRequest request,
[FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader,
[FromHeader(Name = "X-Actor")] string? actor,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
var tenantId = request.TenantId ?? tenantIdHeader;
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." });
}
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.BadRequest(new { error = "Calendar name is required." });
}
if (request.Schedules is null || request.Schedules.Count == 0)
{
return Results.BadRequest(new { error = "At least one schedule is required." });
}
var calendarId = request.CalendarId ?? Guid.NewGuid().ToString("N")[..16];
var calendar = new QuietHoursCalendar
{
CalendarId = calendarId,
TenantId = tenantId,
Name = request.Name,
Description = request.Description,
Enabled = request.Enabled ?? true,
Priority = request.Priority ?? 100,
Schedules = request.Schedules.Select(MapToScheduleEntry).ToList(),
ExcludedEventKinds = request.ExcludedEventKinds,
IncludedEventKinds = request.IncludedEventKinds
};
var created = await calendarService.UpsertCalendarAsync(calendar, actor, cancellationToken);
return Results.Created($"/api/v2/quiet-hours/calendars/{created.CalendarId}", MapToApiResponse(created));
}
private static async Task<IResult> UpdateCalendarAsync(
string calendarId,
[FromBody] QuietHoursCalendarApiRequest request,
[FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader,
[FromHeader(Name = "X-Actor")] string? actor,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
var tenantId = request.TenantId ?? tenantIdHeader;
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." });
}
var existing = await calendarService.GetCalendarAsync(tenantId, calendarId, cancellationToken);
if (existing is null)
{
return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." });
}
var calendar = new QuietHoursCalendar
{
CalendarId = calendarId,
TenantId = tenantId,
Name = request.Name ?? existing.Name,
Description = request.Description ?? existing.Description,
Enabled = request.Enabled ?? existing.Enabled,
Priority = request.Priority ?? existing.Priority,
Schedules = request.Schedules?.Select(MapToScheduleEntry).ToList() ?? existing.Schedules,
ExcludedEventKinds = request.ExcludedEventKinds ?? existing.ExcludedEventKinds,
IncludedEventKinds = request.IncludedEventKinds ?? existing.IncludedEventKinds,
CreatedAt = existing.CreatedAt,
CreatedBy = existing.CreatedBy
};
var updated = await calendarService.UpsertCalendarAsync(calendar, actor, cancellationToken);
return Results.Ok(MapToApiResponse(updated));
}
private static async Task<IResult> DeleteCalendarAsync(
string calendarId,
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
[FromHeader(Name = "X-Actor")] string? actor,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "X-Tenant-Id header is required." });
}
var deleted = await calendarService.DeleteCalendarAsync(tenantId, calendarId, actor, cancellationToken);
if (!deleted)
{
return Results.NotFound(new { error = $"Calendar '{calendarId}' not found." });
}
return Results.NoContent();
}
private static async Task<IResult> EvaluateAsync(
[FromBody] QuietHoursEvaluateApiRequest request,
[FromHeader(Name = "X-Tenant-Id")] string? tenantIdHeader,
[FromServices] IQuietHoursCalendarService calendarService,
CancellationToken cancellationToken)
{
var tenantId = request.TenantId ?? tenantIdHeader;
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "Tenant ID is required via X-Tenant-Id header or request body." });
}
if (string.IsNullOrWhiteSpace(request.EventKind))
{
return Results.BadRequest(new { error = "Event kind is required." });
}
var result = await calendarService.EvaluateAsync(
tenantId,
request.EventKind,
request.EvaluationTime,
cancellationToken);
return Results.Ok(new QuietHoursEvaluateApiResponse
{
IsActive = result.IsActive,
MatchedCalendarId = result.MatchedCalendarId,
MatchedCalendarName = result.MatchedCalendarName,
MatchedScheduleName = result.MatchedScheduleName,
EndsAt = result.EndsAt,
Reason = result.Reason
});
}
private static QuietHoursScheduleEntry MapToScheduleEntry(QuietHoursScheduleApiRequest request) => new()
{
Name = request.Name ?? "Unnamed Schedule",
StartTime = request.StartTime ?? "00:00",
EndTime = request.EndTime ?? "00:00",
DaysOfWeek = request.DaysOfWeek,
Timezone = request.Timezone,
Enabled = request.Enabled ?? true
};
private static QuietHoursCalendarApiResponse MapToApiResponse(QuietHoursCalendar calendar) => new()
{
CalendarId = calendar.CalendarId,
TenantId = calendar.TenantId,
Name = calendar.Name,
Description = calendar.Description,
Enabled = calendar.Enabled,
Priority = calendar.Priority,
Schedules = calendar.Schedules.Select(s => new QuietHoursScheduleApiResponse
{
Name = s.Name,
StartTime = s.StartTime,
EndTime = s.EndTime,
DaysOfWeek = s.DaysOfWeek?.ToList(),
Timezone = s.Timezone,
Enabled = s.Enabled
}).ToList(),
ExcludedEventKinds = calendar.ExcludedEventKinds?.ToList(),
IncludedEventKinds = calendar.IncludedEventKinds?.ToList(),
CreatedAt = calendar.CreatedAt,
CreatedBy = calendar.CreatedBy,
UpdatedAt = calendar.UpdatedAt,
UpdatedBy = calendar.UpdatedBy
};
}
#region API Request/Response Models
/// <summary>
/// Request to create or update a quiet hours calendar.
/// </summary>
public sealed class QuietHoursCalendarApiRequest
{
public string? CalendarId { get; set; }
public string? TenantId { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool? Enabled { get; set; }
public int? Priority { get; set; }
public List<QuietHoursScheduleApiRequest>? Schedules { get; set; }
public List<string>? ExcludedEventKinds { get; set; }
public List<string>? IncludedEventKinds { get; set; }
}
/// <summary>
/// Schedule entry in a quiet hours calendar request.
/// </summary>
public sealed class QuietHoursScheduleApiRequest
{
public string? Name { get; set; }
public string? StartTime { get; set; }
public string? EndTime { get; set; }
public List<int>? DaysOfWeek { get; set; }
public string? Timezone { get; set; }
public bool? Enabled { get; set; }
}
/// <summary>
/// Request to evaluate quiet hours.
/// </summary>
public sealed class QuietHoursEvaluateApiRequest
{
public string? TenantId { get; set; }
public string? EventKind { get; set; }
public DateTimeOffset? EvaluationTime { get; set; }
}
/// <summary>
/// Response for a quiet hours calendar.
/// </summary>
public sealed class QuietHoursCalendarApiResponse
{
public required string CalendarId { get; set; }
public required string TenantId { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public required bool Enabled { get; set; }
public required int Priority { get; set; }
public required List<QuietHoursScheduleApiResponse> Schedules { get; set; }
public List<string>? ExcludedEventKinds { get; set; }
public List<string>? IncludedEventKinds { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string? CreatedBy { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
}
/// <summary>
/// Schedule entry in a quiet hours calendar response.
/// </summary>
public sealed class QuietHoursScheduleApiResponse
{
public required string Name { get; set; }
public required string StartTime { get; set; }
public required string EndTime { get; set; }
public List<int>? DaysOfWeek { get; set; }
public string? Timezone { get; set; }
public required bool Enabled { get; set; }
}
/// <summary>
/// Response for quiet hours evaluation.
/// </summary>
public sealed class QuietHoursEvaluateApiResponse
{
public required bool IsActive { get; set; }
public string? MatchedCalendarId { get; set; }
public string? MatchedCalendarName { get; set; }
public string? MatchedScheduleName { get; set; }
public DateTimeOffset? EndsAt { get; set; }
public string? Reason { get; set; }
}
#endregion