Skip to content

Bank reconciliation

Backend domain: app/graphql/bank_reconciliations/ Related: app/graphql/bank_statements/ (parsed statements + match suggestions) Migration: add_bank_reconciliations RBAC Path: BANK_RECONCILIATIONS

Overview

A BankReconciliation ties a specific GL cash account + bank account to a period (periodStartperiodEnd), the statement's opening + closing balances, and a set of cleared ledger lines / cleared statement transactions. The reconciliation is closed only when the difference equation balances; adjusting lines can be passed in to absorb residuals.

Statuses: DRAFT → IN_PROGRESS → RECONCILED (and REOPENED after a reopen).

The matcher service (app/graphql/bank_statements/services/bank_statement_matcher_service.py) already proposes high-confidence matches; autoApplyMatches promotes them in bulk into a cleared state.

Setup prerequisites

  1. A BankAccount row must exist (Settings → Banking).
  2. The cash GL Account (type = CASH or BANK) must be linked at reconciliation-open time via the cashAccountId argument.
  3. (Optional) An imported BankStatement ID can be passed for traceability.

GraphQL surface

Queries

bankReconciliations(bankAccountId: UUID!): [BankReconciliation!]!
bankReconciliation(reconciliationId: UUID!): BankReconciliation!
bankReconciliationSummary(reconciliationId: UUID!): BankReconciliationSummary!
unreconciledLedgerLines(
  cashAccountId: UUID!,
  periodStart: Date!,
  periodEnd: Date!
): [UnreconciledLedgerLine!]!

UnreconciledLedgerLine fields: id, ledgerEntryId, accountId, debitAmount, creditAmount, memo, entryDate, entryNumber. Drives the right-hand pane of the matching workspace.

BankReconciliation: id, bankAccountId, cashAccountId, statementId, periodStart, periodEnd, openingBalance, closingBalanceStatement, closingBalanceBook, status, reconciledAt, reconciledById, adjustingEntryId.

BankReconciliationSummary: openingBalance, closingBalanceStatement, closingBalanceBook, clearedDebits, clearedCredits, unclearedDebits, unclearedCredits, difference.

Mutations

openBankReconciliation(
  bankAccountId: UUID!,
  cashAccountId: UUID!,
  periodStart: Date!,
  periodEnd: Date!,
  openingBalance: Decimal!,
  closingBalanceStatement: Decimal!,
  statementId: UUID
): BankReconciliation!

markClearedLedgerLines(reconciliationId: UUID!, lineIds: [UUID!]!): Int!
unmarkClearedLedgerLines(reconciliationId: UUID!, lineIds: [UUID!]!): Int!
markClearedStatementTransactions(reconciliationId: UUID!, transactionIds: [UUID!]!): Int!
autoApplyMatches(reconciliationId: UUID!, confidenceThreshold: Float = 0.90): Int!

closeBankReconciliation(
  reconciliationId: UUID!,
  adjustingLines: [ReconciliationAdjustingLineInput!]
): BankReconciliation!

reopenBankReconciliation(reconciliationId: UUID!): BankReconciliation!

ReconciliationAdjustingLineInput: accountId, debitAmount, creditAmount, memo.

Behaviour to handle in the UI

  • Only one in-progress reconciliation per bank account is allowed; the backend rejects with ValueError if the user tries to open a second one. Show a "Resume open reconciliation" CTA if bankReconciliations returns a non-RECONCILED row.
  • The two-column matching screen is the centerpiece:
  • Left: unmatched bank statement transactions for the period (filter bankStatementTransactions by statementId + status: UNMATCHED).
  • Right: unmatched GL lines on cashAccountId for the period (query needed; see follow-up).
  • Checkbox actions feed markClearedStatementTransactions and markClearedLedgerLines.
  • A "Suggested matches" banner runs autoApplyMatches and refreshes the table.
  • The footer always shows bankReconciliationSummary.difference live. The Close button is disabled while |difference| > 0.01 unless the user adds adjusting lines.
  • Adjusting lines: a side panel that lets the user enter a balanced set of EntryLineInputs. The backend posts these as a regular ledger entry tagged to the reconciliation.

Error states

Backend exception When Frontend message
ValueError ("open reconciliation already exists") openBankReconciliation "There is already an open reconciliation for this bank account. Resume it instead."
ValueError ("Cannot modify a reconciled period") mark/unmark mutations after RECONCILED "Reopen the reconciliation before changing cleared items."
ReconciliationNotBalancedError closeBankReconciliation without adjusting lines and a non-zero difference "Difference is $X. Add adjusting lines or recheck cleared items."

Suggested UX flow

  1. Bank reconciliations list per bank account, with status badges.
  2. Open reconciliation wizard — pick statement (optional), enter opening + closing balances, pick the cash account.
  3. Match & reconcile workspace — split view described above.
  4. Close confirmation — final difference, adjusting lines preview, RBAC check.

Audit trail integration

markClearedLedgerLines / unmarkClearedLedgerLines now write rows into ledger_audit_logs with operation CLEARED / UNCLEARED, one per ledger line. The payload captures reconciliation_id, account_id, debit, credit. The hash chain (prev_hash / row_hash) extends naturally — ledgerAuditChainVerify continues to detect tampering.

Open follow-ups (not yet implemented)

  • No bank feeds (Plaid/Belvo) — only manual statement upload via the existing bank-statements module.