using Marathon.Application.Abstractions; using Marathon.Domain.Entities; using Marathon.Domain.Enums; using Microsoft.Extensions.Logging; using DomainEventId = Marathon.Domain.ValueObjects.EventId; namespace Marathon.Application.UseCases; /// /// Records a new entered manually via the Journal UI. /// /// /// /// The use case validates that the referenced event exists, then persists the /// bet. If the event already has a final result the bet is graded on the spot /// via — saves the /// user a round-trip to the resolver page when entering historical wagers. /// /// public sealed class RecordPlacedBetUseCase { private readonly IPlacedBetRepository _bets; private readonly IEventRepository _events; private readonly IResultRepository _results; private readonly ILogger _logger; public RecordPlacedBetUseCase( IPlacedBetRepository bets, IEventRepository events, IResultRepository results, ILogger logger) { _bets = bets ?? throw new ArgumentNullException(nameof(bets)); _events = events ?? throw new ArgumentNullException(nameof(events)); _results = results ?? throw new ArgumentNullException(nameof(results)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Persists . Returns the bet as stored — if the /// event already has a result, the returned instance reflects the graded /// . /// /// /// The bet references an unknown event. The journal does not allow free-form /// event codes — wagers must be on events the scraper has captured so the /// CLV calculator can compare against the closing snapshot. /// public async Task ExecuteAsync(PlacedBet bet, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(bet); // Confirm the event exists in the local store. var ev = await _events.GetAsync(bet.EventId, ct).ConfigureAwait(false); if (ev is null) { throw new InvalidOperationException( $"Cannot record a bet on unknown event '{bet.EventId.Value}'. " + "The event must already be present in the scrape store."); } var toPersist = bet; // Auto-grade if a result is already available. if (bet.Outcome == BetOutcome.Pending) { var result = await _results.GetAsync(bet.EventId, ct).ConfigureAwait(false); if (result is not null) { var graded = Marathon.Domain.Betting.BetOutcomeResolver.Resolve(bet.Selection, result); if (graded is not null) { toPersist = bet.WithOutcome(graded.Value); _logger.LogInformation( "RecordPlacedBetUseCase: bet {BetId} on event {EventId} auto-graded as {Outcome}", toPersist.Id, ((DomainEventId)toPersist.EventId).Value, graded.Value); } } } await _bets.AddAsync(toPersist, ct).ConfigureAwait(false); await _bets.SaveChangesAsync(ct).ConfigureAwait(false); _logger.LogInformation( "RecordPlacedBetUseCase: persisted bet {BetId} on event {EventId} stake={Stake} rate={Rate}", toPersist.Id, ((DomainEventId)toPersist.EventId).Value, toPersist.Stake, toPersist.Selection.Rate.Value); return toPersist; } }