feat(journal): edit a placed bet
Adds an edit flow to the bet journal — an Edit button per row repurposes the inline entry form (edit mode), saving via a new UpdatePlacedBetUseCase that preserves the original placed-at and re-grades the outcome against the result (so a changed selection/event re-settles). A cancel affordance + an editing banner make the mode obvious; the submit button switches to "Save changes". - UpdatePlacedBetUseCase (loads existing for PlacedAt, validates the event, re-grades) + IBetJournalService.UpdateAsync + service impl; Journal page edit-mode state + per-row Edit button; en/ru resx. - 3 tests: unknown-bet, unknown-event, and preserve-PlacedAt + re-grade.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using FluentAssertions;
|
||||
using Marathon.Application.Abstractions;
|
||||
using Marathon.Application.UseCases;
|
||||
using Marathon.Domain.Entities;
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Marathon.Application.Tests.UseCases;
|
||||
|
||||
public sealed class UpdatePlacedBetUseCaseTests
|
||||
{
|
||||
private static readonly TimeSpan Msk = TimeSpan.FromHours(3);
|
||||
private static readonly DateTimeOffset PlacedAt = new(2026, 5, 16, 12, 0, 0, Msk);
|
||||
|
||||
private readonly IPlacedBetRepository _bets = Substitute.For<IPlacedBetRepository>();
|
||||
private readonly IEventRepository _events = Substitute.For<IEventRepository>();
|
||||
private readonly IResultRepository _results = Substitute.For<IResultRepository>();
|
||||
|
||||
private UpdatePlacedBetUseCase CreateSut() =>
|
||||
new(_bets, _events, _results, NullLogger<UpdatePlacedBetUseCase>.Instance);
|
||||
|
||||
private static Bet Selection(Side side = Side.Side1, decimal rate = 2.0m) =>
|
||||
new(MatchScope.Instance, BetType.Win, side, null, new OddsRate(rate));
|
||||
|
||||
private static PlacedBet Existing(Guid id, EventId eventId) =>
|
||||
new(id, eventId, Selection(rate: 1.5m), 50m, PlacedAt, BetOutcome.Pending, "old note");
|
||||
|
||||
private static Event Ev(EventId id) =>
|
||||
new(id, new SportCode(11), "England", "L", "C", PlacedAt.AddDays(1), "Home", "Away");
|
||||
|
||||
[Fact]
|
||||
public async Task Throws_When_BetUnknown()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
_bets.GetAsync(id, Arg.Any<CancellationToken>()).Returns((PlacedBet?)null);
|
||||
|
||||
var act = async () => await CreateSut().ExecuteAsync(id, new EventId("e"), Selection(), 100m, null);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>().WithMessage("*unknown bet*");
|
||||
await _bets.DidNotReceive().UpdateAsync(Arg.Any<PlacedBet>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Throws_When_EventUnknown()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var eid = new EventId("missing");
|
||||
_bets.GetAsync(id, Arg.Any<CancellationToken>()).Returns(Existing(id, eid));
|
||||
_events.GetAsync(eid, Arg.Any<CancellationToken>()).Returns((Event?)null);
|
||||
|
||||
var act = async () => await CreateSut().ExecuteAsync(id, eid, Selection(), 100m, null);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>().WithMessage("*unknown event*");
|
||||
await _bets.DidNotReceive().UpdateAsync(Arg.Any<PlacedBet>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Updates_PreservesPlacedAt_AppliesChanges_AndRegrades()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var eid = new EventId("evt-1");
|
||||
_bets.GetAsync(id, Arg.Any<CancellationToken>()).Returns(Existing(id, eid));
|
||||
_events.GetAsync(eid, Arg.Any<CancellationToken>()).Returns(Ev(eid));
|
||||
// Side1 wins → the (new) Side1 selection re-grades to Won.
|
||||
_results.GetAsync(eid, Arg.Any<CancellationToken>())
|
||||
.Returns(new EventResult(eid, 2, 0, Side.Side1, PlacedAt.AddHours(3)));
|
||||
|
||||
var updated = await CreateSut().ExecuteAsync(
|
||||
id, eid, Selection(side: Side.Side1, rate: 2.5m), stake: 120m, notes: "new note");
|
||||
|
||||
updated.Id.Should().Be(id);
|
||||
updated.PlacedAt.Should().Be(PlacedAt); // original entry time preserved
|
||||
updated.Stake.Should().Be(120m);
|
||||
updated.Selection.Rate.Value.Should().Be(2.5m);
|
||||
updated.Notes.Should().Be("new note");
|
||||
updated.Outcome.Should().Be(BetOutcome.Won); // re-graded against the result
|
||||
|
||||
await _bets.Received(1).UpdateAsync(
|
||||
Arg.Is<PlacedBet>(b => b.Id == id && b.Outcome == BetOutcome.Won && b.Stake == 120m),
|
||||
Arg.Any<CancellationToken>());
|
||||
await _bets.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user