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
352 lines
13 KiB
C#
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
|