Skip to content

B-07 · Upload bank statement & parse

SOP: SOP_AI_Bookkeeping_Automation.md §5.1 (Bank Statement Ingestion and Parsing)Actors: Bookkeeper / Senior Accountant. Pre-state: Bookkeeping-enabled client with at least one approved JournalBatch for the period to reconcile against. Post-state: BankStatement row with parseStatus = PARSED, one BankTransaction row per line, opening + closing balance arithmetic verified.

ZENITH already has a parsed UOB Current statement for April 2026 (6 transactions). Use this flow either to upload a fresh statement against TESTBK or to re-run parsing manually for testing.

0. Prerequisites

  • A bookkeeping-enabled client with bookkeepingConfig populated.
  • A bank statement file (CSV / XLSX / PDF). For testing, the seed fixture ZENITH/bookkeeping/2026-04/uob-current-202604.pdf is in MinIO.

1. Steps

1.1 Open Upload Statement dialog

Web: /dashboard/bookkeeping/clients/<clientId>/bank-statements (or the Bank Statements tab on the client). Click Upload statement.

Or via API:

http
POST /ops/bookkeeping/bank-statements
Authorization: Bearer <staff-jwt>

{
  "clientId": "<clientId>",
  "bankAccountRef": "UOB-0001234567",
  "fileId": "<fileId>",   // produced by presign / PUT / finalize
  "originalName": "uob-current-202604.pdf",
  "periodStart": "2026-04-01",
  "periodEnd": "2026-04-30",
  "currency": "SGD"
}

1.2 Server flow

The route:

  1. Validates the file exists (immutable + fileKind = BANK_STATEMENT).
  2. Persists BankStatement with parseStatus = PENDING.
  3. Enqueues bookkeeping.parse-bank-statement.

1.3 Worker parses

bookkeeping.parse-bank-statement handler:

  1. Downloads the file from S3.
  2. Detects format:
    • CSV / XLSX → headers parsed structurally.
    • PDF → routed through ocr-process first; the OCR adapter applies the bank-statement field schema.
  3. Extracts per line: txnDate, valueDate, description, reference, debitAmount OR creditAmount, balance. Persists BankTransaction rows.
  4. Validates the arithmetic chain:
    openingBalance − sum(debits) + sum(credits) = closingBalance
    Tolerance: 0.01 (rounding).
  5. If arithmetic fails:
    • parseStatus = FAILED.
    • parseError = "Opening + sum(credits) − sum(debits) = X.XX, expected Y.YY".
    • Reviewer is notified via the in-app exception queue.
    • Phase 2 reconciliation does not start until the reviewer either fixes the parse or overrides.
  6. If arithmetic passes:
    • parseStatus = PARSED.
    • parsedAt = now().
    • Writes bookkeeping.bank_statement.parsed audit event.

2. Verification

Database

sql
SELECT id, parse_status, opening_balance, closing_balance, currency, parse_error
  FROM bank_statements WHERE client_id = '<zenithId>'
  ORDER BY ingested_at DESC LIMIT 1;
-- expect parse_status = 'PARSED'

SELECT line_index, txn_date, description, debit_amount, credit_amount, balance
  FROM bank_transactions WHERE statement_id = '<statementId>'
  ORDER BY line_index;
-- ZENITH has 6 rows: SP GROUP 450, UBER 18, FIGMA 101.25, IRAS 25.50, ATM 500.00, CUSTOMER PAYMENT credit 5000

Worker logs

bookkeeping.parse-bank-statement statementId=<id> rows=6 result=PARSED

Web UI

/dashboard/bookkeeping/clients/<clientId>/bank-statements shows the row with PARSED status, transaction count, opening + closing balances.

3. Negative & edge cases

  • Arithmetic mismatchparseStatus = FAILED, run does not auto-start. Reviewer must fix the source file or override.
  • Missing currency in file vs bookkeepingConfig.baseCurrencyBankStatement.currency is whatever the file declares; the reconciliation matcher tolerates currency mismatch by converting at the journal entry's FX rate.
  • PDF with poor OCRparseError carries the OCR confidence summary; reviewer can re-upload a cleaner copy or extract manually and upload as CSV.
  • Re-upload of same file — dedupe by SHA-256. Returns the existing BankStatement id and writes bookkeeping.bank_statement.duplicate_rejected.
  • Statement for a period with no journal entries — allowed. The reconciliation run will create only UnreconciledItem rows.

Next

Proceed to B-08 · Run reconciliation matcher.

Internal use only — BreezyCorp