Skip to content
Draft
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: 20 additions & 1 deletion app/services/event_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ def registration_subtotal_cents
end

def grand_total_cents
registration_subtotal_cents + scholarship_total_cents + cont_ed_total_cents + unallocated_bulk_payment_cents
registration_subtotal_cents + scholarship_total_cents + cont_ed_total_cents +
unallocated_bulk_payment_cents + unallocated_direct_payment_cents
end

# Money received through this event's bulk payment submissions that hasn't been
Expand All @@ -198,6 +199,14 @@ def unallocated_bulk_payment_cents
bulk_payments.sum(:amount_cents_remaining)
end

# Leftover cash on direct (non-bulk) payments applied to this event's
# registrations — e.g. a registrant who overpaid, so the payment still carries
# an unallocated balance. Excludes bulk payments, whose remainder is already
# counted by unallocated_bulk_payment_cents, so the two never double-count.
def unallocated_direct_payment_cents
direct_payments.sum(:amount_cents_remaining)
end

# Cash actually collected from registrants: registration payments received plus
# continuing-education fees paid. Excludes scholarships, which are awarded, not
# collected. With due_cents this sums to monies_made_cents.
Expand Down Expand Up @@ -750,6 +759,16 @@ def bulk_payments
)
end

# Direct (non-bulk) payments allocated to this event's active registrations
# that still carry an unallocated balance. Excludes bulk payments so their
# remainder isn't double-counted against unallocated_bulk_payment_cents.
def direct_payments
@direct_payments ||= Payment
.where(id: registration_allocations.where(source_type: "Payment").select(:source_id))
.where("amount_cents_remaining > 0")
.where.not(id: bulk_payments.select(:id))
end

def scholarships
@scholarships ||= Scholarship
.joins(:allocation)
Expand Down
16 changes: 15 additions & 1 deletion app/views/events/dashboard.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
$grand = $registration + $scholarships + $cont_ed. Left-aligned; the headline
figure dominates while the addends stay muted, and the row wraps on mobile. %>
<% show_bulk_payments = @dashboard.unallocated_bulk_payment_cents.positive? %>
<% show_direct_payments = @dashboard.unallocated_direct_payment_cents.positive? %>
<%# Each equation term is colorless at rest and reveals its domain color on
hover — a tinted fill + darker border, matching the amber bulk-payments
term's look but per-domain. border-transparent reserves the border so
Expand Down Expand Up @@ -134,8 +135,21 @@
<p class="hidden lg:block text-xs text-gray-500">Unallocated bulk payments</p>
<% end %>
<% end %>
<% if show_direct_payments %>
<span class="text-2xl font-light text-gray-300 select-none">+</span>
<%# Same amber treatment as the bulk-payments term — leftover cash on
a registrant's direct payment still needs allocating. Links to the
payments index filtered to payments with an unallocated balance. %>
<%= link_to payments_path(has_remaining: "yes"), class: "whitespace-nowrap leading-tight rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 hover:bg-amber-100 transition-colors", title: "View unallocated payments" do %>
<p class="flex items-center gap-1.5 text-lg sm:text-xl font-semibold text-amber-700 tabular-nums">
<i class="fa-solid fa-circle-exclamation"></i>
<%= dollars_from_cents(@dashboard.unallocated_direct_payment_cents) %>
</p>
<p class="hidden lg:block text-xs text-gray-500">Unallocated direct payments</p>
<% end %>
<% end %>
</div>
<p class="mt-1 text-xs text-gray-500 lg:hidden">Registration fees + scholarships + CE fees<%= " + unallocated bulk payments" if show_bulk_payments %></p>
<p class="mt-1 text-xs text-gray-500 lg:hidden">Registration fees + scholarships + CE fees<%= " + unallocated bulk payments" if show_bulk_payments %><%= " + unallocated direct payments" if show_direct_payments %></p>
</div>

<%# Cash view of the same money: what's collected (registration paid +
Expand Down
20 changes: 20 additions & 0 deletions spec/requests/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,26 @@ def submit_agency_name(name)

expect(response.body).not_to include("Unallocated bulk payments")
end

it "shows unallocated direct payments in the equation, linking to the filtered payments index" do
payment = create(:payment, person: person, amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: payment, allocatable: registration, amount: 3_000)

get dashboard_event_path(event)

expect(response).to have_http_status(:ok)
expect(response.body).to include("Unallocated direct payments")
expect(response.body).to include(payments_path(has_remaining: "yes"))
end

it "omits the direct payments term when every payment is fully applied" do
payment = create(:payment, person: person, amount_cents: 3_000, amount_cents_remaining: 3_000)
create(:allocation, source: payment, allocatable: registration, amount: 3_000)

get dashboard_event_path(event)

expect(response.body).not_to include("Unallocated direct payments")
end
end

context "as non-admin non-owner" do
Expand Down
74 changes: 71 additions & 3 deletions spec/services/event_dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@
create(:allocation, source: create(:payment, amount_cents: 5_000, amount_cents_remaining: 5_000),
allocatable: cancelled_reg, amount: 5_000)

# Money: reg1 fully covered (payment + scholarship), reg2 partly paid.
# Money: reg1 fully covered (payment + scholarship), reg2 partly paid. Each
# payment is fully applied to its registration, so the allocation callback
# decrements its remaining to zero — no unallocated balance left over.
create(:allocation, source: create(:payment, amount_cents: 6_000, amount_cents_remaining: 6_000),
allocatable: reg1, amount: 6_000)
scholarship = create(:scholarship, recipient: person1, amount_cents: 4_000, tasks_completed: true)
Expand Down Expand Up @@ -144,18 +146,23 @@
expect(dashboard.registration_subtotal_cents).to eq(16_000)
end

it "reports grand total as registration subtotal plus completed scholarships plus cont ed plus unallocated bulk payments" do
it "reports grand total as registration subtotal plus completed scholarships plus cont ed plus unallocated bulk and direct payments" do
expect(dashboard.grand_total_cents).to eq(20_000)
expect(dashboard.grand_total_cents).to eq(
dashboard.registration_subtotal_cents + dashboard.scholarship_total_cents +
dashboard.cont_ed_total_cents + dashboard.unallocated_bulk_payment_cents
dashboard.cont_ed_total_cents + dashboard.unallocated_bulk_payment_cents +
dashboard.unallocated_direct_payment_cents
)
end

it "reports no unallocated bulk payments without a bulk payment form" do
expect(dashboard.unallocated_bulk_payment_cents).to eq(0)
end

it "reports no unallocated direct payments when every payment is fully applied" do
expect(dashboard.unallocated_direct_payment_cents).to eq(0)
end

it "is not free when the event has a cost" do
expect(dashboard.free?).to be(false)
end
Expand Down Expand Up @@ -847,4 +854,65 @@ def opt_in(person, text:)
expect(dashboard.unallocated_bulk_payment_cents).to eq(0)
end
end

describe "unallocated direct payments" do
let(:event) { create(:event, cost_cents: 10_000) }
let(:payer) { create(:person) }
let!(:registration) { create(:event_registration, event: event, registrant: payer, status: "registered") }

it "sums the leftover balance on direct payments applied to the event's registrations" do
# Overpaid: a $5,000 payment with only $3,000 applied leaves $2,000 unallocated
# (the allocation callback decrements the payment's remaining).
payment = create(:payment, person: payer, amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: payment, allocatable: registration, amount: 3_000)

expect(dashboard.unallocated_direct_payment_cents).to eq(2_000)
end

it "adds the leftover balance to the grand total" do
payment = create(:payment, person: payer, amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: payment, allocatable: registration, amount: 3_000)

expect(dashboard.grand_total_cents).to eq(
dashboard.registration_subtotal_cents + dashboard.scholarship_total_cents +
dashboard.cont_ed_total_cents + dashboard.unallocated_bulk_payment_cents + 2_000
)
end

it "ignores fully-applied direct payments" do
payment = create(:payment, person: payer, amount_cents: 3_000, amount_cents_remaining: 3_000)
create(:allocation, source: payment, allocatable: registration, amount: 3_000)

expect(dashboard.unallocated_direct_payment_cents).to eq(0)
end

it "ignores payments allocated to other events' registrations" do
other_event = create(:event, cost_cents: 10_000)
other_registration = create(:event_registration, event: other_event, registrant: payer, status: "registered")
payment = create(:payment, person: payer, amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: payment, allocatable: other_registration, amount: 3_000)

expect(dashboard.unallocated_direct_payment_cents).to eq(0)
end

it "ignores payments allocated only to inactive registrations" do
cancelled = create(:event_registration, event: event, registrant: create(:person), status: "cancelled")
payment = create(:payment, person: payer, amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: payment, allocatable: cancelled, amount: 3_000)

expect(dashboard.unallocated_direct_payment_cents).to eq(0)
end

it "does not double-count bulk payments that have been applied to registrations" do
bulk_form = create(:form)
create(:event_form, event: event, form: bulk_form, role: "bulk_payment")
submission = create(:form_submission, person: payer, form: bulk_form, event: event, role: "bulk_payment")
bulk_payment = create(:payment, person: payer, form_submission: submission,
amount_cents: 5_000, amount_cents_remaining: 5_000)
create(:allocation, source: bulk_payment, allocatable: registration, amount: 3_000)

expect(dashboard.unallocated_direct_payment_cents).to eq(0)
expect(dashboard.unallocated_bulk_payment_cents).to eq(2_000)
end
end
end