namespace KArtSell.Modules.ModelOperations.Features.ApprovalWorkflow; using Dapper; using KArtSell.Modules.ModelOperations.Domain.ApprovalWorkflow; using Npgsql; public class ApprovalWorkflowSql { private readonly string _connectionString; public ApprovalWorkflowSql(string connectionString) => _connectionString = connectionString; public async Task GetProposalAsync(Guid proposalId, CancellationToken ct = default) { const string sql = """ SELECT id, model_id, status, created_by, created_at, justification, effective_at, proposed_at, approved_by, approved_at, approval_notes, activated_by, activated_at, published_at, revision, correlation_id FROM model_operations.approval_proposals WHERE id = @proposalId """; using var conn = new NpgsqlConnection(_connectionString); return await conn.QueryFirstOrDefaultAsync(sql, new { proposalId }); } public async Task> ListProposalsAsync(ApprovalStatus? status = null, Guid? modelId = null, int limit = 50, int offset = 0, CancellationToken ct = default) { const string sql = """ SELECT id, model_id, status, created_by, created_at, justification, effective_at, proposed_at, approved_by, approved_at, approval_notes, activated_by, activated_at, published_at, revision, correlation_id FROM model_operations.approval_proposals WHERE (CAST(@status AS VARCHAR) IS NULL OR status = CAST(@status AS VARCHAR)) AND (@modelId::UUID IS NULL OR model_id = @modelId) ORDER BY created_at DESC LIMIT @limit OFFSET @offset """; using var conn = new NpgsqlConnection(_connectionString); var proposals = await conn.QueryAsync(sql, new { status = status?.ToString().ToUpper(), modelId, limit, offset }); return proposals.ToList(); } public async Task InsertProposalAsync(ApprovalProposal proposal, CancellationToken ct = default) { const string sql = """ INSERT INTO model_operations.approval_proposals (id, model_id, status, created_by, created_at, justification, effective_at, published_at, revision, correlation_id) VALUES (@id, @modelId, @status, @createdBy, @createdAt, @justification, @effectiveAt, @publishedAt, @revision, @correlationId) RETURNING id """; using var conn = new NpgsqlConnection(_connectionString); return await conn.QuerySingleAsync(sql, new { proposal.Id, proposal.ModelId, status = proposal.Status.ToString().ToUpper(), proposal.CreatedBy, proposal.CreatedAt, proposal.Justification, proposal.EffectiveAt, proposal.PublishedAt, proposal.Revision, proposal.CorrelationId }); } public async Task UpdateProposalStatusAsync(Guid proposalId, ApprovalStatus newStatus, string? approvedBy = null, string? approvalNotes = null, CancellationToken ct = default) { const string sql = """ UPDATE model_operations.approval_proposals SET status = @status, approved_by = @approvedBy, approved_at = CASE WHEN @approvedBy IS NOT NULL THEN NOW() ELSE approved_at END, approval_notes = @approvalNotes, published_at = NOW(), revision = revision + 1 WHERE id = @proposalId """; using var conn = new NpgsqlConnection(_connectionString); await conn.ExecuteAsync(sql, new { proposalId, status = newStatus.ToString().ToUpper(), approvedBy, approvalNotes }); } public async Task InsertEvidenceAsync(ApprovalEvidence evidence, CancellationToken ct = default) { const string sql = """ INSERT INTO model_operations.approval_evidence (id, approval_proposal_id, evidence_type, evidence_url, reviewer_comment, published_at, correlation_id) VALUES (@id, @proposalId, @type, @url, @comment, @publishedAt, @correlationId) RETURNING id """; using var conn = new NpgsqlConnection(_connectionString); return await conn.QuerySingleAsync(sql, new { evidence.Id, proposalId = evidence.ApprovalProposalId, type = evidence.EvidenceType, url = evidence.EvidenceUrl, comment = evidence.ReviewerComment, evidence.PublishedAt, evidence.CorrelationId }); } public async Task InsertEventAsync(ApprovalEvent evt, CancellationToken ct = default) { const string sql = """ INSERT INTO model_operations.approval_events (id, approval_proposal_id, event_type, actor_email, event_at, details, published_at, correlation_id) VALUES (@id, @proposalId, @type, @email, @at, @details::JSONB, @publishedAt, @correlationId) RETURNING id """; using var conn = new NpgsqlConnection(_connectionString); return await conn.QuerySingleAsync(sql, new { evt.Id, proposalId = evt.ApprovalProposalId, type = evt.EventType, email = evt.ActorEmail, at = evt.EventAt, details = System.Text.Json.JsonSerializer.Serialize(evt.Details ?? new()), evt.PublishedAt, evt.CorrelationId }); } }