fix(journal): stop bet edits silently un-settling graded bets
A stake/notes-only edit re-graded from Pending, which un-settled a won/lost bet once its result row had been pruned by snapshot retention (the journal is FK-free and outlives results). Now only re-grade when the selection or event actually changed, or the bet is still Pending — mirroring RecordPlacedBet.
This commit is contained in:
@@ -55,23 +55,33 @@ public sealed class UpdatePlacedBetUseCase
|
||||
"The event must already be present in the scrape store.");
|
||||
}
|
||||
|
||||
// Preserve the original entry time; re-grade from Pending so a changed
|
||||
// selection/event settles against the current result.
|
||||
// Only the selection or the event affects grading. When neither changed (e.g. a
|
||||
// stake/notes-only edit) keep the existing outcome — re-grading from Pending here
|
||||
// would SILENTLY UN-SETTLE a won/lost bet whose result row has since been pruned by
|
||||
// snapshot retention (the journal is FK-free and outlives result pruning). A
|
||||
// still-Pending bet is always (re)graded, mirroring RecordPlacedBetUseCase.
|
||||
var gradingInputChanged = !existing.EventId.Equals(eventId)
|
||||
|| !existing.Selection.Equals(selection);
|
||||
var regrade = gradingInputChanged || existing.Outcome == BetOutcome.Pending;
|
||||
|
||||
var toPersist = new PlacedBet(
|
||||
Id: id,
|
||||
EventId: eventId,
|
||||
Selection: selection,
|
||||
Stake: stake,
|
||||
PlacedAt: existing.PlacedAt,
|
||||
Outcome: BetOutcome.Pending,
|
||||
Outcome: regrade ? BetOutcome.Pending : existing.Outcome,
|
||||
Notes: notes);
|
||||
|
||||
var result = await _results.GetAsync(eventId, ct).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
if (regrade)
|
||||
{
|
||||
var graded = Marathon.Domain.Betting.BetOutcomeResolver.Resolve(toPersist.Selection, result);
|
||||
if (graded is not null)
|
||||
toPersist = toPersist.WithOutcome(graded.Value);
|
||||
var result = await _results.GetAsync(eventId, ct).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
{
|
||||
var graded = Marathon.Domain.Betting.BetOutcomeResolver.Resolve(toPersist.Selection, result);
|
||||
if (graded is not null)
|
||||
toPersist = toPersist.WithOutcome(graded.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await _bets.UpdateAsync(toPersist, ct).ConfigureAwait(false);
|
||||
|
||||
@@ -82,4 +82,40 @@ public sealed class UpdatePlacedBetUseCaseTests
|
||||
Arg.Any<CancellationToken>());
|
||||
await _bets.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotesOnlyEdit_PreservesSettledOutcome_EvenWhenResultPruned()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var eid = new EventId("evt-1");
|
||||
var sameSelection = Selection(side: Side.Side1, rate: 2.0m);
|
||||
var settledWon = new PlacedBet(id, eid, sameSelection, 50m, PlacedAt, BetOutcome.Won, "old");
|
||||
_bets.GetAsync(id, Arg.Any<CancellationToken>()).Returns(settledWon);
|
||||
_events.GetAsync(eid, Arg.Any<CancellationToken>()).Returns(Ev(eid));
|
||||
// Result row has been pruned by retention.
|
||||
_results.GetAsync(eid, Arg.Any<CancellationToken>()).Returns((EventResult?)null);
|
||||
|
||||
// Same selection/event — only the note changes.
|
||||
var updated = await CreateSut().ExecuteAsync(id, eid, sameSelection, stake: 50m, notes: "new note");
|
||||
|
||||
updated.Outcome.Should().Be(BetOutcome.Won); // NOT silently reset to Pending
|
||||
updated.Notes.Should().Be("new note");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChangedSelection_WithNoResult_ResetsToPending()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var eid = new EventId("evt-1");
|
||||
var settledWon = new PlacedBet(id, eid, Selection(side: Side.Side1, rate: 2.0m), 50m, PlacedAt, BetOutcome.Won, "old");
|
||||
_bets.GetAsync(id, Arg.Any<CancellationToken>()).Returns(settledWon);
|
||||
_events.GetAsync(eid, Arg.Any<CancellationToken>()).Returns(Ev(eid));
|
||||
_results.GetAsync(eid, Arg.Any<CancellationToken>()).Returns((EventResult?)null);
|
||||
|
||||
// Selection changed (Side1 → Side2) but no result to grade against → must be Pending,
|
||||
// never the now-stale Won.
|
||||
var updated = await CreateSut().ExecuteAsync(id, eid, Selection(side: Side.Side2, rate: 3.0m), stake: 50m, notes: "old");
|
||||
|
||||
updated.Outcome.Should().Be(BetOutcome.Pending);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user