< Summary

Information
Class: FirebaseAdapter.FirebaseMatchOutcomeRepository
Assembly: FirebaseAdapter
File(s): /home/runner/work/KicktippAi/KicktippAi/src/FirebaseAdapter/FirebaseMatchOutcomeRepository.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 97
Coverable lines: 97
Total lines: 184
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 44
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/FirebaseAdapter/FirebaseMatchOutcomeRepository.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using FirebaseAdapter.Models;
 3using Google.Cloud.Firestore;
 4using Microsoft.Extensions.Logging;
 5using NodaTime;
 6
 7namespace FirebaseAdapter;
 8
 9public class FirebaseMatchOutcomeRepository : IMatchOutcomeRepository
 10{
 11    private const int ExpectedMatchesPerMatchday = 9;
 12
 13    private readonly FirestoreDb _firestoreDb;
 14    private readonly ILogger<FirebaseMatchOutcomeRepository> _logger;
 15    private readonly string _matchOutcomesCollection;
 16    private readonly string _competition;
 17
 018    public FirebaseMatchOutcomeRepository(FirestoreDb firestoreDb, ILogger<FirebaseMatchOutcomeRepository> logger)
 19    {
 020        _firestoreDb = firestoreDb ?? throw new ArgumentNullException(nameof(firestoreDb));
 021        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 022        _matchOutcomesCollection = "match-outcomes";
 023        _competition = "bundesliga-2025-26";
 024    }
 25
 26    public async Task<MatchOutcomeUpsertResult> UpsertMatchOutcomeAsync(
 27        CollectedMatchOutcome outcome,
 28        string communityContext,
 29        CancellationToken cancellationToken = default)
 30    {
 031        var documentId = BuildDocumentId(outcome);
 032        var docRef = _firestoreDb.Collection(_matchOutcomesCollection).Document(documentId);
 033        var snapshot = await docRef.GetSnapshotAsync(cancellationToken);
 034        var now = Timestamp.GetCurrentTimestamp();
 35
 036        if (!snapshot.Exists)
 37        {
 038            var firestoreOutcome = ToFirestoreMatchOutcome(outcome, communityContext, documentId, now, now);
 039            await docRef.SetAsync(firestoreOutcome, cancellationToken: cancellationToken);
 40
 041            return new MatchOutcomeUpsertResult(
 042                MatchOutcomeWriteDisposition.Created,
 043                ConvertToPersistedMatchOutcome(firestoreOutcome));
 44        }
 45
 046        var existing = snapshot.ConvertTo<FirestoreMatchOutcome>();
 047        if (!NeedsUpdate(existing, outcome))
 48        {
 049            return new MatchOutcomeUpsertResult(
 050                MatchOutcomeWriteDisposition.Unchanged,
 051                ConvertToPersistedMatchOutcome(existing));
 52        }
 53
 054        var updated = ToFirestoreMatchOutcome(outcome, communityContext, documentId, existing.CreatedAt, now);
 055        await docRef.SetAsync(updated, cancellationToken: cancellationToken);
 56
 057        return new MatchOutcomeUpsertResult(
 058            MatchOutcomeWriteDisposition.Updated,
 059            ConvertToPersistedMatchOutcome(updated));
 060    }
 61
 62    public async Task<IReadOnlyList<int>> GetIncompleteMatchdaysAsync(
 63        string communityContext,
 64        int currentMatchday,
 65        CancellationToken cancellationToken = default)
 66    {
 067        var query = _firestoreDb.Collection(_matchOutcomesCollection)
 068            .WhereEqualTo("communityContext", communityContext);
 69
 070        var snapshot = await query.GetSnapshotAsync(cancellationToken);
 071        var groupedOutcomes = snapshot.Documents
 072            .Select(doc => doc.ConvertTo<FirestoreMatchOutcome>())
 073            .Where(outcome => outcome.Competition == _competition && outcome.Matchday <= currentMatchday)
 074            .GroupBy(outcome => outcome.Matchday)
 075            .ToDictionary(group => group.Key, group => group.ToList());
 76
 077        var incompleteMatchdays = new List<int>();
 078        for (var matchday = 1; matchday <= currentMatchday; matchday++)
 79        {
 080            if (!groupedOutcomes.TryGetValue(matchday, out var outcomes))
 81            {
 082                incompleteMatchdays.Add(matchday);
 083                continue;
 84            }
 85
 086            var isComplete = outcomes.Count >= ExpectedMatchesPerMatchday &&
 087                             outcomes.All(outcome => string.Equals(outcome.Availability, nameof(MatchOutcomeAvailability
 88
 089            if (!isComplete)
 90            {
 091                incompleteMatchdays.Add(matchday);
 92            }
 93        }
 94
 095        return incompleteMatchdays.AsReadOnly();
 096    }
 97
 98    public async Task<IReadOnlyList<PersistedMatchOutcome>> GetMatchdayOutcomesAsync(
 99        int matchday,
 100        string communityContext,
 101        CancellationToken cancellationToken = default)
 102    {
 0103        var query = _firestoreDb.Collection(_matchOutcomesCollection)
 0104            .WhereEqualTo("communityContext", communityContext);
 105
 0106        var snapshot = await query.GetSnapshotAsync(cancellationToken);
 0107        return snapshot.Documents
 0108            .Select(doc => doc.ConvertTo<FirestoreMatchOutcome>())
 0109            .Where(outcome => outcome.Competition == _competition && outcome.Matchday == matchday)
 0110            .Select(ConvertToPersistedMatchOutcome)
 0111            .OrderBy(outcome => outcome.HomeTeam)
 0112            .ToList()
 0113            .AsReadOnly();
 0114    }
 115
 116    private static bool NeedsUpdate(FirestoreMatchOutcome existing, CollectedMatchOutcome outcome)
 117    {
 0118        return existing.HomeGoals != outcome.HomeGoals ||
 0119               existing.AwayGoals != outcome.AwayGoals ||
 0120               !string.Equals(existing.Availability, outcome.Availability.ToString(), StringComparison.Ordinal) ||
 0121               existing.TippSpielId != outcome.TippSpielId ||
 0122               existing.StartsAt.ToDateTimeOffset() != outcome.StartsAt.ToInstant().ToDateTimeOffset();
 123    }
 124
 125    private FirestoreMatchOutcome ToFirestoreMatchOutcome(
 126        CollectedMatchOutcome outcome,
 127        string communityContext,
 128        string documentId,
 129        Timestamp createdAt,
 130        Timestamp updatedAt)
 131    {
 0132        return new FirestoreMatchOutcome
 0133        {
 0134            Id = documentId,
 0135            HomeTeam = outcome.HomeTeam,
 0136            AwayTeam = outcome.AwayTeam,
 0137            StartsAt = ConvertToTimestamp(outcome.StartsAt),
 0138            Matchday = outcome.Matchday,
 0139            HomeGoals = outcome.HomeGoals,
 0140            AwayGoals = outcome.AwayGoals,
 0141            Availability = outcome.Availability.ToString(),
 0142            TippSpielId = outcome.TippSpielId,
 0143            CreatedAt = createdAt,
 0144            UpdatedAt = updatedAt,
 0145            Competition = _competition,
 0146            CommunityContext = communityContext
 0147        };
 148    }
 149
 150    private PersistedMatchOutcome ConvertToPersistedMatchOutcome(FirestoreMatchOutcome firestoreOutcome)
 151    {
 0152        return new PersistedMatchOutcome(
 0153            firestoreOutcome.CommunityContext,
 0154            firestoreOutcome.Competition,
 0155            firestoreOutcome.HomeTeam,
 0156            firestoreOutcome.AwayTeam,
 0157            ConvertFromTimestamp(firestoreOutcome.StartsAt),
 0158            firestoreOutcome.Matchday,
 0159            firestoreOutcome.HomeGoals,
 0160            firestoreOutcome.AwayGoals,
 0161            Enum.Parse<MatchOutcomeAvailability>(firestoreOutcome.Availability, ignoreCase: false),
 0162            firestoreOutcome.TippSpielId,
 0163            firestoreOutcome.CreatedAt.ToDateTimeOffset(),
 0164            firestoreOutcome.UpdatedAt.ToDateTimeOffset());
 165    }
 166
 167    private static string BuildDocumentId(CollectedMatchOutcome outcome)
 168    {
 0169        return outcome.TippSpielId ?? throw new InvalidOperationException(
 0170            $"Cannot persist match outcome for {outcome.HomeTeam} vs {outcome.AwayTeam} on matchday {outcome.Matchday} b
 171    }
 172
 173    private static Timestamp ConvertToTimestamp(ZonedDateTime zonedDateTime)
 174    {
 0175        var instant = zonedDateTime.ToInstant();
 0176        return Timestamp.FromDateTimeOffset(instant.ToDateTimeOffset());
 177    }
 178
 179    private static ZonedDateTime ConvertFromTimestamp(Timestamp timestamp)
 180    {
 0181        var instant = Instant.FromDateTimeOffset(timestamp.ToDateTimeOffset());
 0182        return instant.InUtc();
 183    }
 184}