namespace KArtSell.Modules.ModelOperations.PortfolioReconciliation; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FastEndpoints; using KArtSell.BuildingBlocks.Time; /// /// GET /reconciliation/holdings - Returns current portfolio holdings /// public class GetHoldingsEndpoint : EndpointWithoutRequest { private readonly IReconciliationRepository _repository; public GetHoldingsEndpoint(IReconciliationRepository repository) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } public override void Configure() { Get("/reconciliation/holdings"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { var holdings = await _repository.GetOpenHoldingsAsync(); var response = new GetHoldingsResponse { Items = holdings.ConvertAll(h => new HoldingDto { Id = h.Id, SecurityId = h.SecurityId, Quantity = h.Quantity, WeightedAvgCost = h.WeightedAvgCost, TotalCostBasis = h.TotalCostBasis, MarketValue = h.MarketValue, UnrealizedGainLoss = h.UnrealizedGainLoss, UpdatedAt = h.UpdatedAt, CorrelationId = h.CorrelationId }), Total = holdings.Count, Pages = 1 }; await Send.OkAsync(response, ct); } } public class GetHoldingsResponse { public List Items { get; set; } = new(); public int Total { get; set; } public int Pages { get; set; } } public class HoldingDto { public Guid Id { get; set; } public Guid SecurityId { get; set; } public int Quantity { get; set; } public decimal WeightedAvgCost { get; set; } public decimal TotalCostBasis { get; set; } public decimal? MarketValue { get; set; } public decimal? UnrealizedGainLoss { get; set; } public DateTime UpdatedAt { get; set; } public Guid CorrelationId { get; set; } } /// /// GET /reconciliation/mismatches - Returns flagged discrepancies /// public class GetMismatchesEndpoint : EndpointWithoutRequest { private readonly IReconciliationRepository _repository; private readonly IClock _clock; public GetMismatchesEndpoint(IReconciliationRepository repository, IClock clock) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); } public override void Configure() { Get("/reconciliation/mismatches"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { var now = _clock.UtcNow.UtcDateTime; var dateFrom = HttpContext.Request.Query.TryGetValue("dateFrom", out var fromVal) ? DateTime.Parse(fromVal.ToString()) : now.AddDays(-30); var dateTo = HttpContext.Request.Query.TryGetValue("dateTo", out var toVal) ? DateTime.Parse(toVal.ToString()) : now; var logs = await _repository.GetReconciliationLogsAsync(dateFrom, dateTo); var mismatches = logs.Where(l => l.MismatchDetected).ToList(); var response = new GetMismatchesResponse { Items = mismatches.ConvertAll(m => new MismatchDto { Id = m.Id, TradeId = m.TradeId, HoldingId = m.HoldingId, MismatchReason = m.MismatchReason, QuantityBefore = m.QuantityBefore, QuantityAfter = m.QuantityAfter, CostBasisDelta = m.CostBasisDelta, DetectedAt = m.ReconciledAt }), Total = mismatches.Count, Pages = 1 }; await Send.OkAsync(response, ct); } } public class GetMismatchesResponse { public List Items { get; set; } = new(); public int Total { get; set; } public int Pages { get; set; } } public class MismatchDto { public Guid Id { get; set; } public Guid TradeId { get; set; } public Guid HoldingId { get; set; } public string? MismatchReason { get; set; } public int QuantityBefore { get; set; } public int QuantityAfter { get; set; } public decimal CostBasisDelta { get; set; } public DateTime DetectedAt { get; set; } } /// /// POST /reconciliation/reconcile-trade - Trigger trade reconciliation /// public class ReconcileTradeEndpoint : Endpoint { private readonly ReconcileTradeHandler _handler; public ReconcileTradeEndpoint(ReconcileTradeHandler handler) { _handler = handler ?? throw new ArgumentNullException(nameof(handler)); } public override void Configure() { Post("/reconciliation/reconcile-trade"); AllowAnonymous(); } public override async Task HandleAsync(ReconcileTradeRequest request, CancellationToken ct) { var command = new ReconcileTradeCommand { TradeId = request.TradeId, SecurityId = request.SecurityId, ExecutedQuantity = request.ExecutedQuantity, ExecutedPrice = request.ExecutedPrice, ApprovedQuantity = request.ApprovedQuantity, ApprovedPrice = request.ApprovedPrice, TradeDate = request.TradeDate, ExpectedSettlementDate = request.ExpectedSettlementDate, ActualSettlementDate = request.ActualSettlementDate, CorrelationId = request.CorrelationId, IdempotencyKey = request.IdempotencyKey }; await _handler.HandleAsync(command); await Send.NoContentAsync(ct); } } public class ReconcileTradeRequest { public Guid TradeId { get; set; } public Guid SecurityId { get; set; } public int ExecutedQuantity { get; set; } public decimal ExecutedPrice { get; set; } public int ApprovedQuantity { get; set; } public decimal ApprovedPrice { get; set; } public DateTime TradeDate { get; set; } public DateTime ExpectedSettlementDate { get; set; } public DateTime? ActualSettlementDate { get; set; } public Guid CorrelationId { get; set; } public string? IdempotencyKey { get; set; } } /// /// GET /reconciliation/report/daily - Returns daily reconciliation report /// public class GetDailyReportEndpoint : EndpointWithoutRequest { private readonly ReconciliationEngine _engine; private readonly IClock _clock; public GetDailyReportEndpoint(ReconciliationEngine engine, IClock clock) { _engine = engine ?? throw new ArgumentNullException(nameof(engine)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); } public override void Configure() { Get("/reconciliation/report/daily"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { var reportDate = _clock.UtcNow.UtcDateTime; var report = await _engine.GenerateDailyReportAsync(reportDate, Guid.NewGuid()); var response = new ReconciliationReportDto { ReportDate = report.ReportDate, TotalLogsProcessed = report.TotalLogsProcessed, TotalMismatches = report.TotalMismatches, MismatchesByHighSeverity = report.MismatchesByHighSeverity, MismatchesByMediumSeverity = report.MismatchesByMediumSeverity, MismatchPercentage = report.MismatchPercentage, GeneratedAt = report.GeneratedAt }; await Send.OkAsync(response, ct); } } public class ReconciliationReportDto { public DateTime ReportDate { get; set; } public int TotalLogsProcessed { get; set; } public int TotalMismatches { get; set; } public int MismatchesByHighSeverity { get; set; } public int MismatchesByMediumSeverity { get; set; } public double MismatchPercentage { get; set; } public DateTime GeneratedAt { get; set; } }