Files
maraphon-app/src/Marathon.Infrastructure/Persistence/Repositories/PlacedBetRepository.cs
T
alexei.dolgolyov b67030ae7f test+chore: real-SQLite query coverage, batch detect writes, finish date centralization
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.
2026-05-29 01:25:25 +03:00

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