diff --git a/app/services/registration_checker.rb b/app/services/registration_checker.rb index c386b57b..c255cec2 100644 --- a/app/services/registration_checker.rb +++ b/app/services/registration_checker.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true COMMENT_CHARACTER_LIMIT = 240 +PREREGISTRATIONS_FRACTION_ALLOWED = 0.2 class RegistrationChecker def self.create_registration_allowed!(registration_request, competition_info, requesting_user) @@ -54,7 +55,7 @@ def user_can_create_registration! raise RegistrationError.new(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless @requester_user_id == @requestee_user_id # Only organizers can register when registration is closed, and they can only register for themselves - not for other users - raise RegistrationError.new(:forbidden, ErrorCodes::REGISTRATION_CLOSED) unless @competition_info.registration_open? || organizer_modifying_own_registration? + raise RegistrationError.new(:forbidden, ErrorCodes::REGISTRATION_CLOSED) unless @competition_info.registration_open? || user_may_preregister? can_compete = UserApi.can_compete?(@requestee_user_id, @competition_info.start_date) raise RegistrationError.new(:unauthorized, ErrorCodes::USER_CANNOT_COMPETE) unless can_compete @@ -69,8 +70,20 @@ def user_can_modify_registration! raise RegistrationError.new(:forbidden, ErrorCodes::ALREADY_REGISTERED_IN_SERIES) if existing_registration_in_series? end - def organizer_modifying_own_registration? - @competition_info.is_organizer_or_delegate?(@requester_user_id) && (@requester_user_id == @requestee_user_id) + def user_may_preregister? + # Can only preregister if registration hasn't closed (ie, if registration is not yet open) + return false unless @competition_info.registration_not_yet_opened? + + # User must be a listed organizer/delegate to preregister + return false unless @competition_info.is_organizer_or_delegate?(@requester_user_id) + + # Organizer/delegate cannot preregister someone else + return false unless @requester_user_id == @requestee_user_id + + # Can only preregister if preregisration limit is not reached + preregistrations_allowed = (@competition_info.competitor_limit*PREREGISTRATIONS_FRACTION_ALLOWED).to_i + competitor_count = Registration.where(competition_id: @competition_info.competition_id).count + competitor_count < preregistrations_allowed end def can_administer_or_current_user? diff --git a/lib/competition_info.rb b/lib/competition_info.rb index 95271fde..b3540771 100644 --- a/lib/competition_info.rb +++ b/lib/competition_info.rb @@ -81,4 +81,8 @@ def user_can_cancel? def other_series_ids @competition_json['competition_series_ids']&.reject { |id| id == competition_id } end + + def registration_not_yet_opened? + DateTime.now < @competition_json['registration_open'] + end end diff --git a/spec/factories/competition_factory.rb b/spec/factories/competition_factory.rb index 7236af4e..910c681c 100644 --- a/spec/factories/competition_factory.rb +++ b/spec/factories/competition_factory.rb @@ -9,11 +9,11 @@ id { 'CubingZANationalChampionship2023' } name { 'CubingZA National Championship 2023' } event_ids { events } - registration_open { '2023-05-05T04:00:00.000Z' } - registration_close { 1.week.from_now.iso8601 } - announced_at { '2023-05-01T15:59:53.000Z' } - start_date { '2023-06-16' } - end_date { '2023-06-18' } + registration_open { (DateTime.now-2) } + registration_close { (DateTime.now+10) } + announced_at { (DateTime.now-3) } + start_date { (DateTime.now+15).iso8601 } + end_date { (DateTime.now+16) } competitor_limit { 120 } cancelled_at { nil } url { 'https://www.worldcubeassociation.org/competitions/CubingZANationalChampionship2023' } @@ -48,13 +48,13 @@ guests_per_registration_limit { nil } end - trait :closed do + trait :not_open_yet do registration_opened? { false } - event_change_deadline_date { '2022-06-14T00:00:00.000Z' } + registration_open { DateTime.now+1 } end trait :event_change_deadline_passed do - event_change_deadline_date { '2022-06-14T00:00:00.000Z' } + event_change_deadline_date { DateTime.now-1 } end trait :no_guests do @@ -65,6 +65,13 @@ competition_series_ids { ['CubingZANationalChampionship2023', 'CubingZAWarmup2023'] } end + trait :closed do + registration_opened? { false } + registration_open { DateTime.now-3 } + registration_close { DateTime.now-1 } + event_change_deadline_date { DateTime.now-1 } + end + # TODO: Create a flag that returns either the raw JSON (for mocking) or a CompetitionInfo object after(:create) do |competition, evaluator| stub_request(:get, comp_api_url(competition['competition_id'])).to_return(status: evalutor.mocked_status_code, body: competition) if evaluator.mock_competition diff --git a/spec/services/registration_checker_spec.rb b/spec/services/registration_checker_spec.rb index f9795ddb..8a02a3f3 100644 --- a/spec/services/registration_checker_spec.rb +++ b/spec/services/registration_checker_spec.rb @@ -52,7 +52,7 @@ it "after edit deadline/reg close, organizer can change 'status' => #{old_status} to: #{new_status}" do registration = FactoryBot.create(:registration, registration_status: old_status) - competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :not_open_yet)) update_request = FactoryBot.build(:update_request, :organizer_for_user, user_id: registration[:user_id], competing: { 'status' => new_status }) stub_request(:get, UserApi.permissions_path(update_request['submitted_by'])).to_return( status: 200, @@ -241,7 +241,7 @@ it 'user cant register if registration is closed' do registration_request = FactoryBot.build(:registration_request) - competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :not_open_yet)) stub_request(:get, UserApi.permissions_path(registration_request['user_id'])).to_return(status: 200, body: FactoryBot.build(:permissions_response).to_json, headers: { content_type: 'application/json' }) expect { @@ -254,7 +254,7 @@ it 'organizers can register before registration opens' do registration_request = FactoryBot.build(:registration_request, :organizer) - competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :not_open_yet)) stub_request(:get, UserApi.permissions_path(registration_request['submitted_by'])).to_return(status: 200, body: FactoryBot.build(:permissions_response, organized_competitions: [competition_info.competition_id]).to_json, headers: { content_type: 'application/json' }) @@ -262,6 +262,47 @@ .not_to raise_error end + it 'organizers can take up the last pre-registration slot' do + registration_request = FactoryBot.build(:registration_request, :organizer) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :not_open_yet, competitor_limit: 10)) + FactoryBot.create_list(:registration, 1, registration_status: 'accepted') + stub_request(:get, UserApi.permissions_path(registration_request['submitted_by'])).to_return(status: 200, body: FactoryBot.build(:permissions_response, organized_competitions: [competition_info.competition_id]).to_json, + headers: { content_type: 'application/json' }) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.not_to raise_error + end + + it 'organizers cant preregister if pre-registration limit is met' do + registration_request = FactoryBot.build(:registration_request, :organizer) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :not_open_yet, competitor_limit: 10)) + FactoryBot.create_list(:registration, 2, registration_status: 'accepted') + stub_request(:get, UserApi.permissions_path(registration_request['submitted_by'])).to_return(status: 200, body: FactoryBot.build(:permissions_response, organized_competitions: [competition_info.competition_id]).to_json, + headers: { content_type: 'application/json' }) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::REGISTRATION_CLOSED) + end + end + + it 'organizers cant register after registration closes' do + registration_request = FactoryBot.build(:registration_request, :organizer) + competition_info = CompetitionInfo.new(FactoryBot.build(:competition, :closed)) + stub_request(:get, UserApi.permissions_path(registration_request['submitted_by'])).to_return(status: 200, body: FactoryBot.build(:permissions_response, organized_competitions: [competition_info.competition_id]).to_json, + headers: { content_type: 'application/json' }) + + expect { + RegistrationChecker.create_registration_allowed!(registration_request, competition_info, registration_request['submitted_by']) + }.to raise_error(RegistrationError) do |error| + expect(error.http_status).to eq(:forbidden) + expect(error.error).to eq(ErrorCodes::REGISTRATION_CLOSED) + end + end + it 'organizers cannot create registrations for users' do registration_request = FactoryBot.build(:registration_request, :organizer_submits) competition_info = CompetitionInfo.new(FactoryBot.build(:competition))