< Summary

Information
Class: Orchestrator.Commands.Operations.Verify.VerifyMatchdayCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Verify/VerifyMatchdayCommand.cs
Line coverage
100%
Covered lines: 151
Uncovered lines: 0
Coverable lines: 151
Total lines: 356
Line coverage: 100%
Branch coverage
97%
Covered branches: 111
Total branches: 114
Branch coverage: 97.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%88100%
ExecuteVerificationWorkflow()95.59%6868100%
ComparePredictions(...)100%1010100%
CheckPredictionOutdated()100%2424100%
StripDisplaySuffix(...)100%44100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Verify/VerifyMatchdayCommand.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using Microsoft.Extensions.Logging;
 3using Spectre.Console.Cli;
 4using Spectre.Console;
 5using KicktippIntegration;
 6using Orchestrator.Infrastructure.Factories;
 7
 8namespace Orchestrator.Commands.Operations.Verify;
 9
 10public class VerifyMatchdayCommand : AsyncCommand<VerifySettings>
 11{
 12    private readonly IAnsiConsole _console;
 13    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 14    private readonly IKicktippClientFactory _kicktippClientFactory;
 15    private readonly ILogger<VerifyMatchdayCommand> _logger;
 16
 117    public VerifyMatchdayCommand(
 118        IAnsiConsole console,
 119        IFirebaseServiceFactory firebaseServiceFactory,
 120        IKicktippClientFactory kicktippClientFactory,
 121        ILogger<VerifyMatchdayCommand> logger)
 22    {
 123        _console = console;
 124        _firebaseServiceFactory = firebaseServiceFactory;
 125        _kicktippClientFactory = kicktippClientFactory;
 126        _logger = logger;
 127    }
 28
 29    public override async Task<int> ExecuteAsync(CommandContext context, VerifySettings settings)
 30    {
 31
 32        try
 33        {
 134            _console.MarkupLine($"[green]Verify matchday command initialized[/]");
 35
 136            if (settings.Verbose)
 37            {
 138                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 39            }
 40
 141            if (settings.Agent)
 42            {
 143                _console.MarkupLine("[blue]Agent mode enabled - prediction details will be hidden[/]");
 44            }
 45
 146            if (settings.InitMatchday)
 47            {
 148                _console.MarkupLine("[cyan]Init matchday mode enabled - will return error if no predictions exist[/]");
 49            }
 50
 151            if (settings.CheckOutdated)
 52            {
 153                _console.MarkupLine("[cyan]Outdated check enabled - predictions will be checked against latest context d
 54            }
 55
 56            // Execute the verification workflow
 157            var hasDiscrepancies = await ExecuteVerificationWorkflow(settings);
 58
 159            return hasDiscrepancies ? 1 : 0;
 60        }
 161        catch (Exception ex)
 62        {
 163            _logger.LogError(ex, "Error executing verify matchday command");
 164            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 165            return 1;
 66        }
 167    }
 68
 69    private async Task<bool> ExecuteVerificationWorkflow(VerifySettings settings)
 70    {
 171        var kicktippClient = _kicktippClientFactory.CreateClient();
 72
 73        // Try to get the prediction repository (may be null if Firebase is not configured)
 174        var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository();
 175        if (predictionRepository == null)
 76        {
 177            _console.MarkupLine("[red]Error: Database not configured. Cannot verify predictions without database access.
 178            _console.MarkupLine("[yellow]Hint: Set FIREBASE_PROJECT_ID and FIREBASE_SERVICE_ACCOUNT_JSON environment var
 179            return true; // Consider this a failure
 80        }
 81
 82        // Get context repository for outdated checks (may be null if Firebase is not configured)
 183        var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 184        if (settings.CheckOutdated && contextRepository == null)
 85        {
 186            _console.MarkupLine("[red]Error: Database not configured. Cannot check outdated predictions without database
 187            _console.MarkupLine("[yellow]Hint: Set FIREBASE_PROJECT_ID and FIREBASE_SERVICE_ACCOUNT_JSON environment var
 188            return true; // Consider this a failure
 89        }
 90
 91        // Determine community context (use explicit setting or fall back to community name)
 192        string communityContext = settings.CommunityContext ?? settings.Community;
 93
 194        _console.MarkupLine($"[blue]Using community:[/] [yellow]{settings.Community}[/]");
 195        _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 196        _console.MarkupLine("[blue]Getting placed predictions from Kicktipp...[/]");
 97
 98        // Step 1: Get placed predictions from Kicktipp
 199        var placedPredictions = await kicktippClient.GetPlacedPredictionsAsync(settings.Community);
 100
 1101        if (!placedPredictions.Any())
 102        {
 1103            _console.MarkupLine("[yellow]No matches found on Kicktipp[/]");
 1104            return false;
 105        }
 106
 1107        _console.MarkupLine($"[green]Found {placedPredictions.Count} matches on Kicktipp[/]");
 108
 1109        _console.MarkupLine("[blue]Retrieving predictions from database...[/]");
 110
 1111        var hasDiscrepancies = false;
 1112        var totalMatches = 0;
 1113        var matchesWithPlacedPredictions = 0;
 1114        var matchesWithDatabasePredictions = 0;
 1115        var matchingPredictions = 0;
 116
 117        // Step 2: For each match, compare with database predictions
 1118        foreach (var (match, kicktippPrediction) in placedPredictions)
 119        {
 1120            totalMatches++;
 121
 122            // Log warning for cancelled matches - they have inherited times which may affect database lookup reliabilit
 1123            if (match.IsCancelled)
 124            {
 1125                _console.MarkupLine($"[yellow]  ⚠ {match.HomeTeam} vs {match.AwayTeam} is cancelled (Abgesagt). " +
 1126                    $"Database lookup uses inherited time which may not match original prediction time.[/]");
 127            }
 128
 129            try
 130            {
 131                // Get prediction from database
 1132                if (settings.Verbose)
 133                {
 1134                    _console.MarkupLine($"[dim]  Looking up: {match.HomeTeam} vs {match.AwayTeam} at {match.StartsAt}{(m
 135                }
 136
 1137                var databasePrediction = await predictionRepository.GetPredictionAsync(match, settings.Model, communityC
 138
 1139                if (kicktippPrediction != null)
 140                {
 1141                    matchesWithPlacedPredictions++;
 142                }
 143
 1144                if (databasePrediction != null)
 145                {
 1146                    matchesWithDatabasePredictions++;
 1147                    if (settings.Verbose && !settings.Agent)
 148                    {
 1149                        _console.MarkupLine($"[dim]  Found database prediction: {databasePrediction.HomeGoals}:{database
 150                    }
 151                }
 1152                else if (settings.Verbose && !settings.Agent)
 153                {
 1154                    _console.MarkupLine($"[dim]  No database prediction found[/]");
 155                }
 156
 157                // Check if prediction is outdated (if enabled and context repository is available)
 1158                var isOutdated = false;
 1159                if (settings.CheckOutdated && contextRepository != null && databasePrediction != null)
 160                {
 1161                    isOutdated = await CheckPredictionOutdated(predictionRepository, contextRepository, match, settings.
 162                }
 163
 164                // Compare predictions
 1165                var isMatchingPrediction = ComparePredictions(kicktippPrediction, databasePrediction);
 166
 167                // Consider prediction invalid if it's outdated or mismatched
 1168                var isValidPrediction = isMatchingPrediction && !isOutdated;
 169
 1170                if (isValidPrediction)
 171                {
 1172                    matchingPredictions++;
 173
 1174                    if (settings.Verbose)
 175                    {
 1176                        if (settings.Agent)
 177                        {
 1178                            _console.MarkupLine($"[green]✓ {match.HomeTeam} vs {match.AwayTeam}[/] [dim](valid)[/]");
 179                        }
 180                        else
 181                        {
 1182                            var predictionText = kicktippPrediction?.ToString() ?? "no prediction";
 1183                            _console.MarkupLine($"[green]✓ {match.HomeTeam} vs {match.AwayTeam}:[/] {predictionText} [di
 184                        }
 185                    }
 186                }
 187                else
 188                {
 1189                    hasDiscrepancies = true;
 190
 1191                    if (settings.Agent)
 192                    {
 1193                        var reason = isOutdated ? "outdated" : "mismatch";
 1194                        _console.MarkupLine($"[red]✗ {match.HomeTeam} vs {match.AwayTeam}[/] [dim]({reason})[/]");
 195                    }
 196                    else
 197                    {
 1198                        var kicktippText = kicktippPrediction?.ToString() ?? "no prediction";
 1199                        var databaseText = databasePrediction != null ? $"{databasePrediction.HomeGoals}:{databasePredic
 200
 1201                        _console.MarkupLine($"[red]✗ {match.HomeTeam} vs {match.AwayTeam}:[/]");
 1202                        _console.MarkupLine($"  [yellow]Kicktipp:[/] {kicktippText}");
 1203                        _console.MarkupLine($"  [yellow]Database:[/] {databaseText}");
 204
 1205                        if (isOutdated)
 206                        {
 1207                            _console.MarkupLine($"  [yellow]Status:[/] Outdated (context updated after prediction)");
 208                        }
 209                    }
 210                }
 1211            }
 1212            catch (Exception ex)
 213            {
 1214                hasDiscrepancies = true;
 1215                _logger.LogError(ex, "Error verifying prediction for {Match}", $"{match.HomeTeam} vs {match.AwayTeam}");
 216
 1217                if (settings.Agent)
 218                {
 1219                    _console.MarkupLine($"[red]✗ {match.HomeTeam} vs {match.AwayTeam}[/] [dim](error)[/]");
 220                }
 221                else
 222                {
 1223                    _console.MarkupLine($"[red]✗ {match.HomeTeam} vs {match.AwayTeam}:[/] Error during verification");
 224                }
 1225            }
 1226        }
 227
 228        // Step 3: Display summary
 1229        _console.WriteLine();
 1230        _console.MarkupLine("[bold]Verification Summary:[/]");
 1231        _console.MarkupLine($"  Total matches: {totalMatches}");
 1232        _console.MarkupLine($"  Matches with Kicktipp predictions: {matchesWithPlacedPredictions}");
 1233        _console.MarkupLine($"  Matches with database predictions: {matchesWithDatabasePredictions}");
 1234        _console.MarkupLine($"  Matching predictions: {matchingPredictions}");
 235
 236        // Check for init-matchday mode first
 1237        if (settings.InitMatchday && matchesWithDatabasePredictions == 0)
 238        {
 1239            _console.MarkupLine("[yellow]  Init matchday detected - no database predictions exist[/]");
 1240            _console.MarkupLine("[red]Returning error to trigger initial prediction workflow[/]");
 1241            return true; // Return error to trigger workflow
 242        }
 243
 1244        if (hasDiscrepancies)
 245        {
 1246            _console.MarkupLine($"[red]  Discrepancies found: {totalMatches - matchingPredictions}[/]");
 1247            _console.MarkupLine("[red]Verification failed - predictions do not match[/]");
 248        }
 249        else
 250        {
 1251            _console.MarkupLine("[green]  All predictions match - verification successful[/]");
 252        }
 253
 1254        return hasDiscrepancies;
 1255    }
 256
 257    private static bool ComparePredictions(BetPrediction? kicktippPrediction, Prediction? databasePrediction)
 258    {
 259        // Both null - match
 1260        if (kicktippPrediction == null && databasePrediction == null)
 261        {
 1262            return true;
 263        }
 264
 265        // One null, other not - mismatch
 1266        if (kicktippPrediction == null || databasePrediction == null)
 267        {
 1268            return false;
 269        }
 270
 271        // Both have values - compare
 1272        return kicktippPrediction.HomeGoals == databasePrediction.HomeGoals &&
 1273               kicktippPrediction.AwayGoals == databasePrediction.AwayGoals;
 274    }
 275
 276    private async Task<bool> CheckPredictionOutdated(IPredictionRepository predictionRepository, IContextRepository cont
 277    {
 278        try
 279        {
 280            // Get prediction metadata with context document names and timestamps
 1281            var predictionMetadata = await predictionRepository.GetPredictionMetadataAsync(match, model, communityContex
 282
 1283            if (predictionMetadata == null || !predictionMetadata.ContextDocumentNames.Any())
 284            {
 285                // If no context documents were used, prediction can't be outdated based on context changes
 1286                return false;
 287            }
 288
 1289            if (verbose)
 290            {
 1291                _console.MarkupLine($"[dim]  Checking {predictionMetadata.ContextDocumentNames.Count} context documents 
 292            }
 293
 294            // Check if any context document has been updated after the prediction was created
 1295            foreach (var documentName in predictionMetadata.ContextDocumentNames)
 296            {
 297                // Strip any display suffix (e.g., " (kpi-context)") from the context document name
 298                // to get the actual document name stored in the repository
 1299                var actualDocumentName = StripDisplaySuffix(documentName);
 300
 301                // Skip bundesliga-standings.csv from outdated check to reduce unnecessary repredictions
 1302                if (actualDocumentName.Equals("bundesliga-standings.csv", StringComparison.OrdinalIgnoreCase))
 303                {
 1304                    if (verbose)
 305                    {
 1306                        _console.MarkupLine($"[dim]  Skipping outdated check for '{actualDocumentName}' (excluded from c
 307                    }
 1308                    continue;
 309                }
 310
 1311                var latestContextDocument = await contextRepository.GetLatestContextDocumentAsync(actualDocumentName, co
 312
 1313                if (latestContextDocument != null && latestContextDocument.CreatedAt > predictionMetadata.CreatedAt)
 314                {
 1315                    if (verbose)
 316                    {
 1317                        _console.MarkupLine($"[dim]  Context document '{actualDocumentName}' (stored as '{documentName}'
 318                    }
 1319                    return true; // Prediction is outdated
 320                }
 1321                else if (verbose && latestContextDocument == null)
 322                {
 1323                    _console.MarkupLine($"[yellow]  Warning: Context document '{actualDocumentName}' not found in reposi
 324                }
 1325            }
 326
 1327            return false; // Prediction is up-to-date
 328        }
 1329        catch (Exception ex)
 330        {
 331            // Log error but don't fail verification due to outdated check issues
 1332            if (verbose)
 333            {
 1334                _console.MarkupLine($"[yellow]  Warning: Failed to check outdated status: {ex.Message}[/]");
 335            }
 1336            return false;
 337        }
 1338    }
 339
 340    /// <summary>
 341    /// Strips display suffixes like " (kpi-context)" from context document names
 342    /// to get the actual document name used in the repository.
 343    /// </summary>
 344    /// <param name="displayName">The display name that may contain a suffix</param>
 345    /// <returns>The actual document name without any display suffix</returns>
 346    private static string StripDisplaySuffix(string displayName)
 347    {
 348        // Look for patterns like " (some-text)" at the end and remove them
 1349        var lastParenIndex = displayName.LastIndexOf(" (");
 1350        if (lastParenIndex > 0 && displayName.EndsWith(")"))
 351        {
 1352            return displayName.Substring(0, lastParenIndex);
 353        }
 1354        return displayName;
 355    }
 356}