feat: introduce tournament champion tracking and completed state - #156
Merged
Dejmenek merged 18 commits intoSep 16, 2026
Merged
Conversation
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
deleted the
feat/103-no-persisted-tournament-champion-completed-state
branch
September 16, 2026 19:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a
Completedstatus and a persistedTournament.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
TournamentgainsChampionId/Champion, following the same pattern asOwnerId/Owner.TournamentStatusgainsCompleted, appended at the end so the existingOpen/Closedvalues already stored in the database don't shift.BracketCompletionService.SyncChampionAsyncchecks whether the bracket's final round now has a single decided match and sets or clearsCompleted/ChampionIdaccordingly. It runs fromPlayandCorrectMatchResult, 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.Playcall, 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/ApplyCorrectionAsyncnow 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.championandchampionIdare exposed onTournamentin GraphQL.Validation
Completedcan only be reached automatically;CreateTournament/UpdateTournamentreject it as an input value.Completed,UpdateTournamentrejects any further status change.ValidateTournamentCanBeDeletednow also blocks deleting aCompletedtournament with a bracket, the same way it already treatsClosed.MatchValidations.ValidateTournamentIsClosednow acceptsClosedorCompleted, not justClosed. 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
wonTournamentsresolver actually worked at match level: it found the bracket's final match and inferred a tournament win from it. That logic is nowwonMatches, listing every match a user has won regardless of round or tournament outcome.wonTournamentsis rebuilt on top of the persistedChampionId/Completedfields, so it's a direct lookup instead of an inference.Tests
TournamentAPI.UnitTests: new cases forValidateTournamentCanBeDeleted,ValidateStatusIsNotCompleted,ValidateTournamentIsNotCompleted, andValidateTournamentIsClosedwithCompleted.TournamentAPI.IntegrationTests: new or updated coverage inBracketMutationTests,MatchCorrectionMutationTests(including asserting the persisted fields through the champion self-healing scenario),TournamentMutationTests,TournamentQueryTests,TournamentAutoCloseJobTests, andUserQueryTests.