b67030ae7f
Review follow-ups: - (HIGH) Add real-SQLite round-trip tests for the new query methods so the load-bearing lexical O-format date ordering is verified, not just mocked: Anomaly ListByDateRange/CountSince, Snapshot CountSince/ListByEvents grouping, Event Query/GetMany. - (MED) DetectAnomaliesUseCase: one SaveChanges per event instead of per anomaly. - (LOW) Route PlacedBetRepository + ExcelExporter date bounds through SqliteDateText. - (LOW) Backtest: reject a one-sided date range (was silently ignored). - (LOW) Refresh stale comments after the detector fan-out.
88 lines
3.3 KiB
C#
88 lines
3.3 KiB
C#
using Marathon.Application.Abstractions;
|
|
using Marathon.Application.Storage;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Marathon.Infrastructure.Persistence.Repositories;
|
|
|
|
internal sealed class PlacedBetRepository : IPlacedBetRepository
|
|
{
|
|
private readonly MarathonDbContext _db;
|
|
|
|
public PlacedBetRepository(MarathonDbContext db) => _db = db;
|
|
|
|
public async Task<PlacedBet?> GetAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
// AsNoTracking so callers can re-map and UpdateAsync without tripping
|
|
// EF's "another instance with the same key is already tracked" guard.
|
|
var entity = await _db.PlacedBets.AsNoTracking()
|
|
.FirstOrDefaultAsync(b => b.Id == idStr, ct);
|
|
return entity is null ? null : Mapping.ToDomain(entity);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PlacedBet>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
var entities = await _db.PlacedBets.AsNoTracking().ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PlacedBet>> ListByOutcomeAsync(BetOutcome outcome, CancellationToken ct = default)
|
|
{
|
|
var outcomeInt = (int)outcome;
|
|
var entities = await _db.PlacedBets.AsNoTracking()
|
|
.Where(b => b.Outcome == outcomeInt)
|
|
.ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PlacedBet>> ListByDateRangeAsync(DateRange range, CancellationToken ct = default)
|
|
{
|
|
// PlacedAt is stored via SqliteDateText (O-format TEXT) — same lexical-equals-
|
|
// chronological ordering used across the repositories.
|
|
var fromStr = SqliteDateText.Key(range.From);
|
|
var toStr = SqliteDateText.Key(range.To);
|
|
|
|
var entities = await _db.PlacedBets.AsNoTracking()
|
|
.Where(b => b.PlacedAt.CompareTo(fromStr) >= 0
|
|
&& b.PlacedAt.CompareTo(toStr) <= 0)
|
|
.ToListAsync(ct);
|
|
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PlacedBet>> ListByEventAsync(EventId eventId, CancellationToken ct = default)
|
|
{
|
|
var entities = await _db.PlacedBets.AsNoTracking()
|
|
.Where(b => b.EventCode == eventId.Value)
|
|
.ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task AddAsync(PlacedBet entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
await _db.PlacedBets.AddAsync(efEntity, ct);
|
|
}
|
|
|
|
public Task UpdateAsync(PlacedBet entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
_db.PlacedBets.Update(efEntity);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
var entity = await _db.PlacedBets.FirstOrDefaultAsync(b => b.Id == idStr, ct);
|
|
if (entity is not null)
|
|
_db.PlacedBets.Remove(entity);
|
|
}
|
|
|
|
public async Task SaveChangesAsync(CancellationToken ct = default) =>
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|