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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
namespace KArtSell.Modules.ModelOperations.Features.ApprovalWorkflow;
|
||||
|
||||
using FastEndpoints;
|
||||
using KArtSell.Modules.ModelOperations.Domain.ApprovalWorkflow;
|
||||
|
||||
public record CreateApprovalRequest(Guid ModelId, DateOnly EffectiveAt, string Justification);
|
||||
public record CreateApprovalResponse(Guid Id, string Status, DateTime CreatedAt);
|
||||
|
||||
public class CreateApprovalEndpoint : EndpointWithoutRequests<CreateApprovalResponse>
|
||||
{
|
||||
private readonly CreateApprovalProposalHandler _handler;
|
||||
private readonly ApprovalWorkflowSql _sql;
|
||||
|
||||
public CreateApprovalEndpoint(CreateApprovalProposalHandler handler, ApprovalWorkflowSql sql)
|
||||
{
|
||||
_handler = handler;
|
||||
_sql = sql;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/approvals");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var request = await HttpContext.Request.ReadFromJsonAsync<CreateApprovalRequest>(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 SendCreatedAtAsync<CreateApprovalEndpoint>(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<ApprovalDto> Items, int Total, int Pages);
|
||||
|
||||
public class GetApprovalsEndpoint : Endpoint<GetApprovalsRequest, GetApprovalsResponse>
|
||||
{
|
||||
private readonly ApprovalWorkflowSql _sql;
|
||||
|
||||
public GetApprovalsEndpoint(ApprovalWorkflowSql sql) => _sql = sql;
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/approvals");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetApprovalsRequest req, CancellationToken ct)
|
||||
{
|
||||
var status = req.Status != null ? Enum.Parse<ApprovalStatus>(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 SendAsync(new GetApprovalsResponse(items, items.Count, (items.Count + req.Limit - 1) / req.Limit), cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record ApproveApprovalRequest(string ApprovalNotes, List<EvidenceDto> Evidence);
|
||||
public record EvidenceDto(string Type, string Url, string? Comment);
|
||||
public record ApproveApprovalResponse(Guid Id, string Status, DateTime ApprovedAt);
|
||||
|
||||
public class ApproveApprovalEndpoint : Endpoint<ApproveApprovalRequest, ApproveApprovalResponse>
|
||||
{
|
||||
private readonly ApproveApprovalHandler _handler;
|
||||
private readonly ApprovalWorkflowSql _sql;
|
||||
|
||||
public ApproveApprovalEndpoint(ApproveApprovalHandler handler, ApprovalWorkflowSql sql)
|
||||
{
|
||||
_handler = handler;
|
||||
_sql = sql;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/approvals/{id}/approve");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ApproveApprovalRequest req, CancellationToken ct)
|
||||
{
|
||||
var proposalId = Route<Guid>("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 SendAsync(new ApproveApprovalResponse(proposalId, "APPROVED", proposal!.ApprovedAt ?? DateTime.UtcNow), cancellation: ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user