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;
}
}