fix(anomalies): skip orphan-event rows in the feed instead of crashing

AnomalyBrowsingService.TryProject fell back to `new SportCode(0)` when an anomaly's
event was missing — but SportCode throws for 0, which would blow up the whole
feed/dashboard for that row. Anomalies have an FK to events so it was dead in
practice, but an orphaned row now degrades gracefully (skipped, like a row with
unparseable evidence). Closes the flagged latent crash.

- TryProject returns false when the event lookup misses; +1 test.
This commit is contained in:
2026-05-29 13:32:19 +03:00
parent 36178e6d1b
commit 41148a87a6
2 changed files with 25 additions and 7 deletions
@@ -137,14 +137,16 @@ public sealed class AnomalyBrowsingService : IAnomalyBrowsingService
var severity = AnomalySeverityRules.FromScore(anomaly.Score);
events.TryGetValue(anomaly.EventId, out var ev);
// Skip orphan anomalies whose event has been pruned — degrade gracefully rather
// than throwing on a sentinel SportCode(0). Anomalies have an FK to events so this
// is defensive; the feed already drops rows whose evidence won't parse.
if (!events.TryGetValue(anomaly.EventId, out var ev) || ev is null)
return false;
var sport = ev?.Sport ?? new SportCode(0);
var country = ev?.CountryCode ?? string.Empty;
var league = ev?.LeagueId ?? string.Empty;
var title = ev is not null
? ev.Title
: anomaly.EventId.Value;
var sport = ev.Sport;
var country = ev.CountryCode;
var league = ev.LeagueId;
var title = ev.Title;
var preSnap = ToSnapshot(dto.PreSuspension);
var postSnap = ToSnapshot(dto.PostSuspension);
@@ -100,4 +100,20 @@ public sealed class AnomalyBrowsingServiceTests
items.Select(i => i.Score).Should().ContainInOrder(0.80m, 0.60m, 0.40m);
}
[Fact]
public async Task ListAsync_SkipsOrphanAnomaly_When_EventMissing()
{
_anomalies
.ListByDateRangeAsync(Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(), Arg.Any<CancellationToken>())
.Returns(new[] { Make(AnomalyKind.SuspensionFlip) });
// Event pruned → not in the lookup. Must not throw on a SportCode(0) fallback.
_events
.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
.Returns(new Dictionary<EventId, Event>());
var items = await CreateSut().ListAsync(new AnomalyFilter(), CancellationToken.None);
items.Should().BeEmpty();
}
}