using Marathon.Application.UseCases; using Marathon.Domain.AnomalyDetection; using DomainEventId = Marathon.Domain.ValueObjects.EventId; namespace Marathon.UI.Services; /// /// Page-facing implementation of . Runs /// the application use case and reshapes its output for the page — event title /// strings and severity buckets are computed once from the report's payload, so /// the service performs no repository I/O of its own. /// public sealed class AnomalyInsightsService : IAnomalyInsightsService { private readonly EvaluateAnomalyOutcomesUseCase _useCase; public AnomalyInsightsService(EvaluateAnomalyOutcomesUseCase useCase) { _useCase = useCase ?? throw new ArgumentNullException(nameof(useCase)); } public async Task GetReportAsync(CancellationToken ct) { var report = await _useCase.ExecuteAsync(ct).ConfigureAwait(false); var resolvedRows = report.Resolved .Select(r => ToRow(r, report.EventTitles)) .ToList(); var unresolvedRows = report.Unresolved .Select(r => ToRow(r, report.EventTitles)) .ToList(); return new AnomalyInsightsVm( TotalAnomalies: report.TotalAnomalies, ResolvedCount: report.ResolvedCount, UnresolvedCount: report.UnresolvedCount, HitCount: report.HitCount, MissCount: report.MissCount, HitRate: report.HitRate, BySeverity: report.BySeverity, BySport: report.BySport, ByScoreBin: report.ByScoreBin, ByKind: report.ByKind, Resolved: resolvedRows, Unresolved: unresolvedRows); } private static ResolvedAnomalyRow ToRow( ResolvedAnomaly src, IReadOnlyDictionary titles) => new( AnomalyId: src.AnomalyId, EventId: src.EventId, EventTitle: titles.TryGetValue(src.EventId, out var t) ? t : src.EventId.Value, DetectedAt: src.DetectedAt, Score: src.Score, Severity: AnomalySeverityRules.FromScore(src.Score), Sport: src.Sport, PreFlipFavourite: src.PreFlipFavourite, PostFlipFavourite: src.PostFlipFavourite, ActualWinner: src.ActualWinner, Outcome: src.Outcome); }