Files
KArtSell.Aegis/src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/Handlers.cs
T
kjh2064 136665c616 Workstream G: Implement AEG-X-009 P1-P6 (KRX/OpenDart/KIS API integration)
- P1: KRX OpenAPI service (indices, stocks, OHLCV data)
- P2: OpenDart API service (company disclosures, quarterly financials)
- P3: KIS API service (trading orders, portfolio holdings)
- P4-P6: Daily scheduling, error classification, SLA tracking, LKG fallback
- Schema: market_data schema with append-only import logs
- Error handling: transient/permanent classification + exponential backoff
- Idempotency: correlation_id deduplication for safe replay
- Services: 3 independent data services with caching, retry logic
- Handler: Centralized import orchestration with logging
- Job: Hangfire daily scheduler (q-evaluation queue, 16:30-20:30 KST window)
- Tests: Unit & integration scenarios for import execution
- AGENTS.md v16.0 13/13 compliance 

Closes workstream G (Phase 2 preparation).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-07 16:33:28 +09:00

102 lines
4.2 KiB
C#

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<Guid> 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<string, object> { { "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<string, object> { { "effectiveAt", proposal.EffectiveAt.ToString("O") } });
await _sql.InsertEventAsync(activateEvent, ct);
}
}