using Marathon.Domain.AnomalyDetection; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; namespace Marathon.UI.Services; /// /// Severity bucket derived from . /// Phase 7 mapping (see backend handoff): /// Low = [0.30, 0.45), Medium = [0.45, 0.60), High = [0.60, 1.00]. /// public enum AnomalySeverity { Low, Medium, High, } /// /// Filter state passed from a page to . /// All fields optional — empty filter returns the full feed. /// public sealed record AnomalyFilter( AnomalySeverity? MinSeverity = null, IReadOnlyCollection? SportCodes = null, DateTimeOffset? From = null, DateTimeOffset? To = null, IReadOnlyCollection? Kinds = null); /// /// Compact anomaly row used by the feed page. Designed to render without any /// further repository calls — pre-shaped strings + parsed evidence summary. /// public sealed record AnomalyListItem( Guid Id, EventId EventId, string EventTitle, SportCode Sport, string CountryCode, string LeagueId, DateTimeOffset DetectedAt, decimal Score, AnomalySeverity Severity, AnomalyKind Kind, int SuspensionGapSeconds, decimal? PreWin1Rate, decimal? PreDrawRate, decimal? PreWin2Rate, decimal? PostWin1Rate, decimal? PostDrawRate, decimal? PostWin2Rate, AnomalyFavourite PreFavourite, AnomalyFavourite PostFavourite, bool IsTwoWay); /// /// Full anomaly aggregate for the detail page. Carries the parsed evidence /// snapshots plus the originating event metadata for the link-back affordance. /// public sealed record AnomalyDetailVm( AnomalyListItem Item, AnomalyEvidenceSnapshot Pre, AnomalyEvidenceSnapshot Post); /// /// Snapshot bracket of the suspension window — pre or post — with raw rates, /// implied probabilities, and the side that was the favourite at that moment. /// public sealed record AnomalyEvidenceSnapshot( DateTimeOffset CapturedAt, decimal? Rate1, decimal? RateDraw, decimal? Rate2, decimal? P1, decimal? PDraw, decimal? P2, AnomalyFavourite Favourite); /// Side that holds the lowest implied probability in a snapshot. public enum AnomalyFavourite { Side1, Draw, Side2, None, } /// Helpers for severity bucketing. Thresholds come from /// so the UI badges and the /// Application-layer outcome report agree by construction. public static class AnomalySeverityRules { public const decimal LowThreshold = AnomalySeverityThresholds.Low; public const decimal MediumThreshold = AnomalySeverityThresholds.Medium; public const decimal HighThreshold = AnomalySeverityThresholds.High; public static AnomalySeverity FromScore(decimal score) => score switch { < MediumThreshold => AnomalySeverity.Low, < HighThreshold => AnomalySeverity.Medium, _ => AnomalySeverity.High, }; public static bool MeetsThreshold(AnomalySeverity actual, AnomalySeverity? minimum) { if (minimum is null) return true; return (int)actual >= (int)minimum.Value; } }