From f5d8f02a194dbf8f89691185389a9d0501a57ccf Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 24 Mar 2026 15:38:25 -0400 Subject: [PATCH 1/4] Enhance payment processing by accepting whole-dollar amounts formatted as decimals Updated the PaymentsController to handle whole-dollar amounts formatted like decimals in the validated_payment_amount method. Added a corresponding request spec to ensure proper redirection when valid amounts are submitted. This improves the flexibility of payment input handling. --- app/controllers/payments_controller.rb | 11 ++++++++++- spec/requests/payments_spec.rb | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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/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) From 47ad2f274df0db39e02fa5e79eebefd8d5152c2f Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 24 Mar 2026 16:16:03 -0400 Subject: [PATCH 2/4] Refactor admin dashboard and request specs to focus on special invitees Updated the admin dashboard to filter payments to only include special invitees, removing scholarship and other account types. Adjusted the corresponding request specs to reflect this change, ensuring accurate expectations in the response body for special invitees. --- app/admin/dashboard.rb | 3 ++- spec/requests/admin_dashboard_spec.rb | 10 +--------- 2 files changed, 3 insertions(+), 10 deletions(-) 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/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 From 6219240b878cd91b41c5cfadf423d6e3ad45f621 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 24 Mar 2026 16:16:30 -0400 Subject: [PATCH 3/4] update payment specs --- spec/controllers/payments_controller_spec.rb | 6 +- spec/requests/conference_closed_spec.rb | 122 +++++++++++++++++++ spec/support/application_mock.rb | 6 +- spec/support/application_setting_mock.rb | 5 +- 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 spec/requests/conference_closed_spec.rb 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/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/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, From 50a1bac4d0dd34a2c56f28ab81d58ff1f40028fb Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 24 Mar 2026 16:19:43 -0400 Subject: [PATCH 4/4] Add account type filter to payments admin panel Introduced a new filter for account type in the payments admin section, allowing for selection from distinct account types. This enhances the filtering capabilities for better payment management. --- app/admin/payments.rb | 2 ++ 1 file changed, 2 insertions(+) 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