< Summary

Information
Class: Orchestrator.Commands.Operations.Verify.VerifyBonusCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Verify/VerifyBonusCommand.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 171
Coverable lines: 171
Total lines: 388
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 108
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ExecuteAsync()0%7280%
ExecuteVerificationWorkflow()0%3906620%
ValidateBonusPrediction(...)0%110100%
CheckBonusPredictionOutdated()0%272160%
CompareBonusPredictions(...)0%156120%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Verify/VerifyBonusCommand.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 VerifyBonusCommand : AsyncCommand<VerifySettings>
 11{
 12    private readonly IAnsiConsole _console;
 13    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 14    private readonly IKicktippClientFactory _kicktippClientFactory;
 15    private readonly ILogger<VerifyBonusCommand> _logger;
 16
 017    public VerifyBonusCommand(
 018        IAnsiConsole console,
 019        IFirebaseServiceFactory firebaseServiceFactory,
 020        IKicktippClientFactory kicktippClientFactory,
 021        ILogger<VerifyBonusCommand> logger)
 22    {
 023        _console = console;
 024        _firebaseServiceFactory = firebaseServiceFactory;
 025        _kicktippClientFactory = kicktippClientFactory;
 026        _logger = logger;
 027    }
 28
 29    public override async Task<int> ExecuteAsync(CommandContext context, VerifySettings settings)
 30    {
 31
 32        try
 33        {
 034            _console.MarkupLine($"[green]Verify bonus command initialized[/]");
 35
 036            if (settings.Verbose)
 37            {
 038                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 39            }
 40
 041            if (settings.Agent)
 42            {
 043                _console.MarkupLine("[blue]Agent mode enabled - prediction details will be hidden[/]");
 44            }
 45
 046            if (settings.InitMatchday)
 47            {
 048                _console.MarkupLine("[cyan]Init bonus mode enabled - will return error if no predictions exist[/]");
 49            }
 50
 051            if (settings.CheckOutdated)
 52            {
 053                _console.MarkupLine("[cyan]Outdated check enabled - predictions will be checked against latest context d
 54            }
 55
 56            // Execute the verification workflow
 057            var hasDiscrepancies = await ExecuteVerificationWorkflow(settings);
 58
 059            return hasDiscrepancies ? 1 : 0;
 60        }
 061        catch (Exception ex)
 62        {
 063            _logger.LogError(ex, "Error executing verify bonus command");
 064            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 065            return 1;
 66        }
 067    }
 68
 69    private async Task<bool> ExecuteVerificationWorkflow(VerifySettings settings)
 70    {
 071        var kicktippClient = _kicktippClientFactory.CreateClient();
 72
 73        // Try to get the prediction repository (may be null if Firebase is not configured)
 074        var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository();
 075        if (predictionRepository == null)
 76        {
 077            _console.MarkupLine("[red]Error: Database not configured. Cannot verify predictions without database access.
 078            _console.MarkupLine("[yellow]Hint: Set FIREBASE_PROJECT_ID and FIREBASE_SERVICE_ACCOUNT_JSON environment var
 079            return true; // Consider this a failure
 80        }
 81
 82        // Get KPI repository for outdated checks (required for bonus predictions)
 083        var kpiRepository = _firebaseServiceFactory.CreateKpiRepository();
 84
 85        // Determine community context (use explicit setting or fall back to community name)
 086        string communityContext = settings.CommunityContext ?? settings.Community;
 87
 088        _console.MarkupLine($"[blue]Using community:[/] [yellow]{settings.Community}[/]");
 089        _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 090        _console.MarkupLine("[blue]Getting open bonus questions from Kicktipp...[/]");
 91
 92        // Step 1: Get open bonus questions from Kicktipp
 093        var bonusQuestions = await kicktippClient.GetOpenBonusQuestionsAsync(settings.Community);
 94
 095        if (!bonusQuestions.Any())
 96        {
 097            _console.MarkupLine("[yellow]No bonus questions found on Kicktipp[/]");
 098            return false;
 99        }
 100
 0101        _console.MarkupLine($"[green]Found {bonusQuestions.Count} bonus questions on Kicktipp[/]");
 102
 0103        _console.MarkupLine("[blue]Getting placed bonus predictions from Kicktipp...[/]");
 104
 105        // Step 1.5: Get currently placed predictions from Kicktipp
 0106        var placedPredictions = await kicktippClient.GetPlacedBonusPredictionsAsync(settings.Community);
 107
 0108        _console.MarkupLine("[blue]Retrieving predictions from database...[/]");
 109
 0110        var hasDiscrepancies = false;
 0111        var totalQuestions = 0;
 0112        var questionsWithDatabasePredictions = 0;
 0113        var validPredictions = 0;
 114
 115        // Step 2: For each bonus question, check if we have a prediction in database
 0116        foreach (var question in bonusQuestions)
 117        {
 0118            totalQuestions++;
 119
 120            try
 121            {
 122                // Get prediction from database
 0123                if (settings.Verbose)
 124                {
 0125                    _console.MarkupLine($"[dim]  Looking up: {Markup.Escape(question.Text)}[/]");
 126                }
 127
 0128                var databasePrediction = await predictionRepository.GetBonusPredictionByTextAsync(question.Text, setting
 0129                var kicktippPrediction = placedPredictions.GetValueOrDefault(question.FormFieldName ?? question.Text);
 130
 0131                if (databasePrediction != null)
 132                {
 0133                    questionsWithDatabasePredictions++;
 134
 135                    // Validate the prediction against the question
 0136                    var isValidPrediction = ValidateBonusPrediction(question, databasePrediction);
 137
 138                    // Compare database prediction with Kicktipp placed prediction
 0139                    var predictionsMatch = CompareBonusPredictions(databasePrediction, kicktippPrediction);
 140
 141                    // Check if prediction is outdated (if enabled)
 0142                    var isOutdated = false;
 0143                    if (settings.CheckOutdated)
 144                    {
 0145                        isOutdated = await CheckBonusPredictionOutdated(predictionRepository, kpiRepository, question.Te
 146                    }
 147
 148                    // Consider prediction valid if it passes validation, matches Kicktipp, and is not outdated
 0149                    var isPredictionValid = isValidPrediction && predictionsMatch && !isOutdated;
 150
 0151                    if (isPredictionValid)
 152                    {
 0153                        validPredictions++;
 154
 0155                        if (settings.Verbose)
 156                        {
 0157                            if (settings.Agent)
 158                            {
 0159                                _console.MarkupLine($"[green]✓ {Markup.Escape(question.Text)}[/] [dim](valid)[/]");
 160                            }
 161                            else
 162                            {
 0163                                var optionTexts = question.Options
 0164                                    .Where(o => databasePrediction.SelectedOptionIds.Contains(o.Id))
 0165                                    .Select(o => o.Text);
 0166                                _console.MarkupLine($"[green]✓ {Markup.Escape(question.Text)}:[/] {string.Join(", ", opt
 167                            }
 168                        }
 169                    }
 170                    else
 171                    {
 0172                        hasDiscrepancies = true;
 173
 0174                        if (settings.Agent)
 175                        {
 0176                            var status = !isValidPrediction ? "invalid prediction" :
 0177                                        !predictionsMatch ? "mismatch with Kicktipp" : "outdated";
 0178                            _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}[/] [dim]({status})[/]");
 179                        }
 180                        else
 181                        {
 0182                            if (!isValidPrediction)
 183                            {
 0184                                var optionTexts = question.Options
 0185                                    .Where(o => databasePrediction.SelectedOptionIds.Contains(o.Id))
 0186                                    .Select(o => o.Text);
 0187                                _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}:[/] {string.Join(", ", optio
 188                            }
 0189                            else if (!predictionsMatch)
 190                            {
 191                                // Show mismatch details
 0192                                var databaseTexts = question.Options
 0193                                    .Where(o => databasePrediction.SelectedOptionIds.Contains(o.Id))
 0194                                    .Select(o => o.Text);
 0195                                var kicktippTexts = kicktippPrediction != null
 0196                                    ? question.Options
 0197                                        .Where(o => kicktippPrediction.SelectedOptionIds.Contains(o.Id))
 0198                                        .Select(o => o.Text)
 0199                                    : new List<string>();
 200
 0201                                _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}:[/]");
 0202                                _console.MarkupLine($"  [yellow]Database:[/] {string.Join(", ", databaseTexts)}");
 0203                                _console.MarkupLine($"  [yellow]Kicktipp:[/] {(kicktippTexts.Any() ? string.Join(", ", k
 204                            }
 0205                            else if (isOutdated)
 206                            {
 0207                                var optionTexts = question.Options
 0208                                    .Where(o => databasePrediction.SelectedOptionIds.Contains(o.Id))
 0209                                    .Select(o => o.Text);
 0210                                _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}:[/] {string.Join(", ", optio
 0211                                _console.MarkupLine($"  [yellow]Status:[/] Outdated (context updated after prediction)")
 212                            }
 213                        }
 214                    }
 215                }
 216                else
 217                {
 0218                    hasDiscrepancies = true;
 219
 0220                    if (settings.Verbose)
 221                    {
 0222                        if (settings.Agent)
 223                        {
 0224                            _console.MarkupLine($"[yellow]○ {Markup.Escape(question.Text)}[/] [dim](no prediction)[/]");
 225                        }
 226                        else
 227                        {
 0228                            _console.MarkupLine($"[yellow]○ {Markup.Escape(question.Text)}:[/] [dim](no prediction)[/]")
 229                        }
 230                    }
 231                }
 0232            }
 0233            catch (Exception ex)
 234            {
 0235                hasDiscrepancies = true;
 0236                _logger.LogError(ex, "Error verifying bonus prediction for question '{QuestionText}'", question.Text);
 237
 0238                if (settings.Agent)
 239                {
 0240                    _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}[/] [dim](error)[/]");
 241                }
 242                else
 243                {
 0244                    _console.MarkupLine($"[red]✗ {Markup.Escape(question.Text)}:[/] Error during verification");
 245                }
 0246            }
 0247        }
 248
 249        // Step 3: Display summary
 0250        _console.WriteLine();
 0251        _console.MarkupLine("[bold]Verification Summary:[/]");
 0252        _console.MarkupLine($"  Total bonus questions: {totalQuestions}");
 0253        _console.MarkupLine($"  Questions with database predictions: {questionsWithDatabasePredictions}");
 0254        _console.MarkupLine($"  Valid predictions: {validPredictions}");
 255
 256        // Check for init-bonus mode first
 0257        if (settings.InitMatchday && questionsWithDatabasePredictions == 0)
 258        {
 0259            _console.MarkupLine("[yellow]  Init bonus detected - no database predictions exist[/]");
 0260            _console.MarkupLine("[red]Returning error to trigger initial prediction workflow[/]");
 0261            return true; // Return error to trigger workflow
 262        }
 263
 0264        if (hasDiscrepancies)
 265        {
 0266            _console.MarkupLine($"[red]  Missing or invalid predictions: {totalQuestions - validPredictions}[/]");
 0267            _console.MarkupLine("[red]Verification failed - some predictions are missing or invalid[/]");
 268        }
 269        else
 270        {
 0271            _console.MarkupLine("[green]  All predictions are valid - verification successful[/]");
 272        }
 273
 0274        return hasDiscrepancies;
 0275    }
 276
 277    private static bool ValidateBonusPrediction(BonusQuestion question, BonusPrediction prediction)
 278    {
 279        // Check if all selected option IDs exist in the question
 0280        var validOptionIds = question.Options.Select(o => o.Id).ToHashSet();
 0281        var allOptionsValid = prediction.SelectedOptionIds.All(id => validOptionIds.Contains(id));
 282
 0283        if (!allOptionsValid)
 284        {
 0285            return false;
 286        }
 287
 288        // Check if the number of selections is valid
 0289        var selectionCount = prediction.SelectedOptionIds.Count;
 0290        if (selectionCount < 1 || selectionCount > question.MaxSelections)
 291        {
 0292            return false;
 293        }
 294
 295        // Check for duplicates
 0296        var uniqueSelections = prediction.SelectedOptionIds.Distinct().Count();
 0297        if (uniqueSelections != selectionCount)
 298        {
 0299            return false;
 300        }
 301
 0302        return true;
 303    }
 304
 305    private async Task<bool> CheckBonusPredictionOutdated(
 306        IPredictionRepository predictionRepository,
 307        IKpiRepository kpiRepository,
 308        string questionText,
 309        string model,
 310        string communityContext,
 311        bool verbose)
 312    {
 313        try
 314        {
 315            // Get prediction metadata (includes creation timestamp and context document names)
 0316            var predictionMetadata = await predictionRepository.GetBonusPredictionMetadataByTextAsync(
 0317                questionText, model, communityContext);
 318
 0319            if (predictionMetadata == null)
 320            {
 321                // No metadata found, assume not outdated
 0322                return false;
 323            }
 324
 325            // Check if any KPI document has been updated after the prediction was created
 0326            foreach (var contextDocumentName in predictionMetadata.ContextDocumentNames)
 327            {
 0328                var kpiDocument = await kpiRepository.GetKpiDocumentAsync(contextDocumentName, communityContext);
 0329                if (kpiDocument != null)
 330                {
 331                    // Compare the creation timestamps
 332                    // Note: We need to be careful about timezone handling here
 333                    // Both timestamps should be in UTC for proper comparison
 0334                    if (kpiDocument.CreatedAt > predictionMetadata.CreatedAt)
 335                    {
 0336                        if (verbose)
 337                        {
 0338                            _console.MarkupLine($"[yellow]KPI document '{contextDocumentName}' updated after prediction 
 0339                            _console.MarkupLine($"  [dim]Prediction created:[/] {predictionMetadata.CreatedAt:yyyy-MM-dd
 0340                            _console.MarkupLine($"  [dim]KPI document created:[/] {kpiDocument.CreatedAt:yyyy-MM-dd HH:m
 341                        }
 0342                        return true; // Prediction is outdated
 343                    }
 344
 0345                    if (verbose)
 346                    {
 0347                        _console.MarkupLine($"[dim]KPI document '{contextDocumentName}' found, version {kpiDocument.Vers
 348                    }
 349                }
 0350                else if (verbose)
 351                {
 0352                    _console.MarkupLine($"[yellow]Warning: KPI document '{contextDocumentName}' not found[/]");
 353                }
 0354            }
 355
 0356            return false; // No KPI documents are newer than the prediction
 357        }
 0358        catch (Exception ex)
 359        {
 0360            if (verbose)
 361            {
 0362                _console.MarkupLine($"[yellow]Warning: Could not check if prediction is outdated: {ex.Message}[/]");
 363            }
 0364            return false; // Assume not outdated if we can't determine
 365        }
 0366    }
 367
 368    private static bool CompareBonusPredictions(BonusPrediction? databasePrediction, BonusPrediction? kicktippPrediction
 369    {
 370        // Both null - match
 0371        if (databasePrediction == null && kicktippPrediction == null)
 372        {
 0373            return true;
 374        }
 375
 376        // One null, other not - mismatch
 0377        if (databasePrediction == null || kicktippPrediction == null)
 378        {
 0379            return false;
 380        }
 381
 382        // Both have values - compare selected option IDs
 0383        var databaseOptions = databasePrediction.SelectedOptionIds.OrderBy(x => x).ToList();
 0384        var kicktippOptions = kicktippPrediction.SelectedOptionIds.OrderBy(x => x).ToList();
 385
 0386        return databaseOptions.SequenceEqual(kicktippOptions);
 387    }
 388}