c4d87b59d6
Combined-batch reviewer flagged three real blockers + two test-infra
issues across the parallel P2/P3/P5 batch. All resolved:
PHASE 3 — DateTimeOffset UTC-kind constructor (3 sites)
EventListingParserBase.cs:39, EventOddsParser.cs:72, ResultsParser.cs:104
Replaced `new DateTimeOffset(DateTimeOffset.UtcNow.UtcDateTime, MoscowOffset)`
(throws ArgumentException because UtcDateTime has Kind=Utc) with
`DateTimeOffset.UtcNow.ToOffset(MoscowOffset)`.
PHASE 2 — EF string.Compare not translatable (3 sites)
EventRepository.cs:34, SnapshotRepository.cs:46, ExcelExporter.cs:35
Replaced `string.Compare(col, str, StringComparison.Ordinal)` with
`col.CompareTo(str)` so EF Core's SQLite provider can translate the
expression. Semantics unchanged (SQLite default collation = BINARY = ordinal).
PHASE 3 — ServerTimeProvider regex misses JSON-quoted key
Regex `serverTime\s*:\s*"..."` only matched bare-key form. Updated to
`"?serverTime"?\s*:\s*"..."` so the JSON-quoted form (the actual
marathonbet.by production format) is matched.
PHASE 3 — fixture: orphan <td> elements stripped by HTML5 parser
tests/.../Fixtures/marathonbet/event-football-sample.html — wrapped
the <td> blocks in a proper <table><tbody><tr> hierarchy so AngleSharp
preserves them and `td.Closest("td")` succeeds in the parser.
PHASE 2 — InMemoryDbFixture shared state across parallel tests
All fixture instances used `Data Source=marathon_tests` causing xUnit's
parallel-within-class runs to contaminate each other's data. Each fixture
now uses a Guid-suffixed unique data source name.
PLAN.md — P2/P3/P5 rows updated to ✅ Done with batch commit reference.
Test status:
Domain.Tests: 96/96 ✅
Application.Tests: 1/1 ✅
Infrastructure.Tests: 77/77 ✅
UI.Tests: 11/11 ✅
TOTAL: 185/185 ✅
Build: 0 warnings, 0 errors.
Deferred to later phases (per reviewer 🟡 / 🔵 notes):
- SnapshotRepository.GetAsync(Guid) uses lossy GetHashCode workaround;
Phase 4 to fix or remove from interface.
- Excel Sport name column writes string.Empty (need lookup join in Phase 6).
- PeriodScopeMapper football n>2 falls through to "Quarter" token;
guarded by MaxPeriods today, but defensive cleanup at Phase 9.
- Settings.razor duplicate m-rise-5 class on Localization section.
77 lines
2.8 KiB
C#
77 lines
2.8 KiB
C#
using Marathon.Application.Abstractions;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.ValueObjects;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Marathon.Infrastructure.Persistence.Repositories;
|
|
|
|
internal sealed class SnapshotRepository : ISnapshotRepository
|
|
{
|
|
private readonly MarathonDbContext _db;
|
|
|
|
public SnapshotRepository(MarathonDbContext db) => _db = db;
|
|
|
|
public async Task<OddsSnapshot?> GetAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var entity = await _db.Snapshots
|
|
.Include(s => s.Bets)
|
|
.FirstOrDefaultAsync(s => s.Id == (long)key.GetHashCode(), ct);
|
|
// Note: Guid→long mapping is lossy for GetAsync by Guid; the repo interface requires Guid key.
|
|
// Snapshots are typically retrieved by event, not directly by id.
|
|
// A proper implementation would store the Guid as a TEXT column.
|
|
// For now, this method is functionally available — callers prefer ListByEventAsync.
|
|
return entity is null ? null : Mapping.ToDomain(entity);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<OddsSnapshot>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
var entities = await _db.Snapshots.AsNoTracking()
|
|
.Include(s => s.Bets)
|
|
.ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<OddsSnapshot>> ListByEventAsync(
|
|
EventId eventId,
|
|
DateTimeOffset from,
|
|
DateTimeOffset to,
|
|
CancellationToken ct = default)
|
|
{
|
|
var fromStr = from.ToString("O");
|
|
var toStr = to.ToString("O");
|
|
|
|
var entities = await _db.Snapshots.AsNoTracking()
|
|
.Include(s => s.Bets)
|
|
.Where(s => s.EventCode == eventId.Value
|
|
&& s.CapturedAt.CompareTo(fromStr) >= 0
|
|
&& s.CapturedAt.CompareTo(toStr) <= 0)
|
|
.ToListAsync(ct);
|
|
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task AddAsync(OddsSnapshot entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
await _db.Snapshots.AddAsync(efEntity, ct);
|
|
}
|
|
|
|
public Task UpdateAsync(OddsSnapshot entity, CancellationToken ct = default)
|
|
{
|
|
// Snapshots are immutable once written — update is not a typical operation.
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
_db.Snapshots.Update(efEntity);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var entity = await _db.Snapshots.FindAsync([(long)key.GetHashCode()], ct);
|
|
if (entity is not null)
|
|
_db.Snapshots.Remove(entity);
|
|
}
|
|
|
|
public async Task SaveChangesAsync(CancellationToken ct = default) =>
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|