< Summary

Information
Class: EHonda.KicktippAi.Core.MatchPrediction
Assembly: EHonda.KicktippAi.Core
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Core/IPredictionRepository.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 355
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%210%
get_Match()100%11100%
set_Match(...)100%210%
get_Prediction()100%11100%
set_Prediction(...)100%210%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Core/IPredictionRepository.cs

#LineLine coverage
 1using NodaTime;
 2
 3namespace EHonda.KicktippAi.Core;
 4
 5/// <summary>
 6/// Repository interface for persisting and retrieving match predictions.
 7/// Currently designed for Bundesliga 2025/26 season.
 8/// </summary>
 9public interface IPredictionRepository
 10{
 11    /// <summary>
 12    /// Saves a prediction for a specific match.
 13    /// </summary>
 14    /// <param name="match">The match to predict.</param>
 15    /// <param name="prediction">The prediction for the match.</param>
 16    /// <param name="model">The AI model used to generate the prediction.</param>
 17    /// <param name="tokenUsage">JSON string containing token usage information from the API.</param>
 18    /// <param name="cost">The cost in USD for generating the prediction.</param>
 19    /// <param name="communityContext">The community context (rules) used for the prediction.</param>
 20    /// <param name="contextDocumentNames">Names of context documents used for this prediction.</param>
 21    /// <param name="cancellationToken">Cancellation token.</param>
 22    /// <returns>A task representing the asynchronous operation.</returns>
 23    Task SavePredictionAsync(Match match, Prediction prediction, string model, string tokenUsage, double cost, string co
 24
 25    /// <summary>
 26    /// Retrieves a prediction for a specific match using the specified model and community context.
 27    /// </summary>
 28    /// <param name="match">The match to get the prediction for.</param>
 29    /// <param name="model">The AI model used to generate the prediction.</param>
 30    /// <param name="communityContext">The community context used for the prediction.</param>
 31    /// <param name="cancellationToken">Cancellation token.</param>
 32    /// <returns>The prediction if found, otherwise null.</returns>
 33    Task<Prediction?> GetPredictionAsync(Match match, string model, string communityContext, CancellationToken cancellat
 34
 35    /// <summary>
 36    /// Retrieves full prediction metadata for a specific match using the specified model and community context.
 37    /// Includes context document names and timestamps for outdated checks.
 38    /// </summary>
 39    /// <param name="match">The match to get the prediction for.</param>
 40    /// <param name="model">The AI model used to generate the prediction.</param>
 41    /// <param name="communityContext">The community context used for the prediction.</param>
 42    /// <param name="cancellationToken">Cancellation token.</param>
 43    /// <returns>The prediction metadata if found, otherwise null.</returns>
 44    Task<PredictionMetadata?> GetPredictionMetadataAsync(Match match, string model, string communityContext, Cancellatio
 45
 46    /// <summary>
 47    /// Retrieves a prediction for a specific match by team names, start time, model, and community context.
 48    /// </summary>
 49    /// <param name="homeTeam">The home team name.</param>
 50    /// <param name="awayTeam">The away team name.</param>
 51    /// <param name="startsAt">The match start time.</param>
 52    /// <param name="model">The AI model used to generate the prediction.</param>
 53    /// <param name="communityContext">The community context used for the prediction.</param>
 54    /// <param name="cancellationToken">Cancellation token.</param>
 55    /// <returns>The prediction if found, otherwise null.</returns>
 56    Task<Prediction?> GetPredictionAsync(string homeTeam, string awayTeam, ZonedDateTime startsAt, string model, string 
 57
 58    /// <summary>
 59    /// Retrieves all matches for a specific match day.
 60    /// </summary>
 61    /// <param name="matchDay">The match day number (1-34 for Bundesliga).</param>
 62    /// <param name="cancellationToken">Cancellation token.</param>
 63    /// <returns>A collection of matches for the specified match day.</returns>
 64    Task<IReadOnlyList<Match>> GetMatchDayAsync(int matchDay, CancellationToken cancellationToken = default);
 65
 66    /// <summary>
 67    /// Retrieves a stored match by team names and matchday.
 68    /// </summary>
 69    /// <param name="homeTeam">The home team name.</param>
 70    /// <param name="awayTeam">The away team name.</param>
 71    /// <param name="matchDay">The match day number.</param>
 72    /// <param name="model">Optional model used to narrow prediction-based fallback lookups.</param>
 73    /// <param name="communityContext">Optional community context used to narrow prediction-based fallback lookups.</par
 74    /// <param name="cancellationToken">Cancellation token.</param>
 75    /// <returns>The stored match if found, otherwise null.</returns>
 76    Task<Match?> GetStoredMatchAsync(string homeTeam, string awayTeam, int matchDay, string? model = null, string? commu
 77
 78    /// <summary>
 79    /// Retrieves all matches with their predictions for a specific match day using the specified model and community co
 80    /// </summary>
 81    /// <param name="matchDay">The match day number (1-34 for Bundesliga).</param>
 82    /// <param name="model">The AI model used to generate the predictions.</param>
 83    /// <param name="communityContext">The community context used for the predictions.</param>
 84    /// <param name="cancellationToken">Cancellation token.</param>
 85    /// <returns>A collection of matches with their predictions (if any) for the specified match day.</returns>
 86    Task<IReadOnlyList<MatchPrediction>> GetMatchDayWithPredictionsAsync(int matchDay, string model, string communityCon
 87
 88    /// <summary>
 89    /// Retrieves all predictions made with the specified model and community context.
 90    /// </summary>
 91    /// <param name="model">The AI model used to generate the predictions.</param>
 92    /// <param name="communityContext">The community context used for the predictions.</param>
 93    /// <param name="cancellationToken">Cancellation token.</param>
 94    /// <returns>A collection of all match predictions for the specified model and community context.</returns>
 95    Task<IReadOnlyList<MatchPrediction>> GetAllPredictionsAsync(string model, string communityContext, CancellationToken
 96
 97    /// <summary>
 98    /// Checks if a prediction exists for a specific match using the specified model and community context.
 99    /// </summary>
 100    /// <param name="match">The match to check.</param>
 101    /// <param name="model">The AI model used to generate the prediction.</param>
 102    /// <param name="communityContext">The community context used for the prediction.</param>
 103    /// <param name="cancellationToken">Cancellation token.</param>
 104    /// <returns>True if a prediction exists, otherwise false.</returns>
 105    Task<bool> HasPredictionAsync(Match match, string model, string communityContext, CancellationToken cancellationToke
 106
 107    /// <summary>
 108    /// Saves a bonus prediction for a specific question.
 109    /// </summary>
 110    /// <param name="bonusQuestion">The original bonus question (for text observability).</param>
 111    /// <param name="bonusPrediction">The bonus prediction to save.</param>
 112    /// <param name="model">The AI model used to generate the prediction.</param>
 113    /// <param name="tokenUsage">JSON string containing token usage information from the API.</param>
 114    /// <param name="cost">The cost in USD for generating the prediction.</param>
 115    /// <param name="communityContext">The community context (rules) used for the prediction.</param>
 116    /// <param name="contextDocumentNames">Names of context documents used for this prediction.</param>
 117    /// <param name="cancellationToken">Cancellation token.</param>
 118    /// <returns>A task representing the asynchronous operation.</returns>
 119    Task SaveBonusPredictionAsync(BonusQuestion bonusQuestion, BonusPrediction bonusPrediction, string model, string tok
 120
 121    /// <summary>
 122    /// Retrieves a bonus prediction for a specific question using the specified model and community context.
 123    /// </summary>
 124    /// <param name="questionId">The ID of the bonus question.</param>
 125    /// <param name="model">The AI model used to generate the prediction.</param>
 126    /// <param name="communityContext">The community context used for the prediction.</param>
 127    /// <param name="cancellationToken">Cancellation token.</param>
 128    /// <returns>The bonus prediction if found, otherwise null.</returns>
 129    Task<BonusPrediction?> GetBonusPredictionAsync(string questionId, string model, string communityContext, Cancellatio
 130
 131    /// <summary>
 132    /// Retrieves a bonus prediction by question text and community context.
 133    /// This allows lookup for the same question text with different form IDs.
 134    /// </summary>
 135    /// <param name="questionText">The text of the bonus question.</param>
 136    /// <param name="model">The AI model used to generate the prediction.</param>
 137    /// <param name="communityContext">The community context used for the prediction.</param>
 138    /// <param name="cancellationToken">Cancellation token.</param>
 139    /// <returns>The bonus prediction if found, otherwise null.</returns>
 140    Task<BonusPrediction?> GetBonusPredictionByTextAsync(string questionText, string model, string communityContext, Can
 141
 142    /// <summary>
 143    /// Retrieves full bonus prediction metadata by question text and community context.
 144    /// Includes context document names and timestamps for outdated checks.
 145    /// </summary>
 146    /// <param name="questionText">The text of the bonus question.</param>
 147    /// <param name="model">The AI model used to generate the prediction.</param>
 148    /// <param name="communityContext">The community context used for the prediction.</param>
 149    /// <param name="cancellationToken">Cancellation token.</param>
 150    /// <returns>The bonus prediction metadata if found, otherwise null.</returns>
 151    Task<BonusPredictionMetadata?> GetBonusPredictionMetadataByTextAsync(string questionText, string model, string commu
 152
 153    /// <summary>
 154    /// Retrieves all bonus predictions made with the specified model and community context.
 155    /// </summary>
 156    /// <param name="model">The AI model used to generate the predictions.</param>
 157    /// <param name="communityContext">The community context used for the predictions.</param>
 158    /// <param name="cancellationToken">Cancellation token.</param>
 159    /// <returns>A collection of all bonus predictions for the specified model and community context.</returns>
 160    Task<IReadOnlyList<BonusPrediction>> GetAllBonusPredictionsAsync(string model, string communityContext, Cancellation
 161
 162    /// <summary>
 163    /// Checks if a bonus prediction exists for a specific question using the specified model and community context.
 164    /// </summary>
 165    /// <param name="questionId">The ID of the bonus question.</param>
 166    /// <param name="model">The AI model used to generate the prediction.</param>
 167    /// <param name="communityContext">The community context used for the prediction.</param>
 168    /// <param name="cancellationToken">Cancellation token.</param>
 169    /// <returns>True if a bonus prediction exists, otherwise false.</returns>
 170    Task<bool> HasBonusPredictionAsync(string questionId, string model, string communityContext, CancellationToken cance
 171
 172    /// <summary>
 173    /// Gets the current reprediction index for a specific match using the specified model and community context.
 174    /// </summary>
 175    /// <param name="match">The match to check.</param>
 176    /// <param name="model">The AI model used to generate the prediction.</param>
 177    /// <param name="communityContext">The community context used for the prediction.</param>
 178    /// <param name="cancellationToken">Cancellation token.</param>
 179    /// <returns>The current reprediction index, or -1 if no prediction exists.</returns>
 180    Task<int> GetMatchRepredictionIndexAsync(Match match, string model, string communityContext, CancellationToken cance
 181
 182    // ============================================================================
 183    // CANCELLED MATCH LOOKUPS (Team Names Only - No startsAt Constraint)
 184    // ============================================================================
 185    //
 186    // These methods exist to handle a specific edge case with cancelled matches:
 187    //
 188    // PROBLEM:
 189    // When a match is cancelled ("Abgesagt" on Kicktipp), the match time is no longer
 190    // displayed. Different Kicktipp pages handle this inconsistently:
 191    //   - tippabgabe page: Shows multiple matches in a table, allowing time inheritance
 192    //     from the previous row (e.g., 15:30 from the match above)
 193    //   - spielinfo pages: Show one match at a time, so there's no "previous row" to
 194    //     inherit from, resulting in DateTimeOffset.MinValue as fallback
 195    //
 196    // This causes the same cancelled match to have different `startsAt` values depending
 197    // on which page was scraped, leading to database lookup mismatches:
 198    //   - MatchdayCommand uses spielinfo â†’ startsAt = MinValue
 199    //   - VerifyCommand uses tippabgabe â†’ startsAt = inherited time (e.g., 15:30)
 200    //
 201    // SOLUTION:
 202    // For cancelled matches ONLY, we query by team names without the startsAt constraint.
 203    // This finds the prediction regardless of which startsAt value was used when storing.
 204    // We order by createdAt descending to get the most recent prediction.
 205    //
 206    // WHY NOT CHANGE THE NORMAL FLOW:
 207    // The startsAt constraint is important for non-cancelled matches because:
 208    //   1. It's part of the natural composite key (teams can play multiple times)
 209    //   2. It ensures we don't accidentally retrieve predictions for rescheduled matches
 210    //   3. It maintains data integrity for the vast majority of matches
 211    //
 212    // Cancelled matches are rare edge cases where this constraint causes more problems
 213    // than it solves, so we relax it only for this specific scenario.
 214    // ============================================================================
 215
 216    /// <summary>
 217    /// Retrieves a prediction for a cancelled match by team names only (ignoring startsAt).
 218    /// <para>
 219    /// This method should ONLY be used for cancelled matches where the startsAt value
 220    /// may be inconsistent across different Kicktipp pages. For normal matches, use
 221    /// <see cref="GetPredictionAsync(Match, string, string, CancellationToken)"/> instead.
 222    /// </para>
 223    /// </summary>
 224    /// <param name="homeTeam">The home team name.</param>
 225    /// <param name="awayTeam">The away team name.</param>
 226    /// <param name="model">The AI model used to generate the prediction.</param>
 227    /// <param name="communityContext">The community context used for the prediction.</param>
 228    /// <param name="cancellationToken">Cancellation token.</param>
 229    /// <returns>The most recent prediction if found, otherwise null.</returns>
 230    Task<Prediction?> GetCancelledMatchPredictionAsync(string homeTeam, string awayTeam, string model, string communityC
 231
 232    /// <summary>
 233    /// Retrieves prediction metadata for a cancelled match by team names only (ignoring startsAt).
 234    /// <para>
 235    /// This method should ONLY be used for cancelled matches where the startsAt value
 236    /// may be inconsistent across different Kicktipp pages. For normal matches, use
 237    /// <see cref="GetPredictionMetadataAsync(Match, string, string, CancellationToken)"/> instead.
 238    /// </para>
 239    /// </summary>
 240    /// <param name="homeTeam">The home team name.</param>
 241    /// <param name="awayTeam">The away team name.</param>
 242    /// <param name="model">The AI model used to generate the prediction.</param>
 243    /// <param name="communityContext">The community context used for the prediction.</param>
 244    /// <param name="cancellationToken">Cancellation token.</param>
 245    /// <returns>The most recent prediction metadata if found, otherwise null.</returns>
 246    Task<PredictionMetadata?> GetCancelledMatchPredictionMetadataAsync(string homeTeam, string awayTeam, string model, s
 247
 248    /// <summary>
 249    /// Gets the reprediction index for a cancelled match by team names only (ignoring startsAt).
 250    /// <para>
 251    /// This method should ONLY be used for cancelled matches where the startsAt value
 252    /// may be inconsistent across different Kicktipp pages. For normal matches, use
 253    /// <see cref="GetMatchRepredictionIndexAsync(Match, string, string, CancellationToken)"/> instead.
 254    /// </para>
 255    /// </summary>
 256    /// <param name="homeTeam">The home team name.</param>
 257    /// <param name="awayTeam">The away team name.</param>
 258    /// <param name="model">The AI model used to generate the prediction.</param>
 259    /// <param name="communityContext">The community context used for the prediction.</param>
 260    /// <param name="cancellationToken">Cancellation token.</param>
 261    /// <returns>The current reprediction index, or -1 if no prediction exists.</returns>
 262    Task<int> GetCancelledMatchRepredictionIndexAsync(string homeTeam, string awayTeam, string model, string communityCo
 263
 264    /// <summary>
 265    /// Gets the current reprediction index for a specific bonus question using the specified model and community contex
 266    /// </summary>
 267    /// <param name="questionText">The text of the bonus question.</param>
 268    /// <param name="model">The AI model used to generate the prediction.</param>
 269    /// <param name="communityContext">The community context used for the prediction.</param>
 270    /// <param name="cancellationToken">Cancellation token.</param>
 271    /// <returns>The current reprediction index, or -1 if no prediction exists.</returns>
 272    Task<int> GetBonusRepredictionIndexAsync(string questionText, string model, string communityContext, CancellationTok
 273
 274    /// <summary>
 275    /// Saves a repredicted match prediction with the next reprediction index.
 276    /// </summary>
 277    /// <param name="match">The match to predict.</param>
 278    /// <param name="prediction">The prediction for the match.</param>
 279    /// <param name="model">The AI model used to generate the prediction.</param>
 280    /// <param name="tokenUsage">JSON string containing token usage information from the API.</param>
 281    /// <param name="cost">The cost in USD for generating the prediction.</param>
 282    /// <param name="communityContext">The community context (rules) used for the prediction.</param>
 283    /// <param name="contextDocumentNames">Names of context documents used for this prediction.</param>
 284    /// <param name="repredictionIndex">The reprediction index for this prediction.</param>
 285    /// <param name="cancellationToken">Cancellation token.</param>
 286    /// <returns>A task representing the asynchronous operation.</returns>
 287    Task SaveRepredictionAsync(Match match, Prediction prediction, string model, string tokenUsage, double cost, string 
 288
 289    /// <summary>
 290    /// Saves a repredicted bonus prediction with the next reprediction index.
 291    /// </summary>
 292    /// <param name="bonusQuestion">The original bonus question (for text observability).</param>
 293    /// <param name="bonusPrediction">The bonus prediction to save.</param>
 294    /// <param name="model">The AI model used to generate the prediction.</param>
 295    /// <param name="tokenUsage">JSON string containing token usage information from the API.</param>
 296    /// <param name="cost">The cost in USD for generating the prediction.</param>
 297    /// <param name="communityContext">The community context (rules) used for the prediction.</param>
 298    /// <param name="contextDocumentNames">Names of context documents used for this prediction.</param>
 299    /// <param name="repredictionIndex">The reprediction index for this prediction.</param>
 300    /// <param name="cancellationToken">Cancellation token.</param>
 301    /// <returns>A task representing the asynchronous operation.</returns>
 302    Task SaveBonusRepredictionAsync(BonusQuestion bonusQuestion, BonusPrediction bonusPrediction, string model, string t
 303
 304    /// <summary>
 305    /// Get match prediction costs and counts grouped by reprediction index for cost analysis.
 306    /// Used specifically by the cost command to include all repredictions.
 307    /// </summary>
 308    /// <param name="model">The AI model used to generate predictions.</param>
 309    /// <param name="communityContext">The community context used for predictions.</param>
 310    /// <param name="matchdays">Optional list of matchdays to filter by. If null, all matchdays are included.</param>
 311    /// <param name="cancellationToken">Cancellation token.</param>
 312    /// <returns>Dictionary mapping reprediction index to (cost, count) tuple.</returns>
 313    Task<Dictionary<int, (double cost, int count)>> GetMatchPredictionCostsByRepredictionIndexAsync(string model, string
 314
 315    /// <summary>
 316    /// Get bonus prediction costs and counts grouped by reprediction index for cost analysis.
 317    /// Used specifically by the cost command to include all repredictions.
 318    /// </summary>
 319    /// <param name="model">The AI model used to generate predictions.</param>
 320    /// <param name="communityContext">The community context used for predictions.</param>
 321    /// <param name="cancellationToken">Cancellation token.</param>
 322    /// <returns>Dictionary mapping reprediction index to (cost, count) tuple.</returns>
 323    Task<Dictionary<int, (double cost, int count)>> GetBonusPredictionCostsByRepredictionIndexAsync(string model, string
 324
 325    /// <summary>
 326    /// Gets all unique matchdays that have predictions stored.
 327    /// Used by the cost command to discover available matchdays when no filter is specified.
 328    /// </summary>
 329    /// <param name="cancellationToken">Cancellation token.</param>
 330    /// <returns>A sorted list of matchday numbers.</returns>
 331    Task<List<int>> GetAvailableMatchdaysAsync(CancellationToken cancellationToken = default);
 332
 333    /// <summary>
 334    /// Gets all unique AI models that have predictions stored.
 335    /// Used by the cost command to discover available models when no filter is specified.
 336    /// </summary>
 337    /// <param name="cancellationToken">Cancellation token.</param>
 338    /// <returns>A list of model names.</returns>
 339    Task<List<string>> GetAvailableModelsAsync(CancellationToken cancellationToken = default);
 340
 341    /// <summary>
 342    /// Gets all unique community contexts that have predictions stored.
 343    /// Used by the cost command to discover available community contexts when no filter is specified.
 344    /// </summary>
 345    /// <param name="cancellationToken">Cancellation token.</param>
 346    /// <returns>A list of community context names.</returns>
 347    Task<List<string>> GetAvailableCommunityContextsAsync(CancellationToken cancellationToken = default);
 348}
 349
 350/// <summary>
 351/// Represents a match combined with its prediction (if any).
 352/// </summary>
 1353public record MatchPrediction(
 1354    Match Match,
 1355    Prediction? Prediction);