From 2912645499167809031ad005eef773d43a2aa9b1 Mon Sep 17 00:00:00 2001 From: Malaber Date: Wed, 22 Apr 2026 15:19:53 +0200 Subject: [PATCH] fix: require deciders for advancing ties --- app/services/group_stage_service.rb | 7 ++--- spec/controllers/stages_controller_spec.rb | 36 ++++++++++++++++++++++ spec/services/group_stage_service_spec.rb | 34 +++++++++++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index 7480bbc..c4ac9e8 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -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 diff --git a/spec/controllers/stages_controller_spec.rb b/spec/controllers/stages_controller_spec.rb index 355cd6e..55dbcc7 100644 --- a/spec/controllers/stages_controller_spec.rb +++ b/spec/controllers/stages_controller_spec.rb @@ -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 diff --git a/spec/services/group_stage_service_spec.rb b/spec/services/group_stage_service_spec.rb index 6801da3..972009d 100644 --- a/spec/services/group_stage_service_spec.rb +++ b/spec/services/group_stage_service_spec.rb @@ -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)