using Marathon.Application.Abstractions; using Marathon.Domain.Betting; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Microsoft.Extensions.Logging; using DomainEventId = Marathon.Domain.ValueObjects.EventId; namespace Marathon.Application.UseCases; /// /// Sweeps the journal for bets whose events /// have been graded, and updates them in bulk via /// . /// /// /// Called on demand from the Journal page's "Resolve pending" button. The /// design is idempotent — bets that cannot be auto-graded (period-scope, or /// no result yet) are left untouched and surface again on the next pass. /// public sealed class ResolvePendingBetsUseCase { private readonly IPlacedBetRepository _bets; private readonly IResultRepository _results; private readonly ILogger _logger; public ResolvePendingBetsUseCase( IPlacedBetRepository bets, IResultRepository results, ILogger logger) { _bets = bets ?? throw new ArgumentNullException(nameof(bets)); _results = results ?? throw new ArgumentNullException(nameof(results)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Returns the number of bets that were transitioned out of Pending in this pass. /// public async Task ExecuteAsync(CancellationToken ct = default) { var pending = await _bets.ListByOutcomeAsync(BetOutcome.Pending, ct).ConfigureAwait(false); if (pending.Count == 0) { _logger.LogInformation("ResolvePendingBetsUseCase: no pending bets"); return 0; } // Cache results per event so we do not re-query for each bet on the same event. var resultCache = new Dictionary(); var resolvedCount = 0; foreach (var bet in pending) { ct.ThrowIfCancellationRequested(); if (!resultCache.TryGetValue(bet.EventId, out var result)) { result = await _results.GetAsync(bet.EventId, ct).ConfigureAwait(false); resultCache[bet.EventId] = result; } if (result is null) continue; var graded = BetOutcomeResolver.Resolve(bet.Selection, result); if (graded is null) continue; var updated = bet.WithOutcome(graded.Value); await _bets.UpdateAsync(updated, ct).ConfigureAwait(false); resolvedCount++; } // Save before logging — if the batch fails, an exception bubbles out and // the success-count log is never emitted; we never report a graded count // that was rolled back. if (resolvedCount > 0) await _bets.SaveChangesAsync(ct).ConfigureAwait(false); _logger.LogInformation( "ResolvePendingBetsUseCase: graded {Resolved} of {Pending} pending bets", resolvedCount, pending.Count); return resolvedCount; } }