diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index 824c167..a1bc836 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -14,8 +14,9 @@ end end + # Only special payment invitees (not scholarship or other account types). special_invitees = Payment.current_conference_payments - .where(account_type: %w[special scholarship]) + .where(account_type: 'special') .includes(:user) .order(created_at: :desc, id: :desc) .group_by(&:user_id) diff --git a/app/admin/payments.rb b/app/admin/payments.rb index 29fa262..e68dad4 100644 --- a/app/admin/payments.rb +++ b/app/admin/payments.rb @@ -22,6 +22,8 @@ filter :payments_conf_year, as: :select, collection: -> { Payment.order(:conf_year).distinct.pluck(:conf_year) }, label: "Payment Conf Year" + filter :account_type, as: :select, + collection: -> { Payment.order(:account_type).distinct.pluck(:account_type).compact } index do selectable_column diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb index de60361..0f5df1f 100644 --- a/app/controllers/payments_controller.rb +++ b/app/controllers/payments_controller.rb @@ -123,8 +123,17 @@ def generate_hash(current_user, amount=100) end def validated_payment_amount(raw_amount) + return nil if raw_amount.respond_to?(:blank?) ? raw_amount.blank? : raw_amount.nil? + amount = Integer(raw_amount, exception: false) - return nil if amount.nil? || amount <= 0 + if amount.nil? + begin + amount = BigDecimal(raw_amount.to_s).to_i + rescue ArgumentError + return nil + end + end + return nil if amount <= 0 balance_due = current_balance_due return nil if balance_due <= 0 diff --git a/spec/controllers/payments_controller_spec.rb b/spec/controllers/payments_controller_spec.rb index 1ff25a4..29c46ea 100644 --- a/spec/controllers/payments_controller_spec.rb +++ b/spec/controllers/payments_controller_spec.rb @@ -168,10 +168,10 @@ end context 'with different amounts' do - it 'rejects decimal amounts' do + it 'truncates dollar-and-cent strings to whole dollars (Nelnet uses integer dollars)' do post :make_payment, params: { amount: '50.50' } - expect(response).to redirect_to(all_payments_path) - expect(flash[:alert]).to eq('Please enter a valid payment amount.') + expect(response).to be_redirect + expect(response.location).to include('amountDue=5000') end it 'rejects zero amount' do diff --git a/spec/requests/admin_dashboard_spec.rb b/spec/requests/admin_dashboard_spec.rb index 7258ef9..a26fc0b 100644 --- a/spec/requests/admin_dashboard_spec.rb +++ b/spec/requests/admin_dashboard_spec.rb @@ -35,12 +35,8 @@ ) special_user = create(:user, email: 'special@example.com') - scholarship_user = create(:user, email: 'scholarship@example.com') - other_user = create(:user, email: 'other@example.com') create(:payment, :special, user: special_user, conf_year: application_setting.contest_year) - create(:payment, :scholarship, user: scholarship_user, conf_year: application_setting.contest_year) - create(:payment, user: other_user, conf_year: application_setting.contest_year) application = create( :application, @@ -54,15 +50,11 @@ get admin_root_path expect(response).to be_successful - expect(response.body).to include('Special invitees (2)') + expect(response.body).to include('Special invitees (1)') expect(response.body).to include('special@example.com') - expect(response.body).to include('scholarship@example.com') - expect(response.body).to include('Needs to submit an application to') expect(response.body).to include('special') - expect(response.body).to include('scholarship') expect(response.body).to include("href=\"#{admin_application_path(application)}\"") expect(response.body).to include('Ada Lovelace') - expect(response.body.index('scholarship@example.com')).to be < response.body.index('special@example.com') end end end diff --git a/spec/requests/conference_closed_spec.rb b/spec/requests/conference_closed_spec.rb new file mode 100644 index 0000000..390cb60 --- /dev/null +++ b/spec/requests/conference_closed_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Opt out of spec/support/application_setting_mock.rb and application_mock.rb so views use real +# ApplicationSetting rows and Application.active_conference_applications (see those files). +RSpec.describe 'Conference closed page', type: :request, real_application_settings: true do + # Avoid multiple active ApplicationSetting rows: get_current_app_settings uses active.first (unordered). + before { ApplicationSetting.delete_all } + + let(:conf_year) { Time.current.year } + let(:user) { create(:user, email: "conference-closed-#{SecureRandom.hex(8)}@example.com") } + let!(:lodging) { create(:lodging, description: 'Standard', cost: 100.0) } + let(:partner_registration) { create(:partner_registration, cost: 75.0) } + + let!(:application_setting) do + create( + :application_setting, + active_application: true, + contest_year: conf_year, + opendate: 1.month.ago, + application_open_period: 48, + application_closed_directions: '

UniqueClosedDirectionsHtml

', + registration_fee: 50.0 + ) + end + + def create_active_application(offer_status:) + create( + :application, + user: user, + email: user.email, + email_confirmation: user.email, + conf_year: application_setting.contest_year, + offer_status: offer_status, + partner_registration: partner_registration, + lodging_selection: 'Standard' + ) + end + + describe 'GET /conference_closed' do + context 'when the user is signed in with an offer and no payments' do + before { sign_in user } + + %w[registration_offered registration_accepted].each do |status| + it "shows the application fee payment button when offer_status is #{status}" do + create_active_application(offer_status: status) + + get conference_closed_path + + expect(response).to be_successful + expect(response.body).to include('To accept your offer') + expect(response.body).to include('Pay the Application Fee') + expect(response.body).to include('make_payment') + end + end + end + + context 'when the user is signed in with an offer but already has a conference payment' do + before { sign_in user } + + it 'shows the payments sidebar message instead of the pay button' do + create_active_application(offer_status: 'registration_offered') + create( + :payment, + :current_conference, + user: user, + transaction_status: '1', + conf_year: application_setting.contest_year + ) + + get conference_closed_path + + expect(response).to be_successful + expect(response.body).to include( + "You may use the links in the 'Your Details' box to the right to view or manage your payments." + ) + expect(response.body).not_to include('Pay the Application Fee') + end + end + + context 'when the user is signed in but offer_status is not offered or accepted' do + before { sign_in user } + + it 'shows application_closed_directions instead of the pay button' do + create_active_application(offer_status: 'not_offered') + + get conference_closed_path + + expect(response).to be_successful + expect(response.body).to include('UniqueClosedDirectionsHtml') + expect(response.body).not_to include('Pay the Application Fee') + end + end + + context 'when opendate is in the future (pre-open messaging)' do + before do + sign_in user + application_setting.update!(opendate: 1.month.from_now) + create_active_application(offer_status: 'registration_offered') + end + + it 'shows the upcoming application window copy, not the pay button' do + get conference_closed_path + + expect(response).to be_successful + expect(response.body).to include('Thank you for your interest in Bear River.') + expect(response.body).not_to include('Pay the Application Fee') + end + end + + context 'when the visitor is not signed in' do + it 'shows application_closed_directions' do + get conference_closed_path + + expect(response).to be_successful + expect(response.body).to include('UniqueClosedDirectionsHtml') + expect(response.body).not_to include('Pay the Application Fee') + end + end + end +end diff --git a/spec/requests/payments_spec.rb b/spec/requests/payments_spec.rb index 99a008e..1097f38 100644 --- a/spec/requests/payments_spec.rb +++ b/spec/requests/payments_spec.rb @@ -97,6 +97,14 @@ expect(response).to redirect_to("https://payment-url.example.com") end + it "accepts whole-dollar amounts formatted like decimals (e.g. registration_fee from DB)" do + expect_any_instance_of(PaymentsController) + .to receive(:generate_hash).with(user, 50).and_return("https://payment-url.example.com") + + post make_payment_path, params: { amount: "50.0" } + expect(response).to redirect_to("https://payment-url.example.com") + end + it "rejects missing amount input" do post make_payment_path, params: { amount: "" } expect(response).to redirect_to(all_payments_path) diff --git a/spec/support/application_mock.rb b/spec/support/application_mock.rb index ea15f4c..f3810ef 100644 --- a/spec/support/application_mock.rb +++ b/spec/support/application_mock.rb @@ -1,7 +1,9 @@ RSpec.configure do |config| - config.before(:each, type: :request) do + config.before(:each, type: :request) do |example| # Skip mock when testing admin applications index (needs real ActiveRecord chain) - next if RSpec.current_example.metadata[:no_application_mock] + next if example.metadata[:no_application_mock] + # Skip when using real ApplicationSetting rows (same examples need real scopes; see conference_closed_spec) + next if example.metadata[:real_application_settings] # Create a mock for Application.active_conference_applications mock_active_applications = double("ActiveApplications") diff --git a/spec/support/application_setting_mock.rb b/spec/support/application_setting_mock.rb index d4c620c..ed83723 100644 --- a/spec/support/application_setting_mock.rb +++ b/spec/support/application_setting_mock.rb @@ -1,5 +1,8 @@ RSpec.configure do |config| - config.before(:each, type: :request) do + config.before(:each, type: :request) do |example| + # Tag examples with `real_application_settings: true` (or `:real_application_settings`) to use DB rows. + next if example.metadata[:real_application_settings] + # Create a mock ApplicationSetting object mock_app_setting = double("ApplicationSetting", contest_year: Time.current.year,