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 (periodStart … periodEnd), 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¶
- A
BankAccountrow must exist (Settings → Banking). - The cash GL
Account(type =CASHorBANK) must be linked at reconciliation-open time via thecashAccountIdargument. - (Optional) An imported
BankStatementID 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
ValueErrorif the user tries to open a second one. Show a "Resume open reconciliation" CTA ifbankReconciliationsreturns a non-RECONCILEDrow. - The two-column matching screen is the centerpiece:
- Left: unmatched bank statement transactions for the period (filter
bankStatementTransactionsbystatementId+status: UNMATCHED). - Right: unmatched GL lines on
cashAccountIdfor the period (query needed; see follow-up). - Checkbox actions feed
markClearedStatementTransactionsandmarkClearedLedgerLines. - A "Suggested matches" banner runs
autoApplyMatchesand refreshes the table. - The footer always shows
bankReconciliationSummary.differencelive. The Close button is disabled while|difference| > 0.01unless 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¶
- Bank reconciliations list per bank account, with status badges.
- Open reconciliation wizard — pick statement (optional), enter opening + closing balances, pick the cash account.
- Match & reconcile workspace — split view described above.
- 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.