Skip to main content

Reconciliation Manager

Reconciliation Manager

✨ Recent Updates (March 2026)

Status:FULLY INTEGRATED WITH DATABASE + DURABLE MATCH HISTORY

P0/P1: Bank reconciliation flow hardening (April 24, 2026)

  • Bank statement import spacer columns: bankStatementParser.ts preserves raw header indexes while omitting blank header labels from the mapping UI. CSV/Excel files with blank spacer columns (for example Date,,Description,Amount) no longer shift row values into the wrong mapped fields. Regression: src/lib/bankStatementParser.test.ts.
  • Import URL safety: orphan-clearing relink checks chunk reconciliation_match_entries.ledger_entry_id IN (...) lookups (MATCH_ENTRY_ID_CHUNK_SIZE = 25) to avoid browser/PostgREST URL-length failures on large clearing sets.
  • Unreconciled type filter: Deposits now follows cash-account semantics (debit > 0), and Expenses/Payments follow credit > 0. The filter is still client-side on the current loaded page, so pagination continues to show the real server page/count instead of pretending the filtered current page is the full result set. Regression: ReconciliationUnreconciledTab.typeFilter.test.tsx.
  • Clearing target clicks: Clicking a bank or GL row already marked c promotes it to R instead of toggling it back to unreconciled. Grouped GL reconcile finalizes every child that is still clearing, not only the first group child. Helper: getClearingLedgerIdsToFinalize().
  • Grouped Clearing tab: Grouped General Ledger clearing view now loads the full statement-bounded clearing set through fetchReconciliationLedgerClearingAll() and applies local search/date/sort before group pagination. This avoids hiding rows beyond the first server page.
  • Finish snapshot parity: get_reconciliation_finish_snapshot now includes bank-only clearing rows (bank rows with no active match and no ledger_entry_id) so the saved/PDF reconciliation report matches finalize_reconciliation_statement, which finalizes both GL and bank clearing rows. Migration: 20260724200300_include_bank_only_reconciliation_finish_snapshot.sql.
  • Unreconcile cleanup: bank rows moved back to unreconciled clear stale bank_transactions.ledger_entry_id in both single and bulk unreconcile paths. Regressions: bank-transactions.reconciliation.test.ts, financial-reports.reconciliation.test.ts.

Tab Export — CSV / Excel (April 18, 2026)

  • Reported by: Maribeth Carlton — needed to triage which entries to unreconcile offline without mutating Financial Stories source data, then come back and act on specific entries.
  • What: New Export button in the reconciliation toolbar (next to More Actions). Available to all roles — read-only included — because the action is a pure read.
  • Tabs supported: Reconciled, Unreconciled, Clearing. The Report tab keeps its existing dedicated PDF + CSV controls.
  • Filters honored at export time: selected GL account, statement-ending date (where applicable), date-range filters, and the search box on the active tab. The exported workbook matches what is on screen.
  • Formats:
  • CSV — UTF‑8 with BOM, RFC‑4180 escaping (works in Sheets and Excel). For tabs that show both Bank and GL (Unreconciled, Clearing), CSV emits a single combined file with a leading Side column (Bank / GL).
  • Excel (.xlsx) — multi-sheet workbook (one sheet for Bank, one for GL on Unreconciled/Clearing; one sheet for Reconciled). Date and currency columns carry native Excel formats (yyyy-mm-dd, EXCEL_CURRENCY_FORMAT) so sort/sum work without re-formatting.
  • Identifiers in every row: Journal Entry Id, Line Id, and on Bank rows Matched Ledger Line Id. This is the key bit for Maribeth's workflow — operators can mark candidates in the spreadsheet, return to the app, and unreconcile the exact entries by id.
  • Implementation: src/features/accounting/hooks/useReconciliationExport.ts (calls existing read RPCs get_reconciled_entries_paginated, get_reconciliation_bank_paginated, get_reconciliation_ledger_paginated with pageSize: 50000 to fetch all matching rows in one shot). Wired into ReconciliationToolbar via the shared ExportButton. No backend or RPC changes.
  • Future — Google Sheets: Once the in-progress Google verification ships, a third dropdown item ("Send to Google Sheets") can reuse the same row builders (ledgerRow / bankRow). Until then, exported .xlsx files can be dragged into Google Drive and opened with "Open with Google Sheets" for the cloud copy.

Calculator-style amount entry (April 9, 2026)

  • The Statement Balance CurrencyInput on the reconciliation balance tiles is part of the live fund-accounting calculator-input rollout.
  • Users can type arithmetic directly into that balance field and commit the evaluated amount on blur or Enter.
  • This is limited to the reconciliation amount input on this page, not a blanket change to every amount box in unrelated areas of the app.

P0: Top tiles include clearing totals even when Unreconciled grid is empty (April 18, 2026)

  • Reported by: Maribeth Carlton — Deposits/Payments tiles could show $0 while the Clearing tab showed hundreds of c rows.
  • Root cause: Unreconciled paginated ledger RPC excludes clearing, and ledgerTransactions is intentionally cleared outside the Unreconciled tab. Tile math that only read unreconciled page rows dropped active clearing amounts.
  • Fix: useReconciliationStats now combines (1) selected unreconciled GL rows from ledgerTransactions and (2) all statement-bounded clearing GL rows from clearingLedgerLines (fetchReconciliationLedgerClearingAll / get_reconciliation_ledger_clearing_all) for Deposits/Payments and line counts.
  • Cleanup: Removed dead "clearing rows in unreconciled table" fallback branch from ledgerDepositPaymentFromUnreconciledTable; clearing totals are sourced from the dedicated clearing query path only.
  • Files: src/features/accounting/hooks/useReconciliationStats.ts, src/features/accounting/hooks/useReconciliationStats.test.ts

P1: Stripe payout rows — Source Donations drawer + in-place assign (April 3, 2026)

  • Problem: Operators could see payout allocation amounts on reconciliation rows, but had no direct workflow to inspect likely source donations or correct mis-assigned fund attribution from that context.
  • UX update: Payout allocation rows now expose a direct Source donations action (no tooltip-only helper path). This opens a right-side drawer that lists likely source donations in a 21-day window before payout date, prioritized by target fund and recency.
  • Drawer standard compliance: Implemented with the shared Stacked Drawer Pattern components from documentation/frontend/STYLING-GUIDE.md (StackedDrawerRoot, StackedDrawerContent, StackedDrawerHeader, StackedDrawerBody, StackedDrawerFooter) and no custom edge-close affordances.
  • Shared component: PayoutAllocationBadge is now the single reusable entry point across Unreconciled/Clearing/Reconciled/Report tabs; the drawer UI is centralized in PayoutSourceDonationsDrawer to avoid per-tab duplication.

Schema: status-only reconciliation (April 7, 2026)

  • `reconciled` BOOLEAN removed from journal_entry_lines and bank_transactions. All RPC predicates and PostgREST filters use `reconciliation_status` only. Migrations: 20260507180000_reconciliation_status_guardrails.sql, 20260507190000_retire_reconciled_boolean_columns.sql.
  • Operational SQL under supabase/queries/ (duplicate cleanup, audits) was updated to use reconciliation_status instead of the dropped column.
  • Assign action: Each candidate donation row can now run Assign donation (when user has accounting write permission and the target fund is resolvable), calling updateDonation({ organizationId }) so donation + linked accounting metadata stay synchronized.
  • Data source: fetchLikelyPayoutSourceDonations (src/lib/db/payout-source-donations.ts) queries active donations in-window and marks rows that are already in the target fund vs assignable.
  • Files: PayoutAllocationBadge.tsx, PayoutSourceDonationsDrawer.tsx, reconciliation tab components, src/lib/db/payout-source-donations.ts, src/lib/db.ts.

P1: Match feedback — rows leave Unreconciled immediately (March 30, 2026)

  • Reported by: Maribeth Carlton — after matching bank to GL, statement lines did not seem to “move” or clearly indicate success; operators expected QuickBooks-style feedback on the same grid.
  • Cause: handleReconcile optimistically set reconciliation_status = 'clearing' on in-memory rows but left them in bankTransactions / ledgerTransactions. The Unreconciled RPC excludes clearing, so after refetch they would disappear — but on large accounts refetch lag left rows visible with c in the status column, which felt broken (and older builds lacked the bank status column entirely).
  • Fix: On successful match, filter out matched bank and ledger ids from local state immediately (useReconciliationActions.ts). Toast + tab hints already describe the Clearing tab; en accounting.json copy tightened (transactionsMarkedClearingDetail, unreconciledTabWorkflowHint, bankStatementUnreconciledHint).
  • Product model: Standard two-step flow — Unreconciled = open items to pair; Clearing (c) = paired / in progress; Finish promotes c → R. Rows are not sorted to the “bottom” of Unreconciled; they exit that list once matched.

P1: Unreconciled — bank status column + selection parity (March 30, 2026)

  • Bank Statement table: First column read-only R / c (same semantics as GL). Second column target = handleToggleBankReconciliationStatus (mark bank clearing without a GL line). Row body click = handleBankSelect for match selection (unreconciled or clearing bank rows).
  • GL table: Row body selects unreconciled or clearing lines so a bank line can match a GL line already marked c. handleReconcile allows ledger unreconciled | clearing, bank unreconciled only.
  • `useReconciliationContext`: selectedBankItems / selectedLedgers use !reconciled && reconciliation_status !== 'reconciled' so clearing lines participate in totals and auto-match.
  • Files: ReconciliationUnreconciledTab.tsx, useReconciliationContext.ts, useReconciliationActions.ts, en/accounting.json (bankStatusColumnHint, bankTargetClearingAria).

P1: Bank import — skip duplicates against clearing/reconciled bank lines (March 30, 2026)

  • Behavior: partitionIncomingBankImport in bank-transactions.ts skips an incoming row when it semantically matches an existing bank line already clearing or reconciled (date + amount + type + loose description), even if import_fingerprint differs (e.g. CSV text changed). Prevents a second unreconciled bank row for an already-paired transaction.
  • Preview: BankImportPreview.skippedSettledBankDuplicates + dialog line importPreviewSkippedSettledBank.
  • Supersedes in spirit: Older doc bullets that said only fingerprint/legacy dedupe applied.

P1: Tab hints + Clearing grouped GL + ledger group keys (March 30, 2026)

  • Tab strip copy: The line under PillTabs is per-tab: Unreconciled states that when bank and ledger target totals match, lines leave Unreconciled and appear under Clearing (the working “paired for this statement” list); Clearing reframes matched c lines as the in-progress checked-off list; Reconciled / Report get one-line subtitles. Replaces the old generic “click targets · click amounts” string on all non-Clearing tabs.
  • i18n (en): unreconciledTabWorkflowHint, reconciledTabShortHint, reportTabShortHint, updated clearingTabHint, statusCycleHint (names Clearing tab explicitly).
  • Clearing tab: Grouped / Flat toggle for the General Ledger (clearing) table (same UX pattern as Unreconciled), backed by useGroupedLedger. When grouped, the ledger RPC requests a wider page so grouped headers are not empty from under-fetch.
  • Group keys: getLedgerGroupKey() — external refs and JE keys no longer include `transaction_date` in the key (multi-fund / payout lines can differ slightly in date but belong to one logical entry). `resolveExternalGroupRef()` can extract Stripe-style ids embedded in description when reference_id / display ref are weak or missing.
  • Files: ReconciliationManager.tsx, ReconciliationClearingTab.tsx, useGroupedLedger.ts, src/i18n/locales/en/accounting.json
  • DB / Edge: No new migrations or Edge Functions in this change set; deploy is frontend + docs only.

P1: Entry grouped view — one logical JE across funds (March 30, 2026)

  • Reported by: Maribeth Carlton — one manual/imported journal entry with many lines to the same bank GL account (e.g. multi-fund Aplos payout) appeared as many separate rows in Entry mode on the Unreconciled General Ledger pane instead of one collapsible group.
  • Cause: getLedgerGroupKey() in src/features/accounting/hooks/useGroupedLedger.ts mixed fund display name (organizationName from the reconciliation RPC row) into the group key together with entry_number / journal_entry_id. Multi-fund JEs share a single entry_number but each line is attributed to a different fund, so the UI formed one group per fund for the same JE.
  • Fix: Keys for JE-backed lines use transaction date + `journal_entry_id` or date + `entry_number` only (Stripe-style external refs in reference_id / ref still win when present). Child rows still show per-line fund names; grouped search still scans combined fund text and totals.
  • Files: useGroupedLedger.ts; type/API comments in src/features/accounting/types/reconciliation.ts, src/lib/db/financial-reports.ts

P0: Unreconciled GL excludes clearing — parity with bank (March 27, 2026)

  • Migration: 20260403220000_reconciliation_unreconciled_ledger_exclude_clearing.sqlp_scope = 'unreconciled' for get_reconciliation_ledger_paginated uses `NOT IN ('clearing', 'reconciled')`, matching get_reconciliation_bank_paginated. GL lines moved to clearing (c) (Match Selected or ledger target) disappear from the Unreconciled tab after refetch and appear only on the Clearing tab.
  • `get_reconciliation_ledger_batch_match_candidates` and `get_reconciliation_unreconciled_line_counts` also exclude clearing, so batch hints and unreconciled base counts align with the visible Unreconciled grid. Top tiles then add clearing totals from get_reconciliation_ledger_clearing_all (clearingLedgerLines) as a separate source of truth.
  • Supersedes: The short-lived 20260403210000_reconciliation_unreconciled_include_clearing.sql behavior (unreconciled ledger scope included clearing) is reversed for product clarity. If all GL activity for the account is already in clearing, the Unreconciled GL pane may be empty while the bank side still shows rows — operators use the Clearing tab.

P1: Bank statement re-import — auto re-link orphan clearing (c) GL lines (March 27, 2026)

  • Problem: Clearing Remove statement / clear bank import deletes bank_transactions (and CASCADE removes reconciliation_match_* rows). GL lines that were already matched to clearing (c) often stay in clearing with no active match. Re-uploading the CSV recreated every bank line as unreconciled and forced operators to match again from scratch.
  • Behavior: On reconciliation import with a selected GL account, insertBankTransactions() (src/lib/db/bank-transactions.ts) loads orphan GL lines: journal_entry_lines.reconciliation_status = 'clearing' and no active reconciliation_match_entries row (unmatched_at IS NULL). For each incoming row that is not skipped as a fingerprint/legacy duplicate, if statement date and absolute amount match a line in that pool, the app inserts the bank row, calls create_reconciliation_match, then updateBankTransactionReconciled / updateLedgerEntryReconciliationStatus to clearing — equivalent to Match Selected for a 1:1 pair. When several GL lines share the same date and amount, normalized description (loose match) picks the best candidate; each GL line is consumed at most once per import.
  • Limits: 1 bank row ↔ 1 GL line only. Prior N bank ↔ M ledger matches are not reconstructed automatically; operators re-match those manually if needed.
  • Preview: previewBankTransactionImport adds autoRelinkOrphanClearing (confirmation card). Toasts: importedWithAutoRelink, importAutoRelinkFailed if match RPC fails after insert.
  • Audit: ReconciliationImportDialog passes importMatchedBy (reconciledById) into InsertBankImportOptions.matchedByUserId.
  • Files: bank-transactions.ts, ReconciliationImportDialog.tsx, ReconciliationManager.tsx, en / es accounting.json

P0: Unreconciled / Clearing — server-side RPC pagination (March 27, 2026)

  • Problem class: Large bank + GL datasets (tens of thousands of rows) cannot use PostgREST full-table loads without hitting max-rows, RLS per-row cost, or client memory limits — the same rationale as the March 17 Reconciled tab get_reconciled_entries_paginated fix.
  • Architecture: Unreconciled and Clearing bank/ledger tables load one page at a time via fetchReconciliationBankPaginated / fetchReconciliationLedgerPaginated in financial-reports.ts, which call SECURITY DEFINER RPCs `get_reconciliation_bank_paginated` and `get_reconciliation_ledger_paginated` (p_scope: 'unreconciled' | 'clearing'). Access is gated by _assert_reconciliation_account_accessget_user_org_tree_ids() (same pattern as other reconciliation RPCs).
  • Supporting RPCs: count_bank_transactions_for_reconciliation_account, count_bank_clearing_for_reconciliation_account, get_reconciliation_ledger_clearing_all (stats tiles, finish/report paths, bounded), get_reconciliation_ledger_batch_match_candidates, get_clearing_ledger_entry_ids_for_account, get_reconciliation_finish_snapshot, get_reconciliation_unreconciled_line_counts.
  • DB migration: supabase/migrations/20260401100000_reconciliation_unreconciled_paginated_rpcs.sql
  • Follow-up migration (ledger rows missing when `reconciled` IS NULL): supabase/migrations/20260402100000_reconciliation_ledger_null_reconciled.sql — reconciliation bank RPCs use COALESCE(bt.reconciled, false) = false, but the first ledger RPCs used jel.reconciled = false, which excludes NULL in SQL. Lines with null reconciled (still unreconciled by status) vanished from the Unreconciled/Clearing GL pane while the bank side still showed activity. Replaced with COALESCE(jel.reconciled, false) = false in all affected ledger reconciliation functions. (Original file omitted $function$; between some CREATE OR REPLACE blocks; fixed so supabase db push applies cleanly.)
  • Migration history baseline: supabase/migrations/20260327203735_remote_history_baseline.sql — production already had version 20260327203735 recorded before a matching file existed in this repo; the placeholder aligns local history with remote so db push no longer errors. Replace with real DDL from your team if you need greenfield replays to match that version exactly.
  • Grouped GL + NULL `transaction_date` (March 2026): DB migration 20260403120000_reconciliation_ledger_null_transaction_date.sql includes jel.transaction_date IS NULL in statement/date filters so undated lines are not dropped by SQL three-valued logic. Frontend grouped mode now uses the same server unreconciledLedgerPage as flat mode (no forced RPC page 1), so date coverage/pagination remains consistent while switching sort direction.
  • Unreconciled ledger scope (March 2026): 20260403220000_reconciliation_unreconciled_ledger_exclude_clearing.sqlp_scope = 'unreconciled' for get_reconciliation_ledger_paginated excludes clearing (same contract as bank). See P0: Unreconciled GL excludes clearing at the top of this doc. Historical note: 20260403210000_reconciliation_unreconciled_include_clearing.sql temporarily included clearing in the unreconciled ledger scope; that is superseded.
  • Selection across pages: Target selections are stored in `bankSelectionsRef` / `ledgerSelectionsRef` plus persisted ID lists (reconciliation_bank_selection_ids_{accountId}, reconciliation_ledger_selection_ids_{accountId}) so operators can match bank rows and GL lines that are not on the same visible page.
  • Legacy exports: fetchBankTransactionsForReconciliation and fetchLedgerEntriesForReconciliation remain in financial-reports.ts for ad-hoc or legacy callers — do not wire them back into the workspace Unreconciled/Clearing tabs as a full-table data source.

P2 (historical): Unreconciled bank column — superseded March 30, 2026

  • March 27 note briefly removed the bank R/c column in favor of target-only selection. March 30 restored bank + GL parity (status column + row-body selection + bank clearing toggle). See P1: Unreconciled — bank status column + selection parity and P1: Match feedback — rows leave Unreconciled immediately above.

P2: Clearing tab — standard table pagination (March 27, 2026)

  • UX: The Clearing tab (ReconciliationClearingTab.tsx) now follows the app table pagination standard (STYLING-GUIDE § Table Pagination Pattern): shared rows per page Select in the filter bar (100 / 250 / 500), a Showing X to Y of Z summary (plus common.pagination.pageOf when multiple pages) above each table, and `PaginationControls` above and below both the bank-clearing and ledger-clearing tables. Page indices clamp when filters or page size shrink the result set. Clearing page size is local to this tab (not tied to the Unreconciled tab’s fixed page size).
  • i18n: pages.reconciliation.clearingShowingRange (en / es accounting.json).
  • Files: ReconciliationClearingTab.tsx, locale accounting.json

P2: Finish Reconciliation — when the button appears (March 27, 2026 — docs alignment)

  • Product: Finish Reconciliation is not shown on Unreconciled or other tabs. In ReconciliationToolbar.tsx it renders only when all of: activeTab === 'clearing', canWriteAccounting, clearingCount > 0, and `hasStatementBalance` (user has set a statement ending balance for the workspace — persisted via reconciliation workspace / local snapshot). If the statement balance was never set, the control is hidden, not disabled.
  • When visible: The button is disabled when the calculated ending balance does not match the statement balance within $0.01: |clearedBalance + deposits − payments − statementBalance| >= 0.01.
  • Files: ReconciliationToolbar.tsx, useReconciliationContext.ts (hasStatementBalance, statementBalance)

P0: Reconciliation local save state persistence (March 27, 2026)

1. Added per-account persisted selection keys:

2. Added loadSelectionState() and saveSelectionState() in useReconciliationSession.ts. 3. useReconciliationContext.ts now restores saved IDs into bankSelectionsRef / ledgerSelectionsRef on account load. 4. Selection refs are autosaved when reconciliation rows change. 5. clearReconciliationSession() now removes both new selection keys. 6. Reconcile/unreconcile action paths now clear selection refs for affected rows to prevent stale visual re-selection after refetch.

  • Reported by: Maribeth Carlton — in-progress reconciliation selections appeared to disappear after refresh/navigation/error recovery.
  • Root cause: Local persistence only stored statement date, statement balance, and active tab. Bank/ledger checked rows lived in in-memory selection refs, so they were lost across refresh/session restart.
  • Fix:
  • reconciliation_bank_selection_ids_{accountId}
  • reconciliation_ledger_selection_ids_{accountId}
  • Files: useReconciliationSession.ts, useReconciliationContext.ts, useReconciliationActions.ts

P0: Manual Match Selected — no amount or direction blocking (March 26–27, 2026)

  • Change: Match Selected persists clearing (c) + createReconciliationMatch() after bank target + GL row-body selection (unreconciled GL lines). The tab may also auto-invoke handleReconcile when both sides are selected and totals match within $0.01 (ReconciliationUnreconciledTab.tsx useEffect).
  • No blocking guards: handleReconcile does not validate that bank and GL lines use the same debit/credit column or that amounts match within $0.01. Operators may pair rows and clear to c as needed; removed i18n keys: directionMismatch, amountDifferenceRemaining, autoMatchWhenBalancedHint.
  • Safety: matchInFlightRef prevents double-submit; lastFailedAutoMatchKeyRef blocks retry after a failed RPC until the user changes the bank/ledger selection.
  • Files: useReconciliationActions.ts, ReconciliationUnreconciledTab.tsx, locale accounting.json files, DEVELOPER-PLAYBOOK.md.

P0: Mass ledger selection + no match confirmation modal (March 2026)

  • Root cause: Hydrating selected: true whenever reconciliation_status === 'clearing' made every clearing GL line count as selectedLedgers (e.g. 686 lines), blowing up match totals and direction checks.
  • Fix: On merge from reconciliationData, default `selected: false` for bank and ledger unless refs override (useReconciliationContext.ts). Clearing `c` remains a read-only status indicator only.
  • UX: Removed Match Selected AlertDialog; the centered button calls `handleReconcile` directly. matchToClearingHint shows as small text under the button.
  • Safety: handleReconcile blocks when more than 50 ledger lines are selected (tooManyLedgerSelections toast).
  • Files: useReconciliationContext.ts, useReconciliationActions.ts, ReconciliationDialogs.tsx, ReconciliationUnreconciledTab.tsx, locales accounting.json, DEVELOPER-PLAYBOOK.md.

P2: More actions — Clear All Clearing Marks (March 2026)

  • Change: Restored More actions → Clear All Clearing Marks (clearClearingTitle label) calling handleClearAllClearingMarks → existing confirmation dialog (ReconciliationDialogs.tsx). Disabled when no account, clearingCount === 0, or clearingInProgress.
  • Files: ReconciliationToolbar.tsx

P2: Match Selected — one bank to many ledger lines hint (March 2026)

  • Change: matchToClearingHint (inline under the Match Selected button): select every ledger line before matching; after a match exists, create_reconciliation_match rejects a second save for the same bank—operators must unreconcile or clear all clearing marks, then rematch.
  • Files: ReconciliationUnreconciledTab.tsx, locale accounting.json files.

Design note: Editing a match after confirm (March 2026)

  • Database: One active reconciliation_match_groups row per bank transaction (idx_reconciliation_match_groups_active_bank); each ledger line may appear in at most one active match (idx_reconciliation_match_entries_active_ledger). create_reconciliation_match raises if either constraint would be violated.
  • Product path today: Unreconcile via unreconcile_reconciliation_match_by_ledger_entry (Reconciled tab / row actions) or Clear All Clearing Marks (More actions) for bulk reset, then target-select the bank line and all ledger lines, then Match Selected once.
  • Future (optional): A replace_reconciliation_match (or similar) RPC could swap p_ledger_entry_ids in one transaction without bulk-clearing unrelated clearing rows—only needed if product requires “add one more GL line to this bank match” without touching other staged matches.

P1: Removed directionMismatch toast entirely (March 27, 2026)

  • Reported by: Maribeth Carlton — pairing a bank deposit with a ledger line to cash triggered a misleading “opposite entries” / direction error.
  • Product decision: Do not block or warn on debit/credit column pairing for match-to-clearing. The directionMismatch key and client guard were removed from handleReconcile and from locale files (no toast, no inline error for direction).

Removed: Bulk bank-matching journal entries (March 2026)

  • Removed: The More actions → Create bank-matching journal entries menu item, dialog, and create_bank_statement_matching_journal_entries RPC client usage. Reconciliation continues to use target-match, Match selected, and Finish reconciliation; journal entries are created manually via Journal Entries (or other posting flows) when needed.
  • Rationale: The bulk RPC posted one JE per imported bank row against a clearing account and was easy to misapply (e.g. double-counting cash when detail lines already existed). It is no longer part of the product surface.
  • DB: Migration 20260326225024_drop_create_bank_statement_matching_journal_entries.sql drops the RPC from Postgres. The historical migration that created it remains in the repo for migration history.
  • Files: ReconciliationToolbar.tsx, financial-reports.ts.

P1: Target-Match Unreconciled UX (March 2026)

  • Change: Replaced checkbox selection with a target icon on the bank side and (on the GL side) target = clearing toggle plus row-body click = match selection for unreconciled lines. See P2: Unreconciled tables — bank column trim + GL target = clearing (March 27, 2026) above for the current column layout.
  • Change: Removed gray/strikethrough visual hint behavior entirely.
  • Interaction model: First GL column on Unreconciled is read-only status (normally empty for unreconciled rows). Clearing without a bank line uses the ledger target (handleToggleReconciliationStatus) — row then leaves this grid after refetch; view it on Clearing. Bank↔GL clearing uses bank target + GL row selection + Match Selected (handleReconcile). Finish reconciliation promotes c → R.
  • Toolbar: Match Selected is for explicit bank+ledger pairing (target-selected rows only) and is centered in the Unreconciled tab between the search/filter card and the Bank Statement / General Ledger tables (ReconciliationUnreconciledTab.tsx). Finish Reconciliation appears in the controls row only on the Clearing tab when clearing items exist, the user can write accounting, and a statement balance has been set (hasStatementBalance); reconciled progress text stays in the same row (ReconciliationToolbar.tsx).
  • Finish action: Finish Reconciliation finalizes c rows by statement date cutoff (PDF + snapshot gate, then bulk promote). It is disabled (but may still be visible) when ending balance ≠ statement balance beyond $0.01.
  • Toolbar actions menu: Set Starting Balance, Clear All Clearing Marks, Upload New File, and Clear Statement are grouped under one left-side More actions dropdown to keep the controls row compact.
  • Reconciled tab scope: Reconciled history list shows R items only; c items live on the Clearing tab until Finish Reconciliation promotes them to R (they no longer appear on the Unreconciled GL grid).
  • Reconciled counters: Labels that say "Reconciled" now represent finalized R counts only (clearing c is tracked separately).
  • JE drawer integrity: Journal entries with any c or R lines are blocked from edit/void in reconciliation context; operator must unreconcile first.
  • Report integrity: Cleared sections in Reconciliation Report tab/export are R-only; c items appear only in Uncleared.

P2: Reconciled Tab — Unified Search + Date Bar (March 24, 2026)

  • UX parity: The Reconciled tab now uses the same Card layout as the Unreconciled tab: one row with the text search field, from / to DatePicker controls, and a single Clear action when any filter is active (ReconciliationReconciledTab.tsx).
  • Removed: The inline “Accidentally reconciled something?…” helper line under the filters (users can still unreconcile from the Unreconcile column).
  • Actions: handleClearFilters in useReconciliationActions.ts now resets reconciled text search as well as the date range and page.
  • i18n: Dropped unused reconciledRecoveryHint from all locale accounting.json files.

P0: Stripe Payout Settlement Standardization (March 17, 2026)

1. Fee calculation used uncleared_1131_balance - payout_amount as "fees" — booked $21,649 to account 5002 (real fees: ~$874) 2. DIRECT_STRIPE_PAYOUT_ORG_ID env var empty → Mar 16+17 payouts failed ($1,597 missing) 3. Payout > uncleared race condition silently discarded $488 4. Dead duplicate handlePayoutSettlement in platform-webhook

  • Problem: 4 compounding bugs in handlePayoutPaid (stripe-webhook):
  • Fix:
  • Rewrote handlePayoutPaid to use Stripe Balance Transactions API for actual per-charge fees
  • Compound JE per fund (one entry_number with settlement + fee lines on same JE)
  • Added UUID validation guard on DIRECT_STRIPE_PAYOUT_ORG_ID
  • Voided 70 bogus fee JE lines ($21,649.48 on 5002 Bank Fees)
  • Deleted dead handlePayoutSettlement from platform-webhook
  • Stripe Payout Summary card: Now shows only clean settlement lines (bogus fee lines are voided and excluded by is_voided = false filter)
  • Files: supabase/functions/stripe-webhook/index.ts, supabase/functions/platform-webhook/index.ts, documentation/frontend/DEVELOPER-PLAYBOOK.md
  • DB Migration: void_bogus_payout_fee_je_lines

P0: Reconciled Tab Empty — RLS Per-Row Timeout Fix (March 17, 2026)

P0: "Why aren't my reconciled entries showing up?"

  • Reported by: Steven Leipzig — Reconciled tab showed "No reconciled entries found for this date range" despite 23,736 reconciled entries. Tab badge count was correct (from get_reconciliation_summary RPC) but the table was empty.
  • Root Cause: fetchReconciledEntriesPaginated used a direct PostgREST client query with { count: 'exact' }. Two RLS SELECT policies (jwt_is_in_org_tree(organization_id)) fired per-row on 23K+ entries, exceeding the 8-second authenticated statement timeout. Same bug class as the get_journal_entries_page_core fix from March 9 — SECURITY INVOKER queries with RLS per-row JWT parsing timeout on large IFM datasets.
  • Fix: Created get_reconciled_entries_paginated SECURITY DEFINER RPC that validates p_account_id against get_user_org_tree_ids() once, then queries journal_entry_lines directly (bypassing per-row RLS). Returns paginated JSON + total count in a single call.
  • Frontend: Rewrote fetchReconciledEntriesPaginated in financial-reports.ts to call supabase.rpc(...) instead of PostgREST client query. Removed dead RECONCILED_OR_CLEARING_FILTER constant.
  • DB Migration: create_get_reconciled_entries_paginated_rpc
  • Files: src/lib/db/financial-reports.ts, documentation/pages/accounting/09-RECONCILIATION-MANAGER.md, documentation/frontend/DEVELOPER-PLAYBOOK.md

Audit Remediation — Reconciliation Snapshot Correctness, Audit Coverage, Local-State Cleanup (March 6, 2026)

Reconciliation Report Snapshot Correctness

  • Finalized totals derived from clearing entries: handleFinishReconciliation now computes finalizedTotalDeposits and finalizedTotalPayments from the actual clearing entries being promoted, not from the top-level stat tiles (which could include stale or double-counted values).
  • Uncleared entries captured in snapshot: The saved JSONB snapshot and generated PDF now include unclearedEntries — ledger transactions that are not reconciled, not in clearing status, and dated on or before the statement date. This matches the QuickBooks "Outstanding Checks and Deposits" section.
  • PDF includes uncleared section: generateReconciliationReportPDF now renders a "Uncleared Transactions" table with Debit/Credit columns and totals after Cleared Payments. The ReconciliationReportData interface accepts an optional unclearedEntries array.
  • Saved report PDF download passes uncleared data: BankReconciliationReports now includes unclearedEntries when calling generateReconciliationReportPDF from a saved snapshot.
  • Timestamp formatting fixed: Saved report list and detail views now use formatDateInTimezone / formatDateTimeInTimezone instead of brittle createdAt.split('T')[0] string slicing, preventing UTC/local date mismatch on the "Date Saved" column and "Snapshot from" label.

Financial Audit Log Coverage Extended to Reconciliation Artifacts

  • New DB triggers: fin_audit_reconciliation_reports, fin_audit_reconciliation_match_groups, fin_audit_reconciliation_match_entries — all fire the shared financial_audit_trigger_func().
  • Trigger function updated: financial_audit_trigger_func now resolves organization_id for reconciliation_match_entries rows by looking up the parent reconciliation_match_groups.organization_id via match_group_id. Previously, match entries had no organization_id or fund_id column, so the trigger silently skipped them.
  • Frontend audit types extended: AuditTableName union, TABLE_DISPLAY_NAMES, TABLE_OPTIONS, TABLE_LABEL_KEYS in audit-log.ts and AuditLog.tsx now include reconciliation_reports, reconciliation_match_groups, reconciliation_match_entries.
  • Internal admin audit hook updated: useAuditLog.ts mapTableToCategory now routes reconciliation artifact tables to 'system' category. Added getAuditTableLabel and getAuditActionLabel helpers so the internal admin view shows human-readable action descriptions (e.g., "Created Reconciliation Reports") instead of raw INSERT reconciliation_reports.
  • i18n: Added audit table labels for the 3 new reconciliation tables in all 6 locales (en/es/fr/de/zh/th).

Local-State Cleanup on Logout

  • Centralized draft purge: clearStorage() in auth/storage.ts now removes all entity-scoped accounting draft and session keys on logout: alignmint-check-batch-*, alignmint-deposit-batch-*, alignmint-reimbursement-batch-*, reconciliation_session_*, reconciliation_statement_date_*, reconciliation_statement_balance_*. This prevents shared-device data leakage of in-progress deposit batches (which may contain check images), reimbursement receipts, and reconciliation session state.
  • Regular Deposit Manager: Entity switch now rehydrates the batch from the new entity's storage key and resets the draft form, preventing cross-entity batch contamination.
  • Reimbursements Manager: Entity switch now rehydrates the batch from the new entity's storage key and clears the in-progress receipt, preventing cross-entity receipt leakage.

Migration File

  • backend/migrations/20260306_extend_financial_audit_for_reconciliation_artifacts.sql

Files Modified

  • src/lib/exportUtils.tsReconciliationReportData + PDF uncleared section
  • src/features/reports/components/BankReconciliationReports.tsx — timestamp formatting + uncleared PDF passthrough
  • src/lib/db/audit-log.ts — 3 new AuditTableName entries + display names
  • src/features/accounting/components/AuditLog.tsx — filter options + label keys
  • src/features/internal-admin/hooks/useAuditLog.ts — category mapping + label helpers
  • src/features/accounting/components/ReconciliationManager.tsx — finalized totals + uncleared snapshot
  • src/features/deposits/components/RegularDepositManager.tsx — entity-switch rehydration
  • src/features/tools/components/ReimbursementsManager.tsx — entity-switch rehydration
  • src/contexts/auth/storage.ts — centralized draft purge on logout
  • src/i18n/locales/{en,es,fr,de,zh,th}/accounting.json — 3 new audit table labels each
  • src/lib/db/financial-reports.tssaveReconciliationReport + fetchReconciliationReports already included uncleared fields (prior session)

P1: Standardized Accounting Search — Grouped Ledger + Empty States (March 6, 2026)

P1: Reconciliation search now uses shared matchesAccountingSearch utility

1. Shared matcher: Replaced inline matchesSearch function with matchesAccountingSearch from src/lib/searchUtils.ts. Search now supports multi-word text tokens, strict dollar-amount matching (with $, commas, decimals), and date-like token matching (multiple formats: YYYY-MM-DD, MM/DD/YYYY, Jan 15, 2026, etc.). 2. Grouped ledger search: Previously, search filtered individual ledger lines *before* grouping — searching for an amount that only appeared as a group total would show nothing. Now: ledger lines are filtered by statement date first, then grouped by date+description, then the search matcher runs against the grouped row (aggregated totals, combined fund names, shared description/ref/date). Single-child groups and expanded children are also individually searchable. 3. Filtered empty states: Bank and ledger tables now distinguish between "no matches for your search" (shows search-specific empty message) and "no unreconciled items before statement date" (shows date-contextual message). Pagination counts use the visible filtered row count, not the raw array length. 4. Bank search fields: description, ref, date, fundName, entryNumber, plus debit/credit amounts and date values. 5. Ledger search fields (grouped): description, ref, date, fundNames, plus totalDebit/totalCredit amounts and date values.

  • Context: Search across all fund accounting surfaces (Reconciliation, Pledges, Grants, Expenses, Chart of Accounts) was inconsistent — each component had its own ad-hoc string matching with different numeric parsing, no date matching, and no multi-word support.
  • Fix (Reconciliation-specific):
  • Files: ReconciliationManager.tsx, searchUtils.ts

P0: Bank + GL Checkbox Marks Not Persisting Across Navigation (March 6, 2026)

P0: "Did not save the marked off items on the bank statement side when I left" / "Correction — didn't save on the G/L side either"

1. handleBankSelect → calls updateBankTransactionReconciled() to persist reconciliation_status = 'clearing' on check, 'unreconciled' on uncheck. Optimistic update with error rollback. 2. handleLedgerSelect → calls updateLedgerEntryReconciliationStatus() to persist clearing status. Same optimistic pattern. 3. handleGroupSelect → now persists grouped-header checkbox changes for every child ledger row, not just local selection state. 4. Data restore useEffect → bank + ledger transactions with reconciliation_status === 'clearing' from DB are restored as selected: true on component mount. 5. handleConfirmClearClearingMarks → clears both ledger AND bank clearing marks via bulkUnreconcileLedgerEntries() + bulkUnreconcileBankTransactions(). 6. handleConfirmClearClearingMarks → clears both bankSelectionsRef and ledgerSelectionsRef, then immediately normalizes local selected / reconciliationStatus state so stale marks cannot visually re-apply. 7. handleFinishReconciliation → promotes both ledger AND bank clearing marks via bulkFinishReconciliation() + bulkFinishBankReconciliation(). 8. handleFinishReconciliation → clears both refs and immediately normalizes local checkbox state before query invalidation. 9. handleApplyBatchSuggestion → sets selected: true on suggested ledger rows locally; when totals then balance the selected bank line, auto `handleReconcile` persists clearing (same as manual target-selection flow). 10. Bank-side checkbox click now stops propagation so clicking the checkbox does not double-trigger the row click handler.

  • Reported by: Maribeth Carlton — Checked items on both bank statement and general ledger sides lost their marks when navigating away and returning.
  • Root Cause (Bank side): handleBankSelect originally only toggled selected in React state + useRef. No database write occurred. Bank reconciliation_status existed in bank_transactions but was never updated on checkbox click.
  • Root Cause (GL side): handleLedgerSelect had the same local-only bug, and grouped ledger header selection (handleGroupSelect) was still a second GL checkbox path that only mutated local selected state + ledgerSelectionsRef.
  • Root Cause (Clear/Finish cleanup): The restore useEffect prefers selection refs over DB state. handleConfirmClearClearingMarks / handleFinishReconciliation only cleared bankSelectionsRef, so stale ledger selections could re-apply after refetch.
  • Fix (10 changes):
  • New / Updated DB Functions: bulkUnreconcileBankTransactions(), bulkFinishBankReconciliation(), createReconciliationMatch(), unreconcileReconciliationMatchByLedgerEntry() in financial-reports.ts
  • Files: ReconciliationManager.tsx, financial-reports.ts, backend/migrations/20260306_create_reconciliation_match_tables.sql, DEVELOPER-PLAYBOOK.md

P0: Historical Unreconcile Failed After Reload / Batch Matches Had No Durable Link

1. reconciliation_match_groups stores the bank transaction + account + matcher + matched/unmatched timestamps. 2. reconciliation_match_entries stores every ledger line included in the match group. 3. create_reconciliation_match RPC creates the durable match record after a successful bank↔ledger reconcile. 4. unreconcile_reconciliation_match_by_ledger_entry RPC resolves the entire historical match by any ledger line, unreconciles both sides atomically, clears the legacy bank ledger_entry_id, and archives the match with unmatched_at / unmatched_by. 5. handleUnreconcileLedgerEntry in ReconciliationManager now uses the durable RPC result instead of depending on in-memory reconciledPairs for correctness.

  • Root Cause: reconciledPairs only lived in React state for the current session. After reload, the Reconciled tab had no durable way to reconstruct which bank transaction belonged to a ledger entry. The legacy bank_transactions.ledger_entry_id field only stored the first linked ledger line, so it could not represent 1 bank → N ledger batch matches.
  • Fix: Added durable reconciliation match history in the database:
  • Authority Rule: bank_transactions.ledger_entry_id is now a legacy compatibility pointer only. Durable match history for historical unreconcile lives in reconciliation_match_groups + reconciliation_match_entries.

P0: Phantom Void Entries in Bank Reconciliation — Structural + Data Fix (March 6, 2026)

P0: "3 duplicate deposits showing for Kevin and Maribeth Carlton on Deeper Walk"

1. financial-reports.tsfetchLedgerEntriesForReconciliation and fetchReconciledEntriesPaginated now apply boolean-first void filters, excluding is_void_reversal = true and active is_voided = true originals from the working UI. 2. get_reconciliation_summary RPC — updated to use boolean-first void handling, keeping reconciled voided originals in cleared-balance calculations while excluding unreconciled/c-status voided noise. 3. Shared JE void helpers and display logic now resolve void state from is_voided, is_void_reversal, and explicit void metadata instead of legacy reference_type markers.

  • Reported by: Steven Leipzig — Voided deposit JEs appeared as phantom unreconciled entries in bank reconciliation. Multiple orgs affected.
  • Root Cause (Structural): Legacy reconciliation paths only filtered is_void_reversal = false, so active is_voided = true originals could still appear in the working UI. The final fix standardizes reconciliation on boolean-first void handling: working lists exclude unreconciled/c-status voided originals and all reversals, while reconciled voided originals remain in summary math so historical cleared activity is preserved.
  • Root Cause (Data): Maribeth manually created deposit batch JEs for donations that already had Aplos-imported JE lines, then voided the duplicates. The void pairs (original + reversal) both sat unreconciled as noise ($0 net impact but cluttering the view).
  • Code Fixes:
  • Data Fixes: Reconciled orphaned void pairs across Deeper Walk, CWW, CTLU, Cornerstone, ReImagine. Voided Cornerstone duplicate $800 Carlton donation + JE-2026-827.
  • Files: journal-entries.ts, financial-reports.ts, migration file, DEVELOPER-PLAYBOOK.md §20

P1: Bank Reconciliation Reports — Auto-Save + Standalone Viewer (March 5, 2026)

P1: "Can I get historical reconciliation reports I can pull up later?"

1. Snapshot required on Finish Reconciliation: When user clicks "Finish Reconciliation" (promoting c→R), the reconciliation snapshot is persisted to reconciliation_reports as JSONB between PDF generation (Step 1) and status promotion (Step 2). If snapshot save fails, finish is blocked and no statuses are promoted. 2. Standalone "Bank Reconciliation Reports" tool: New tile in the Reports hub. Shows a list of past reconciliation snapshots with account filter dropdown + pagination. Each report can be viewed inline (Detailed shows the full 4-section layout like the live Report tab; Summary shows Balance Summary only) or downloaded as PDF. Download PDF always opens a dialog to pick Detailed vs Summary PDF (see documentation/pages/reports/16-BANK-RECONCILIATION-REPORTS.md). Reports can be unreconciled by parent-org accounting users, and only reversed reports are deletable (active snapshots are immutable artifacts).

  • Requested by: Maribeth Carlton — Needs to view past reconciliation reports for audit and reference, similar to QuickBooks "Previous Reconciliation" feature.
  • Solution (QuickBooks pattern):
  • DB Table: reconciliation_reports — JSONB snapshot columns (deposit_entries, payment_entries, uncleared_entries) store immutable line-item data. Not FK'd to live journal_entry_lines — if entries are later voided/edited, the historical report still shows what it looked like at save time.
  • Embedded Report tab preserved: The existing Report tab (3rd pill tab) in ReconciliationManager stays as a live preview tool. The standalone viewer in Reports hub is for historical snapshots only.
  • Files: ReconciliationManager.tsx (auto-save in handleFinishReconciliation), financial-reports.ts (3 new functions + types), BankReconciliationReports.tsx (NEW), store/types.ts, ReportsHub.tsx, PageRouter.tsx, 12 locale files, migration file

P0: Unreconciled Tab — Fixed-Height Scrollable Tables (March 5, 2026)

  • Reported by: Steven Leipzig — Bank Statement and General Ledger tables on the Unreconciled tab extended the full page, causing the entire page to scroll instead of each table scrolling independently.
  • Root Cause: max-h-[min(800px,calc(100vh-280px))] CSS class was never defined in the pre-compiled index.css. The class had no effect, so tables grew unbounded. Additionally, min-h-[400px] forced a minimum height that contributed to page overflow.
  • Fix: Changed both table containers to max-h-[min(700px,calc(100vh-340px))] (already defined in CSS) and min-h-[200px] (added to CSS). Set unreconciledPageSize to 50 so tables load 50 rows per page, with ~10-15 rows visible in the fixed-height container and the rest accessible via scroll. Each table now scrolls independently within its card.
  • Files: ReconciliationManager.tsx, index.css

P0: Auto-Save Session Persistence (March 4, 2026)

P0: "I don't see a save for later button here anymore. Nothing saved."

1. Statement Date now auto-persists to localStorage on every change (dedicated key reconciliation_statement_date_{accountId}, matching the existing statementBalance pattern) 2. Active Tab now auto-persists to localStorage via useEffect on every tab change (key reconciliation_session_{accountId}) 3. Auto-restore on return — When switching to an account, all 3 values (statementBalance, statementDate, activeTab) are restored from their localStorage keys 4. Session cleanup — All 3 localStorage keys cleared when "Finish Reconciliation" completes 5. Removed — "Save for Later" button, "Resume Session" banner, sessionBannerDismissed state, handleSaveForLater, handleResumeSession, handleDismissSession handlers, Save icon import

  • Reported by: Maribeth Carlton — Was working on February reconciliation, left, returned and found nothing saved.
  • Root Cause: The previous "Save for Later" button required an explicit click to persist statement date and active tab to localStorage. Clearing marks (c) already persisted to DB on every click, and statement balance auto-saved on every change — but statement date and active tab were only saved on manual button click. If the user navigated away without clicking the button, those values were lost. The "Resume Session" banner also only appeared if an explicit save had been made.
  • Fix — Full Auto-Save (replaces manual Save for Later):
  • Persistence layer: §40 Layer 2 (raw localStorage) — correct for draft/WIP data
  • localStorage keys: reconciliation_statement_balance_{accountId}, reconciliation_statement_date_{accountId}, reconciliation_session_{accountId} (activeTab only)
  • Files: ReconciliationManager.tsx

P1: Report Tab — Inline Reconciliation Report + Uncleared Transactions (March 2, 2026)

P1: "We used to have a bank reconciliation report"

1. Balance Summary — Cleared Balance, + Deposits, − Payments, = Ending Balance, Statement Balance, Difference (green when balanced, red when not) 2. Cleared Deposits — Itemized table with Date, Ref, Description, Fund, Amount. Click any amount to open JE drawer. Totals row. 3. Cleared Payments — Same format. Click-to-view JE. Totals row. 4. Uncleared Transactions — Items with clearing (c) status. Amber-bordered card. Debit + Credit columns. Click-to-view JE. Totals row. Helps bookkeeper investigate what's still in process.

  • Requested by: Maribeth Carlton — Needs a standard bank reconciliation report showing balance summary, cleared deposits/payments detail, and uncleared transactions — all viewable inline without exporting.
  • Solution: Added the Report pill tab (today’s full set is Unreconciled · Clearing · Reconciled · Report). The Report tab renders a full inline HTML version of the standard fund accounting reconciliation report.
  • Report layout (4 sections):
  • Date range selector at top of Report tab — filters cleared deposits/payments to the selected period
  • Download PDF / Export CSV buttons at top-right of Report tab — replaces the old Export Dialog
  • Files: ReconciliationManager.tsx, 6 locale files

P0: Reconciled Tab Header Overlap + "actions" Lowercase

  • Root Cause (header): table-fixed with "Transaction Date" header at w-[110px] overflowed into Ref column. Fixed by shortening to "Date" and reducing column widths (110→100, 130→100).
  • Root Cause (actions): t('actions', { ns: 'common' }) but common.json has no actions key → i18next fell back to raw key string "actions" (lowercase). Fixed by using t('pages.reconciliation.actionsHeader').

P2: Export Dialog Removed — Replaced by Report Tab

  • The custom Export Dialog (radio groups for export type/format, date range pickers, ~110 lines JSX) has been removed. Export functionality now lives on the Report tab as "Download PDF" and "Export CSV" buttons. Simpler, no dialogs, no extra buttons.

P2: Breadcrumb i18n

  • Fixed hardcoded "Fund Accounting" / "Bank Reconciliation" in Breadcrumb → t() calls

P3: Unused Imports Cleaned

  • Removed RadioGroup, RadioGroupItem, exportToExcel, exportToPDF imports (all from removed Export Dialog)

P1: Clickable Amounts — Navigate to Journal Entry (March 1, 2026)

P1: "Is there a way to go to the entry if I click on the amount?"

  • Requested by: Maribeth Carlton — "Is there a way to go to the entry if I click on the amount or something. I know it is tricky because I am also clicking on it to reconcile it. Any ideas?"
  • Solution: Made debit/credit amounts clickable in both Unreconciled and Reconciled tabs. Clicking an amount opens a Sheet with JournalEntryEditDrawer showing the full journal entry details.
  • Implementation:
  • Amounts wrapped in <button> elements with hover:underline styling and e.stopPropagation() to prevent row click conflicts
  • Uses fetchJournalEntryByLineId(transaction.id) to fetch the full JE by the line's UUID (matches pattern from Balance Sheet, Income Statement, General Ledger)
  • Tooltip on hover: "Click to view journal entry"
  • Updated hint text below tabs: "Click R to mark items — changes save automatically · Click any amount to view the journal entry"
  • Read-only mode when !canWriteAccounting
  • i18n: Added 4 new keys to all 6 locales: clickAmountHint, clickToViewEntry, jeNotFound, jeLoadFailed
  • Files: ReconciliationManager.tsx, 6 locale files (en/es/fr/de/zh/th accounting.json)

P1: GL Ref Column — Smart Ref Derivation (March 1, 2026)

P1: "LED doesn't tell me anything that helps me reconcile"

1. reference_id if present (rare — Stripe PI refs, backfill markers) 2. Description-derived label: "Check Dep." for check deposits, "Deposit" for cash deposits, vendor name for payments (e.g., "Costco", "Calendly LLC"), "Stripe" for payout settlements, "Gusto" for payroll, "Wire" / "Wire Fee" for transfers 3. entry_number as final meaningful fallback (e.g., JE-2026-0000558; legacy shorter historical values can still appear) 4. 'JE' as absolute fallback (should never hit — all rows have entry_number)

  • Reported by: Maribeth Carlton — "I need a different ref in General Ledger block. LED doesn't tell me anything that helps me reconcile. Check # or the part of the Journal entry number that makes sense or Gusto or Deposit would help"
  • Root Cause: fetchLedgerEntriesForReconciliation and fetchReconciledEntriesPaginated used row.reference_id || \LED-${row.id.substring(0, 4)}\` as the ref. 99.9% of JE lines have reference_id = NULL (23,834 of 23,859 on IFM Checking), so the fallback showed truncated UUID prefixes (LED-cff6, LED-0b31`) — meaningless for reconciliation.
  • Fix: Added deriveSmartRef() helper in financial-reports.ts with priority cascade:
  • Impact: 23,834 rows immediately get useful refs. GL Ref column now shows vendor names, deposit types, and JE numbers that match what Maribeth sees on her bank statement.
  • Files: financial-reports.ts (both mapping functions + new helper)

P0: Stat Tiles Inflated by Bank Selections — GL-Only Fix (March 1, 2026)

P0: "Top boxes are totaling from all four columns. Top totals should only be General Ledger columns."

  • Reported by: Maribeth Carlton — With 2 bank items selected ($1,000) and 2 GL items selected ($1,000), the Deposits/Payments tiles showed $2,000 instead of $1,000.
  • Root Cause: The hasBankData branch of the stat tile useMemo summed selectedBank + selectedLedger + clearingLedger for Deposits and Payments. Bank checkbox selections were feeding into the balance tiles, effectively double-counting against the Statement Balance input (which already represents the bank side).
  • Fix: Removed all selectedBank / unreconciledBank contributions from the stat tile deposits/payments/counts. The hasBankData branch now matches the ledger-only branch pattern — only GL-side data (selectedLedger + clearingLedger) feeds into the balance tiles. Bank selections continue to feed the selection summary bar for match-pairing.
  • QuickBooks Rule: Balance tiles = book-side running totals. Bank side = Statement Balance input. Bank checkboxes = visual tracking + match-pairing only.
  • Files: ReconciliationManager.tsx, DEVELOPER-PLAYBOOK.md (§16 + §20)

P0: Bank Debit/Credit Swapped in Stat Tiles + Direction Matching (February 28, 2026)

P0: "The bank side is showing up in the deposits on top"

1. Stat tile deposits/payments formulapayments = bank.debit changed to payments = bank.credit; deposits = bank.credit changed to deposits = bank.debit 2. Selected/total deposit/payment counts — same debit↔credit swap for count filters 3. Selection summary barbankDeposits/bankPayments/bankDirection logic corrected 4. Batch match detectionbankAmount resolution corrected (debit > 0 ? debit : credit) 5. Match Selected direction validation — direction mismatch check corrected (bank debit should match ledger debit, not ledger credit) 6. Match Selected amount comparisonbankAmount resolution corrected

  • Reported by: Maribeth Carlton — "It should be the debits on the G/L side for deposits and the credits on the G/L side for the payments."
  • Root Cause: The imported bank statement data uses GL conventions (transaction_type: 'debit' = money IN = deposit, transaction_type: 'credit' = money OUT = payment). However, the stat tile formula treated bank debits as payments and bank credits as deposits — the opposite of what the data means. This caused deposits from the bank side to inflate the Payments tile and vice versa.
  • Scope: 6 locations affected:
  • Key Insight: Bank CSV imports store amounts using the customer's GL perspective (matching asset account conventions), NOT the bank's perspective. Debit = increase = deposit. Credit = decrease = payment. This is consistent across all imported bank data in the bank_transactions table.
  • Files: ReconciliationManager.tsx

Bank Multi-Select + Stat Tiles Source Fix (February 28, 2026)

P0: Bank Checkboxes Only Allow One Selection at a Time

  • Reported by: Maribeth Carlton — "When a transaction is marked on the bank side it should stay marked until I unmark it. Now it is only one at a time."
  • Root cause: handleBankSelect used selected: t.id === id ? !t.selected : false — selecting any bank item deselected all others. This was a deliberate single-select design for the 1-bank-to-N-ledger pairing workflow, but Maribeth needs to check multiple bank items to track what she's reviewed and to build running totals.
  • Fix: Changed to t.id === id ? !t.selected : t.selected — bank checkboxes now persist like the GL side. Added selectedBankItems (plural array) for stat tile totals. selectedBank (singular, first item) still used for "Match Selected" pairing action. "Match Selected" button now requires exactly 1 bank item selected (selectedBankItems.length === 1).

P0: Stat Tiles Source — Bank-Side Only, Ignoring GL Checkbox Selections

  • Historical note: This was an intermediate February fix. Current behavior is the March 1 GL-only rule below: summary tiles accumulate from GL-side only, while bank checkbox selections feed the selection summary bar for match-pairing.
  • Reported by: Maribeth Carlton — "The top cells is taking the marks from the bank side not the GL side. Payments total on top is $400 (item marked on the bank side) but total marked in GL for payments is $1221.67"
  • Root cause: In the hasBankData branch, the useMemo calculated deposits/payments from selectedBank (checkbox-selected bank items) + clearingLedger (R/c toggle items). It completely ignored selectedLedgers (checkbox-selected GL items). When Maribeth checked GL entries via checkbox, they appeared in the selection summary bar but NOT in the stat tiles.
  • Fix: Added selectedLedger (checkbox-selected, non-clearing GL items) to the stat tile formula: payments = selectedBank.debits + selectedLedger.credits + clearingLedger.credits. Now every checked item on both sides immediately contributes to the running totals — matching QuickBooks behavior.

P1: Selection Summary Bar Updated for Multi-Bank

  • Fix: Summary bar now shows bank item count ("2 item(s) selected") instead of single direction label. Match difference indicator only shown when exactly 1 bank item selected (for 1-to-N pairing display).

P1: Batch Match Guard

  • Fix: suggestedBatchMatch only fires when exactly 1 bank item is selected (selectedBankItems.length === 1). Multi-bank selections are for stat tile accumulation, not batch matching.

Custom Export Dialog with Date Range + Report Type (February 28, 2026)

  • Historical note: This section is changelog context only. The custom export dialog was later removed and replaced by the March 2 Report tab + the March 5 historical reconciliation reports viewer.

P0: Export Dialog Missing Date Range Pickers

  • Problem: The top-right Export button opened SimpleExportDialog (format-only picker) calling handleExportReconciled. It silently used the Reconciled tab's startDate/endDate state — but the user couldn't see or set those dates from the export dialog. Exporting from the Unreconciled tab exported all time with no filter.
  • Fix: Replaced SimpleExportDialog with a custom inline export dialog that has:
  • Export Type selector (radio group): Reconciliation Report (PDF) vs Reconciled Entries (CSV/Excel/PDF)
  • Date Range pickers visible in the dialog, with clear button. Pre-populated from nothing (all dates) — user picks the period they want.
  • Format selector (only for Reconciled Entries — Report is always PDF)
  • Reconciliation Report queries reconciled entries within the date range, splits into deposits/payments, computes balance summary, and generates PDF via generateReconciliationReportPDF().
  • Reconciled Entries exports tabular data with the dialog's own date range (not the tab's).

P1: Dead Code — handleGenerateReconciliationReport

  • Problem: The standalone "Generate Report" button was removed during button consolidation, but the handler function (45 lines) was left behind as dead code.
  • Fix: Deleted. Its logic is now unified in handleExport and also still runs automatically inside handleFinishReconciliation.

P1: DEVELOPER-PLAYBOOK §16 — Stripe Payout Summary Paragraph Stale

  • Problem: §16 still documented the Stripe Payout Summary card that was removed in the previous session.
  • Fix: Replaced with Export Dialog documentation.

i18n Keys Added (all 6 locales)

  • exportTitle, exportDescription, exportTypeLabel, exportTypeReport, exportTypeEntries, exportDateRange, exportAllDates, exportFormatLabel, exporting, exportReportBtn, exportEntriesBtn, exportSuccess, exportFailed

QuickBooks-Style Overhaul + Export Module (February 28, 2026)

P0: Remove Stripe Payout Deposits Section

  • Decision: The collapsible Stripe Payout Summary card was a diagnostic tool that leaked into production UI. It duplicated information already visible in the GL table. The batch-match suggestion system handles the underlying problem (matching one bank deposit to N ledger entries).
  • Removed: stripePayouts query, stripePayoutExpanded/showPayoutSummary state, entire Stripe Payout Summary Card UI, CreditCard icon import. Kept fetchStripePayoutSummary() in financial-reports.ts for future use.

P0: GL Table Header Text Overlap

  • Root cause: 8 columns with table-fixed w-full totaled 640px of fixed widths, leaving only ~80px for the Description flex column. "Transaction Date" header text was too long. Sort icons ate additional space.
  • Fix: Renamed "Transaction Date" → "Date". Reduced widths per STYLING-GUIDE tier system: Date w-[110px]w-[100px], Ref w-[130px]w-[100px], Fund w-[120px]w-[100px]. Added truncate to all sortable header spans. Added overflow-hidden to GL Card.

P0: Clear Marks (422) but Deposits/Payments = $0.00

  • Root cause: When bankTransactions.length > 0, the useMemo calculated deposits/payments from selected bank transactions only. Clearing ledger entries were ignored because the code assumed bank-pairing mode.
  • Fix: In the hasBankData branch, clearing ledger entries now contribute to deposits/payments: deposits = selectedBank.credits + clearingLedger.debits, payments = selectedBank.debits + clearingLedger.credits. Supports the hybrid workflow.

P0: Button Consolidation (QuickBooks-Style)

  • Problem: Two confusing buttons visible simultaneously — "Reconcile" (pairs bank+ledger) and "Finish Reconciliation" (bulk c→R). Users didn't understand why there were two.
  • Fix: "Reconcile" button now hidden unless both a bank tx AND ledger entry(s) are selected — then appears as "Match Selected" (primary button). "Finish Reconciliation" renamed to "Reconcile" — this is the primary action. Requires statement balance entry before enabling (QuickBooks standard). Controls row restructured with flex-wrap to prevent overflow.

P1: Export Module (Top-Right)

  • Design: Export button added to the page header area next to Breadcrumb, matching the standard pattern used by GL, JE Manager, Pledges, Expenses, and Grants (SimpleExportDialog). Removed standalone export button from Reconciled tab (consolidated).

P1: Cleared Items Sink to Bottom

  • Maribeth's feedback: "When I click C on the ledger, it should kick it to the bottom of the table."
  • Fix: sortCompare now has a primary tier: unreconciled items sort first, clearing (c) items sort second. Within each tier, existing column sort applies. Both bank and ledger tables.

P1: Auto-Match Bank Visual Gray-Out

  • Maribeth's feedback: "It should automatically draw a line through or gray it out on the bank statement side."
  • Fix: clearingMatchedBankIds useMemo maps clearing ledger entry amounts to bank transaction IDs using exact amount matching. Matched bank rows get opacity-50 + line-through on description. Purely visual — no DB change on bank side.

i18n Keys Added (all 6 locales)

  • matchSelected, export

Audit: reconciledBy Passthrough + Stripe Payout Summary + i18n (February 27, 2026)

P2: reconciledBy Not Passed to Bank-Pairing and Toggle Calls

  • Root cause: handleReconcile (bank-to-ledger pairing) and handleToggleReconciliationStatus (R/c toggle) called updateBankTransactionReconciled, updateLedgerEntryReconciled, and updateLedgerEntryReconciliationStatus without passing reconciledById. The reconciled_by column would remain NULL on those entries.
  • Fix: Pass reconciledById (UUID) to all three DB functions.
  • Files: ReconciliationManager.tsx

P1: Stripe Payout Summary Card (Maribeth's Request)

  • Problem: Bank statement shows one Stripe deposit per day (e.g., "$17,114.98"), but the General Ledger shows 25 individual settlement JE lines (per-fund allocations). Maribeth was manually summing entries hoping to match the bank deposit.
  • Solution: New collapsible "Stripe Payout Deposits" card in the Unreconciled tab that:
  • Queries settlement JE lines on the selected account (description matches stripe payout / stripe settlement, or reference_id matches payout patterns)
  • Groups by date, showing one row per payout date with the aggregate deposit total
  • Expandable per-fund breakdown (click row to see which funds received what portion)
  • Footer shows grand total across all payouts
  • Shows R/c/— status per payout group
  • DB function: fetchStripePayoutSummary(accountId, options?) in financial-reports.ts
  • Diagnostic confirmed: All 25 Stripe entries on IFM Checking (account b4fb3e0c) are settlement backfill JEs from 2026-02-24, totaling $17,114.98 across 25 funds. No legacy individual donations incorrectly posted to 1000.
  • Historical clarification (Mar 6): Stripe's charge export now shows 23 real payout IDs in the Jan–Mar window, but the database still has 0 historical stripe_payouts rows and 0 settlement JE lines keyed by reference_id = po_.... The 2026-02-24 entries are therefore a legacy coarse balance-clear snapshot, not true payout-by-payout historical settlement.
  • Remaining blocker: The charge export provides payout composition, but not payout arrival dates. Do not rewrite the legacy historical settlement rows until Stripe payout master data or bank deposit dates are available.
  • Files: financial-reports.ts, ReconciliationManager.tsx

P1: Stripe Payout Allocation Context + Audit Details (Mar 22, 2026)

  • Why: Users saw valid payout totals but perceived a mismatch when fund-level breakout order/detail differed from Stripe dashboard exports.
  • UX behavior: Unreconciled ledger rows that map to Stripe payout settlement show a compact "Payout allocation" hint with tooltip context (allocation is from uncleared Stripe receivable + fee attribution + largest-remainder cent rounding). Fee lines on those JEs are Stripe processing fees (bank_fees); they do not include the Alignmint Connect application fee (0.5%), which is platform-side — see STRIPE-SETUP-GUIDE.md § *Journal Entry Format*.
  • JE drawer behavior: For entries with reference_id = po_..., the drawer now shows a read-only Stripe payout audit panel with payout amount, settlement allocated total, fee allocated total, unallocated amount, allocation strategy, and backfill markers.
  • Scope guardrail: This change does not alter accounting-basis logic (cash / accrual / modified accrual). It only explains settlement allocation and exposes audit metadata.
  • Files: ReconciliationUnreconciledTab.tsx, JournalEntryEditDrawer.tsx, financial-reports.ts

P1: Payout Drift Health (Admin Billing) + Daily Monitor (Mar 22, 2026)

  • Health query: public.get_stripe_payout_drift_health(p_limit) compares each `stripe_payouts` row with `status = 'paid'` to the sum of settlement debits on `journal_entry_lines` (reference_id like po_%, description Stripe payout settlement%). Rows with `abs(payout_amount - settled_debits) >= 0.01` are drift candidates. Migration `20260404121000_stripe_payout_drift_all_paid_backfill_je_ref.sql` drops the legacy RPC filter `journal_entry_ref = 'PAYOUT-SETTLE'` (so all paid payouts are evaluated) and backfills `stripe_payouts.journal_entry_ref` from posted `entry_number`s where the audit column was still `PAYOUT-SETTLE`. Field semantics: `DEVELOPER-PLAYBOOK.md` §16 *Stripe payout settlement — ledger vs audit columns*.
  • Acknowledged exceptions (Apr 6, 2026): Migrations `20260509130000_stripe_payout_drift_exception_policy.sql` and `20260509133000_harden_stripe_payout_drift_exception_policy.sql` add structured `stripe_payouts.metadata` for payouts that should not count as actionable drift (e.g. fully refunded, finance-approved no settlement JE). Required for exclusion: `drift_exception_v1 = true`, `drift_exception_scope = exclude_from_unresolved`, plus `drift_exception_reason` and audit stamps (`drift_exception_set_by`, `drift_exception_set_at`). The RPC returns `is_exception`, `exception_reason`, `exception_scope`, `unresolved_actionable`, and `total_unresolved` = count of actionable drift rows only. Verification: `supabase/queries/verify_payout_drift_exception_policy.sql`.
  • Runbook — when to add an exception (finance + engineering): Use only when (1) drift is expected after documented policy (e.g. no settlement JE by design), and (2) Stripe + books story is written down (ticket link or note in `drift_exception_notes`). Do not use exceptions to hide unknown mismatches. Prefer fixing settlement JEs or stripe_payouts.amount when the root cause is a posting bug.
  • Admin surface: Internal Admin Billing shows actionable drift count; the drift table defaults to actionable rows and can show acknowledged exceptions for audit.
  • Daily monitor: enforce-subscription-expiry daily cron uses RPC `total_unresolved` (actionable-only after the migration). It writes `stripe.payout_drift.detected` when that count is &gt; 0. Phase 2 (optional): split metadata payload into actionable vs exception counts — not required for Phase 1.
  • Ownership: Internal admin billing is the first-line visibility surface; actionable drift should be triaged there before manual accounting remediation.

P3: i18n Export Status Labels

  • Export CSV/Excel status column was hardcoded 'Reconciled' / 'Clearing' / 'Unreconciled'. Now uses t() calls.
  • New keys: statusReconciled, statusClearing, statusUnreconciled, stripePayoutSummary, stripePayoutHint, payout, payouts, funds
  • Added to all 6 locales (en, es, fr, de, zh, th)

---

P0: "Finish Reconciliation" Fails — reconciled_by UUID Type Mismatch (February 27, 2026)

P0: "It keeps telling me it failed to finish the reconciliation"

  • Reported by: Maribeth Carlton — Postgres error 22P02: invalid input syntax for type uuid: "Maribeth Carlton"
  • Root Cause: The reconciled_by column on journal_entry_lines is type UUID, but bulkFinishReconciliation() and bulkReconcileLedgerEntries() were passed reconciledByName (display name string like "Maribeth Carlton") instead of user.id (UUID). The display name variable was introduced in the Feb 14 audit when replacing 'Current User' hardcodes, but was incorrectly routed to the DB write path.
  • Fix: Added reconciledById = user?.id variable. Changed both DB call sites to pass reconciledById (UUID) instead of reconciledByName (string). Display name is still used for PDF reports and CustomEvent metadata.
  • Affected calls: bulkFinishReconciliation(accountId, reconciledById) (Finish Reconciliation) and bulkReconcileLedgerEntries(accountId, cutoffDate, reconciledById) (Set Starting Balance)
  • Files: ReconciliationManager.tsx

UX: Clickable Row Toggle + Larger Touch Targets

  • Ledger-only mode: clicking anywhere on a ledger row now toggles the R/c status (previously required clicking the small R/c button)
  • Clearing rows get subtle amber background (bg-amber-50/50 dark:bg-amber-900/10)
  • R/c buttons enlarged to min-h-[44px] min-w-[44px] for accessibility (WCAG touch target minimum)
  • Applied to both Unreconciled and Reconciled tab R/c buttons

UX: QuickBooks-Style Column Sorting

  • Requested by: Maribeth Carlton — "in Quickbooks I can click on the date column and the transactions will list by date, I can also click on the description column..."
  • Clickable column headers on both Bank and Ledger tables: Date, Description, Debit, Credit
  • Sort icons: ArrowUpDown (inactive), ArrowUp/ArrowDown (active + direction)
  • Click same column toggles direction; click different column sorts asc (date defaults desc)
  • sortField state: 'date' | 'description' | 'debit' | 'credit'
  • Shared sort state across both tables (bank + ledger sort together)
  • Old standalone "Newest First / Oldest First" button removed (replaced by column headers)
  • Stable sort tiebreaker: date → id

Code Cleanup

  • Removed unused AlertDialogAction import
  • Removed dead reconciledByUserId variable (duplicate of reconciledById)

i18n: Hardcoded Strings Cleanup

  • Export headers now use t() calls instead of hardcoded English
  • Account selector placeholder: t('pages.reconciliation.selectAccount')
  • R/c button aria-label: t('pages.reconciliation.statusAriaLabel')
  • New keys added to all 6 locales: selectAccount, statusAriaLabel

P1: Bank Statement R/c Status Column — "Items are not marking on the bank statement side"

  • Reported by: Maribeth Carlton — "Feedback items are not marking on the bank statement side. Need some way (maybe shade it some way) to know that transaction has been marked off."
  • Root Cause: The bank statement table (left column) had no reconciliation status indicator. Bank transactions only had a checkbox for selection and a bg-primary/10 highlight when selected — no persistent visual state showing which items had been matched/reconciled. The ledger table (right column) had a full R/c status column with color-coded backgrounds, but the bank side had nothing equivalent.
  • Fix (historical): Added an R status column to the bank statement table with tooltip. Bank rows showed R / c / empty and row shading.
  • Superseded (March 27, 2026): The dedicated bank R column was removed from the Unreconciled tab again — clearing bank lines belong in the Clearing tab workflow; the bank table is reduced to target + data columns. See changelog P2: Unreconciled tables — bank column trim + GL target = clearing at the top of this doc.
  • Files: ReconciliationUnreconciledTab.tsx (current); ReconciliationManager.tsx (historical)

P2: Table Height Improvement for Small Screens

  • Reported by: Maribeth Carlton — "Would be helpful if each side (bank statement and GL) could slide independently." Screen resolution: 1536×695.
  • Root Cause: Both table containers used max-h-[min(600px,calc(100vh-420px))]. On Maribeth's 695px screen, this gave only 275px of visible table height per side — very cramped.
  • Fix: Changed offset from 420px to 380px, giving ~40px more visible table area. Both bank and ledger tables already had independent overflow-y-auto scrolling — the issue was insufficient visible height making it feel broken.
  • Files: ReconciliationManager.tsx

P2: Missing DialogDescription — a11y Warning Fix

  • Root Cause: Phase A import confirmation dialog was missing <DialogDescription>, causing Warning: Missing Description or aria-describedby={undefined} for {DialogContent} console warnings.
  • Fix: Added <DialogDescription> with importConfirmDescription key to Phase A import dialog.
  • Files: ReconciliationManager.tsx

P0 Hardening: reconciledByreconciledByUserId Parameter Rename

  • Purpose: Prevent future UUID/name confusion. The reconciled_by column on journal_entry_lines is UUID type, but the function parameters were ambiguously named reconciledBy which could be mistaken for a display name.
  • Fix: Renamed reconciledByreconciledByUserId in both bulkFinishReconciliation() and bulkReconcileLedgerEntries() with inline comment: "Must be a UUID (user.id), NOT a display name".
  • Files: financial-reports.ts

i18n: New Keys (6 Locales)

  • bankStatusTooltip — Tooltip for bank table R column header
  • importConfirmDescription — DialogDescription for Phase A import dialog
  • Files: accounting.json (×6 locales: en, es, fr, de, zh, th)

P0: Cleared Balance Must Not Include Clearing (c) Entries (February 26, 2026)

P0: "Cleared balance should not change until I say the account is in balance"

1. Cleared Balance = R only — Removed + (summaryStats.clearingBalance || 0) from the clearedBalance useMemo. Fallback filter changed from t.reconciled || t.reconciliationStatus === 'clearing' to t.reconciled && t.reconciliationStatus === 'reconciled'. 2. Ledger-only Deposits/Payments = clearing items only — Previously summed selectedLedger + clearingLedger. Since checkboxes are hidden in ledger-only mode, selectedLedger was semantically empty but could carry stale ref data. Changed to sum only clearingLedger (items marked c). Deposits/Payments start at $0 and grow as items are marked (c). 3. Wired `bulkFinishReconciliation` — The handler referenced bulkFinishReconciliation which existed in financial-reports.ts but was not imported. Fixed the import in ReconciliationManager.tsx so handleFinishReconciliation() correctly calls the DB function.

  • Reported by: Maribeth Carlton — "I had 11/30/25 balanced. Now I have started to clear (c) transactions in December and it is changing the cleared balance which is incorrect."
  • Root Cause: clearedBalance was computed as summaryStats.clearedBalance + summaryStats.clearingBalance, meaning every entry marked (c) immediately changed the Cleared Balance tile. The correct QuickBooks behavior is: Cleared Balance = sum of only Reconciled (R) entries. Clearing (c) entries flow into Deposits/Payments, which feed the Ending Balance calculation. The Cleared Balance only updates when a reconciliation is finalized (c → R).
  • Fix (3 parts):
  • Correct Reconciliation Cycle: Cleared Balance (R only) + Deposits (c debits) − Payments (c credits) = Ending Balance. When Ending Balance matches Statement Balance → user clicks "Finish Reconciliation" → all (c) become (R) → Cleared Balance updates → Deposits/Payments reset to $0.
  • Files: ReconciliationManager.tsx, financial-reports.ts, 09-RECONCILIATION-MANAGER.md

P0: "Finish Reconciliation" Button — Ledger-Only Workflow Fix (February 26, 2026)

P0: "Nothing happens when I try the button" — Reconcile Button Disabled in Ledger-Only Mode

1. New "Finish Reconciliation" button — Visible when: `activeTab === 'clearing'` AND clearingCount > 0 AND `hasStatementBalance` (statement ending balance set) AND canWriteAccounting. Hidden entirely if the statement balance was never set — not just disabled. Enabled only when the ending balance matches the statement balance (difference < $0.01). Opens a confirmation dialog showing a full balance summary. 2. Bank-pairing "Reconcile" button now hidden when bankTransactions.length === 0 — eliminates the confusing disabled button in ledger-only mode. 3. Auto-generates reconciliation report PDF before promoting statuses, so the clearing entries are captured in the report. Then bulk-promotes all clearingreconciled.

  • Reported by: Maribeth Carlton — "I would think that I would be pushing the button to reconcile/finalize the cleared entries. Nothing happens when I try the button."
  • Root Cause: The "Reconcile" button requires both a bank transaction AND ledger entry to be selected (disabled={!selectedBank || !selectedLedger}). Maribeth works in ledger-only mode (no bank statement import), so selectedBank is always null and the button is permanently disabled/grayed out. The component had no mechanism to bulk-promote clearing (c) entries to reconciled (R) without importing a bank statement — a fundamental gap in the QuickBooks-style workflow.
  • Fix (3 parts):
  • New DB Function: finalizeReconciliation(accountId, reconciledBy?) in financial-reports.ts — Updates journal_entry_lines where reconciliation_status = 'clearing' to reconciled = true, reconciliation_status = 'reconciled', with reconciled_at and reconciled_by timestamps. Excludes void reversals.
  • New Handler: handleFinishReconciliation() — Step 1: Generate PDF report (before status change). Step 2: Call finalizeReconciliation(). Step 3: Dispatch reconciliation-update event. Step 4: Invalidate queries.
  • New i18n Keys (6 locales): finishReconciliation, finishTitle, finishDescription, finishConfirm, finishSuccess, finishFailed, finishReportNote
  • Files: ReconciliationManager.tsx, financial-reports.ts, accounting.json (×6 locales)

Reconciliation Enhancements — Toggle Fix, Bulk Unreconcile, Fund Column, Batch Matching, Report (February 25, 2026)

P0: R/c Toggle Causing Entries to Disappear

  • Reported by: Maribeth Carlton — "When I have clicked to mark an entry then realize that it is not reconciled on this statement - I click it again and it disappears."
  • Root Cause: handleToggleReconciliationStatus implemented a 3-state cycle: unreconciled → clearing → reconciled → unreconciled. Clicking twice advanced to 'reconciled' (R), moving the entry to the Reconciled tab.
  • Fix: Changed to 2-state toggle: unreconciled ↔ clearing. The 'reconciled' status is now ONLY set via the "Reconcile" button (bank-to-ledger matching). Clicking the R/c button toggles between empty and 'c' (clearing).
  • File: ReconciliationManager.tsx line 978

P0: No Bulk Unreconcile — "Need to unreconcile one at a time"

  • Reported by: Maribeth Carlton — "need some way to unmark all the transactions that you just marked in this reconciliation easily"
  • Fix: Added "Clear Marks (N)" button in toolbar (amber-colored, only visible when clearing marks exist). Shows confirmation dialog with count. Calls new bulkUnreconcileLedgerEntries() DB function.
  • New DB Function: bulkUnreconcileLedgerEntries(accountId, { status?, startDate?, endDate? }) — resets entries to unreconciled status
  • Files: ReconciliationManager.tsx, financial-reports.ts

P1: Fund Name Column — Distinguish Identical Entries Across Funds

  • Reported by: Maribeth Carlton — "It is hard for me to determine which check for the amount $2,272.72 when there are multiple."
  • Fix: Added "Fund" column to both Unreconciled and Reconciled tabs. Joins organizations table via fund_id to show fund name (e.g., "Called to Love Uganda"). Helps distinguish between identical-amount entries from different ministries.
  • DB Change: fetchLedgerEntriesForReconciliation() and fetchReconciledEntriesPaginated() now select entry_number and organizations!fund_id(name)
  • Files: ReconciliationManager.tsx, financial-reports.ts

P2: Smart Batch Match Detection

  • Reported by: Maribeth Carlton — "There are 6 entries in Alignmint for $11.95 but only one entry on the bank statement for $71.70 for Donor Elf."
  • Fix: Added suggestedBatchMatch useMemo that detects when multiple ledger entries sum to a selected bank transaction. Uses two strategies: (1) N × same-amount pattern detection (e.g., 6 × $11.95 = $71.70), (2) greedy combination of different amounts. Shows blue suggestion banner with "Apply Suggestion" button.
  • File: ReconciliationManager.tsx

P2: Reconciliation Report PDF Generation

  • Reported by: Maribeth Carlton — "It is usual to get a reconciliation report when the date is balanced."
  • Fix: Added "Reconciliation Report" button in toolbar (visible when statement balance is set). Generates professional PDF with: header (org, account, dates, reconciled by), balance summary (cleared + deposits − payments = ending vs statement), itemized deposits table, itemized payments table, Alignmint footer.
  • New Function: generateReconciliationReportPDF() in exportUtils.ts
  • Files: ReconciliationManager.tsx, exportUtils.ts

i18n: New Keys Added (6 Locales)

  • fund, clearAllClearingMarks, clearClearingTitle, clearClearingDescription, clearClearingConfirm, clearClearingSuccess, clearClearingFailed, batchMatchFound, applySuggestion, generateReport, reportGenerated, reportFailed
  • Updated statusCycleHint to reflect 2-state toggle behavior

P0 Fix — Reconciliation Guard False Positive on Multi-Date Entries (February 25, 2026)

P0: "No reconciled entries" but edit blocked as reconciled

  • Reported by: Maribeth Carlton — Reconciliation Manager shows no reconciled entries for 12/17/2025, but editing a journal entry from Balance Sheet drill-down says "Cannot edit a reconciled entry."
  • Root Cause: updateFullJournalEntry checked ALL sibling lines sharing the same reference_type prefix for reconciliation status. Aplos imports created multi-date mega-entries (e.g., JE-2025-140 has 244 lines across 6 dates from 2025-01-14 to 2025-12-20). bulkReconcileLedgerEntries reconciled the 22 January lines but left 222 December lines unreconciled. The guard found the reconciled January siblings and blocked editing the unreconciled December lines.
  • Scope: 35 entry_numbers had mixed reconciled/unreconciled state due to multi-date Aplos imports.
  • Fix: Changed reconciliation guard in updateFullJournalEntry (journal-entries.ts) to check only the specific lines being edited/deleted (via .in('id', editedLineIds)) instead of all siblings (via .like('reference_type', prefix)). This still protects genuinely reconciled lines while allowing unreconciled sibling lines to be edited independently.

Audit Fixes — Search/Filter, Void Reversal Exclusion, CurrencyInput & i18n (February 25, 2026)

P0: "Can't see all transactions" — No Search/Filter on Unreconciled Tab

  • Reported by: Maribeth Carlton — "I can't see all the transactions to know if something is reconciled or unreconciled. So then I can't start to balance."
  • Root Cause: With 1,075 unreconciled entries paginated at 50/page, Maribeth had to page through 22 pages with no way to search or filter by date.
  • Fix: Added search bar + date range filter to the Unreconciled tab. Search filters by description and ref fields (300ms debounce). Date pickers narrow the visible range. Clear button resets all filters. Filter summary shows count of filtered vs total results.

P0: get_reconciliation_summary RPC Counted Void Reversal Lines

  • Root Cause: The get_reconciliation_summary Postgres RPC had no is_void_reversal = false filter, counting 14 void reversal lines in the unreconciled total (1,089 vs actual 1,075). This caused a mismatch between the tab label count and the tile subtitle counts.
  • Fix: DB migration added AND is_void_reversal = false to the RPC's WHERE clause.

P0: fetchLedgerEntriesForReconciliation Missing .limit() Override

  • Root Cause: Unlike fetchBankTransactionsForReconciliation (which had .limit(50000)), the ledger query had no .limit() — relying on PostgREST max-rows (2500). With 1,075 unreconciled entries this worked, but any account crossing 2,500 would silently lose data.
  • Fix: Added .limit(50000) to match the bank transactions query pattern.

P1: fetchReconciledEntriesPaginated Missing is_void_reversal Filter

  • Root Cause: Reconciled tab query could show void reversal lines.
  • Fix: Added .eq('is_void_reversal', false) to the query.

P1: ~20 Remaining Hardcoded English Strings (Playbook §21 Violation)

  • Fixed: Tile subtitles, DesktopOnlyWarning props, Export button, Ref header, From/To date placeholders, import dialog strings (transaction count, Debits/Credits labels, Preview header, column mapper field labels), pagination labels, required fields indicator, save settings label
  • Added 9 new keys to pages.reconciliation in all 6 locale files: searchPlaceholder, fromDate, toDate, showingFiltered, desktopOnlyDescription, enterPlaceholder, reconciledEntriesCount, entries, preview (some already existed)

P1: toISOString().split('T')[0] in Export Filename (Playbook §22 Violation)

  • Fixed: Replaced with toLocalDateString(new Date())

P2: Statement Balance Uses type="number" Instead of CurrencyInput (Playbook §28 Violation)

  • Root Cause: Raw <Input type="number"> showed unformatted value (no $ or commas)
  • Fix: Replaced with CurrencyInput component. State converted from string to number. localStorage persistence maintained. hasStatementBalance flag distinguishes "not entered" from "$0.00".

P0 Bug Fix — "Reconciled 0" & Missing Pre-12/1 Entries (February 24, 2026)

P0: "Reconciled 0" — PostgREST 1000-Row Cap Silently Truncated Results

1. `financial-reports.ts`: fetchLedgerEntriesForReconciliation now defaults to fetching only unreconciled + clearing entries via .or('reconciled.eq.false,reconciliation_status.eq.clearing'). The Reconciled tab already has its own fetchReconciledEntriesPaginated() with server-side pagination. 2. `ReconciliationManager.tsx`: reconciledCount and unreconciledCount now use the summaryStats RPC data (getReconciliationSummary) instead of counting from the truncated local array. The RPC performs a server-side aggregate and is not affected by PostgREST row limits. 3. Supabase Dashboard: PostgREST max-rows increased from 1000 → 2500 to accommodate accounts with >1000 unreconciled entries.

  • Reported by: Maribeth Carlton — "This says Reconciled 0 - there are lots of reconciled transactions?"
  • Second report: "Reconciliation shows nothing unreconciled prior to 12/1/25. I know that there are other entries. i.e. Credit of $18.60 in 11/24/25 shows unreconciled in Journal entry but not here."
  • Root Cause: fetchLedgerEntriesForReconciliation fetched ALL entries (reconciled + unreconciled) for the account — 23,841 rows for the main checking account. PostgREST's server-side max-rows default of 1000 silently capped the results. Since the query sorted by transaction_date DESC, the newest 1,000 entries were all unreconciled (dates 12/1/2025–2036-02-12), and all 22,768 reconciled entries (newest: 11/30/2025) were invisible. The reconciledCount was computed client-side from this truncated dataset → always 0.
  • Fix (3 parts):

P1: Stale reconciledPairs Built from Truncated Data

  • Root Cause: The useEffect that built reconciledPairs from initial fetch data scanned the truncated 1000-row dataset for reconciled bank+ledger matches — always found zero. After the query fix (unreconciled-only), reconciled entries are no longer in the initial fetch at all.
  • Fix: Removed stale pair-building logic. Pairs are now only tracked for reconciliations created during the current session (via handleReconcile). The Reconciled tab uses fetchReconciledEntriesPaginated() for historical data.

P2: Remaining Hardcoded English Toast Messages

  • Fixed: ~14 remaining hardcoded English toast strings wrapped with t() calls using existing locale keys
  • Fixed: Pre-existing TS error — ofReconciled i18n call passed percent as number but i18next expected string

Audit Fixes — Pagination Bug, Entity Mapping & i18n (February 24, 2026)

P0: "125 entries but only 50 shown" — Unreconciled Tab Visibility Bug

1. fetchBankTransactionsForReconciliation had no `.limit()` override — Supabase silently capped at 1000 rows (default). Fixed: added .limit(50000) to match ledger query. 2. No pagination on Unreconciled tab — all entries rendered in DOM inside max-h-[600px] scroll container. On Maribeth's 840×695 screen, only ~15–18 rows visible without scrolling, creating perception of missing entries. 3. `max-h-[600px]` too tall for small screens — replaced with responsive max-h-[min(600px,calc(100vh-420px))] on all 3 table containers.

  • Reported by: Maribeth Carlton — reconciled entries count didn't match visible rows
  • Root Cause (multi-factor):
  • Resolved (Feb 2026): Client-side pagination added (50 items/page per table) with independent PaginationControls for bank and ledger tables. Pages reset on account change.
  • Superseded (Mar 27, 2026): Unreconciled/Clearing tables now use server-side RPC pagination (see P0: Unreconciled / Clearing — server-side RPC pagination above). Only the current page of bank/ledger rows is held in memory; cross-page selection uses refs + persisted IDs.

P2: Export Button for Reconciled Entries

  • Added: SimpleExportDialog on the Reconciled tab (PDF, CSV, XLSX formats)
  • Handler: handleExportReconciled fetches ALL reconciled entries (up to 50,000) for export, not just the current page
  • Filename format: Reconciled_Entries_{AccountName}_{YYYY-MM-DD}
  • Respects date filters: If user has set start/end date filters, the export includes only filtered entries
  • i18n: Added exportReconciledTitle key to all 6 locale files

P1: Missing isMappingInitialized() Guard (Playbook §11 Violation)

  • Root Cause: All 4 React Query calls used enabled: !!selectedAccountId but none checked isMappingInitialized(), risking queries firing before entity mapping is ready.
  • Fix: Added enabled: isMappingInitialized() && ... to all 4 queries: reconcilableAccounts, reconciliation, reconciliationSummary, reconciledEntries.

P1: ~28 Hardcoded English Strings (Playbook §21 Violation)

  • Fixed: All user-facing strings in handlers and JSX wrapped with t() calls
  • Added 28 new keys to pages.reconciliation in all 6 locale files (en, es, fr, de, zh, th)
  • Covers: Permission errors, import validation, reconcile/unreconcile toasts, batch dialog headers, status messages
  • Fixed: ofReconciled key changed from {{count}} to {{num}} (count is reserved by i18next for pluralization)

P2: Stat Tiles Animation (Playbook §16)

  • Fixed: Added stagger-fade-in-blur class to reconciliation balance cards grid

Bug Fixes — Ghost Entry & Selection-Based Totals (February 20, 2026)

P0: Ghost Entry Bug (Unreconciled entry disappears after unreconcile)

  • Reported by: Maribeth Carlton — unreconciled a $991.76 entry dated 2/25/25, entry disappeared from both Reconciled and Unreconciled tabs
  • Root Cause: handleUnreconcileLedgerEntry, handleToggleReconciliationStatus, handleReconcile, and handleUnreconcile all invalidated ['reconciliationSummary'] and ['reconciledEntries'] but never ['reconciliation'] — the main query that feeds the Unreconciled tab. After unreconciling, the stale cached data overwrote the local state update.
  • Fix: Added queryClient.invalidateQueries({ queryKey: ['reconciliation'] }) to all 4 mutation handlers

P0: Selection-Based Totals Not Working

  • Reported by: Maribeth Carlton — Deposits and Payments tiles showed totals of ALL unreconciled items instead of starting at $0
  • Root Cause: The useMemo that computed deposits/payments summed all unreconciled entries unconditionally, despite documentation claiming this was fixed
  • Fix: Reworked useMemo to sum only selected entries + clearing entries. Tiles now start at $0 and accumulate as items are checked. Tile subtitles show "X of Y selected" when items are checked, "Y unreconciled" otherwise.

P1: Sort Toggle Not Working on Reconciled Tab

  • Root Cause: fetchReconciledEntriesPaginated hardcoded .order('transaction_date', { ascending: false }) and ignored the sortOrder state
  • Fix: Added sortOrder parameter to fetchReconciledEntriesPaginated, added to React Query key. Both tabs now respect the sort toggle.

P1: Secondary Sort Tiebreaker

  • Issue: Transactions with the same date appeared in random order on sort toggle due to unstable .sort()
  • Fix: Added id as secondary sort key in both client-side memos and server-side query

P1: toLocaleDateString()formatDate() (Playbook §22)

  • Fixed: 3 instances in import dialog replaced with formatDate() utility

P2: Column Mapper Dark Mode (WCAG Compliance)

  • Fixed: dark:bg-*-900/40dark:bg-*-950 for proper contrast ratios

P2: Internationalization Cleanup

  • Fixed: ~15 remaining hardcoded English strings replaced with t() calls
  • Added: batchMatchingHint and adjustMappingHint keys to all 6 locale files
  • Bank table headers: Now use t() calls instead of hardcoded English

QuickBooks-Style Reconciliation Audit (February 14, 2026)

P0: Selection-Based Running Totals (Core Bug Fix)

  • Fixed: Deposits and Payments summary tiles now start at $0 and accumulate only as items are selected/checked
  • Previous behavior: Tiles showed totals of ALL unreconciled items regardless of selection
  • New behavior: Matches QuickBooks reconciliation — user builds running total by checking items
  • Tile subtitles: Show "X of Y selected" when items are checked, "Y unreconciled" otherwise
  • Selection summary bar: Removed redundant bar; replaced with compact match indicator for bank+ledger pairing only
  • Ledger-only workflow: Checkboxes now always visible (previously hidden when no bank data imported)

P1: Functional Fixes

  • Date handling: All 4 toISOString().split('T')[0] calls replaced with toLocalDateString() to prevent timezone bugs
  • User identity: All 4 'Current User' hardcodes replaced with actual user name from useAuth() (ReconciliationManager + GeneralLedger)
  • Clearing items: Resolved by selection-based totals — clearing items no longer inflate deposits/payments

P2: Styling & Playbook Compliance

  • Column mapper dark mode: dark:bg-*-900/40dark:bg-*-950 for WCAG-compliant contrast
  • Import dialog dates: toLocaleDateString()formatDate() utility
  • Clear statement confirmation: window.confirm() → Radix AlertDialog component
  • Summary tiles animation: Added stagger-fade-in-blur CSS class
  • Summary stats query: Added placeholderData: keepPreviousData to prevent flash on refetch

P2: Internationalization (i18n)

  • Added 70+ reconciliation keys to all 6 locale files (en, es, fr, de, zh, th)
  • Replaced hardcoded English strings with t() calls throughout the component
  • Covers: Summary tiles, tabs, buttons, dialogs, table headers, tooltips, match indicator, import wizard

Bug Fixes (February 2, 2026)

DatePicker Component Integration

  • Fixed: Date inputs now use the reusable DatePicker component instead of native HTML date inputs
  • Benefit: Users can type dates manually in familiar formats (MM/DD/YYYY, M/D/YYYY, etc.)
  • Affected inputs: Start date filter, end date filter, bulk reconciliation cutoff date
  • Consistent UX: Matches date input behavior in other accounting tools (Journal Entry Manager, Bills Manager, etc.)

Reconciled Tab Count Mismatch Fix

  • Fixed: Reconciled tab now shows the correct count matching Summary Stats
  • Root Cause: Tab was counting bank-to-ledger pairs instead of reconciled ledger entries
  • Solution: Changed to count ledgerTransactions.filter(t => t.reconciled) directly
  • New Reconciled Tab: Now displays all reconciled ledger entries with Date, Ref, Description, Debit, Credit, and Unreconcile action
  • Unreconcile: Individual ledger entries can be unreconciled directly from the Reconciled tab

Checkbox Selection Persistence Fix (useRef)

  • Fixed: Checkbox selections no longer reset when data refreshes (e.g., window focus, network reconnect)
  • Root Cause: useEffect had stale closure bug - read from closure-captured state instead of current state
  • Solution: Selection state now stored in useRef maps that persist across React Query refetches
  • Impact: Users can select multiple entries without losing selections due to background data refreshes

Date Sort Order Toggle

  • New Feature: Toggle button to sort transactions by date (oldest first or newest first)
  • UI: "Newest First" / "Oldest First" button next to Date Filter
  • Client-side sorting: No API refetch required, instant toggle
  • Applies to: Both Unreconciled and Reconciled tabs, bank transactions and ledger entries

---

Previous Updates (January 2026)

Major Enhancements (January 27, 2026)

Cross-Organization Query Fix (Critical)

  • Fixed: Parent org users can now see ALL fund entries for a shared GL account
  • Root Cause: Query filtered by organization_id, but journal entries are stored under individual funds while accounts belong to parent org
  • Solution: When accountId is provided, query by account_id across ALL organizations instead of filtering by org
  • Impact: Maribeth (parent_org role) can now see all 23,000+ entries for the main checking account across 60 funds

Date Range Filtering

  • New Feature: Filter transactions by date range to manage large datasets
  • UI: "Date Filter" button toggles start/end date inputs
  • Active indicator: Badge shows when filters are active
  • Clear button: Quick reset of date filters

Enhanced Balance Summary with Reconciliation Formula

  • Summary Stats Row: Shows total entries, reconciled count, unreconciled count
  • Reconciliation Formula Display:
  • Cleared Balance (previously reconciled)
  • + Deposits (unreconciled credits)
  • − Payments (unreconciled debits)
  • = Ending Balance (calculated)
  • Statement Balance (user-entered for comparison)
  • Visual Feedback: Green ring when balanced, red ring when difference exists

Bulk Reconciliation ("Mark All Prior")

  • New Feature: Bulk reconcile all entries before a cutoff date
  • Use Case: Establish a starting balance when first using the reconciliation tool
  • Dialog: Shows account info, unreconciled count, and warning about irreversibility
  • Database Function: bulkReconcileLedgerEntries(accountId, cutoffDate)

New Database Functions

  • bulkReconcileLedgerEntries(accountId, cutoffDate) - Bulk mark entries as reconciled
  • getReconciliationSummary(accountId) - Get stats for an account (total, reconciled, unreconciled, balances)

Checkbox Selection Persistence Fix (January 27, 2026)

  • Fixed: Checkbox selections now persist when scrolling or when data refreshes
  • Root Cause: The useEffect that processed query data was resetting all selected states to false on every data refresh
  • Solution: Selection state is now preserved across data refreshes by maintaining a map of current selections before rebuilding transaction arrays
  • Impact: Users can now check multiple transactions and scroll without losing their selections

---

Previous Updates (December 2025)

Database Persistence Fix (December 19, 2025)

  • Reconciliation now persists to database - Previously only updated local state
  • `updateBankTransactionReconciled()` - Updates bank_transactions table with reconciled, reconciled_at, reconciled_by, ledger_entry_id (legacy pointer to the primary matched GL line)
  • `updateJournalEntryLineReconciled()` - Updates journal_entry_lines table with reconciled, reconciled_at, reconciled_by
  • Unreconcile also persists - Clearing reconciliation status writes to database
  • Error handling - Try/catch with toast notifications on failure

*The primary ledger table is journal_entry_lines (the ledger_entries table was dropped in January 2026).*

Bank Statement Import Improvements

  • Smart auto-detect import - Two-phase dialog: auto-detect confirmation card (90% of imports) with visual column mapper fallback
  • Preamble row detection - Automatically skips bank name/account info rows before the actual header row
  • Hardened parsers - Date parser supports 10+ formats (ISO, US slash/dash/dot, named months, short year); amount parser handles spaces, CR/DR suffixes, trailing negatives, parentheses, multiple currency symbols
  • Save Header Settings - Checkbox to persist column mappings in localStorage for future imports
  • Smart Mapping Restore - When uploading a new file, saved mappings are automatically applied if column names match
  • Duplicate Detection - Per-GL-account duplicate detection based on transaction_date, amount, and description
  • Entity mapping guard - Import button hidden until entity mapping is resolved; explicit error if entity not ready
  • Actionable error messages - Import failures show the actual error message instead of generic toast

GL Account Selector

  • Asset account dropdown - Select which GL asset account to reconcile (from Chart of Accounts)
  • Inline with action buttons - Selector on left, Upload/Reconcile buttons on right
  • Entity-aware - Shows only asset accounts for the selected organization
  • Parent-org disambiguation - When deduplicating accounts by code, prefers the selected entity's own account row
  • Auto-select - First available asset account is auto-selected on load

Dynamic Balance Calculations

  • Payments - Sum of debits from unreconciled bank transactions
  • Deposits - Sum of credits from unreconciled bank transactions
  • Cleared Balance - Net of reconciled ledger entries (source of truth)
  • Difference - Unreconciled deposits minus payments
  • No hardcoded values - All stats calculated from actual database data

Database Integration

  • `bank_transactions` table - Filtered by organization_id and optionally account_id (GL account)
  • `journal_entry_lines` table - Filtered by account_id (GL account) across all organizations for the selected account
  • `reconciliation_status` column - 3-state text field: 'unreconciled', 'clearing', 'reconciled' on both bank_transactions and journal_entry_lines
  • RLS policies - is_parent_org_admin() OR organization_id = ANY(get_user_org_ids()) on bank_transactions

---

General Ledger Integration

Status:FULLY INTEGRATED The Reconciliation Manager is now fully integrated with the General Ledger, providing real-time reconciliation status updates across both components.

Key Features

Direct Ledger View (No Upload Step)

  • Opens directly to reconciliation view - no intermediate file upload screen
  • Bank transactions and ledger entries load automatically from Supabase
  • File upload available on the main reconciliation page for importing additional transactions
  • Streamlined workflow reduces clicks and confusion

Current Reconciliation Ownership Model

  • Reconciliation Manager is the authoritative write surface for statement-to-ledger pairing and reconciliation-status transitions
  • General Ledger reflects reconciliation state and responds to reconciliation-update events, but it does not provide a separate manual reconcile/unreconcile write path
  • Working reconciliation lists exclude active voided originals and void reversals so the operator only sees actionable items
  • Historical reconciled activity is browsed from the Reconciled tab with server-side pagination and optional date filters

Real-Time Synchronization

  • Reconciling transactions in Reconciliation Manager immediately updates the General Ledger
  • Check-deposit flags disappear when affected rows become reconciled
  • Status badges change between unreconciled, clearing, and reconciled immediately
  • Toast notifications confirm reconciliation with transaction count

Supported Reconciliation Paths

  • Bank-pairing workflow: Match one bank transaction to one or more ledger rows, then mark both sides reconciled
  • Ledger-only workflow: Use the ledger target on the Unreconciled tab to move unreconciled GL lines into clearing (c) without a bank match, then open the Clearing tab and Finish Reconciliation to promote them to reconciled
  • Historical unreconcile: Uses durable reconciliation match history so matched bank/ledger relationships can be reversed after reload

Metadata Tracking

All reconciliations include:

  • reconciled_by: User who performed the reconciliation
  • reconciliation_method: Values such as import, batch, manual, finish, or bulk_clear depending on the workflow
  • bank_statement_ref: Reference to bank transaction
  • reconciled_at: Timestamp of reconciliation

User Workflows

Workflow 1: Bank Statement Reconciliation

1. Open Reconciliation Manager and select the asset/bank account to work on 2. Optionally upload a bank statement file to import additional bank transactions 3. Use the bank target to select bank row(s) and click the body of unreconciled General Ledger rows to select them for pairing, then use Match Selected (amounts do not need to match; debit/credit column pairing is not blocked). Alternatively, use the ledger target alone to mark a GL line clearing without a bank match. 4. Click Match Selected so both sides persist as clearing (c) and create_reconciliation_match records the durable link 5. Open the Clearing tab, enter the statement ending balance if you have not already (otherwise Finish Reconciliation will not appear), confirm the balance tiles match, then click Finish Reconciliation to promote clearing rows to reconciled (R) for the period 6. General Ledger invalidates/refetches and shows the updated reconciliation state immediately

Workflow 2: Ledger-Only Reconciliation

1. Open Reconciliation Manager without relying on imported bank rows 2. On the Unreconciled tab, use the ledger target on each line to move relevant rows into clearing (c) (no bank match required) 3. Enter / confirm the statement ending balance (hasStatementBalance) and review the Deposits, Payments, and Ending Balance tiles 4. Switch to the Clearing tab and use Finish Reconciliation to bulk-promote all clearing rows to reconciled (button only appears on that tab when prerequisites are met) 5. A reconciliation report PDF is generated and a report snapshot is auto-saved for historical viewing

Workflow 3: Historical Unreconcile / Review

1. Open a reconciled row from the Reconciled tab (or a current-session matched pair) 2. Trigger unreconcile from Reconciliation Manager 3. Durable match history resolves the linked bank transaction and all matched ledger rows 4. Both sides are reverted to unreconciled and the General Ledger refreshes immediately 5. Report snapshots remain available separately for historical reference

Benefits

  • Single authoritative write surface - Reconciliation writes stay centralized
  • Consistent State - Both components always in sync
  • Audit Trail - Full metadata for every reconciliation
  • User Friendly - Immediate visual feedback
  • Flexible - Supports bank-pairing, ledger-only, and historical review workflows

---

Component File: src/features/accounting/components/ReconciliationManager.tsx Route / navigation: Path /fund-accounting, Zustand accountingTool = bank-reconciliation. See 00-ACCOUNTING-HUB.md. Access Level: Parent Org and Fund Users with Accounting access (position-based)

Overview

The Reconciliation Manager allows users to select a bank/asset account, upload bank statements when needed, match bank transactions against ledger rows, work ledger entries through clearing to reconciled, and generate live or historical reconciliation reports.

UI Features

Tabs

  • Unreconciled — Two-column layout: Bank Statement (left) + General Ledger (right). Search + date filters use a 300ms debounce, then `fetchReconciliationBankPaginated` / `fetchReconciliationLedgerPaginated` (RPCs) return server-paginated rows for the active page. Grouped ledger view still applies matchesAccountingSearch() client-side to the current page’s grouped rows (aggregated totals, combined fund names) so group-level amount search behaves correctly.
  • Clearing — Two stacked tables: bank lines in clearing (c) and ledger lines in clearing (c) (respecting statement cutoff). Data loads via the same paginated RPCs with scope: 'clearing'. Search + optional from/to date filters. Each table uses a shared 100 / 250 / 500 row-size control, Showing X–Y of Z text, and `PaginationControls` above and below the scrollable table (ReconciliationClearingTab.tsx). Row actions: Undo (back to unreconciled) and Reconcile (promote single match to R). Finish Reconciliation (bulk finalize for the period) lives in the toolbar and only shows on this tab when hasStatementBalance, clearingCount > 0, and canWriteAccounting.
  • Reconciled — Single table of finalized reconciled (R) ledger entries with date filter, text search, sortable column headers (ArrowUp / ArrowDown / ArrowUpDown), explicit unreconcile actions, and server-side pagination
  • Report — Inline reconciliation report (balance summary, cleared detail, uncleared). See Report tab changelog entries above.

Bank Statement Import

  • Two-phase dialog:
  • Phase A (Confirmation Card): Auto-detect mapping → show transaction count, date range, totals, detected columns with green checkmarks, 3-row parsed preview. User clicks "Import N".
  • Phase B (Visual Column Mapper): If auto-detect fails or user clicks "Adjust Mapping" → interactive spreadsheet preview with color-coded column header tags.
  • File formats: CSV, Excel (.xlsx, .xls) via xlsx library
  • Preamble detection: Skips bank name/account info rows before header
  • Saved settings: Column mappings persist to localStorage
  • Duplicate preview: Confirmation card includes import dedupe preview (totalIncoming, insertable, exactDuplicates, possibleDuplicates, `skippedSettledBankDuplicates`) before insertion, plus `autoRelinkOrphanClearing` when rows will be re-linked to orphan clearing (c) GL lines (see P1: Bank statement re-import — auto re-link and P1: Bank import — skip duplicates against clearing/reconciled bank lines in Recent Updates).
  • Import idempotency metadata: New imports write import_batch_id, import_fingerprint, import_source, and imported_at on bank_transactions.
  • DB uniqueness guard: uq_bank_transactions_org_account_import_fingerprint blocks duplicate inserts when fingerprints match within the same org/account scope.

Reconciliation Workflow

  • Bank↔GL match: Click bank row body to select bank row(s) for matching; bank target toggles unreconciled ↔ clearing without a GL line (handleToggleBankReconciliationStatus). Click GL row body selects ledger row(s) (handleLedgerSelect) for unreconciled or clearing lines. When totals match within $0.01, auto-handleReconcile or Match Selected runs → clearing (c) + createReconciliationMatch. Matched rows are removed from Unreconciled local state immediately and appear under the Clearing tab after refetch.
  • GL-only / bank-only clearing: Ledger target = handleToggleReconciliationStatus; bank target = handleToggleBankReconciliationStatus.
  • First bank and GL columns show R / c / empty as read-only status; Finish Reconciliation (toolbar, Clearing tab only) promotes cR in bulk after snapshot/PDF gate.
  • `Finish Reconciliation` visibility: Shown only when activeTab === 'clearing', canWriteAccounting, clearingCount > 0, and `hasStatementBalance`. Disabled when ending balance and statement balance differ by ≥ $0.01. Per-row Reconcile on the Clearing tab promotes a single match without finishing the whole statement.
  • Mutation safety: persisted status changes go through Match Selected, bank/ledger targets, or Finish Reconciliation — not through accidental clicks on amount links (JE drawer) or other controls.

Reconciliation Summary (Top Tiles — QuickBooks-Style)

  • Cleared Balance — Sum of reconciled (R) entries only (previously reconciled). Does NOT change when items are marked (c). Only updates when a reconciliation is finalized.
  • + Deposits — GL-side amount to add: all clearing (c) debits up to statement date + selected unreconciled debits.
  • − Payments — GL-side amount to subtract: all clearing (c) credits up to statement date + selected unreconciled credits.
  • = Ending Balance — Calculated: Cleared Balance + Deposits − Payments
  • Statement Balance — User-entered via CurrencyInput (formatted with $ and commas). Persisted to localStorage per account.
  • QuickBooks behavior: Cleared Balance is frozen from prior reconciliation. Clearing rows always contribute to Deposits/Payments; selecting unreconciled rows previews additional in-period matches before they are moved to clearing. When Ending Balance = Statement Balance, open the Clearing tab and click Finish Reconciliation (requires statement balance set) to promote all (c) → (R), which updates the Cleared Balance for the next period.

Bank Transactions Table (Unreconciled tab)

  • Target column (bank row selection for Match Selected), Date, Ref, Debit, Credit, Description — no separate status column; use Clearing / Reconciled tabs for bank line lifecycle review
  • Clickable column-header sorting (Date, Description, Debit, Credit) with arrow indicators
  • Reconciliation writes follow bank target + GL selection + Match Selected (or Finish Reconciliation for bulk cR)

Ledger Transactions Table (Unreconciled tab)

  • Status column (header R): read-only R / c / empty in cells
  • Target column: clearing toggle (handleToggleReconciliationStatus) — not used for match selection on flat/child rows
  • Row body click (outside buttons): toggles Match Selected selection for unreconciled lines only
  • Date, Ref, Debit, Credit, Description, Fund
  • Batch indicator button for batched transactions
  • Entry / Flat toggle (GL pane): Entry collapses related ledger lines into one row per logical journal entry (same entry_number or same journal_entry_id, after strong external refs such as Stripe po_ / ch_). Flat shows one row per journal_entry_lines row. Multi-fund entries that share one entry_number must appear as one Entry group so operators can match a single bank line to the net activity (expand the group to select sub-lines if needed — see i18n groupedPartialMatchHint).
  • Grouped/Flat view toggle uses compact control sizing so the GL card header aligns with the Bank Statement header
  • In grouped view, expanded children are rendered within the active page block so users can scroll to all visible child entries

Supabase Client Functions

The actual implementation uses Supabase client functions (no REST API endpoints):

| Function | File | Purpose |
|----------|------|---------|
| `insertBankTransactions()` | `src/lib/db/bank-transactions.ts` | Import parsed bank transactions with fingerprint/legacy duplicate detection; optional **auto re-link** to orphan clearing GL lines (see Recent Updates P1) |
| `fetchReconciliationBankPaginated()` | `src/lib/db/financial-reports.ts` | **Unreconciled / Clearing** bank rows — server-side pagination via `get_reconciliation_bank_paginated` |
| `fetchReconciliationLedgerPaginated()` | `src/lib/db/financial-reports.ts` | **Unreconciled / Clearing** GL rows — server-side pagination via `get_reconciliation_ledger_paginated` |
| `fetchBankTransactionsForReconciliation()` | `src/lib/db/financial-reports.ts` | Legacy/ad-hoc bank fetch (not used for workspace full-table load) |
| `fetchLedgerEntriesForReconciliation()` | `src/lib/db/financial-reports.ts` | Legacy/ad-hoc ledger fetch (not used for workspace full-table load) |
| `fetchReconcilableAccounts()` | `src/lib/db/financial-reports.ts` | Get asset accounts from Chart of Accounts |
| `updateBankTransactionReconciled()` | `src/lib/db/financial-reports.ts` | Mark bank transaction reconciled with ledger link |
| `updateLedgerEntryReconciled()` | `src/lib/db/financial-reports.ts` | Mark ledger entry reconciled |
| `updateLedgerEntryReconciliationStatus()` | `src/lib/db/financial-reports.ts` | Toggle R/c/empty status |
| `fetchReconciledEntriesPaginated()` | `src/lib/db/financial-reports.ts` | Server-side paginated finalized `R` entries only (Reconciled tab; excludes `clearing`) |
| `getReconciliationSummary()` | `src/lib/db/financial-reports.ts` | Get account reconciliation stats |
| `bulkReconcileLedgerEntries()` | `src/lib/db/financial-reports.ts` | Bulk reconcile entries before cutoff date |
| `bulkUnreconcileLedgerEntries()` | `src/lib/db/financial-reports.ts` | Bulk unreconcile entries (clear marks) |
| `bulkFinishReconciliation()` | `src/lib/db/financial-reports.ts` | Promote all clearing → reconciled (Finish Reconciliation) |
| `bulkUnreconcileBankTransactions()` | `src/lib/db/financial-reports.ts` | Bulk unreconcile bank transactions (clear marks) |
| `bulkFinishBankReconciliation()` | `src/lib/db/financial-reports.ts` | Promote all clearing bank transactions → reconciled |
| `createReconciliationMatch()` | `src/lib/db/financial-reports.ts` | Persist durable bank↔ledger match history |
| `unreconcileReconciliationMatchByLedgerEntry()` | `src/lib/db/financial-reports.ts` | Reverse a historical match via any matched ledger row |
| `saveReconciliationReport()` | `src/lib/db/financial-reports.ts` | Store immutable reconciliation report snapshots |

TypeScript Interfaces

// From src/lib/db/financial-reports.ts
interface ReconcilableAccount {
  id: string;
  code: string;
  name: string;
  fullName: string; // "1000 - Checking Account"
  type: string;
  isActive: boolean;
}

type ReconciliationStatus = 'unreconciled' | 'clearing' | 'reconciled';

interface ReconciliationTransaction {
  id: string;
  date: string;
  ref: string;
  description: string;
  debit: number;
  credit: number;
  reconciled: boolean;
  reconciliationStatus: ReconciliationStatus;
  ledgerEntryId?: string;
  accountId?: string;
}

// From src/lib/bankStatementParser.ts
interface ParsedTransaction {
  date: string;
  description: string;
  amount: number;
  type: 'credit' | 'debit';
  reference?: string;
  checkNumber?: string;
}

interface ColumnMapping {
  date: string;
  description: string;
  amount?: string;
  debit?: string;
  credit?: string;
  reference?: string;
  checkNumber?: string;
}

Authentication & Authorization

Permission Model (Role / Position / Access Level)

  • ROLE (org scope): parent_org (sees all funds), fund_user (sees assigned funds)
  • POSITION (feature access): director or bookkeeper can access accounting
  • ACCESS_LEVEL: read_write can import/reconcile; read_only can only view
  • `canWriteAccounting`: Requires canAccessAccounting(position) AND canWrite(accessLevel)
  • `canImport`: Requires canWriteAccounting AND entity mapping resolved

RLS Policies on bank_transactions

  • INSERT: is_parent_org_admin() OR organization_id = ANY(get_user_org_ids())
  • ALL: is_parent_org_admin() OR organization_id = ANY(get_user_org_ids())

State Management

Local State (React useState/useRef)

  • activeTab'unreconciled' | 'clearing' | 'reconciled' | 'report'
  • bankTransactions / ledgerTransactions — Transaction arrays with selected flag
  • reconciledPairs — Matched bank-to-ledger pairs
  • selectedAccountId — Currently selected GL asset account
  • sortOrder'asc' | 'desc'
  • statementBalance — User-entered statement balance (number, persisted to localStorage per account via CurrencyInput)
  • hasStatementBalance — Boolean flag distinguishing "not entered" from "$0.00"
  • unreconciledSearch / debouncedSearch — Search input for Unreconciled tab (300ms debounce)
  • unreconciledStartDate / unreconciledEndDate — Date range filter for Unreconciled tab
  • bankSelectionsRef / ledgerSelectionsRef — useRef maps to persist selections across React Query refetches
  • Import dialog state: importDialogState, uploadedFile, fileHeaders, filePreviewRows, columnMapping, importing

Global State (Zustand)

  • selectedEntity — Current organization slug (from useAppStore)
  • Entity mapping resolved via getActualOrgId(selectedEntity)

React Query Keys

  • ['reconcilableAccounts', selectedEntity]
  • ['reconciliationWorkspace', selectedAccountId] — persisted workspace snapshot (statement fields, tab, etc.)
  • ['reconciliationBankPage', selectedAccountId, scope, …] — paginated bank data (reconciliationBankPage / reconciliationLedgerPage prefixes; see useReconciliationContext.ts)
  • ['reconciliationLedgerPage', selectedAccountId, scope, …]
  • ['reconciliationBankClearing', …] / ['reconciliationLedgerClearing', …] — Clearing tab paginated queries (ReconciliationClearingTab.tsx)
  • ['reconciliationSummary', selectedAccountId]
  • ['reconciledEntries', selectedAccountId, startDate, endDate, reconciledPage, reconciledPageSize, reconciledSortField, reconciledSortOrder]
  • Related: reconciliationBankTotalCount, reconciliationBankClearingCount, reconciliationLedgerClearingAll, reconciliationBatchMatchPool, reconciliationUnreconciledLineCounts

> Critical: Mutations must invalidate the paginated workspace keys (reconciliationBankPage, reconciliationLedgerPage, clearing variants), reconciliationLedgerClearingAll, batch pool / counts as needed, `reconciliationSummary`, and `reconciledEntries` — see refetchReconciliation in useReconciliationContext.ts.

Error Handling

| Scenario | User Sees | Console |
|----------|-----------|---------|
| Parse failure | Actual parse error message | `[ReconciliationManager] Parse failed: {error, file, mapping}` |
| Zero transactions | "No valid transactions found. Check column mapping." | `[ReconciliationManager] Parse returned 0 transactions: {file, mapping}` |
| Insert failure | `Import failed: {actual error}` | `[ReconciliationManager] Import failed: {stage, error, entity, accountId}` |
| Entity not ready | "Please select a specific organization before importing." | — |
| Permission denied | Upload button hidden | — |

Related Documentation


Synced from IFMmvp-Frontend documentation: pages/accounting/09-RECONCILIATION-MANAGER.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