namespace KArtSell.Modules.ModelOperations.Features.ApprovalWorkflow; using FastEndpoints; using KArtSell.BuildingBlocks.Time; using KArtSell.Modules.ModelOperations.Domain.ApprovalWorkflow; using Microsoft.AspNetCore.Http; public record CreateApprovalRequest(Guid ModelId, DateOnly EffectiveAt, string Justification); public record CreateApprovalResponse(Guid Id, string Status, DateTime CreatedAt); public class CreateApprovalEndpoint : EndpointWithoutRequest { private readonly CreateApprovalProposalHandler _handler; private readonly ApprovalWorkflowSql _sql; public CreateApprovalEndpoint(CreateApprovalProposalHandler handler, ApprovalWorkflowSql sql) { _handler = handler; _sql = sql; } public override void Configure() { Post("/approvals"); Roles("Maker"); } public override async Task HandleAsync(CancellationToken ct) { var request = await HttpContext.Request.ReadFromJsonAsync(cancellationToken: ct); var userEmail = HttpContext.User.FindFirst("email")?.Value ?? "anonymous"; var userRole = HttpContext.User.FindFirst("role")?.Value ?? "Guest"; var proposalId = await _handler.Handle(userEmail, userRole, request!.ModelId, request.EffectiveAt, request.Justification, Guid.NewGuid(), ct); var proposal = await _sql.GetProposalAsync(proposalId, ct); await Send.CreatedAtAsync(new { id = proposalId }, new CreateApprovalResponse(proposalId, "DRAFT", proposal!.CreatedAt), cancellation: ct); } } public record GetApprovalsRequest(string? Status, Guid? ModelId, int Limit = 50, int Offset = 0); public record ApprovalDto(Guid Id, Guid ModelId, string Status, string CreatedBy, DateTime CreatedAt, string Justification); public record GetApprovalsResponse(List Items, int Total, int Pages); public class GetApprovalsEndpoint : Endpoint { private readonly ApprovalWorkflowSql _sql; public GetApprovalsEndpoint(ApprovalWorkflowSql sql) => _sql = sql; public override void Configure() { Get("/approvals"); Roles("Maker", "Checker", "SRE"); } public override async Task HandleAsync(GetApprovalsRequest req, CancellationToken ct) { ApprovalStatus? status = req.Status != null ? Enum.Parse(req.Status, ignoreCase: true) : null; var proposals = await _sql.ListProposalsAsync(status, req.ModelId, req.Limit, req.Offset, ct); var items = proposals.Select(p => new ApprovalDto(p.Id, p.ModelId, p.Status.ToString(), p.CreatedBy, p.CreatedAt, p.Justification)).ToList(); await Send.OkAsync(new GetApprovalsResponse(items, items.Count, (items.Count + req.Limit - 1) / req.Limit), ct); } } public record ProposeForReviewResponse(Guid Id, string Status); public class ProposeForReviewEndpoint : EndpointWithoutRequest { private readonly ProposeForReviewHandler _handler; public ProposeForReviewEndpoint(ProposeForReviewHandler handler) => _handler = handler; public override void Configure() { Post("/approvals/{id}/propose"); Roles("Maker"); } public override async Task HandleAsync(CancellationToken ct) { var proposalId = Route("id"); var userEmail = HttpContext.User.FindFirst("email")?.Value ?? "anonymous"; await _handler.Handle(proposalId, userEmail, Guid.NewGuid(), ct); await Send.OkAsync(new ProposeForReviewResponse(proposalId, "PROPOSED"), ct); } } public record ActivateApprovalResponse(Guid Id, string Status, DateTime? ActivatedAt); /// /// DEBT-028: before this endpoint existed, ActivateModelHandler was registered in DI and fully /// implemented but had no HTTP entry point — an Approved proposal could never reach Active, the /// final step of the maker-checker gate this whole slice exists for. /// public class ActivateApprovalEndpoint : EndpointWithoutRequest { private readonly ActivateModelHandler _handler; private readonly ApprovalWorkflowSql _sql; public ActivateApprovalEndpoint(ActivateModelHandler handler, ApprovalWorkflowSql sql) { _handler = handler; _sql = sql; } public override void Configure() { Post("/approvals/{id}/activate"); Roles("SRE"); } public override async Task HandleAsync(CancellationToken ct) { var proposalId = Route("id"); var userEmail = HttpContext.User.FindFirst("email")?.Value ?? "anonymous"; var userRole = HttpContext.User.FindFirst("role")?.Value ?? "Guest"; await _handler.Handle(proposalId, userEmail, userRole, Guid.NewGuid(), ct); var proposal = await _sql.GetProposalAsync(proposalId, ct); await Send.OkAsync(new ActivateApprovalResponse(proposalId, "ACTIVE", proposal!.ActivatedAt), ct); } } public record ApprovalDetailResponse(Guid Id, Guid ModelId, string Status, string CreatedBy, DateTime CreatedAt, string Justification, DateOnly EffectiveAt, string? ApprovedBy, DateTime? ApprovedAt, string? ApprovalNotes, string? ActivatedBy, DateTime? ActivatedAt, List Evidence); public class GetApprovalByIdEndpoint : EndpointWithoutRequest { private readonly ApprovalWorkflowSql _sql; public GetApprovalByIdEndpoint(ApprovalWorkflowSql sql) => _sql = sql; public override void Configure() { Get("/approvals/{id}"); Roles("Maker", "Checker", "SRE"); } public override async Task HandleAsync(CancellationToken ct) { var proposalId = Route("id"); var proposal = await _sql.GetProposalAsync(proposalId, ct); if (proposal is null) { await Send.NotFoundAsync(ct); return; } var evidence = await _sql.GetEvidenceForProposalAsync(proposalId, ct); var evidenceDtos = evidence.Select(e => new EvidenceDto(e.EvidenceType, e.EvidenceUrl, e.ReviewerComment)).ToList(); await Send.OkAsync(new ApprovalDetailResponse( proposal.Id, proposal.ModelId, proposal.Status.ToString(), proposal.CreatedBy, proposal.CreatedAt, proposal.Justification, proposal.EffectiveAt, proposal.ApprovedBy, proposal.ApprovedAt, proposal.ApprovalNotes, proposal.ActivatedBy, proposal.ActivatedAt, evidenceDtos), ct); } } public record ApproveApprovalRequest(string ApprovalNotes, List Evidence); public record EvidenceDto(string Type, string Url, string? Comment); public record ApproveApprovalResponse(Guid Id, string Status, DateTime ApprovedAt); public class ApproveApprovalEndpoint : Endpoint { private readonly ApproveApprovalHandler _handler; private readonly ApprovalWorkflowSql _sql; private readonly IClock _clock; public ApproveApprovalEndpoint(ApproveApprovalHandler handler, ApprovalWorkflowSql sql, IClock clock) { _handler = handler; _sql = sql; _clock = clock; } public override void Configure() { Post("/approvals/{id}/approve"); Roles("Checker"); } public override async Task HandleAsync(ApproveApprovalRequest req, CancellationToken ct) { var proposalId = Route("id"); var userEmail = HttpContext.User.FindFirst("email")?.Value ?? "anonymous"; var userRole = HttpContext.User.FindFirst("role")?.Value ?? "Guest"; var evidence = req.Evidence.Select(e => (e.Type, e.Url, e.Comment)).ToList(); await _handler.Handle(proposalId, userEmail, userRole, req.ApprovalNotes, evidence, Guid.NewGuid(), ct); var proposal = await _sql.GetProposalAsync(proposalId, ct); await Send.OkAsync(new ApproveApprovalResponse(proposalId, "APPROVED", proposal!.ApprovedAt ?? _clock.UtcNow.UtcDateTime), ct); } }