Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
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
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
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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
|
||||
Reference in New Issue
Block a user