From b56e49c3447b6a5eda6c390ffdb2f5e0f094bbbe Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 7 Jun 2026 17:31:22 -0400 Subject: [PATCH] Validate scholarship amount against event registration due Saving a scholarship amount above the registration's remaining due previously raised an unhandled error (or silently over-allocated) via the Allocation after_update callback. Validate up front on the scholarship so the user gets a flash message and an inline field error. Co-Authored-By: Claude Opus 4.8 --- app/controllers/scholarships_controller.rb | 2 + app/models/scholarship.rb | 13 ++++++ app/views/scholarships/_form.html.erb | 3 ++ spec/models/scholarship_spec.rb | 47 +++++++++++++++++++ spec/requests/scholarships_spec.rb | 53 ++++++++++++++++++++++ 5 files changed, 118 insertions(+) diff --git a/app/controllers/scholarships_controller.rb b/app/controllers/scholarships_controller.rb index a54f1a30e6..85e8244de7 100644 --- a/app/controllers/scholarships_controller.rb +++ b/app/controllers/scholarships_controller.rb @@ -61,6 +61,7 @@ def create redirect_to scholarship_save_path, notice: "Scholarship created." else @grants = Grant.selectable_for(@scholarship) + flash.now[:alert] = @scholarship.errors.full_messages.to_sentence render :new, status: :unprocessable_content end end @@ -87,6 +88,7 @@ def update redirect_to scholarship_save_path, notice: "Scholarship updated." else @grants = Grant.selectable_for(@scholarship) + flash.now[:alert] = @scholarship.errors.full_messages.to_sentence render :edit, status: :unprocessable_content end end diff --git a/app/models/scholarship.rb b/app/models/scholarship.rb index 91f7d4e6b8..7deff65c5d 100644 --- a/app/models/scholarship.rb +++ b/app/models/scholarship.rb @@ -11,6 +11,7 @@ class Scholarship < ApplicationRecord validates :amount_cents, numericality: { greater_than_or_equal_to: 0 } validate :recipient_must_match_allocation_registrant validate :within_grant_budget, if: :grant + validate :amount_within_event_registration_due after_update :sync_allocation_amount, if: -> { saved_change_to_amount_cents? } after_create_commit :flag_event_registration_scholarship_requested @@ -44,6 +45,18 @@ def recipient_must_match_allocation_registrant end end + def amount_within_event_registration_due + registration = allocation&.allocatable + return unless registration.is_a?(EventRegistration) + return if amount_cents.to_i <= 0 + + other_allocations_sum = registration.allocations.where.not(id: allocation.id).sum(:amount) + due_cents = registration.event.cost_cents.to_i - other_allocations_sum + return if amount_cents <= due_cents + + errors.add(:amount_dollars, "cannot exceed the event registration amount due (#{MoneyFormatter.dollars_from_cents(due_cents)})") + end + def sync_allocation_amount return unless allocation diff --git a/app/views/scholarships/_form.html.erb b/app/views/scholarships/_form.html.erb index 2b2102909b..59f7dc0fe8 100644 --- a/app/views/scholarships/_form.html.erb +++ b/app/views/scholarships/_form.html.erb @@ -39,6 +39,9 @@ $ <%= f.number_field :amount_dollars, step: 0.01, min: 0, autocomplete: "off", class: "w-full rounded-lg border border-gray-300 bg-white pl-7 pr-3 py-2 text-gray-800 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:outline-none", data: { "scholarship-preview-target": "amount", action: "input->scholarship-preview#update", lpignore: "true", "1p-ignore": "true", bwignore: "true", "form-type": "other" } %> + <% if f.object.errors[:amount_dollars].any? %> +

<%= f.object.errors[:amount_dollars].to_sentence %>

+ <% end %>