feat(matches): stop in-progress matches

This commit is contained in:
Daniel Schädler 2026-04-29 19:56:25 +02:00
parent 0141d78e4e
commit df227f9397
3 changed files with 82 additions and 7 deletions

View File

@ -45,12 +45,8 @@ class MatchesController < ApplicationController
Match.transaction do
if @match.update(match_params)
handle_match_end if new_state == 'finished'
if @match.group_match? && new_state == 'in_progress'
group = @match.group
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
logger.warn "Updating groups group score failed for #{group}"
end
end
handle_match_stop if new_state == 'not_started'
update_group_scores if @match.group_match? && %w[in_progress not_started].include?(new_state)
render json: @match
synced_tournament = @match.tournament
@ -113,6 +109,19 @@ class MatchesController < ApplicationController
raise ActiveRecord::Rollback
end
def handle_match_stop
@match.match_scores.each do |match_score|
match_score.update!(points: 0, hidden_points: 0)
end
end
def update_group_scores
group = @match.group
return if UpdateGroupsGroupScoresAndSave.call(group: group).success?
logger.warn "Updating groups group score failed for #{group}"
end
def validate_params
case match_params['state']
when 'in_progress'
@ -121,6 +130,9 @@ class MatchesController < ApplicationController
when 'finished'
render json: { error: 'Match can\'t finish in this state' }, status: :unprocessable_content \
unless @match.in_progress?
when 'not_started'
render json: { error: 'Match can\'t stop in this state' }, status: :unprocessable_content \
unless @match.in_progress?
else
render json: { error: 'Invalid target state' }, status: :unprocessable_content
end

View File

@ -233,6 +233,21 @@ RSpec.describe MatchesController, type: :controller do
expect(body[:state]).to eq(valid_update[:state])
end
it 'stops an in-progress match and resets its scores' do
@match.update!(state: :in_progress)
@match.match_scores.each do |match_score|
match_score.update!(points: 7, hidden_points: 5)
end
expect(TournamentLiveBroadcast).to receive(:call).with(@match.tournament.id)
put :update, params: { id: @match.to_param, state: 'not_started' }
expect(response).to be_successful
expect(@match.reload.state).to eq('not_started')
expect(@match.match_scores.reload.map(&:points)).to all(eq(0))
expect(@match.match_scores.reload.map(&:hidden_points)).to all(eq(0))
end
context 'on a running playoff match' do
let(:finished) do
{
@ -385,6 +400,14 @@ RSpec.describe MatchesController, type: :controller do
expect(response).to have_http_status(:unprocessable_content)
end
end
context 'with stop params on a not-started match' do
it 'renders an unprocessable entity response' do
put :update, params: { id: @match.to_param, state: 'not_started' }
expect(response).to have_http_status(:unprocessable_content)
expect(deserialize_response(response)[:error]).to eq("Match can't stop in this state")
end
end
end
context 'as another user' do
@ -401,6 +424,34 @@ RSpec.describe MatchesController, type: :controller do
end
end
context 'on a running group match' do
let(:tournament) { create(:group_stage_tournament, group_count: 1, match_factory: :filled_group_match) }
let(:group) { tournament.stages.find_by(level: -1).groups.first }
let(:group_match) { group.matches.first }
before do
apply_authentication_headers_for tournament.owner
group_match.update!(state: :in_progress)
group_match.match_scores.order(:id).each_with_index do |match_score, index|
match_score.update!(points: index.zero? ? 9 : 2, hidden_points: index.zero? ? 4 : 1)
end
UpdateGroupsGroupScoresAndSave.call(group: group)
end
it 'stops the match, clears scores, and recalculates group scores' do
put :update, params: { id: group_match.to_param, state: 'not_started' }
expect(response).to be_successful
expect(group_match.reload.state).to eq('not_started')
expect(group_match.match_scores.reload.map(&:points)).to all(eq(0))
expect(group_match.match_scores.reload.map(&:hidden_points)).to all(eq(0))
involved_group_scores = group.group_scores.reload.select { |score| group_match.teams.include?(score.team) }
expect(involved_group_scores.map(&:group_points)).to all(eq(0))
expect(involved_group_scores.map(&:scored_points)).to all(eq(0))
expect(involved_group_scores.map(&:received_points)).to all(eq(0))
end
end
context 'on a playoff match that isn\'t ready yet' do
let(:invalid_update) do
{

View File

@ -270,11 +270,14 @@ RSpec.describe 'Backend API surface HTTP E2E' do
started_match = anonymous_client.get("/matches/#{match.fetch(:id)}")
score_to_update = started_match.fetch(:json).fetch(:match_scores).first
score_show = anonymous_client.get("/match_scores/#{score_to_update.fetch(:id)}")
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7 })
score_update = owner_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 7, hidden_points: 11 })
forbidden_score_update = other_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
unauthenticated_score_update = anonymous_client.patch("/match_scores/#{score_to_update.fetch(:id)}", body: { points: 3 })
bet_while_running = owner_client.post("/matches/#{match.fetch(:id)}/bets", body: { team: team.fetch(:id) })
restart_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'in_progress' })
stop_started_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'not_started' })
stopped_match = anonymous_client.get("/matches/#{match.fetch(:id)}")
restop_match = owner_client.patch("/matches/#{match.fetch(:id)}", body: { state: 'not_started' })
playoff_stats = anonymous_client.get("/tournaments/#{playoff_only.fetch(:id)}/statistics")
tied_playoff_match = playoff_only.fetch(:stages)
.flat_map { |stage| stage.fetch(:matches) }
@ -349,12 +352,21 @@ RSpec.describe 'Backend API surface HTTP E2E' do
expect(score_show[:status]).to eq(200)
expect(score_update[:status]).to eq(200)
expect(score_update.dig(:json, :points)).to eq(7)
expect(score_update.dig(:json, :hidden_points)).to eq(11)
expect(forbidden_score_update[:status]).to eq(403)
expect(unauthenticated_score_update[:status]).to eq(401)
expect(bet_while_running[:status]).to eq(422)
expect(bet_while_running.dig(:json, :error)).to eq('Betting is not allowed while match is in_progress')
expect(restart_started_match[:status]).to eq(422)
expect(restart_started_match.dig(:json, :error)).to eq("Match can't start in this state")
expect(stop_started_match[:status]).to eq(200)
expect(stop_started_match.dig(:json, :state)).to eq('not_started')
expect(stopped_match[:status]).to eq(200)
expect(stopped_match.dig(:json, :state)).to eq('not_started')
expect(stopped_match.dig(:json, :match_scores).map { |score| score.fetch(:points) }).to all(eq(0))
expect(stopped_match.dig(:json, :match_scores).map { |score| score.fetch(:hidden_points) }).to all(eq(0))
expect(restop_match[:status]).to eq(422)
expect(restop_match.dig(:json, :error)).to eq("Match can't stop in this state")
expect(playoff_stats[:status]).to eq(501)
expect(playoff_stats[:json]).to eq({})