Skip to content

feat: introduce tournament champion tracking and completed state - #156

Merged
Dejmenek merged 18 commits into
mainfrom
feat/103-no-persisted-tournament-champion-completed-state
Sep 16, 2026
Merged

Dejmenek merged 18 commits into
mainfrom
feat/103-no-persisted-tournament-champion-completed-state

Conversation

@Dejmenek

Copy link
Copy Markdown
Owner

Summary

This PR adds a Completed status and a persisted Tournament.ChampionId, set automatically once the final match is decided and reverted automatically if a later correction invalidates that match. Both are exposed over GraphQL.

Changes

Champion tracking

  • Tournament gains ChampionId/Champion, following the same pattern as OwnerId/Owner.
  • TournamentStatus gains Completed, appended at the end so the existing Open/Closed values already stored in the database don't shift.
  • BracketCompletionService.SyncChampionAsync checks whether the bracket's final round now has a single decided match and sets or clears Completed/ChampionId accordingly. It runs from Play and CorrectMatchResult, after any correction cascade has settled, so it catches both a bye auto-advancing into the final and a correction that invalidates an already-decided final.
  • To avoid running that check on every Play call, it first does one cheap query asking whether a later round already exists for the match that was touched; only when the answer is no does it fetch the round and check whether it's down to a single match. MatchCorrectionService.PropagateAsync/ApplyCorrectionAsync now return the match where the cascade actually stopped, so the completion check runs against the right round even when a correction ripples several rounds forward.
  • champion and championId are exposed on Tournament in GraphQL.

Validation

  • Completed can only be reached automatically; CreateTournament/UpdateTournament reject it as an input value.
  • Once a tournament is Completed, UpdateTournament rejects any further status change.
  • ValidateTournamentCanBeDeleted now also blocks deleting a Completed tournament with a bracket, the same way it already treats Closed.
  • MatchValidations.ValidateTournamentIsClosed now accepts Closed or Completed, not just Closed. Match corrections and replays have to keep working after a tournament is decided, since that's what lets a correction on an already-completed bracket drop and later restore the champion.

wonTournaments / wonMatches

  • The old wonTournaments resolver actually worked at match level: it found the bracket's final match and inferred a tournament win from it. That logic is now wonMatches, listing every match a user has won regardless of round or tournament outcome.
  • wonTournaments is rebuilt on top of the persisted ChampionId/Completed fields, so it's a direct lookup instead of an inference.

Tests

  • TournamentAPI.UnitTests: new cases for ValidateTournamentCanBeDeleted, ValidateStatusIsNotCompleted, ValidateTournamentIsNotCompleted, and ValidateTournamentIsClosed with Completed.
  • TournamentAPI.IntegrationTests: new or updated coverage in BracketMutationTests, MatchCorrectionMutationTests (including asserting the persisted fields through the champion self-healing scenario), TournamentMutationTests, TournamentQueryTests, TournamentAutoCloseJobTests, and UserQueryTests.

Added a new relationship in `ApplicationDbContext` to associate a `Tournament` with its `Champion`, including a foreign key (`ChampionId`) and `Restrict` delete behavior.

Introduced a `WonTournaments` collection in `ApplicationUser` to track tournaments won by a user.

Added a nullable `ChampionId` property and a `Champion` navigation property to the `Tournament` class to store and reference the champion of a tournament.

Updated the `TournamentStatus` enum to include a new `Completed` status for concluded tournaments.
Introduce `BracketCompletionService` to handle tournament status
and champion synchronization based on the final round of a bracket.
The `SyncChampionAsync` method ensures the tournament's status
and champion are updated when match scores are modified.

Integrate `SyncChampionAsync` into `MatchMutations` to update
tournament state after match updates.
Updated `ApplyCorrectionAsync` and `PropagateAsync` in
`MatchCorrectionService` to return `Match` objects instead
of `void`, enabling downstream operations to use the
corrected or propagated match data. Modified `return`
statements to align with the updated method signatures.

In `MatchMutations`, captured the result of
`ApplyCorrectionAsync` in a new variable `frontierMatch`
to ensure the corrected match object is used in subsequent
logic. Updated `isReplay` logic to utilize `frontierMatch`.
Updated `ValidateTournamentIsClosed` to consider a tournament valid if its status is either `TournamentStatus.Closed` or `TournamentStatus.Completed`. Previously, only `TournamentStatus.Closed` was accepted. This change ensures broader validation for completed tournaments.
Added a new `DefaultMatchOrder` extension method for default sorting of `Match` entities. Updated `MatchService` to include new dependencies (`IUserWonMatchIdsBatchingContext` and `ApplicationDbContext`) and added the `GetWonMatchesByUserAsync` method to retrieve paginated matches won by a specific user.

Enhanced `ApplicationUserResolvers` with a new `GetWonMatches` resolver for fetching user-won matches via GraphQL, with support for pagination, filtering, and sorting. Introduced `UserWonMatchIdsDataLoaders` for efficient batch loading of match IDs grouped by `WinnerId`.
Refactor `UserWonTournamentIdsDataLoaders` to query the
`Tournaments` table directly for completed tournaments with
defined champions. This replaces the previous approach of
analyzing match-level data from the `Matches` table, reducing
complexity and improving performance by leveraging tournament-
level data.
Added two new error codes to `TournamentErrorCodes`:
- `StatusCannotBeSetManually`
- `CannotChangeCompletedStatus`

Introduced corresponding error methods in `TournamentErrors`:
- `TournamentStatusCannotBeSetManually`
- `CannotChangeCompletedTournamentStatus`

Enhanced `TournamentMutations` with new validation checks:
- Prevent manually setting status to "Completed".
- Block modifications to tournaments already "Completed".

Updated `TournamentValidations`:
- Added `ValidateStatusIsNotCompleted` and `ValidateTournamentIsNotCompleted`.
- Updated `ValidateTournamentCanBeDeleted` to handle "Completed" status.
A new asynchronous method `GetChampion` was added to the
`TournamentResolvers` class. This method retrieves the champion
of a tournament based on the `ChampionId` property of the
`Tournament` object. If `ChampionId` is null, the method returns
null; otherwise, it fetches the champion's details using the
`ApplicationUserService.GetApplicationUserByIdAsync` method.
Updated `tournament3` and `tournament16` to reflect their
completed status and added champion details. Introduced
`tournament17`, a new "Bracket Validation Fixture" with a
two-round bracket for testing purposes. Updated the
`AddRangeAsync` call to include the new tournament.
Added new test methods to validate error handling for scenarios
where tournament status is set to `Completed` manually or
modified improperly:
- `CreateTournament_ReturnsStatusCannotBeSetManuallyError_WhenStatusIsCompleted`: Ensures creating a tournament with `Completed` status fails.
- `UpdateTournament_ReturnsStatusCannotBeSetManuallyError_WhenSettingStatusToCompleted`: Ensures updating a tournament's status to `Completed` manually fails.
- `UpdateTournament_ReturnsCannotChangeCompletedStatusError_WhenTournamentIsAlreadyCompleted`: Ensures modifying the status of an already `Completed` tournament fails.
- `DeleteTournament_ReturnsCannotDeleteWithBracketError_WhenTournamentIsCompleted`: Ensures deleting a `Completed` tournament with a bracket fails.
 Updated existing tests to reflect new scenarios:
- `GenerateBracket_ReturnsBracketAlreadyExistsError_WhenBracketAlreadyExists`: Adjusted setup for a closed tournament with an unplayed final.
- `UpdateRound_ReturnsNoMatchesInRoundError_WhenNoMatchesInRound`: Adjusted setup for a round with no matches.
- `UpdateRound_ReturnsNextRoundAlreadyExistsError_WhenNextRoundAlreadyExists`: Adjusted setup for a next round already generated.
- Renamed and updated `UpdateRound_ReturnsBracketAlreadyHasWinnerError_WhenBracketAlreadyHasWinner` to `UpdateRound_ReturnsRoundUpdateNotAllowedError_WhenTournamentIsAlreadyCompleted`.

Added two new tests:
- `Play_MarksTournamentCompletedAndSetsChampion_WhenFinalMatchIsPlayed`: Verifies tournament completion and champion assignment after the final match.
- `Play_DoesNotChangeTournamentCompletionState_WhenMatchIsNotTheFinal`: Verifies no state change for non-final matches.
Added two new test methods to the `TournamentQueryTests` class:
- `GetTournamentById_WithChampion_ReturnsCompletedStatusAndChampion_ForCompletedTournament` verifies that a "COMPLETED" tournament returns the champion's details, including ID and first name.
- `GetTournamentById_WithChampion_ReturnsNullChampion_ForInProgressTournament` ensures that a "CLOSED" tournament with an in-progress bracket does not return champion details.
Updated the `TournamentQueryTests.cs` file to reflect the increase
in the total count of tournaments from 15 to 16. Adjusted the
expected `TotalCount` value in the following test cases:
- `GetAllWithTotalCount`
- `GetAllWithParticipants`
- `GetAllWithBracketAndMatches`
- `GetAllWithOwner`
- `GetAllWithSorting`
- Test ensuring "Cancelled Spring Event" is not present.
Added two new test methods in `UserQueryTests`:
1. `GetMe_WonMatches_IncludesMatchWinsRegardlessOfTournamentCompletion`:
   - Ensures match wins are included in the response even if the tournament is undecided.
   - Validates that the match win appears in `WonMatches` but not in `WonTournaments`.
2. `GetMe_WonMatches_ExcludesMatchesNeedingReplay`:
   - Ensures matches marked as `NeedsReplay` are excluded from the `WonMatches` response.
…o closing

A new test method, `RunAsync_LeavesCompletedTournamentsUntouched_EvenPastStartDate`,
was added to the `TournamentAutoCloseJobTests` class. This test verifies that
tournaments with a status of `Completed` remain unaffected by the
`TournamentAutoCloseJob`, even if their `StartDate` is in the past.
This test ensures that the `ValidateTournamentIsClosed` method correctly returns `null` when the tournament's status is set to `Completed`. The test creates a `Tournament` object with `Id = 1` and `Status = TournamentStatus.Completed`, calls the method, and asserts the result is `null`.
Added tests to validate tournament deletion behavior:
- Ensure error is returned when deleting tournaments with brackets
  in `Closed` or `Completed` statuses.
- Ensure no error is returned when deleting tournaments with
  `Open` status and a bracket or `Closed` status without a bracket.

Added tests to validate status change restrictions:
- Ensure error is returned when setting status to `Completed`
  manually or modifying tournaments with `Completed` status.
- Ensure no error is returned for statuses other than `Completed`.
@Dejmenek Dejmenek linked an issue Sep 16, 2026 that may be closed by this pull request
@Dejmenek
Dejmenek merged commit 68e8969 into main Sep 16, 2026
3 checks passed
@Dejmenek
Dejmenek deleted the feat/103-no-persisted-tournament-champion-completed-state branch September 16, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No persisted tournament champion / completed state

1 participant