From 29fe84da85a9f5b706afb6322f2b434d0fc3c23f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:43:09 +0000 Subject: [PATCH 01/11] Bump @hotwired/turbo in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [@hotwired/turbo](https://github.com/hotwired/turbo). Updates `@hotwired/turbo` from 8.0.2 to 8.0.21 - [Release notes](https://github.com/hotwired/turbo/releases) - [Commits](https://github.com/hotwired/turbo/compare/v8.0.2...v8.0.21) --- updated-dependencies: - dependency-name: "@hotwired/turbo" dependency-version: 8.0.21 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5937ba5..e8f2440 100644 --- a/yarn.lock +++ b/yarn.lock @@ -141,9 +141,9 @@ "@rails/actioncable" "^7.0" "@hotwired/turbo@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.2.tgz#c31cdadfe66b98983066a94073b26fc7e15835f0" - integrity sha512-3K6QZkwWfosAV8zuM5bY+kKF02jp1lMQGsWfSE6wXdZBRBP3ah+Vj26YNqYtkEomBwRWA0QKhZgyJP7xOQkVEg== + version "8.0.21" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.21.tgz#a3e80c01d70048200f64bbe3582b84f9bfac034e" + integrity sha512-fJTv3JnzFHeDxBb23esZSOhT4r142xf5o3lKMFMvzPC6AllkqbBKk5Yb31UZhtIsKQCwmO/pUQrtTUlYl5CHAQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" From 78c7428e6878f49a469f0714d2c722300db13e42 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 14:50:46 -0500 Subject: [PATCH 02/11] Enhance ActiveAdmin Application index by batch-loading payment data and optimizing lodging cost retrieval to prevent N+1 queries --- app/admin/applications.rb | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index 3e43eb7..ee2c2bc 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -28,15 +28,30 @@ filter :subscription, as: :select filter :conf_year, as: :select + controller do + def scoped_collection + end_of_association_chain.includes(:partner_registration) + end + + def index + # Batch-load data to avoid N+1 in index columns + @payments_total_by_user_and_year = Payment + .where(transaction_status: '1') + .group(:user_id, :conf_year) + .sum(Arel.sql("total_amount::numeric")) + @lodgings_by_description = Lodging.all.index_by(&:description) + super + end + end + index do selectable_column actions column :offer_status column "Balance Due" do |application| - users_current_payments = Payment.where(user_id: application.user_id, conf_year: application.conf_year) - ttl_paid = Payment.where(user_id: application.user_id, conf_year: application.conf_year, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 - cost_lodging = Lodging.find_by(description: application.lodging_selection).cost.to_f - cost_partner = application.partner_registration.cost.to_f + ttl_paid = (@payments_total_by_user_and_year[[application.user_id, application.conf_year]] || 0).to_f / 100 + cost_lodging = (@lodgings_by_description[application.lodging_selection]&.cost || 0).to_f + cost_partner = application.partner_registration&.cost.to_f total_cost = cost_lodging + cost_partner balance_due = total_cost - ttl_paid number_to_currency(balance_due) @@ -49,7 +64,7 @@ column :workshop_selection3 column :lodging_selection column "partner_registration_id" do |application| - application.partner_registration.display_name + application.partner_registration&.display_name end column :birth_year end @@ -66,14 +81,14 @@ table_for application.user.payments.where(conf_year: application.conf_year) do #application.user.payments.current_conference_payments column(:id) { |aid| link_to(aid.id, admin_payment_path(aid.id)) } column(:account_type) { |atype| atype.account_type.titleize } - column(:transaction_type) + column(:transaction_type) column(:transaction_date) {|td| Date.parse(td.transaction_date) } column(:total_amount) { |ta| number_to_currency(ta.total_amount.to_f / 100) } end text_node link_to("[Add Manual Payment]", new_admin_payment_path(:user_id => application)) end - + attributes_table do row :user row :conf_year From 9e335ece7fc5cd1bcc1fd11cad949d5279438814 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 14:51:34 -0500 Subject: [PATCH 03/11] Remove redundant payment queries in ActiveAdmin Application show view for improved performance --- app/admin/applications.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index ee2c2bc..76d2ca2 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -71,7 +71,6 @@ def index show do - users_current_payments = Payment.where(user_id: application.user_id, conf_year: application.conf_year) # Payment.current_conference_payments.where(user_id: application.user_id) ttl_paid = Payment.where(user_id: application.user_id, conf_year: application.conf_year, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 cost_lodging = Lodging.find_by(description: application.lodging_selection).cost.to_f cost_partner = application.partner_registration.cost.to_f @@ -176,7 +175,6 @@ def index column :lottery_position column :offer_status column "Balance Due" do |application| - users_current_payments = Payment.current_conference_payments.where(user_id: application.user_id) ttl_paid = Payment.current_conference_payments.where(user_id: application.user_id, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 cost_lodging = Lodging.find_by(description: application.lodging_selection).cost.to_f cost_partner = application.partner_registration.cost.to_f From ab3b767db4e6fce05abffdedebc821f3a64d8bb0 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:03:59 -0500 Subject: [PATCH 04/11] Skip application mock for admin applications index in request specs to ensure real ActiveRecord chain is used --- spec/requests/admin_applications_spec.rb | 38 ++++++++++++++++++++++++ spec/support/application_mock.rb | 3 ++ 2 files changed, 41 insertions(+) create mode 100644 spec/requests/admin_applications_spec.rb diff --git a/spec/requests/admin_applications_spec.rb b/spec/requests/admin_applications_spec.rb new file mode 100644 index 0000000..23aee5d --- /dev/null +++ b/spec/requests/admin_applications_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Applications index', type: :request, no_application_mock: true do + let(:admin_user) { create(:admin_user) } + + describe 'GET /admin/applications' do + context 'when admin is not signed in' do + it 'redirects to admin sign in page' do + get admin_applications_path + expect(response).to redirect_to(new_admin_user_session_path) + end + end + + context 'when admin is signed in' do + before { sign_in admin_user } + + it 'renders successfully with default scope' do + get admin_applications_path + expect(response).to be_successful + end + + it 'renders successfully with scope=all (batch data loaded)' do + get admin_applications_path, params: { scope: 'all' } + expect(response).to be_successful + end + + it 'includes Balance Due column when applications exist' do + create(:lodging, description: 'Standard') + create(:application, lodging_selection: 'Standard') + get admin_applications_path + expect(response).to be_successful + expect(response.body).to include('Balance Due') + end + end + end +end diff --git a/spec/support/application_mock.rb b/spec/support/application_mock.rb index ed7936b..fcd2fec 100644 --- a/spec/support/application_mock.rb +++ b/spec/support/application_mock.rb @@ -1,5 +1,8 @@ RSpec.configure do |config| config.before(:each, type: :request) do + # Skip mock when testing admin applications index (needs real ActiveRecord chain) + next if RSpec.current_example.metadata[:no_application_mock] + # Create a mock for Application.active_conference_applications mock_active_applications = double("ActiveApplications") From 1e15adff40993908b076a3c26695259e9c8e6eea Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:04:06 -0500 Subject: [PATCH 05/11] Optimize ActiveAdmin Application index by implementing a before_action to batch-load payment data, enhancing performance and preventing N+1 queries in the index view. --- app/admin/applications.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index 76d2ca2..87d33a7 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -29,18 +29,24 @@ filter :conf_year, as: :select controller do + before_action :load_index_batch_data, only: [:index] + def scoped_collection end_of_association_chain.includes(:partner_registration) end def index - # Batch-load data to avoid N+1 in index columns + super + end + + private + + def load_index_batch_data @payments_total_by_user_and_year = Payment .where(transaction_status: '1') .group(:user_id, :conf_year) .sum(Arel.sql("total_amount::numeric")) @lodgings_by_description = Lodging.all.index_by(&:description) - super end end @@ -49,8 +55,10 @@ def index actions column :offer_status column "Balance Due" do |application| - ttl_paid = (@payments_total_by_user_and_year[[application.user_id, application.conf_year]] || 0).to_f / 100 - cost_lodging = (@lodgings_by_description[application.lodging_selection]&.cost || 0).to_f + payments_totals = @payments_total_by_user_and_year || {} + lodgings_by_desc = @lodgings_by_description || {} + ttl_paid = (payments_totals[[application.user_id, application.conf_year]] || 0).to_f / 100 + cost_lodging = (lodgings_by_desc[application.lodging_selection]&.cost || 0).to_f cost_partner = application.partner_registration&.cost.to_f total_cost = cost_lodging + cost_partner balance_due = total_cost - ttl_paid From c7a925bc832c92c0f64f45c9d1df34ec437cf176 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:18:46 -0500 Subject: [PATCH 06/11] Refactor ActiveAdmin Dashboard to optimize applicant balance calculations by batch-loading payment data and lodging descriptions, improving performance and reducing query overhead. --- app/admin/dashboard.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index 9826543..f736280 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -45,10 +45,14 @@ end column do panel "#{ApplicationSetting.get_current_app_year} Applicants who accepted their offer (#{Application.application_accepted.count})" do - table_for Application.application_accepted.sort.reverse do + applications = Application.application_accepted.includes(:partner_registration).sort.reverse + raw_payments = Payment.where(transaction_status: '1').group(:user_id, :conf_year).sum(Arel.sql("total_amount::numeric")) + payments_totals = raw_payments.transform_keys { |k| [k[0].to_i, k[1].to_i] } + lodgings_by_desc = Lodging.all.index_by(&:description) + table_for applications do column("Applicant") { |u| link_to(u.display_name, admin_application_path(u.id)) } column("Offer Date") { |od| od.offer_status_date } - column("Balance Due") { |a| number_to_currency a.balance_due } + column("Balance Due") { |a| number_to_currency(a.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)) } end end end From 39fbbe6438bdbbae52d0e357899e6ba027b0a765 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:19:04 -0500 Subject: [PATCH 07/11] Implement balance_due_with_batch method in Application model for optimized balance calculations, and update ActiveAdmin to utilize this method, enhancing performance and reducing query complexity. --- app/admin/applications.rb | 11 ++++------- app/models/application.rb | 14 +++++++++++++- spec/models/application_spec.rb | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index 87d33a7..298b19b 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -42,10 +42,12 @@ def index private def load_index_batch_data - @payments_total_by_user_and_year = Payment + raw = Payment .where(transaction_status: '1') .group(:user_id, :conf_year) .sum(Arel.sql("total_amount::numeric")) + # Normalize keys to [Integer, Integer] so lookup matches regardless of DB return type + @payments_total_by_user_and_year = raw.transform_keys { |k| [k[0].to_i, k[1].to_i] } @lodgings_by_description = Lodging.all.index_by(&:description) end end @@ -57,12 +59,7 @@ def load_index_batch_data column "Balance Due" do |application| payments_totals = @payments_total_by_user_and_year || {} lodgings_by_desc = @lodgings_by_description || {} - ttl_paid = (payments_totals[[application.user_id, application.conf_year]] || 0).to_f / 100 - cost_lodging = (lodgings_by_desc[application.lodging_selection]&.cost || 0).to_f - cost_partner = application.partner_registration&.cost.to_f - total_cost = cost_lodging + cost_partner - balance_due = total_cost - ttl_paid - number_to_currency(balance_due) + number_to_currency(application.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)) end column :first_name column :last_name diff --git a/app/models/application.rb b/app/models/application.rb index 5b15d72..11f0124 100644 --- a/app/models/application.rb +++ b/app/models/application.rb @@ -87,7 +87,7 @@ def total_user_has_paid end def lodging_cost - Lodging.find_by(description: self.lodging_selection).cost.to_f + Lodging.find_by(description: self.lodging_selection).cost.to_f end def partner_registration_cost @@ -102,6 +102,18 @@ def balance_due total_cost - total_user_has_paid end + # Balance due in dollars using preloaded data (avoids N+1). Payments total is in cents. + # Returns a rounded float suitable for number_to_currency. + def balance_due_with_batch(payments_totals:, lodgings_by_desc:) + key = [user_id.to_i, conf_year.to_i] + ttl_paid_cents = (payments_totals[key] || 0).to_f + ttl_paid_dollars = ttl_paid_cents / 100 + cost_lodging = (lodgings_by_desc[lodging_selection]&.cost || 0).to_f + cost_partner = (partner_registration&.cost || 0).to_f + total_cost = cost_lodging + cost_partner + (total_cost - ttl_paid_dollars).round(2) + end + def first_workshop_instructor Workshop.find(workshop_selection1).instructor end diff --git a/spec/models/application_spec.rb b/spec/models/application_spec.rb index 84fed9f..81d5bdd 100644 --- a/spec/models/application_spec.rb +++ b/spec/models/application_spec.rb @@ -191,6 +191,40 @@ end end + describe '#balance_due_with_batch' do + it 'returns balance as total cost minus paid (payments in cents), rounded to 2 decimals' do + app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'Standard') + allow(app).to receive(:partner_registration).and_return(double('PartnerRegistration', cost: 50.0)) + payments_totals = { [100, 2026] => 10_000 } # $100 in cents + lodgings_by_desc = { 'Standard' => double('Lodging', cost: 100.0) } + expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(50.0) + end + + it 'uses 0 for paid when key is missing in payments_totals' do + app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'Standard') + allow(app).to receive(:partner_registration).and_return(double('PartnerRegistration', cost: 50.0)) + payments_totals = {} + lodgings_by_desc = { 'Standard' => double('Lodging', cost: 100.0) } + expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(150.0) + end + + it 'returns negative balance when paid exceeds total cost (overpayment)' do + app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'Standard') + allow(app).to receive(:partner_registration).and_return(double('PartnerRegistration', cost: 50.0)) + payments_totals = { [100, 2026] => 20_000 } # $200 in cents + lodgings_by_desc = { 'Standard' => double('Lodging', cost: 100.0) } + expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(-50.0) + end + + it 'uses 0 for missing lodging or partner and rounds to 2 decimals' do + app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'Unknown') + allow(app).to receive(:partner_registration).and_return(nil) + payments_totals = { [100, 2026] => 0 } + lodgings_by_desc = {} + expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(0.0) + end + end + describe '#first_workshop_instructor' do it 'returns the instructor of the first workshop' do workshop = double("Workshop", instructor: "John Smith") From 1b0e5adffc5c14dfe25d1b6b20bb913d40ef64f0 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:31:52 -0500 Subject: [PATCH 08/11] Refactor ActiveAdmin Application balance calculation to use controller instance variables for improved clarity and maintainability. --- app/admin/applications.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index 298b19b..ec9ef47 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -57,8 +57,8 @@ def load_index_batch_data actions column :offer_status column "Balance Due" do |application| - payments_totals = @payments_total_by_user_and_year || {} - lodgings_by_desc = @lodgings_by_description || {} + payments_totals = controller.instance_variable_get(:@payments_total_by_user_and_year) || {} + lodgings_by_desc = controller.instance_variable_get(:@lodgings_by_description) || {} number_to_currency(application.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)) end column :first_name From c02285dd31a81d73a7f82d657e87a685b8b6a1e8 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:51:09 -0500 Subject: [PATCH 09/11] Enhance error handling in Application model and ActiveAdmin for missing partner_registration, ensuring data integrity and preventing incorrect balance calculations. --- app/admin/applications.rb | 10 ++++++++-- app/models/application.rb | 4 +++- spec/models/application_spec.rb | 5 +++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index ec9ef47..9fd62a0 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -78,6 +78,9 @@ def load_index_batch_data ttl_paid = Payment.where(user_id: application.user_id, conf_year: application.conf_year, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 cost_lodging = Lodging.find_by(description: application.lodging_selection).cost.to_f + if application.partner_registration.nil? + raise "Partner registration is missing for this application (id=#{application.id}); cannot compute balance. Fix data and retry." + end cost_partner = application.partner_registration.cost.to_f total_cost = cost_lodging + cost_partner balance_due = total_cost - ttl_paid @@ -119,7 +122,7 @@ def load_index_batch_data row :workshop_selection3 row :lodging_selection row "partner_registration_id" do |app| - app.partner_registration.display_name + app.partner_registration&.display_name end row :partner_first_name row :partner_last_name @@ -180,6 +183,9 @@ def load_index_batch_data column :lottery_position column :offer_status column "Balance Due" do |application| + if application.partner_registration.nil? + raise "Application #{application.id}: partner_registration is missing; cannot compute balance for CSV export." + end ttl_paid = Payment.current_conference_payments.where(user_id: application.user_id, transaction_status: '1').pluck(:total_amount).map(&:to_f).sum / 100 cost_lodging = Lodging.find_by(description: application.lodging_selection).cost.to_f cost_partner = application.partner_registration.cost.to_f @@ -196,7 +202,7 @@ def load_index_batch_data column :workshop_selection3 column :lodging_selection column "partner_registration_id" do |app| - app.partner_registration.display_name + app.partner_registration&.display_name end column :birth_year column :street diff --git a/app/models/application.rb b/app/models/application.rb index 11f0124..8ac5f9d 100644 --- a/app/models/application.rb +++ b/app/models/application.rb @@ -104,12 +104,14 @@ def balance_due # Balance due in dollars using preloaded data (avoids N+1). Payments total is in cents. # Returns a rounded float suitable for number_to_currency. + # Raises if partner_registration is missing (data integrity: do not treat missing as $0). def balance_due_with_batch(payments_totals:, lodgings_by_desc:) + raise "Application #{id}: partner_registration is missing; cannot compute balance." if partner_registration.nil? key = [user_id.to_i, conf_year.to_i] ttl_paid_cents = (payments_totals[key] || 0).to_f ttl_paid_dollars = ttl_paid_cents / 100 cost_lodging = (lodgings_by_desc[lodging_selection]&.cost || 0).to_f - cost_partner = (partner_registration&.cost || 0).to_f + cost_partner = partner_registration.cost.to_f total_cost = cost_lodging + cost_partner (total_cost - ttl_paid_dollars).round(2) end diff --git a/spec/models/application_spec.rb b/spec/models/application_spec.rb index 81d5bdd..1e3ec49 100644 --- a/spec/models/application_spec.rb +++ b/spec/models/application_spec.rb @@ -216,12 +216,13 @@ expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(-50.0) end - it 'uses 0 for missing lodging or partner and rounds to 2 decimals' do + it 'raises when partner_registration is missing (do not treat as $0)' do app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'Unknown') allow(app).to receive(:partner_registration).and_return(nil) payments_totals = { [100, 2026] => 0 } lodgings_by_desc = {} - expect(app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc)).to eq(0.0) + expect { app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc) } + .to raise_error(/partner_registration is missing/) end end From 082ebc756aa278c4fba1ac9bbf1eabc786cb0cba Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 15:55:15 -0500 Subject: [PATCH 10/11] Remove unused index method from ActiveAdmin Application, streamlining the controller and improving code clarity. --- app/admin/applications.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/admin/applications.rb b/app/admin/applications.rb index 9fd62a0..3bc5eff 100644 --- a/app/admin/applications.rb +++ b/app/admin/applications.rb @@ -35,10 +35,6 @@ def scoped_collection end_of_association_chain.includes(:partner_registration) end - def index - super - end - private def load_index_batch_data From b6cc89c4fc009ce6860f2116da8705bd70b7b741 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 3 Feb 2026 16:13:44 -0500 Subject: [PATCH 11/11] Enhance balance_due_with_batch method in Application model to raise errors for missing lodging selections, improving data integrity and error handling in balance calculations. --- app/models/application.rb | 6 ++++-- spec/models/application_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/models/application.rb b/app/models/application.rb index 8ac5f9d..49e40f5 100644 --- a/app/models/application.rb +++ b/app/models/application.rb @@ -104,13 +104,15 @@ def balance_due # Balance due in dollars using preloaded data (avoids N+1). Payments total is in cents. # Returns a rounded float suitable for number_to_currency. - # Raises if partner_registration is missing (data integrity: do not treat missing as $0). + # Raises if partner_registration or lodging is missing (data integrity: do not treat missing as $0). def balance_due_with_batch(payments_totals:, lodgings_by_desc:) raise "Application #{id}: partner_registration is missing; cannot compute balance." if partner_registration.nil? + lodging = lodgings_by_desc[lodging_selection] + raise "Application #{id}: lodging not found for selection '#{lodging_selection}'; cannot compute balance." if lodging.nil? key = [user_id.to_i, conf_year.to_i] ttl_paid_cents = (payments_totals[key] || 0).to_f ttl_paid_dollars = ttl_paid_cents / 100 - cost_lodging = (lodgings_by_desc[lodging_selection]&.cost || 0).to_f + cost_lodging = lodging.cost.to_f cost_partner = partner_registration.cost.to_f total_cost = cost_lodging + cost_partner (total_cost - ttl_paid_dollars).round(2) diff --git a/spec/models/application_spec.rb b/spec/models/application_spec.rb index 1e3ec49..c1aa56b 100644 --- a/spec/models/application_spec.rb +++ b/spec/models/application_spec.rb @@ -224,6 +224,15 @@ expect { app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc) } .to raise_error(/partner_registration is missing/) end + + it 'raises when lodging is not found in lodgings_by_desc (do not treat as $0)' do + app = build(:application, user_id: 100, conf_year: 2026, lodging_selection: 'DeletedOrInvalidLodging') + allow(app).to receive(:partner_registration).and_return(double('PartnerRegistration', cost: 50.0)) + payments_totals = { [100, 2026] => 0 } + lodgings_by_desc = { 'Standard' => double('Lodging', cost: 100.0) } + expect { app.balance_due_with_batch(payments_totals: payments_totals, lodgings_by_desc: lodgings_by_desc) } + .to raise_error(/lodging not found for selection 'DeletedOrInvalidLodging'/) + end end describe '#first_workshop_instructor' do