using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; using System; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Platform.WebService.Endpoints; public static class ReleaseReadModelEndpoints { public static IEndpointRouteBuilder MapReleaseReadModelEndpoints(this IEndpointRouteBuilder app) { var releases = app.MapGroup("/api/v2/releases") .WithTags("Releases V2") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); releases.MapGet(string.Empty, async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, [AsParameters] ReleaseListQuery query, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var page = await service.ListReleasesAsync( requestContext!, query.Region, query.Environment, query.Type, query.Status, query.Limit, query.Offset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, page.Items, page.Total, page.Limit, page.Offset)); }) .WithName("ListReleasesV2") .WithSummary("List Pack-22 release projections") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); releases.MapGet("/{releaseId:guid}", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid releaseId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var detail = await service.GetReleaseDetailAsync( requestContext!, releaseId, cancellationToken).ConfigureAwait(false); if (detail is null) { return Results.NotFound(new { error = "release_not_found", releaseId }); } return Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, detail)); }) .WithName("GetReleaseDetailV2") .WithSummary("Get Pack-22 release detail projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); releases.MapGet("/activity", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, [AsParameters] ReleaseActivityQuery query, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var page = await service.ListActivityAsync( requestContext!, query.Region, query.Environment, query.Limit, query.Offset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, page.Items, page.Total, page.Limit, page.Offset)); }) .WithName("ListReleaseActivityV2") .WithSummary("List cross-release activity timeline") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); releases.MapGet("/approvals", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, [AsParameters] ReleaseApprovalsQuery query, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var page = await service.ListApprovalsAsync( requestContext!, query.Status, query.Region, query.Environment, query.Limit, query.Offset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, page.Items, page.Total, page.Limit, page.Offset)); }) .WithName("ListReleaseApprovalsV2") .WithSummary("List cross-release approvals queue projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); var runs = releases.MapGroup("/runs"); runs.MapGet(string.Empty, async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, [AsParameters] ReleaseRunListQuery query, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var page = await service.ListRunsAsync( requestContext!, query.Status, query.Lane, query.Environment, query.Region, query.Outcome, query.NeedsApproval, query.BlockedByDataIntegrity, query.Limit, query.Offset, cancellationToken).ConfigureAwait(false); return Results.Ok(new PlatformListResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, page.Items, page.Total, page.Limit, page.Offset)); }) .WithName("ListReleaseRunsV2") .WithSummary("List run-centric release projections for Pack-22 contracts") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunDetailV2") .WithSummary("Get canonical release run detail projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/timeline", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunTimelineV2") .WithSummary("Get release run timeline projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/gate-decision", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunGateDecisionV2") .WithSummary("Get release run gate decision projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/approvals", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunApprovalsV2") .WithSummary("Get release run approvals checkpoints projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/deployments", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunDeploymentsV2") .WithSummary("Get release run deployments projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/security-inputs", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunSecurityInputsV2") .WithSummary("Get release run security inputs projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/evidence", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunEvidenceV2") .WithSummary("Get release run evidence capsule projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/rollback", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunRollbackV2") .WithSummary("Get release run rollback projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/replay", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunReplayV2") .WithSummary("Get release run replay projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); runs.MapGet("/{runId:guid}/audit", async Task( HttpContext context, PlatformRequestContextResolver resolver, ReleaseReadModelService service, TimeProvider timeProvider, Guid runId, CancellationToken cancellationToken) => { if (!TryResolveContext(context, resolver, out var requestContext, out var failure)) { return failure!; } var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false); return item is null ? Results.NotFound(new { error = "run_not_found", runId }) : Results.Ok(new PlatformItemResponse( requestContext!.TenantId, requestContext.ActorId, timeProvider.GetUtcNow(), Cached: false, CacheTtlSeconds: 0, item)); }) .WithName("GetReleaseRunAuditV2") .WithSummary("Get release run audit projection") .RequireAuthorization(PlatformPolicies.ReleaseControlRead); return app; } private static bool TryResolveContext( HttpContext context, PlatformRequestContextResolver resolver, out PlatformRequestContext? requestContext, out IResult? failure) { if (resolver.TryResolve(context, out requestContext, out var error)) { failure = null; return true; } failure = Results.BadRequest(new { error = error ?? "tenant_missing" }); return false; } public sealed record ReleaseListQuery( string? Region, string? Environment, string? Type, string? Status, int? Limit, int? Offset); public sealed record ReleaseActivityQuery( string? Region, string? Environment, int? Limit, int? Offset); public sealed record ReleaseApprovalsQuery( string? Status, string? Region, string? Environment, int? Limit, int? Offset); public sealed record ReleaseRunListQuery( string? Status, string? Lane, string? Environment, string? Region, string? Outcome, bool? NeedsApproval, bool? BlockedByDataIntegrity, int? Limit, int? Offset); }