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.tspreserves raw header indexes while omitting blank header labels from the mapping UI. CSV/Excel files with blank spacer columns (for exampleDate,,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 followcredit > 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_snapshotnow includes bank-only clearing rows (bank rows with no active match and noledger_entry_id) so the saved/PDF reconciliation report matchesfinalize_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_idin 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
Exportbutton 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 rowsMatched 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 RPCsget_reconciled_entries_paginated,get_reconciliation_bank_paginated,get_reconciliation_ledger_paginatedwithpageSize: 50000to fetch all matching rows in one shot). Wired intoReconciliationToolbarvia the sharedExportButton. 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.xlsxfiles 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
CurrencyInputon 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
$0while the Clearing tab showed hundreds ofcrows. - Root cause: Unreconciled paginated ledger RPC excludes
clearing, andledgerTransactionsis intentionally cleared outside the Unreconciled tab. Tile math that only read unreconciled page rows dropped active clearing amounts. - Fix:
useReconciliationStatsnow combines (1) selected unreconciled GL rows fromledgerTransactionsand (2) all statement-bounded clearing GL rows fromclearingLedgerLines(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:
PayoutAllocationBadgeis now the single reusable entry point across Unreconciled/Clearing/Reconciled/Report tabs; the drawer UI is centralized inPayoutSourceDonationsDrawerto avoid per-tab duplication.
Schema: status-only reconciliation (April 7, 2026)
- `reconciled` BOOLEAN removed from
journal_entry_linesandbank_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 usereconciliation_statusinstead 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:
handleReconcileoptimistically setreconciliation_status = 'clearing'on in-memory rows but left them inbankTransactions/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; enaccounting.jsoncopy 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 =handleBankSelectfor 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.
handleReconcileallows ledger unreconciled | clearing, bank unreconciled only. - `useReconciliationContext`:
selectedBankItems/selectedLedgersuse!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:
partitionIncomingBankImportinbank-transactions.tsskips an incoming row when it semantically matches an existing bank line already clearing or reconciled (date + amount + type + loose description), even ifimport_fingerprintdiffers (e.g. CSV text changed). Prevents a second unreconciled bank row for an already-paired transaction. - Preview:
BankImportPreview.skippedSettledBankDuplicates+ dialog lineimportPreviewSkippedSettledBank. - 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
PillTabsis 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, updatedclearingTabHint,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 whenreference_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()insrc/features/accounting/hooks/useGroupedLedger.tsmixed fund display name (organizationNamefrom the reconciliation RPC row) into the group key together withentry_number/journal_entry_id. Multi-fund JEs share a singleentry_numberbut 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/refstill 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 insrc/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.sql—p_scope = 'unreconciled'forget_reconciliation_ledger_paginateduses `NOT IN ('clearing', 'reconciled')`, matchingget_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.sqlbehavior (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 removesreconciliation_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 activereconciliation_match_entriesrow (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, callscreate_reconciliation_match, thenupdateBankTransactionReconciled/updateLedgerEntryReconciliationStatusto 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:
previewBankTransactionImportaddsautoRelinkOrphanClearing(confirmation card). Toasts:importedWithAutoRelink,importAutoRelinkFailedif match RPC fails after insert. - Audit:
ReconciliationImportDialogpassesimportMatchedBy(reconciledById) intoInsertBankImportOptions.matchedByUserId. - Files:
bank-transactions.ts,ReconciliationImportDialog.tsx,ReconciliationManager.tsx,en/esaccounting.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 tabget_reconciled_entries_paginatedfix. - Architecture: Unreconciled and Clearing bank/ledger tables load one page at a time via
fetchReconciliationBankPaginated/fetchReconciliationLedgerPaginatedinfinancial-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_access→get_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 useCOALESCE(bt.reconciled, false) = false, but the first ledger RPCs usedjel.reconciled = false, which excludesNULLin SQL. Lines with nullreconciled(stillunreconciledby status) vanished from the Unreconciled/Clearing GL pane while the bank side still showed activity. Replaced withCOALESCE(jel.reconciled, false) = falsein all affected ledger reconciliation functions. (Original file omitted$function$;between someCREATE OR REPLACEblocks; fixed sosupabase db pushapplies cleanly.) - Migration history baseline:
supabase/migrations/20260327203735_remote_history_baseline.sql— production already had version20260327203735recorded before a matching file existed in this repo; the placeholder aligns local history with remote sodb pushno 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.sqlincludesjel.transaction_date IS NULLin statement/date filters so undated lines are not dropped by SQL three-valued logic. Frontend grouped mode now uses the same serverunreconciledLedgerPageas 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.sql—p_scope = 'unreconciled'forget_reconciliation_ledger_paginatedexcludes clearing (same contract as bank). See P0: Unreconciled GL excludes clearing at the top of this doc. Historical note:20260403210000_reconciliation_unreconciled_include_clearing.sqltemporarily 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:
fetchBankTransactionsForReconciliationandfetchLedgerEntriesForReconciliationremain infinancial-reports.tsfor 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 pageSelectin the filter bar (100 / 250 / 500), a Showing X to Y of Z summary (pluscommon.pagination.pageOfwhen 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/esaccounting.json). - Files:
ReconciliationClearingTab.tsx, localeaccounting.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.tsxit 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-invokehandleReconcilewhen both sides are selected and totals match within $0.01 (ReconciliationUnreconciledTab.tsxuseEffect). - No blocking guards:
handleReconciledoes 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 tocas needed; removed i18n keys:directionMismatch,amountDifferenceRemaining,autoMatchWhenBalancedHint. - Safety:
matchInFlightRefprevents double-submit;lastFailedAutoMatchKeyRefblocks retry after a failed RPC until the user changes the bank/ledger selection. - Files:
useReconciliationActions.ts,ReconciliationUnreconciledTab.tsx, localeaccounting.jsonfiles,DEVELOPER-PLAYBOOK.md.
P0: Mass ledger selection + no match confirmation modal (March 2026)
- Root cause: Hydrating
selected: truewheneverreconciliation_status === 'clearing'made every clearing GL line count asselectedLedgers(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.matchToClearingHintshows as small text under the button. - Safety:
handleReconcileblocks when more than 50 ledger lines are selected (tooManyLedgerSelectionstoast). - Files:
useReconciliationContext.ts,useReconciliationActions.ts,ReconciliationDialogs.tsx,ReconciliationUnreconciledTab.tsx, localesaccounting.json,DEVELOPER-PLAYBOOK.md.
P2: More actions — Clear All Clearing Marks (March 2026)
- Change: Restored More actions → Clear All Clearing Marks (
clearClearingTitlelabel) callinghandleClearAllClearingMarks→ existing confirmation dialog (ReconciliationDialogs.tsx). Disabled when no account,clearingCount === 0, orclearingInProgress. - 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_matchrejects a second save for the same bank—operators must unreconcile or clear all clearing marks, then rematch. - Files:
ReconciliationUnreconciledTab.tsx, localeaccounting.jsonfiles.
Design note: Editing a match after confirm (March 2026)
- Database: One active
reconciliation_match_groupsrow 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_matchraises 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 swapp_ledger_entry_idsin 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
directionMismatchkey and client guard were removed fromhandleReconcileand 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_entriesRPC 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.sqldrops 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 promotesc → R. - Toolbar:
Match Selectedis 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
crows 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, andClear Statementare grouped under one left-side More actions dropdown to keep the controls row compact. - Reconciled tab scope: Reconciled history list shows
Ritems only;citems live on the Clearing tab until Finish Reconciliation promotes them toR(they no longer appear on the Unreconciled GL grid). - Reconciled counters: Labels that say "Reconciled" now represent finalized
Rcounts only (clearingcis tracked separately). - JE drawer integrity: Journal entries with any
corRlines are blocked from edit/void in reconciliation context; operator must unreconcile first. - Report integrity: Cleared sections in Reconciliation Report tab/export are
R-only;citems appear only in Uncleared.
P2: Reconciled Tab — Unified Search + Date Bar (March 24, 2026)
- UX parity: The Reconciled tab now uses the same
Cardlayout as the Unreconciled tab: one row with the text search field, from / toDatePickercontrols, 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:
handleClearFiltersinuseReconciliationActions.tsnow resets reconciled text search as well as the date range and page. - i18n: Dropped unused
reconciledRecoveryHintfrom all localeaccounting.jsonfiles.
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
handlePayoutPaidto use Stripe Balance Transactions API for actual per-charge fees - Compound JE per fund (one
entry_numberwith 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
handlePayoutSettlementfromplatform-webhook - Stripe Payout Summary card: Now shows only clean settlement lines (bogus fee lines are voided and excluded by
is_voided = falsefilter) - 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_summaryRPC) but the table was empty. - Root Cause:
fetchReconciledEntriesPaginatedused 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-secondauthenticatedstatement timeout. Same bug class as theget_journal_entries_page_corefix from March 9 — SECURITY INVOKER queries with RLS per-row JWT parsing timeout on large IFM datasets. - Fix: Created
get_reconciled_entries_paginatedSECURITY DEFINER RPC that validatesp_account_idagainstget_user_org_tree_ids()once, then queriesjournal_entry_linesdirectly (bypassing per-row RLS). Returns paginated JSON + total count in a single call. - Frontend: Rewrote
fetchReconciledEntriesPaginatedinfinancial-reports.tsto callsupabase.rpc(...)instead of PostgREST client query. Removed deadRECONCILED_OR_CLEARING_FILTERconstant. - 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:
handleFinishReconciliationnow computesfinalizedTotalDepositsandfinalizedTotalPaymentsfrom 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:
generateReconciliationReportPDFnow renders a "Uncleared Transactions" table with Debit/Credit columns and totals after Cleared Payments. TheReconciliationReportDatainterface accepts an optionalunclearedEntriesarray. - Saved report PDF download passes uncleared data:
BankReconciliationReportsnow includesunclearedEntrieswhen callinggenerateReconciliationReportPDFfrom a saved snapshot. - Timestamp formatting fixed: Saved report list and detail views now use
formatDateInTimezone/formatDateTimeInTimezoneinstead of brittlecreatedAt.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 sharedfinancial_audit_trigger_func(). - Trigger function updated:
financial_audit_trigger_funcnow resolvesorganization_idforreconciliation_match_entriesrows by looking up the parentreconciliation_match_groups.organization_idviamatch_group_id. Previously, match entries had noorganization_idorfund_idcolumn, so the trigger silently skipped them. - Frontend audit types extended:
AuditTableNameunion,TABLE_DISPLAY_NAMES,TABLE_OPTIONS,TABLE_LABEL_KEYSinaudit-log.tsandAuditLog.tsxnow includereconciliation_reports,reconciliation_match_groups,reconciliation_match_entries. - Internal admin audit hook updated:
useAuditLog.tsmapTableToCategorynow routes reconciliation artifact tables to'system'category. AddedgetAuditTableLabelandgetAuditActionLabelhelpers so the internal admin view shows human-readable action descriptions (e.g., "Created Reconciliation Reports") instead of rawINSERT 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()inauth/storage.tsnow 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.ts—ReconciliationReportData+ PDF uncleared sectionsrc/features/reports/components/BankReconciliationReports.tsx— timestamp formatting + uncleared PDF passthroughsrc/lib/db/audit-log.ts— 3 newAuditTableNameentries + display namessrc/features/accounting/components/AuditLog.tsx— filter options + label keyssrc/features/internal-admin/hooks/useAuditLog.ts— category mapping + label helperssrc/features/accounting/components/ReconciliationManager.tsx— finalized totals + uncleared snapshotsrc/features/deposits/components/RegularDepositManager.tsx— entity-switch rehydrationsrc/features/tools/components/ReimbursementsManager.tsx— entity-switch rehydrationsrc/contexts/auth/storage.ts— centralized draft purge on logoutsrc/i18n/locales/{en,es,fr,de,zh,th}/accounting.json— 3 new audit table labels eachsrc/lib/db/financial-reports.ts—saveReconciliationReport+fetchReconciliationReportsalready 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):
handleBankSelectoriginally only toggledselectedin React state +useRef. No database write occurred. Bankreconciliation_statusexisted inbank_transactionsbut was never updated on checkbox click. - Root Cause (GL side):
handleLedgerSelecthad the same local-only bug, and grouped ledger header selection (handleGroupSelect) was still a second GL checkbox path that only mutated localselectedstate +ledgerSelectionsRef. - Root Cause (Clear/Finish cleanup): The restore
useEffectprefers selection refs over DB state.handleConfirmClearClearingMarks/handleFinishReconciliationonly clearedbankSelectionsRef, so stale ledger selections could re-apply after refetch. - Fix (10 changes):
- New / Updated DB Functions:
bulkUnreconcileBankTransactions(),bulkFinishBankReconciliation(),createReconciliationMatch(),unreconcileReconciliationMatchByLedgerEntry()infinancial-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:
reconciledPairsonly 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 legacybank_transactions.ledger_entry_idfield 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_idis now a legacy compatibility pointer only. Durable match history for historical unreconcile lives inreconciliation_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.ts — fetchLedgerEntriesForReconciliation 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 activeis_voided = trueoriginals 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 livejournal_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 inhandleFinishReconciliation),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-compiledindex.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) andmin-h-[200px](added to CSS). SetunreconciledPageSizeto 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-fixedwith "Transaction Date" header atw-[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' })butcommon.jsonhas noactionskey → i18next fell back to raw key string"actions"(lowercase). Fixed by usingt('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,exportToPDFimports (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
SheetwithJournalEntryEditDrawershowing the full journal entry details. - Implementation:
- Amounts wrapped in
<button>elements withhover:underlinestyling ande.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/thaccounting.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:
fetchLedgerEntriesForReconciliationandfetchReconciledEntriesPaginatedusedrow.reference_id || \LED-${row.id.substring(0, 4)}\`as the ref. 99.9% of JE lines havereference_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 infinancial-reports.tswith 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
hasBankDatabranch of the stat tileuseMemosummedselectedBank + selectedLedger + clearingLedgerfor 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/unreconciledBankcontributions from the stat tile deposits/payments/counts. ThehasBankDatabranch 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 formula — payments = 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 bar — bankDeposits/bankPayments/bankDirection logic corrected 4. Batch match detection — bankAmount 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 comparison — bankAmount 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_transactionstable. - 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:
handleBankSelectusedselected: 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. AddedselectedBankItems(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
hasBankDatabranch, theuseMemocalculated deposits/payments fromselectedBank(checkbox-selected bank items) +clearingLedger(R/c toggle items). It completely ignoredselectedLedgers(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:
suggestedBatchMatchonly 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) callinghandleExportReconciled. It silently used the Reconciled tab'sstartDate/endDatestate — 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
SimpleExportDialogwith 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
handleExportand also still runs automatically insidehandleFinishReconciliation.
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:
stripePayoutsquery,stripePayoutExpanded/showPayoutSummarystate, entire Stripe Payout Summary Card UI,CreditCardicon import. KeptfetchStripePayoutSummary()infinancial-reports.tsfor future use.
P0: GL Table Header Text Overlap
- Root cause: 8 columns with
table-fixed w-fulltotaled 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], Refw-[130px]→w-[100px], Fundw-[120px]→w-[100px]. Addedtruncateto all sortable header spans. Addedoverflow-hiddento GL Card.
P0: Clear Marks (422) but Deposits/Payments = $0.00
- Root cause: When
bankTransactions.length > 0, theuseMemocalculated deposits/payments from selected bank transactions only. Clearing ledger entries were ignored because the code assumed bank-pairing mode. - Fix: In the
hasBankDatabranch, 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-wrapto 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:
sortComparenow 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:
clearingMatchedBankIdsuseMemo maps clearing ledger entry amounts to bank transaction IDs using exact amount matching. Matched bank rows getopacity-50+line-throughon 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) andhandleToggleReconciliationStatus(R/c toggle) calledupdateBankTransactionReconciled,updateLedgerEntryReconciled, andupdateLedgerEntryReconciliationStatuswithout passingreconciledById. Thereconciled_bycolumn 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, orreference_idmatches 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?)infinancial-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_payoutsrows and 0 settlement JE lines keyed byreference_id = po_.... The2026-02-24entries 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 — seeSTRIPE-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_idlikepo_%, 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.amountwhen 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-expirydaily cron uses RPC `total_unresolved` (actionable-only after the migration). It writes `stripe.payout_drift.detected` when that count is > 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 usest()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_bycolumn onjournal_entry_linesis typeUUID, butbulkFinishReconciliation()andbulkReconcileLedgerEntries()were passedreconciledByName(display name string like"Maribeth Carlton") instead ofuser.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?.idvariable. Changed both DB call sites to passreconciledById(UUID) instead ofreconciledByName(string). Display name is still used for PDF reports and CustomEvent metadata. - Affected calls:
bulkFinishReconciliation(accountId, reconciledById)(Finish Reconciliation) andbulkReconcileLedgerEntries(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)
sortFieldstate:'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
AlertDialogActionimport - Removed dead
reconciledByUserIdvariable (duplicate ofreconciledById)
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/10highlight 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
420pxto380px, giving ~40px more visible table area. Both bank and ledger tables already had independentoverflow-y-autoscrolling — 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>, causingWarning: Missing Description or aria-describedby={undefined} for {DialogContent}console warnings. - Fix: Added
<DialogDescription>withimportConfirmDescriptionkey to Phase A import dialog. - Files:
ReconciliationManager.tsx
P0 Hardening: reconciledBy → reconciledByUserId Parameter Rename
- Purpose: Prevent future UUID/name confusion. The
reconciled_bycolumn onjournal_entry_linesis UUID type, but the function parameters were ambiguously namedreconciledBywhich could be mistaken for a display name. - Fix: Renamed
reconciledBy→reconciledByUserIdin bothbulkFinishReconciliation()andbulkReconcileLedgerEntries()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 headerimportConfirmDescription— 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:
clearedBalancewas computed assummaryStats.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 clearing → reconciled.
- 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), soselectedBankis always null and the button is permanently disabled/grayed out. The component had no mechanism to bulk-promoteclearing(c) entries toreconciled(R) without importing a bank statement — a fundamental gap in the QuickBooks-style workflow. - Fix (3 parts):
- New DB Function:
finalizeReconciliation(accountId, reconciledBy?)infinancial-reports.ts— Updatesjournal_entry_lineswherereconciliation_status = 'clearing'toreconciled = true,reconciliation_status = 'reconciled', withreconciled_atandreconciled_bytimestamps. Excludes void reversals. - New Handler:
handleFinishReconciliation()— Step 1: Generate PDF report (before status change). Step 2: CallfinalizeReconciliation(). Step 3: Dispatchreconciliation-updateevent. 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:
handleToggleReconciliationStatusimplemented 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.tsxline 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
organizationstable viafund_idto show fund name (e.g., "Called to Love Uganda"). Helps distinguish between identical-amount entries from different ministries. - DB Change:
fetchLedgerEntriesForReconciliation()andfetchReconciledEntriesPaginated()now selectentry_numberandorganizations!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
suggestedBatchMatchuseMemo 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()inexportUtils.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
statusCycleHintto 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:
updateFullJournalEntrychecked ALL sibling lines sharing the samereference_typeprefix for reconciliation status. Aplos imports created multi-date mega-entries (e.g.,JE-2025-140has 244 lines across 6 dates from 2025-01-14 to 2025-12-20).bulkReconcileLedgerEntriesreconciled 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_summaryPostgres RPC had nois_void_reversal = falsefilter, 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 = falseto 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 PostgRESTmax-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.reconciliationin 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
CurrencyInputcomponent. State converted fromstringtonumber. localStorage persistence maintained.hasStatementBalanceflag 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:
fetchLedgerEntriesForReconciliationfetched ALL entries (reconciled + unreconciled) for the account — 23,841 rows for the main checking account. PostgREST's server-sidemax-rowsdefault of 1000 silently capped the results. Since the query sorted bytransaction_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. ThereconciledCountwas computed client-side from this truncated dataset → always 0. - Fix (3 parts):
P1: Stale reconciledPairs Built from Truncated Data
- Root Cause: The
useEffectthat builtreconciledPairsfrom 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 usesfetchReconciledEntriesPaginated()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 —
ofReconciledi18n call passedpercentasnumberbut i18next expectedstring
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
PaginationControlsfor 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:
SimpleExportDialogon the Reconciled tab (PDF, CSV, XLSX formats) - Handler:
handleExportReconciledfetches 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
exportReconciledTitlekey to all 6 locale files
P1: Missing isMappingInitialized() Guard (Playbook §11 Violation)
- Root Cause: All 4 React Query calls used
enabled: !!selectedAccountIdbut none checkedisMappingInitialized(), 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.reconciliationin all 6 locale files (en, es, fr, de, zh, th) - Covers: Permission errors, import validation, reconcile/unreconcile toasts, batch dialog headers, status messages
- Fixed:
ofReconciledkey changed from{{count}}to{{num}}(count is reserved by i18next for pluralization)
P2: Stat Tiles Animation (Playbook §16)
- Fixed: Added
stagger-fade-in-blurclass 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, andhandleUnreconcileall 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
useMemothat computeddeposits/paymentssummed all unreconciled entries unconditionally, despite documentation claiming this was fixed - Fix: Reworked
useMemoto sum onlyselectedentries +clearingentries. 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:
fetchReconciledEntriesPaginatedhardcoded.order('transaction_date', { ascending: false })and ignored thesortOrderstate - Fix: Added
sortOrderparameter tofetchReconciledEntriesPaginated, 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
idas 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/40→dark:bg-*-950for proper contrast ratios
P2: Internationalization Cleanup
- Fixed: ~15 remaining hardcoded English strings replaced with
t()calls - Added:
batchMatchingHintandadjustMappingHintkeys 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 withtoLocalDateString()to prevent timezone bugs - User identity: All 4
'Current User'hardcodes replaced with actual user name fromuseAuth()(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/40→dark:bg-*-950for WCAG-compliant contrast - Import dialog dates:
toLocaleDateString()→formatDate()utility - Clear statement confirmation:
window.confirm()→ RadixAlertDialogcomponent - Summary tiles animation: Added
stagger-fade-in-blurCSS class - Summary stats query: Added
placeholderData: keepPreviousDatato 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
DatePickercomponent 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:
useEffecthad stale closure bug - read from closure-captured state instead of current state - Solution: Selection state now stored in
useRefmaps 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
accountIdis provided, query byaccount_idacross 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 reconciledgetReconciliationSummary(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
useEffectthat processed query data was resetting allselectedstates tofalseon 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_transactionstable withreconciled,reconciled_at,reconciled_by,ledger_entry_id(legacy pointer to the primary matched GL line) - `updateJournalEntryLineReconciled()` - Updates
journal_entry_linestable withreconciled,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, anddescription - 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_idand optionallyaccount_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 bothbank_transactionsandjournal_entry_lines - RLS policies -
is_parent_org_admin() OR organization_id = ANY(get_user_org_ids())onbank_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-updateevents, 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, andreconciledimmediately - 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 reconciliationreconciliation_method: Values such asimport,batch,manual,finish, orbulk_cleardepending on the workflowbank_statement_ref: Reference to bank transactionreconciled_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 inclearing (c)(respecting statement cutoff). Data loads via the same paginated RPCs withscope: '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 toR). Finish Reconciliation (bulk finalize for the period) lives in the toolbar and only shows on this tab whenhasStatementBalance,clearingCount > 0, andcanWriteAccounting. - 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
xlsxlibrary - 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, andimported_atonbank_transactions. - DB uniqueness guard:
uq_bank_transactions_org_account_import_fingerprintblocks 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-handleReconcileor 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
c→Rin 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
c→R)
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_numberor samejournal_entry_id, after strong external refs such as Stripepo_/ch_). Flat shows one row perjournal_entry_linesrow. Multi-fund entries that share oneentry_numbermust 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 i18ngroupedPartialMatchHint). - 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):
directororbookkeepercan access accounting - ACCESS_LEVEL:
read_writecan import/reconcile;read_onlycan only view - `canWriteAccounting`: Requires
canAccessAccounting(position)ANDcanWrite(accessLevel) - `canImport`: Requires
canWriteAccountingAND 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 withselectedflagreconciledPairs— Matched bank-to-ledger pairsselectedAccountId— Currently selected GL asset accountsortOrder—'asc' | 'desc'statementBalance— User-entered statement balance (number, persisted to localStorage per account viaCurrencyInput)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 tabbankSelectionsRef/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 (fromuseAppStore)- 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/reconciliationLedgerPageprefixes; seeuseReconciliationContext.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
- 04-GENERAL-LEDGER.md - Source of ledger transactions
- 08-CHECK-DEPOSIT-MANAGER.md - Creates deposits to reconcile
- 03-CHART-OF-ACCOUNTS.md - Asset accounts for reconciliation
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