Merge branch 'codex/TUR-81-decider-advancing-seeding' into 'master'

TUR-81 require deciders for advancing seeding ties

Closes TUR-81

See merge request turniere/turniere-backend!46
This commit is contained in:
Daniel Schädler 2026-04-22 13:52:24 +00:00
commit ed0484c79c
3 changed files with 72 additions and 5 deletions

View File

@ -224,12 +224,11 @@ class GroupStageService
nil
end
def unresolved_tie_crosses_advancing_cutoff?(sorted_group_scores, group_score, advancing_slots)
def unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
return false if advancing_slots.zero?
cluster = tie_cluster_for(sorted_group_scores, group_score)
occupied_slots = (cluster[:start_index] + 1)..(cluster[:end_index] + 1)
occupied_slots.begin <= advancing_slots && occupied_slots.end > advancing_slots
cluster[:start_index] < advancing_slots
end
def tie_cluster_for(sorted_group_scores, group_score)
@ -298,7 +297,7 @@ class GroupStageService
tied: comparison_reason[:tied],
needs_decider_match: group_ready_for_decider_check &&
comparison_reason[:tied] &&
unresolved_tie_crosses_advancing_cutoff?(sorted_group_scores, group_score, advancing_slots)
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
}
end

View File

@ -201,5 +201,41 @@ RSpec.describe StagesController, type: :controller do
expect(body[:blocking_ties].first[:team_names]).to eq(%w[A B])
end
end
context 'when tied advancing teams still need a decider to fix playoff seeding' do
let(:group_stage) do
create(:group_stage, tournament: create(:tournament, teams_count: 0), group_count: 0)
end
before do
tournament = group_stage.tournament
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
group = create(:group, stage: group_stage, match_count: 0, number: 1)
group_stage.groups = [group]
team_a = create(:team, tournament: tournament, name: 'A')
team_b = create(:team, tournament: tournament, name: 'B')
create(:group_match, group: group, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2)
end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b]).each(&:save!)
apply_authentication_headers_for group_stage.owner
put :update, params: { id: group_stage.to_param }.merge(finished)
end
it 'returns unprocessable entity' do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'returns a helpful decider error' do
body = deserialize_response(response)
expect(body[:error]).to eq('Playoffs cannot be generated yet because a decider match is required for an advancing place')
expect(body[:blocking_ties].size).to eq(1)
expect(body[:blocking_ties].first[:team_names]).to eq(%w[A B])
end
end
end
end

View File

@ -268,7 +268,7 @@ RSpec.describe GroupStageService do
)
end
it 'keeps shared positions and requests a decider only when an unresolved tie crosses the cutoff' do
it 'keeps shared positions and requests a decider when an unresolved tie affects advancing placements' do
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :finished).tap do |match|
@ -300,6 +300,38 @@ RSpec.describe GroupStageService do
)
end
it 'requests a decider when tied teams both advance but their seeding stays unresolved' do
tournament.update!(playoff_teams_amount: 2, instant_finalists_amount: 2, intermediate_round_participants_amount: 0)
create(:group_match, group: group, state: :finished).tap do |match|
create(:match_score, match: match, team: team_a, points: 2)
create(:match_score, match: match, team: team_b, points: 2)
end
score_a = create(:group_score, group: group, team: team_a, group_points: 7, scored_points: 4, received_points: 1)
score_b = create(:group_score, group: group, team: team_b, group_points: 7, scored_points: 4, received_points: 1)
score_c = create(:group_score, group: group, team: team_c, group_points: 3, scored_points: 2, received_points: 3)
score_d = create(:group_score, group: group, team: team_d, group_points: 0, scored_points: 1, received_points: 7)
GroupStageService.send(:recalculate_position_of_group_scores!, [score_a, score_b, score_c, score_d]).each(&:save!)
expect([score_a.position, score_b.position, score_c.position, score_d.position]).to eq([1, 1, 3, 4])
expect(GroupStageService.ranking_decision_for(score_a)).to include(
resolved_by: 'tie',
direct_comparison_result: 'draw',
hidden_points_result: 'not_played',
tied: true,
needs_decider_match: true
)
expect(GroupStageService.ranking_decision_for(score_b)).to include(
resolved_by: 'tie',
direct_comparison_result: 'draw',
hidden_points_result: 'not_played',
tied: true,
needs_decider_match: true
)
end
it 'does not expose unresolved tie metadata while regular matches in the group are still unfinished' do
tournament.update!(playoff_teams_amount: 1, instant_finalists_amount: 1, intermediate_round_participants_amount: 0)