namespace KArtSell.Modules.ModelOperations.ApprovalWorkflow; using System; using System.Collections.Generic; using System.Linq; public class ApprovalPolicy { private readonly IClock _clock; public ApprovalPolicy(IClock clock) { _clock = clock; } public bool CanCreateProposal(string userEmail, string? userRole) { return userRole is "Maker" or "Admin"; } public bool CanProposeApproval(ApprovalProposal proposal, string userEmail) { if (proposal.Status != ApprovalStatus.Draft) return false; return proposal.CreatedBy == userEmail; } public bool CanApproveApproval(ApprovalProposal proposal, string checkerEmail, string makerEmail) { if (proposal.Status != ApprovalStatus.Proposed) return false; if (checkerEmail == makerEmail) return false; // Separation of duties: Maker cannot approve own proposal return true; } public bool CanActivateApproval(ApprovalProposal proposal, string userRole) { if (proposal.Status != ApprovalStatus.Approved) return false; return userRole is "SRE" or "Admin"; } public ApprovalProposal CreateProposal( Guid modelId, string createdBy, string justification, DateOnly effectiveAt) { return new ApprovalProposal { Id = Guid.NewGuid(), ModelId = modelId, Status = ApprovalStatus.Draft, CreatedBy = createdBy, CreatedAt = _clock.Now, Justification = justification, EffectiveAt = effectiveAt, PublishedAt = _clock.Now, Revision = 1, CorrelationId = Guid.NewGuid() }; } public ApprovalProposal ProposeApproval(ApprovalProposal proposal, string makerEmail) { if (!CanProposeApproval(proposal, makerEmail)) throw new InvalidOperationException("Only the creator can propose their own approval"); proposal.Status = ApprovalStatus.Proposed; proposal.ProposedAt = _clock.Now; proposal.Revision++; proposal.PublishedAt = _clock.Now; return proposal; } public ApprovalProposal ApproveApproval( ApprovalProposal proposal, string checkerEmail, string approvalNotes, List evidence) { if (!CanApproveApproval(proposal, checkerEmail, proposal.CreatedBy)) throw new InvalidOperationException("Checker cannot approve their own proposals"); proposal.Status = ApprovalStatus.Approved; proposal.ApprovedBy = checkerEmail; proposal.ApprovedAt = _clock.Now; proposal.ApprovalNotes = approvalNotes; proposal.Revision++; proposal.PublishedAt = _clock.Now; // Add evidence foreach (var evt in evidence) { proposal.Evidence.Add(new ApprovalEvidence { Id = Guid.NewGuid(), ApprovalProposalId = proposal.Id, EvidenceType = evt.Type, EvidenceUrl = evt.Url, ReviewerComment = evt.Comment, PublishedAt = _clock.Now, CorrelationId = proposal.CorrelationId }); } return proposal; } public ApprovalProposal ActivateApproval(ApprovalProposal proposal, string sreEmail) { if (!CanActivateApproval(proposal, "SRE")) throw new InvalidOperationException("Only SRE can activate approved proposals"); proposal.Status = ApprovalStatus.Active; proposal.ActivatedBy = sreEmail; proposal.ActivatedAt = _clock.Now; proposal.Revision++; proposal.PublishedAt = _clock.Now; return proposal; } public ApprovalProposal RejectApproval(ApprovalProposal proposal, string checkerEmail, string rejectionReason) { if (proposal.Status != ApprovalStatus.Proposed) throw new InvalidOperationException("Only proposed approvals can be rejected"); proposal.Status = ApprovalStatus.Rejected; proposal.ApprovalNotes = $"Rejected: {rejectionReason}"; proposal.Revision++; proposal.PublishedAt = _clock.Now; return proposal; } public ApprovalEvent CreateProposalEvent( ApprovalProposal proposal, string eventType, string actorEmail, Dictionary? details = null) { return new ApprovalEvent { Id = Guid.NewGuid(), ApprovalProposalId = proposal.Id, EventType = eventType, ActorEmail = actorEmail, EventAt = _clock.Now, Details = details, PublishedAt = _clock.Now, CorrelationId = proposal.CorrelationId }; } } public interface IClock { DateTimeOffset Now { get; } } public class SystemClock : IClock { public DateTimeOffset Now => DateTimeOffset.UtcNow; }