--- type: object cluster: money universe: live status: verified entity: app/models/earnings_summary.rb --- # EarningsSummary A plain Ruby object (**not** an Active Record model) that totals a [portfolio](portfolio.md)'s earnings by reason for the "where did my money come from" panel. Verified 2026-08-16 against commit `63732df`. ## Why this shape It lives in `app/models/` but has no table and no superclass (`app/models/earnings_summary.rb:3`). It wraps a portfolio and runs one grouped sum per reason (`:36-41`) — five queries per render, deliberately simple rather than a single grouped query, because it is only ever built for one student at a time. **Known defect — `transaction_fees_cents` always returns 0.** `sum_by_reason` filters `.deposits`, i.e. `transaction_type: :deposit` (`:38`), but fee rows are written with `transaction_type: :fee` by `TransactionFeeProcessor` (`app/services/transaction_fee_processor.rb:26-29`). The two never intersect, so `transaction_fees_cents` (`:30-32`) sums an empty set. It is rendered to students as "Transaction Fees" at `app/views/portfolios/_earnings_summary_card.html.erb:22`, where it always shows $0.00. The fix is to drop `.deposits` for that one reason — but note that `total_earnings_cents` (`:26-28`) deliberately excludes fees, so changing `sum_by_reason` wholesale would alter the total too. ## Shape - PORO; `initialize(portfolio)` (`:6-8`) - Readers, all in **cents**: `attendance_earnings_cents`, `reading_earnings_cents`, `math_earnings_cents`, `awards_cents`, `total_earnings_cents`, `transaction_fees_cents` (`:10-32`) - `total_earnings_cents` = attendance + reading + math + awards (`:26-28`). Fees are **not** subtracted. - No caching, no memoization — each reader hits the database It covers four of the seven `reason` values. `administrative_adjustments` and `transaction_fees` are not part of the total; `grade_earnings` is leftover. ## Connected to - **owns:** — - **owned-by:** [portfolio](portfolio.md) (by construction, not by association) - **joins:** reads [portfolio-transaction](portfolio-transaction.md) - **looks-like-but-is-not:** not an Active Record model — `EarningsSummary.find` and any scope or callback do not exist. It also is **not** the balance: it counts income only and ignores debits, credits, and withdrawals entirely. ## If you change this - **Hits:** `PortfoliosController#show` (`app/controllers/portfolios_controller.rb:10`) and `Admin::StudentsController#show` (`app/controllers/admin/students_controller.rb:21`); the two views that render it — `app/views/portfolios/_earnings_summary_card.html.erb` and `app/views/admin/students/show.html.erb:85-100`. - **Does not hit:** [portfolio](portfolio.md)`#cash_balance`. This class is read-only and entirely parallel to the balance calculation — correcting the fee bug here changes a displayed figure, not anyone's spendable money. ## Surfaces | Surface | Role | |---|---| | `PortfoliosController#show` | student/teacher read | | `Admin::StudentsController#show` | admin read | ## See - Source: `app/models/earnings_summary.rb`