Income Statement Report
Income Statement Report
Component File: src/features/reports/components/IncomeStatementReport.tsx Route / navigation: Path /reports, Zustand reportTool = income-statement. See 00-REPORTS-HUB.md. Access Level: Parent Org and Fund Users with report access (position-based) Last Updated: April 12, 2026
Parity contract (do not drift): `documentation/contracts/INCOME-STATEMENT-PARITY.md` — RPC names, p_exclude_close_entries, shared fetchers, verification SQL, regression gate. Shared drawer contract: `documentation/contracts/LEDGER-DRAWER-INVARIANTS.md` — shared scope and opening-balance rules for report/accounting pullouts.
✨ Recent Updates
Shared donor context helper for revenue JE drill-down (April 6, 2026)
- Refactor: Revenue donation context in the nested
JournalEntryEditDrawernow uses the sharedbuildDonationContextFromLedgerRow()helper instead of report-local logic. - Behavior: Revenue rows continue to show donor attribution in the JE drawer, but the amount/date/donor banner now matches the same derivation rules used across Balance Sheet, Cash Flow, and by-fund report drawers.
Parent-org drawer scope hardening (April 12, 2026)
- Shared helper fix:
fetchLedgerEntriesByAccountCode()now restricts parent-org/admin drawer account lookup and journal-line aggregation to the selected parent organization’s child-fund tree instead of every org in the database that happens to reuse the same account code. - Affected pullouts: Income Statement, Balance Sheet, Cash Flow, Comparative Report, Revenue Reconciliation, and Chart of Accounts.
- Why it matters: Without this scope hardening, opening balances for common codes such as
1000can be massively overstated in parent-org drawers.
Drill-down drawer date sync (April 2, 2026)
- While the drawer is open, changing the report start/end range does not overwrite the drawer’s date pickers.
- While the drawer is closed, drawer dates stay aligned with the report range.
- Clicking a revenue or expense line resets the drawer range to the current report start and end so each drill-down matches what is on screen.
Ledger drill-down paging & drawer refresh (March 21, 2026)
- Data: Paged calls to
fetchLedgerEntriesByAccountCode()load drawer rows in chronological ascending order when the Income Statement drill-down opens (transactionOrder: 'asc'). Load more appends the next rows forward in time. - UX: In-drawer hint
reports.drawer.recentEntriesFirstHintreflects oldest-first loading for the selected range. - JE save/void: Drawer refetch uses the same first page (
limit+offset: 0) as the initial open and resets “has more” state. - Affected files:
IncomeStatementReport.tsx,ledger-reads.ts(shared with Cash Flow, Balance Sheet, Chart of Accounts)
Revenue parity guardrail with Revenue Reconciliation (March 26, 2026; contract locked April 12, 2026)
- Source of truth: Income Statement revenue remains ledger-backed (
journal_entry_lines) viaget_income_statement_data. - No UI override layer: Single-fund and by-fund reports both rely on the RPC contract directly (the old client-side “parity resolver” that swapped totals was removed). For the same fund UUID and date range,
get_income_statement_dataand the correspondingget_income_statement_by_fundslice are expected to agree; investigate RPC/data if they do not. - RPC contract:
balanceis canonical; frontend supports legacyamountonly as fallback. - App parameters:
fetchIncomeStatementDatapassesp_exclude_close_entries: trueso fiscal-year close batches do not zero out closed periods. - Cross-surface rule: For identical entity/date scope, Income Statement total revenue must match Revenue Reconciliation GL revenue baseline.
- Verification script:
supabase/queries/verify_income_statement_revenue_parity.sql(use the same exclude-close semantics as the app — see parity contract doc).
P&L Drawer Standardization — Running Balance Removed (March 6, 2026)
- Changed: Drill-down drawer no longer shows a "Running Balance" or "Amount" column. P&L accounts (revenue/expense) are period-based, not cumulative — a running balance is conceptually misleading for Income Statement items.
- New columns: Date, Description, Debit, Credit (matching the simplified by-fund drawer pattern)
- Donor enrichment: Revenue account drill-downs now show donor names inline via donation-to-journal-line attribution resolved in
ledger-reads.ts - Parent account fix: Clicking a parent account (e.g.,
4000) now correctly fetches transactions for the parent + all subaccounts (e.g.,4000.01,4000.02). Previously, only the parent code was queried, producing a balance mismatch. - Combined balance calculation:
fetchLedgerEntriesByAccountCode()now computes a combined debit/credit aggregate across all account IDs whenadditionalAccountCodesare provided, instead of callingget_account_balancefor only the primary code. - Affected files:
IncomeStatementReport.tsx,ledger-reads.ts
Overview
The Income Statement Report (also known as Statement of Activities) provides a detailed breakdown of revenue and expenses over a specific time period, showing the organization's financial performance. It categorizes income and expenses into standard nonprofit categories and calculates net income/loss. The report supports drill-down to transaction details and export capabilities.
UI Features
Main Features
- Report Header:
- Organization name
- Date range selector (start and end dates)
- Accounting basis line from Chart of Accounts → Account Defaults; included on PDF export subtitle
- Export button
- Back to Reports Hub button
- Revenue Section:
- Donations (Direct Public Support)
- Earned Income
- Book Sales
- Initial Fees
- Interest Income
- Miscellaneous Revenue
- Admin Fees (parent org revenue)
- Total Revenue
- Expense Sections:
- Program Services:
- Tithe
- Donations (outgoing)
- Family Support
- Foreign Supplies
- Foreign Equipment
- Foreign Construction
- Subtotal
- Personnel:
- Salaries - Officers
- Salaries - Other
- Pension/Retirement
- Benefits
- Payroll Taxes
- Subtotal
- Administrative:
- Legal
- Accounting
- Advertising
- Office Supplies
- Postage
- Printing
- IT
- Software
- Subtotal
- Facilities:
- Rent
- Utilities
- Telephone
- Repairs
- Mortgage Interest
- Subtotal
- Other:
- Travel
- Meals
- Training
- Insurance
- Bank Fees
- Contract Fees
- Donor Appreciation
- IFM Admin Fee
- Miscellaneous
- Subtotal
- Total Expenses
- Net Income/Loss:
- Calculated as Total Revenue - Total Expenses
- Color-coded (green for profit, red for loss)
- Drill-Down Capability:
- Click any line item to view transactions
- Transaction drawer with JE-line detail
- Filter by date range
- Revenue transactions can show donor attribution and donation context in the JE drawer
Income Statement Layout
REVENUE
Donations (Direct Public Support) $47,204.68
Earned Income $5,000.00
Book Sales $1,200.00
Initial Fees $2,500.00
Interest Income $150.00
Miscellaneous Revenue $300.00
Admin Fees $1,500.00
─────────────────────────────────────────────────
Total Revenue $57,854.68
EXPENSES
Program Services
Tithe $4,500.00
Donation $2,000.00
Family Support $3,500.00
Foreign Supplies $1,200.00
Foreign Equipment $800.00
Foreign Construction $5,000.00
─────────────────────────────────────────────
Total Program Services $17,000.00
Personnel
Salaries - Officers $12,000.00
Salaries - Other $8,000.00
Pension/Retirement $1,500.00
Benefits $2,000.00
Payroll Taxes $1,800.00
─────────────────────────────────────────────
Total Personnel $25,300.00
Administrative
Legal $500.00
Accounting $1,200.00
Advertising $800.00
Office Supplies $400.00
Postage $200.00
Printing $300.00
IT $600.00
Software $450.00
─────────────────────────────────────────────
Total Administrative $4,450.00
Facilities
Rent $3,000.00
Utilities $500.00
Telephone $300.00
Repairs $200.00
Mortgage Interest $0.00
─────────────────────────────────────────────
Total Facilities $4,000.00
Other
Travel $1,500.00
Meals $600.00
Training $400.00
Insurance $1,200.00
Bank Fees $150.00
Contract Fees $800.00
Donor Appreciation $300.00
IFM Admin Fee $2,500.00
Miscellaneous $200.00
─────────────────────────────────────────────
Total Other $7,650.00
TOTAL EXPENSES $58,400.00
NET INCOME (LOSS) ($545.32)This layout block is an illustrative report example using standard chart labels. It is not saying donation/refund/event posting code hard-codes these numeric accounts; posting paths use purpose-based mappings / organization defaults where implemented.
Transaction Drill-Down Sheet
- Date
- Description
- Donor Name (for donations)
- Debit
- Credit
- Fund (when applicable)
- Voided status
Data Requirements
Income Statement Data
- organization_id (uuid) - Organization
- start_date (date) - Period start
- end_date (date) - Period end
- revenue (object) - Revenue categories and amounts
- expenses (object) - Expense categories and amounts
- net_income (decimal) - Calculated net income/loss
Revenue Data
- donations (decimal) - Direct public support
- earned_income (decimal) - Program service revenue
- book_sales (decimal) - Product sales
- initial_fees (decimal) - Membership/initial fees
- interest_income (decimal) - Investment income
- misc_revenue (decimal) - Other revenue
- admin_fees (decimal) - Administrative fee revenue
- total_revenue (decimal) - Sum of all revenue
Expense Data (by category)
Each category contains line items with amounts:
- program_services (object) - Program-related expenses
- personnel (object) - Staff-related expenses
- administrative (object) - Admin expenses
- facilities (object) - Facility-related expenses
- other (object) - Other operating expenses
- total_expenses (decimal) - Sum of all expenses
Transaction Detail Data (for drill-down)
- id (uuid) - Transaction ID
- date (date) - Transaction date
- description (string) - Description
- debit (decimal) - Debit amount
- credit (decimal) - Credit amount
- account_code (string) - GL account code
- fund_name (string, nullable) - Organization/fund label shown in the drawer
- donor_id (uuid, nullable) - Donor ID (for donations)
- donor_name (string, nullable) - Donor name
Access & Permissions
- Requires report read access to view the report
- Requires report export access for CSV / Excel / PDF export flows
- Parent-org users can view consolidated activity across child organizations
- Fund users are limited to organizations available through the entity selector mapping
Validation & Error States
- Start and end dates are required
- End date must not precede start date
- Empty periods render a dedicated empty-activity state
- Main report fetch failures render a dedicated error state with retry
- Drawer fetch failures surface toast feedback and clear the drawer table safely
- Export failures surface toast feedback without changing report state
Loading States
- Initial report load uses skeleton / placeholder UI
- Date changes preserve prior data while React Query refetches
- Drawer fetches load independently from the main report body
- Nested journal-entry fetches load independently inside the JE drawer
Report State Model (Main Report Body)
- Loading: Header-level loading indicator while the initial query resolves
- Error: Main report body displays an error state with retry action
- Empty: Main report body displays an empty-activity state when query succeeds with zero revenue/expense rows
- Success: Main report body renders grouped account lines, totals, and net income
Current Frontend Data Sources
fetchIncomeStatementData(entityId, startDate, endDate)supplies the report body totals and grouped account datafetchLedgerEntriesByAccountCode(entityId, accountCode, startDate, endDate, { limit, offset, additionalAccountCodes, enrichDonorNames, transactionOrder? })supplies the drill-down drawer rows (see March 21, 2026 update for paging defaults)applyVoidedEntryDisplay()annotates voided entries / reversals for drawer displayfetchJournalEntryByLineIdForEntity()hydrates the nested Journal Entry Edit Drawer when a row is clicked. In fund-scoped reports, this keeps Aplos batch imports readable by restricting the JE drawer to the current entity’s attributed lines (fund_id preferred, organization_id fallback when fund_id is null) even when the batchentry_numberspans many funds.
Void integrity (multi-fund / shared entry_number)
- Invariant: For a given
entry_number, all original lines (not void-reversal rows) must share the sameis_voidedflag. The application void path updates every line in the batch; mixed state usually indicates import drift or manual SQL and should be repaired injournal_entry_lines. - Frontend:
computeJournalEntryStatusFromLines()injournal-entries.tsdrives entry-level status forfetchJournalEntryByLineId,fetchJournalEntryByLineIdForEntity, andfetchJournalEntriesPaginated(RPC list). Inconsistent batches are surfaced as posted with a console warning until data is fixed. - Drill-down:
fetchLedgerEntriesByAccountCodereturns per-lineis_voidedfor styling viaapplyVoidedEntryDisplay().fetchLedgerEntriesByAccountAndFundexcludes voided lines entirely—so mixed metadata still makes consolidated vs by-fund views disagree until the DB is aligned. - Ops / verification:
supabase/queries/repair_inconsistent_void_flags.sql,supabase/queries/verify_journal_void_integrity.sql - Grouping:
groupTransactionsByEntry()usesisVoidedEntry/isVoidReversalfromapplyVoidedEntryDisplaywhen present so grouped-row badges match row styling.
Drawer Transaction Shape
- date / displayDate - Transaction date plus the void-aware display date
- description - Journal-entry line description
- debit / credit - Raw JE amounts shown in the 4-column drawer
- fundName - Organization / fund label shown beside the row when available
- donorName / donorId / donorNames - Revenue-only donor attribution fields
- isVoidedEntry / isVoidReversal / voidedOn - Voided-entry display state
Data Source
- Primary Table:
journal_entry_linesjoined withaccounts - Filtering: By
organization_id(or all if "All Nonprofits" selected) - Date Range: User-selectable start/end dates
- Account Type: Filters by
accounts.type('revenue' or 'expense')
*All ledger data lives in journal_entry_lines (the ledger_entries table was dropped in January 2026).*
Key Functions
fetchIncomeStatementData()- Aggregates revenue/expenses by account code (uses RPC)fetchLedgerEntriesByAccountCode()— Fetches JE-line transaction details for drawer drill-down (paginated “recent slice first”; see March 21, 2026 update and Developer Playbook §17.5)
RPC Data Contract Notes (March 2026; see INCOME-STATEMENT-PARITY.md)
get_income_statement_datareturns account value inbalance(canonical)- Frontend mapper reads
balancefirst and temporarily tolerates legacyamount(warning once) for backward compatibility - Rows missing both
balanceandamountdefault to0and emit a warning once per fetch context - Exclude close entries: the UI uses the overload that accepts
p_exclude_close_entriesand passestrue(seefetchIncomeStatementDatainfinancial-reports.ts)
Recent Changes (March 2026)
- Income Statement data-contract hardening (March 20, 2026) — Frontend mapping now treats
balanceas canonical for income-statement RPC responses, with guarded compatibility for legacyamountpayloads to prevent all-zero regressions. - Main report state differentiation (March 20, 2026) — Main report body now separates error, empty-period, and success rendering so failures no longer appear as generic no-data.
- Donor attribution in drilldown — Revenue account drilldowns now show the donor name next to each transaction row. The data layer resolves donors at the journal-entry level by matching sibling JE lines against the
donationstable (buildDonorAttributionMapinledger-reads.ts). This works even though reports display credit-side lines while donations link to debit-side lines. - Donation context in JE drawer — When clicking a revenue transaction that has donor attribution, the Journal Entry Edit Drawer now shows a "Donation Context" banner with the donor name, amount, and date. Single-donor entries include a clickable link that deep-links to the Donor CRM profile via
setPendingSearchResult+navigateTo. - Duplicate close button fix — The shell
StackedDrawerContentclose button is now hidden (hideCloseButton) while the JE sub-drawer is open. TheJournalEntryEditDrawerowns its own guarded close button that intercepts unsaved-change dismissal. Applied across all 11 JE drawer consumers (reports, accounting, donors). - Shell close-control primitives —
StackedDrawerContentandSheetContentboth gained an optionalhideCloseButtonprop to let child content own the close action. - View-mode footer Close removed — The redundant footer "Close" button in view mode was removed from
JournalEntryEditDrawer. The top-right X button is the single close affordance.
Recent Changes (February 2026)
- Historical note — The earlier February drawer simplification was later superseded by the March 6, 2026 P&L drawer standardization at the top of this doc.
- Wired `highlightLineId` — The clicked transaction line is now highlighted and scrolled-to in the Journal Entry Edit Drawer.
- Net Income conditional coloring — Net Income line now shows green when positive, red when negative (per STYLING-GUIDE §Financial Indicator Colors).
- Removed transaction grouping toggle — The "Grouped / All Lines" toggle was removed; the drawer now shows a flat list of revenue/expense items.
- JournalEntryEditDrawer uses color helpers — Hardcoded debit/credit colors replaced with
DEBIT_TEXT_CLASS/CREDIT_TEXT_CLASSfromaccountingAmountColors.ts.
Recent Changes (January 2026)
- Removed Functional Expense Summary - This Form 990-specific feature was moved to the Form 990 Builder tool where it belongs
- Removed Cash/Accrual Toggle - The toggle was non-functional (data query doesn't support accrual basis yet). Will be re-added when accrual accounting is fully implemented
Recent Fixes (December 2025)
- Historical note - This fix removed the old 2-character prefix bug in drill-down filtering.
Related Documentation
- INCOME-STATEMENT-PARITY.md — locked parity rules for IS / by-fund / reconciliation
- 01-BALANCE-SHEET-REPORT.md - Balance sheet
- 12-INCOME-STATEMENT-BY-FUND.md - Income statement by fund
- ../donors/02-DONATIONS-MANAGER.md - Donation source
- ../accounting/06-EXPENSES-MANAGER.md - Expense source
- ../accounting/04-GENERAL-LEDGER.md - Transaction source
- 01-DATA-SCHEMA.md - Data models
Regression Checklist (Income Mapping)
Use this checklist after any change to report RPCs or financial-reports.ts mapping logic. The canonical gate list lives in `INCOME-STATEMENT-PARITY.md` section 9.
1. RPC contract parity
2. Entity scoping parity (Playbook §11)
3. Cross-report consistency
4. Revenue reconciliation
5. 2026 regression case
6. Automated tests
7. Main report state differentiation
get_income_statement_datareturnsbalanceand frontend maps non-zero values correctly.get_income_statement_by_fundreturnsbalanceand by-fund columns show non-zero values when JE lines exist.- UI calls use
p_exclude_close_entries: truefor statement views (seefetchIncomeStatementData). getOrgId(entityId)is used for read queries.- Parent org /
allpassesnullscope for aggregate reads (nogetActualOrgIdfallback in reads). - Same date range + entity produces consistent totals in Income Statement, Cash Flow, and Comparative report income sections.
- Same scope as Income Statement: GL revenue baseline matches
fetchIncomeStatementDatatotals (revenue-reconciliation.ts). - For Bloom Strong (or equivalent seeded org with known 2026 JE activity),
total_revenueis not zero. npm run test -- src/lib/incomeStatementByFundAggregation.test.ts src/lib/revenueReconciliationParity.test.ts- Force an RPC failure and verify error state + retry appear (not generic no-data).
- Use a no-activity date window and verify empty-activity state appears.
- Restore healthy data and verify success state re-renders account lines and totals.
Additional Notes
Account Code Mapping (Standard Chart of Accounts)
Revenue accounts (4xxx):
| Code | Account Name | Form 990 Line |
|------|--------------|---------------|
| 4000 | Direct Public Support | 1f |
| 4010 | Program Income | 8 |
| 4020 | Book Sales | 11d |
| 4030 | Initial Fee | - |
| 4040 | Interest Income | 3 |
| 4050 | Miscellaneous Revenue | 11d |
| 4100 | Admin Fees from Ministries | - |
Expense accounts (5xxx):
| Code Range | Category | Form 990 Lines |
|------------|----------|----------------|
| 5000-5080 | Program Services (Grants & Foreign Ops) | 1-3 |
| 5100-5140 | Personnel (Compensation & Benefits) | 5-10 |
| 5200-5410 | Administrative (Professional Fees, Office, IT) | 11-14 |
| 5500-5540 | Facilities (Occupancy) | 16, 20 |
| 5600-5670 | Travel & Education | 17, 19 |
| 5700-5710 | Insurance | 23 |
| 5800-5899 | Other Expenses | 24 |
| 5900 | IFM Admin Fee | - |
See STANDARD-CHART-OF-ACCOUNTS.md for complete mapping.
Nonprofit Accounting Standards
Follows FASB ASC 958 (Not-for-Profit Entities):
- Statement of Activities format
- Net asset classification
Synced from IFMmvp-Frontend documentation: pages/reports/02-INCOME-STATEMENT-REPORT.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org