namespace KArtSell.Modules.ModelOperations.Features.ApprovalWorkflow; using KArtSell.Modules.ModelOperations.Domain.ApprovalWorkflow; public class CreateApprovalProposalHandler { private readonly ApprovalWorkflowSql _sql; public CreateApprovalProposalHandler(ApprovalWorkflowSql sql) => _sql = sql; public async Task Handle(string userEmail, string userRole, Guid modelId, DateOnly effectiveAt, string justification, Guid correlationId, CancellationToken ct = default) { if (!ApprovalWorkflowPolicy.CanCreateProposal(userEmail, userRole)) throw new UnauthorizedAccessException("Only Maker role can create proposals"); var proposal = new ApprovalProposal { Id = Guid.NewGuid(), ModelId = modelId, Status = ApprovalStatus.Draft, CreatedBy = userEmail, CreatedAt = DateTime.UtcNow, Justification = justification, EffectiveAt = effectiveAt, PublishedAt = DateTime.UtcNow, Revision = 1, CorrelationId = correlationId }; var proposalId = await _sql.InsertProposalAsync(proposal, ct); var createEvent = ApprovalWorkflowPolicy.CreateStateChangeEvent(proposalId, ApprovalStatus.Draft, userEmail, correlationId); await _sql.InsertEventAsync(createEvent, ct); return proposalId; } } public class ApproveApprovalHandler { private readonly ApprovalWorkflowSql _sql; public ApproveApprovalHandler(ApprovalWorkflowSql sql) => _sql = sql; public async Task Handle(Guid proposalId, string userEmail, string userRole, string approvalNotes, List<(string Type, string Url, string? Comment)> evidence, Guid correlationId, CancellationToken ct = default) { var proposal = await _sql.GetProposalAsync(proposalId, ct) ?? throw new KeyNotFoundException($"Proposal {proposalId} not found"); if (!ApprovalWorkflowPolicy.CanApprove(proposal, userEmail, userRole)) throw new UnauthorizedAccessException("Only Checker role (different from Maker) can approve proposals"); ApprovalWorkflowPolicy.ValidateProposalState(proposal.Status, ApprovalStatus.Approved); await _sql.UpdateProposalStatusAsync(proposalId, ApprovalStatus.Approved, userEmail, approvalNotes, ct); foreach (var (type, url, comment) in evidence) { var evt = new ApprovalEvidence { Id = Guid.NewGuid(), ApprovalProposalId = proposalId, EvidenceType = type, EvidenceUrl = url, ReviewerComment = comment, PublishedAt = DateTime.UtcNow, CorrelationId = correlationId }; await _sql.InsertEvidenceAsync(evt, ct); } var approvalEvent = ApprovalWorkflowPolicy.CreateStateChangeEvent(proposalId, ApprovalStatus.Approved, userEmail, correlationId, new Dictionary { { "notes", approvalNotes } }); await _sql.InsertEventAsync(approvalEvent, ct); } } public class ActivateModelHandler { private readonly ApprovalWorkflowSql _sql; public ActivateModelHandler(ApprovalWorkflowSql sql) => _sql = sql; public async Task Handle(Guid proposalId, string userEmail, string userRole, Guid correlationId, CancellationToken ct = default) { var proposal = await _sql.GetProposalAsync(proposalId, ct) ?? throw new KeyNotFoundException($"Proposal {proposalId} not found"); if (!ApprovalWorkflowPolicy.CanActivate(proposal, userEmail, userRole)) throw new UnauthorizedAccessException("Only SRE role can activate approved proposals"); ApprovalWorkflowPolicy.ValidateProposalState(proposal.Status, ApprovalStatus.Active); await _sql.UpdateProposalStatusAsync(proposalId, ApprovalStatus.Active, userEmail, "Model activated by SRE", ct); var activateEvent = ApprovalWorkflowPolicy.CreateStateChangeEvent(proposalId, ApprovalStatus.Active, userEmail, correlationId, new Dictionary { { "effectiveAt", proposal.EffectiveAt.ToString("O") } }); await _sql.InsertEventAsync(activateEvent, ct); } }