< Summary

Information
Class: FirebaseAdapter.FirebaseKpiRepository
Assembly: FirebaseAdapter
File(s): /home/runner/work/KicktippAi/KicktippAi/src/FirebaseAdapter/FirebaseKpiRepository.cs
Line coverage
83%
Covered lines: 109
Uncovered lines: 22
Coverable lines: 131
Total lines: 296
Line coverage: 83.2%
Branch coverage
92%
Covered branches: 24
Total branches: 26
Branch coverage: 92.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
SaveKpiDocumentAsync()100%6690.63%
GetKpiDocumentAsync()100%2255.56%
GetKpiDocumentAsync()100%2276.47%
GetKpiDocumentVersionsAsync()100%2282.61%
GetAllKpiDocumentsAsync()100%8886.36%
GetLatestVersionAsync()100%2275%
BuildDocumentId(...)50%22100%

File(s)

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

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using FirebaseAdapter.Models;
 3using Google.Cloud.Firestore;
 4using Microsoft.Extensions.Logging;
 5
 6namespace FirebaseAdapter;
 7
 8/// <summary>
 9/// Firebase Firestore implementation of the KPI repository.
 10/// </summary>
 11public class FirebaseKpiRepository : IKpiRepository
 12{
 13    private readonly FirestoreDb _firestoreDb;
 14    private readonly ILogger<FirebaseKpiRepository> _logger;
 15    private readonly string _competition;
 16
 17    private const string KpiCollectionName = "kpi-documents";
 18
 119    public FirebaseKpiRepository(FirestoreDb firestoreDb, ILogger<FirebaseKpiRepository> logger, string? competition = n
 20    {
 121        _firestoreDb = firestoreDb;
 122        _logger = logger;
 123        _competition = string.IsNullOrWhiteSpace(competition)
 124            ? CompetitionIds.Bundesliga2025_26
 125            : competition.Trim();
 126    }
 27
 28    /// <summary>
 29    /// Saves a KPI document with versioning support.
 30    /// Creates a new version if content differs from the latest version.
 31    /// </summary>
 32    /// <param name="documentName">The document name.</param>
 33    /// <param name="content">The document content.</param>
 34    /// <param name="description">The document description.</param>
 35    /// <param name="communityContext">The community context for filtering.</param>
 36    /// <param name="cancellationToken">Cancellation token.</param>
 37    /// <returns>The version number of the saved document.</returns>
 38    public async Task<int> SaveKpiDocumentAsync(
 39        string documentName,
 40        string content,
 41        string description,
 42        string communityContext,
 43        CancellationToken cancellationToken = default)
 44    {
 45        try
 46        {
 147            var now = Timestamp.GetCurrentTimestamp();
 48
 49            // Get the latest version to check if content has changed
 150            var latestVersion = await GetLatestVersionAsync(documentName, communityContext, cancellationToken);
 151            var version = 0;
 52
 153            if (latestVersion >= 0)
 54            {
 55                // Check if content has changed
 156                var latestDocument = await GetKpiDocumentAsync(documentName, communityContext, latestVersion, cancellati
 157                if (latestDocument != null && latestDocument.Content == content)
 58                {
 159                    _logger.LogInformation("Content unchanged for KPI document: {DocumentName} version {Version}", docum
 160                    return latestVersion; // Return existing version if content is the same
 61                }
 62
 163                version = latestVersion + 1;
 164                _logger.LogDebug("Creating new version {Version} for KPI document: {DocumentName}", version, documentNam
 65            }
 66            else
 67            {
 168                _logger.LogDebug("Creating first version (0) for new KPI document: {DocumentName}", documentName);
 69            }
 70
 171            var versionedDocumentId = BuildDocumentId(documentName, communityContext, version);
 172            var docRef = _firestoreDb.Collection(KpiCollectionName).Document(versionedDocumentId);
 73
 174            var firestoreKpiDocument = new FirestoreKpiDocument
 175            {
 176                Id = versionedDocumentId,
 177                DocumentName = documentName,
 178                Content = content,
 179                Description = description,
 180                Version = version,
 181                CreatedAt = now,
 182                Competition = _competition,
 183                CommunityContext = communityContext
 184            };
 85
 186            await docRef.SetAsync(firestoreKpiDocument, cancellationToken: cancellationToken);
 87
 188            _logger.LogInformation("Created KPI document: {DocumentName} version {Version} for community: {CommunityCont
 189                documentName, version, communityContext);
 90
 191            return version;
 92        }
 093        catch (Exception ex)
 94        {
 095            _logger.LogError(ex, "Failed to save KPI document: {DocumentName}", documentName);
 096            throw;
 97        }
 198    }
 99
 100    /// <summary>
 101    /// Retrieves the latest version of a KPI document.
 102    /// </summary>
 103    /// <param name="documentName">The document name.</param>
 104    /// <param name="communityContext">The community context to filter by.</param>
 105    /// <param name="cancellationToken">Cancellation token.</param>
 106    /// <returns>The latest KPI document if found, otherwise null.</returns>
 107    public async Task<KpiDocument?> GetKpiDocumentAsync(string documentName, string communityContext, CancellationToken 
 108    {
 109        try
 110        {
 1111            var latestVersion = await GetLatestVersionAsync(documentName, communityContext, cancellationToken);
 1112            if (latestVersion < 0)
 113            {
 1114                return null;
 115            }
 116
 1117            return await GetKpiDocumentAsync(documentName, communityContext, latestVersion, cancellationToken);
 118        }
 0119        catch (Exception ex)
 120        {
 0121            _logger.LogError(ex, "Failed to get latest KPI document: {DocumentName} for community: {CommunityContext}",
 0122                documentName, communityContext);
 0123            throw;
 124        }
 1125    }
 126
 127    /// <summary>
 128    /// Retrieves a specific version of a KPI document.
 129    /// </summary>
 130    /// <param name="documentName">The document name.</param>
 131    /// <param name="communityContext">The community context to filter by.</param>
 132    /// <param name="version">The specific version to retrieve.</param>
 133    /// <param name="cancellationToken">Cancellation token.</param>
 134    /// <returns>The KPI document version if found, otherwise null.</returns>
 135    public async Task<KpiDocument?> GetKpiDocumentAsync(string documentName, string communityContext, int version, Cance
 136    {
 137        try
 138        {
 1139            var versionedDocumentId = BuildDocumentId(documentName, communityContext, version);
 1140            var docRef = _firestoreDb.Collection(KpiCollectionName).Document(versionedDocumentId);
 1141            var snapshot = await docRef.GetSnapshotAsync(cancellationToken);
 142
 1143            if (!snapshot.Exists)
 144            {
 1145                return null;
 146            }
 147
 1148            var firestoreDoc = snapshot.ConvertTo<FirestoreKpiDocument>();
 1149            return new KpiDocument(
 1150                firestoreDoc.DocumentName,
 1151                firestoreDoc.Content,
 1152                firestoreDoc.Description,
 1153                firestoreDoc.Version,
 1154                firestoreDoc.CreatedAt.ToDateTimeOffset());
 155        }
 0156        catch (Exception ex)
 157        {
 0158            _logger.LogError(ex, "Failed to get KPI document: {DocumentName} version {Version} for community: {Community
 0159                documentName, version, communityContext);
 0160            throw;
 161        }
 1162    }
 163
 164    /// <summary>
 165    /// Retrieves all versions of a KPI document.
 166    /// </summary>
 167    /// <param name="documentName">The document name.</param>
 168    /// <param name="communityContext">The community context to filter by.</param>
 169    /// <param name="cancellationToken">Cancellation token.</param>
 170    /// <returns>A collection of all versions of the KPI document.</returns>
 171    public async Task<IReadOnlyList<KpiDocument>> GetKpiDocumentVersionsAsync(string documentName, string communityConte
 172    {
 173        try
 174        {
 1175            var query = _firestoreDb.Collection(KpiCollectionName)
 1176                .WhereEqualTo("documentName", documentName)
 1177                .WhereEqualTo("communityContext", communityContext)
 1178                .WhereEqualTo("competition", _competition)
 1179                .OrderBy("version");
 180
 1181            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 182
 1183            var versions = new List<KpiDocument>();
 1184            foreach (var document in snapshot.Documents)
 185            {
 1186                var firestoreDoc = document.ConvertTo<FirestoreKpiDocument>();
 1187                versions.Add(new KpiDocument(
 1188                    firestoreDoc.DocumentName,
 1189                    firestoreDoc.Content,
 1190                    firestoreDoc.Description,
 1191                    firestoreDoc.Version,
 1192                    firestoreDoc.CreatedAt.ToDateTimeOffset()));
 193            }
 194
 1195            _logger.LogInformation("Retrieved {Count} versions for KPI document: {DocumentName} in community: {Community
 1196                versions.Count, documentName, communityContext);
 197
 1198            return versions.AsReadOnly();
 199        }
 0200        catch (Exception ex)
 201        {
 0202            _logger.LogError(ex, "Failed to get versions for KPI document: {DocumentName} in community: {CommunityContex
 0203                documentName, communityContext);
 0204            throw;
 205        }
 1206    }
 207
 208    /// <summary>
 209    /// Retrieves all KPI documents for a specific community context (latest versions only).
 210    /// </summary>
 211    /// <param name="communityContext">The community context to filter by.</param>
 212    /// <param name="cancellationToken">Cancellation token.</param>
 213    /// <returns>A collection of KPI documents for the specified community.</returns>
 214    public async Task<IReadOnlyList<KpiDocument>> GetAllKpiDocumentsAsync(string communityContext, CancellationToken can
 215    {
 216        try
 217        {
 1218            var query = _firestoreDb.Collection(KpiCollectionName)
 1219                .WhereEqualTo("competition", _competition)
 1220                .WhereEqualTo("communityContext", communityContext);
 221
 1222            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 223
 224            // Group by documentName and get the latest version for each
 1225            var documentGroups = snapshot.Documents
 1226                .Select(doc => doc.ConvertTo<FirestoreKpiDocument>())
 1227                .GroupBy(doc => doc.DocumentName)
 1228                .ToList();
 229
 1230            var latestDocuments = new List<KpiDocument>();
 1231            foreach (var group in documentGroups)
 232            {
 1233                var latestDoc = group.OrderByDescending(d => d.Version).First();
 1234                latestDocuments.Add(new KpiDocument(
 1235                    latestDoc.DocumentName,
 1236                    latestDoc.Content,
 1237                    latestDoc.Description,
 1238                    latestDoc.Version,
 1239                    latestDoc.CreatedAt.ToDateTimeOffset()));
 240            }
 241
 1242            _logger.LogInformation("Retrieved {Count} latest KPI documents for community: {CommunityContext}",
 1243                latestDocuments.Count, communityContext);
 244
 1245            return latestDocuments.AsReadOnly();
 246        }
 0247        catch (Exception ex)
 248        {
 0249            _logger.LogError(ex, "Failed to get all KPI documents for community: {CommunityContext}", communityContext);
 0250            throw;
 251        }
 1252    }
 253
 254    /// <summary>
 255    /// Gets the latest version number for a specific KPI document.
 256    /// </summary>
 257    /// <param name="documentName">The document name.</param>
 258    /// <param name="communityContext">The community context to filter by.</param>
 259    /// <param name="cancellationToken">Cancellation token.</param>
 260    /// <returns>The latest version number, or -1 if the document doesn't exist.</returns>
 261    public async Task<int> GetLatestVersionAsync(string documentName, string communityContext, CancellationToken cancell
 262    {
 263        try
 264        {
 1265            var query = _firestoreDb.Collection(KpiCollectionName)
 1266                .WhereEqualTo("documentName", documentName)
 1267                .WhereEqualTo("communityContext", communityContext)
 1268                .WhereEqualTo("competition", _competition)
 1269                .OrderByDescending("version")
 1270                .Limit(1);
 271
 1272            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 273
 1274            if (snapshot.Documents.Count == 0)
 275            {
 1276                return -1; // Document doesn't exist
 277            }
 278
 1279            var latestDoc = snapshot.Documents.First().ConvertTo<FirestoreKpiDocument>();
 1280            return latestDoc.Version;
 281        }
 0282        catch (Exception ex)
 283        {
 0284            _logger.LogError(ex, "Failed to get latest version for KPI document: {DocumentName} in community: {Community
 0285                documentName, communityContext);
 0286            throw;
 287        }
 1288    }
 289
 290    private string BuildDocumentId(string documentName, string communityContext, int version)
 291    {
 1292        return string.Equals(_competition, CompetitionIds.Bundesliga2025_26, StringComparison.OrdinalIgnoreCase)
 1293            ? $"{documentName}_{communityContext}_{version}"
 1294            : $"{_competition}_{documentName}_{communityContext}_{version}";
 295    }
 296}