Skip to main content

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 JournalEntryEditDrawer now uses the shared buildDonationContextFromLedgerRow() 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 1000 can 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.recentEntriesFirstHint reflects 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) via get_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_data and the corresponding get_income_statement_by_fund slice are expected to agree; investigate RPC/data if they do not.
  • RPC contract: balance is canonical; frontend supports legacy amount only as fallback.
  • App parameters: fetchIncomeStatementData passes p_exclude_close_entries: true so 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 when additionalAccountCodes are provided, instead of calling get_account_balance for 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 data
  • fetchLedgerEntriesByAccountCode(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 display
  • fetchJournalEntryByLineIdForEntity() 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 batch entry_number spans 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 same is_voided flag. The application void path updates every line in the batch; mixed state usually indicates import drift or manual SQL and should be repaired in journal_entry_lines.
  • Frontend: computeJournalEntryStatusFromLines() in journal-entries.ts drives entry-level status for fetchJournalEntryByLineId, fetchJournalEntryByLineIdForEntity, and fetchJournalEntriesPaginated (RPC list). Inconsistent batches are surfaced as posted with a console warning until data is fixed.
  • Drill-down: fetchLedgerEntriesByAccountCode returns per-line is_voided for styling via applyVoidedEntryDisplay(). fetchLedgerEntriesByAccountAndFund excludes 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() uses isVoidedEntry / isVoidReversal from applyVoidedEntryDisplay when 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_lines joined with accounts
  • 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_data returns account value in balance (canonical)
  • Frontend mapper reads balance first and temporarily tolerates legacy amount (warning once) for backward compatibility
  • Rows missing both balance and amount default to 0 and emit a warning once per fetch context
  • Exclude close entries: the UI uses the overload that accepts p_exclude_close_entries and passes true (see fetchIncomeStatementData in financial-reports.ts)

Recent Changes (March 2026)

  • Income Statement data-contract hardening (March 20, 2026) — Frontend mapping now treats balance as canonical for income-statement RPC responses, with guarded compatibility for legacy amount payloads 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 donations table (buildDonorAttributionMap in ledger-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 StackedDrawerContent close button is now hidden (hideCloseButton) while the JE sub-drawer is open. The JournalEntryEditDrawer owns its own guarded close button that intercepts unsaved-change dismissal. Applied across all 11 JE drawer consumers (reports, accounting, donors).
  • Shell close-control primitivesStackedDrawerContent and SheetContent both gained an optional hideCloseButton prop 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_CLASS from accountingAmountColors.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

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_data returns balance and frontend maps non-zero values correctly.
  • get_income_statement_by_fund returns balance and by-fund columns show non-zero values when JE lines exist.
  • UI calls use p_exclude_close_entries: true for statement views (see fetchIncomeStatementData).
  • getOrgId(entityId) is used for read queries.
  • Parent org / all passes null scope for aggregate reads (no getActualOrgId fallback 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 fetchIncomeStatementData totals (revenue-reconciliation.ts).
  • For Bloom Strong (or equivalent seeded org with known 2026 JE activity), total_revenue is 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

Ready to get started?Start Plus Trial