Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-close registrations after enough paid registrations #10772

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/models/competition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class Competition < ApplicationRecord
competitor_limit_enabled
competitor_limit
competitor_limit_reason
auto_close_threshold
forbid_newcomers
forbid_newcomers_reason
guests_enabled
Expand Down Expand Up @@ -318,6 +319,16 @@ def has_administrative_notes?
end
end

validate :auto_close_threshold_validations
private def auto_close_threshold_validations
errors.add(:auto_close_threshold, I18n.t('competitions.errors.auto_close_not_negative')) if auto_close_threshold < 0
if auto_close_threshold != 0
errors.add(:auto_close_threshold, I18n.t('competitions.errors.use_wca_registration')) unless use_wca_registration
errors.add(:auto_close_threshold, I18n.t('competitions.errors.must_exceed_competitor_limit')) if
competitor_limit.present? && auto_close_threshold <= competitor_limit
end
end

# Only validate on update: nobody can confirm competition on creation.
# The only exception to this is within tests, in which case we actually don't want to run this validation.
validate :schedule_must_match_rounds, if: :confirmed_at_changed?, on: :update
Expand Down Expand Up @@ -925,6 +936,8 @@ def receive_registration_emails=(r)
update_column :external_website, nil
end

after_save :auto_close, if: -> { saved_change_to_auto_close_threshold? }

def website
generate_website ? internal_website : external_website
end
Expand Down Expand Up @@ -2375,6 +2388,7 @@ def to_form_data
"enabled" => competitor_limit_enabled,
"count" => competitor_limit,
"reason" => competitor_limit_reason,
"autoCloseThreshold" => auto_close_threshold,
},
"staff" => {
"staffDelegateIds" => staff_delegates.to_a.pluck(:id),
Expand Down Expand Up @@ -2476,6 +2490,7 @@ def form_errors
"enabled" => errors[:competitor_limit_enabled],
"count" => errors[:competitor_limit],
"reason" => errors[:competitor_limit_reason],
"autoCloseThreshold" => errors[:auto_close_threshold],
},
"staff" => {
"staffDelegateIds" => errors[:staff_delegate_ids],
Expand Down Expand Up @@ -2613,6 +2628,7 @@ def self.form_data_to_attributes(form_data)
competitor_limit_enabled: form_data.dig('competitorLimit', 'enabled'),
competitor_limit: form_data.dig('competitorLimit', 'count'),
competitor_limit_reason: form_data.dig('competitorLimit', 'reason'),
auto_close_threshold: form_data.dig('competitorLimit', 'autoCloseThreshold'),
extra_registration_requirements: form_data.dig('registration', 'extraRequirements'),
on_the_spot_registration: form_data.dig('registration', 'allowOnTheSpot'),
on_the_spot_entry_fee_lowest_denomination: form_data.dig('entryFees', 'onTheSpotEntryFee'),
Expand Down Expand Up @@ -2883,4 +2899,9 @@ def self.form_data_json_schema
},
}
end

def auto_close
threshold_reached = registrations.with_payments.count >= auto_close_threshold && auto_close_threshold > 0
update!(closing_full_registration: true, registration_close: Time.now) if threshold_reached
end
end
6 changes: 6 additions & 0 deletions app/models/registration_payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class RegistrationPayment < ApplicationRecord
belongs_to :refunded_registration_payment, class_name: 'RegistrationPayment', optional: true
has_many :refunding_registration_payments, class_name: 'RegistrationPayment', inverse_of: :refunded_registration_payment, foreign_key: :refunded_registration_payment_id, dependent: :destroy

after_create :attempt_auto_close

monetize :amount_lowest_denomination,
as: "amount",
allow_nil: true,
Expand All @@ -26,4 +28,8 @@ def payment_status
receipt.determine_wca_status
end
end

private def attempt_auto_close
registration.competition.auto_close
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default function CompetitorLimit() {
<InputNumber id="count" min={0} />
<InputTextArea id="reason" />
</ConditionalSection>
<ConditionalSection showIf={hasLimit}>
<InputNumber id="autoCloseThreshold" min={0} />
</ConditionalSection>
</SubSection>
);
}
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,7 @@ en:
enabled: "Competitor limit"
count: "Maximum number of competitors"
reason: "The reason for the competitor limit"
auto_close_threshold: "Auto-close threshold"
staff:
staff_delegate_ids: "WCA Delegate(s)"
trainee_delegate_ids: "WCA Trainee Delegate(s)"
Expand Down Expand Up @@ -1725,6 +1726,7 @@ en:
enabled: "A Competitor Limit is required. Only simultaneous FMC competitions with multiple locations can have no Competitor Limit."
count: "The number of competitors allowed in this competition. For now this number is informational only, and does not yet prevent more people from registering. We are working on adding explicit support for this to the registration flow, but it requires some other work first."
reason: "What is the reason for the limit on competitors? Please fill this out in English!"
auto_close_threshold: "Registration will automatically close when this number of paid registrations is received. Leave at 0 to disable, ignore if you aren't using integrated payments."
staff:
staff_delegate_ids: "WCA Delegates for the competition."
trainee_delegate_ids: "WCA Trainee Delegates for the competition."
Expand Down Expand Up @@ -1934,6 +1936,9 @@ en:
-6002: "You need to finish your registration before you can pay"
#context: and when an error occured
errors:
auto_close_not_negative: "Auto-close threshold must be a positive number"
must_exceed_competitor_limit: "Auto-close threshold must be greater than the competitor limit"
use_wca_registration: "Competition must use WCA registration"
invalid_name_message: "must end with a year and must contain only alphanumeric characters, dashes(-), ampersands(&), periods(.), colons(:), apostrophes('), and spaces( )"
cannot_manage: "Cannot manage competition."
cannot_delete_public: "Cannot delete a competition that is publicly visible."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddAutoCloseToCompetitionsTable < ActiveRecord::Migration[7.2]
def change
add_column :Competitions, :auto_close_threshold, :integer, default: 0, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.2].define(version: 2024_11_24_050607) do
ActiveRecord::Schema[7.2].define(version: 2025_02_04_104514) do
create_table "Competitions", id: { type: :string, limit: 32, default: "" }, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", limit: 50, default: "", null: false
t.string "cityName", limit: 50, default: "", null: false
Expand Down Expand Up @@ -82,6 +82,7 @@
t.boolean "forbid_newcomers", default: false, null: false
t.string "forbid_newcomers_reason"
t.integer "registration_version", default: 0, null: false
t.integer "auto_close_threshold", default: 0, null: false
t.index ["cancelled_at"], name: "index_Competitions_on_cancelled_at"
t.index ["countryId"], name: "index_Competitions_on_countryId"
t.index ["end_date"], name: "index_Competitions_on_end_date"
Expand Down
1 change: 1 addition & 0 deletions lib/database_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def self.actions_to_column_sanitizers(columns_by_action)
registration_version
forbid_newcomers
forbid_newcomers_reason
auto_close_threshold
),
db_default: %w(
connected_stripe_account_id
Expand Down
67 changes: 67 additions & 0 deletions spec/models/competition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1584,4 +1584,71 @@ def change_and_check_activities(new_start_date, new_end_date)
expect(new_competition).not_to be_valid
end
end

context 'auto-close registrations' do
let(:auto_close_comp) { FactoryBot.create(:competition, :registration_open, auto_close_threshold: 5) }
let(:comp) { FactoryBot.create(:competition, :registration_open, :with_competitor_limit, competitor_limit: 3) }

it 'doesnt auto-close if threshold not reached' do
FactoryBot.create(:registration, :paid, competition: auto_close_comp)
expect(auto_close_comp.registration_past?).to eq(false)
end

it 'doesnt auto-close if threshold is 0' do
FactoryBot.create(:registration, :paid, competition: comp)
expect(comp.registration_past?).to eq(false)
end

it 'closes registrations when the close threshold is reached' do
FactoryBot.create_list(:registration, 5, :paid, competition: auto_close_comp)
expect(auto_close_comp.registration_past?).to eq(true)
end

it 'closes registrations when the close threshold is exceeded' do
FactoryBot.create_list(:registration, 5, :paid, competition: comp)

comp.auto_close_threshold = 5
FactoryBot.create(:registration, :paid, competition: comp)

expect(comp.registration_past?).to eq(true)
end

it 'if close threshold is added == number of registrations, registration will close' do
FactoryBot.create_list(:registration, 5, :paid, competition: comp)
expect(comp.registration_past?).to eq(false)

comp.update!(auto_close_threshold: 5)
expect(comp.registration_past?).to eq(true)
end

it 'only auto-closes if the registrations are paid registrations' do
FactoryBot.create_list(:registration, 5, competition: auto_close_comp)
expect(auto_close_comp.registration_past?).to eq(false)
end

context 'validations' do
it 'auto-close threshold must be positive' do
comp.auto_close_threshold = -1
expect(comp).not_to be_valid
expect(comp.errors[:auto_close_threshold]).to include("Auto-close threshold must be a positive number")
end

it 'must be greater than competitor limit' do
comp.auto_close_threshold = comp.competitor_limit
expect(comp).not_to be_valid
expect(comp.errors[:auto_close_threshold]).to include("Auto-close threshold must be greater than the competitor limit")

comp.auto_close_threshold = comp.competitor_limit - 1
expect(comp).not_to be_valid
expect(comp.errors[:auto_close_threshold]).to include("Auto-close threshold must be greater than the competitor limit")
end

it 'comp must use wca registration' do
comp.use_wca_registration = false
comp.auto_close_threshold = 1
expect(comp).not_to be_valid
expect(comp.errors[:auto_close_threshold]).to include("Competition must use WCA registration")
end
end
end
end