Bank Statement Parsing & Financial Reporting
Turning bank statements, spreadsheets and alert emails into one clean, categorised ledger
Project Overview
Financial data rarely comes in one tidy format. Banks export PDFs whose layouts differ, wallets offer CSV or Excel downloads, bookkeeping lives in hand-built Google Sheets, and the most up-to-date record of a payment is often a transaction-alert email. We built a set of tools that brings all of these sources together. At the centre is a Laravel application with a dedicated parser for each supported bank format. Around it sit a Google Sheets snapshot importer and a Google Apps Script pipeline that scans Gmail for transaction alerts. The combined output is a de-duplicated, categorised ledger in which internal transfers are identified and paired.
Skills Applied
The Challenge
Every bank and wallet formats its statements differently. The same PDF can wrap a single transaction across several lines, place the running balance in different positions, or mention another bank's name in a way that fools a naive detector. Uploading statements whose date ranges overlap creates duplicates. Moving money between your own accounts shows up as an expense in one place and income in another, which inflates both totals unless the two sides are matched. Manual spreadsheets add a further problem: their header row is not always the first row, and income and expenses may sit side by side in the same rows. Finally, correcting a parser should not mean losing the categories and notes already added by hand.
Our Solution
We built the core in Laravel with a pluggable parser architecture. An abstract bank parser provides shared date, amount and text normalisation. Each bank has its own parser class that detects its own layout: Meezan Bank, Bank Alfalah, NayaPay and SadaPay, plus a generic fallback. PDFs are read with smalot/pdfparser, and CSV, XLS and XLSX files with Laravel Excel. An account can store a preferred parser to override auto-detection. A regression test suite locks in the layout edge cases each parser has had to handle. On import, a de-duplication service checks a content hash and then looks for same-amount transactions within a day either side, matching them by normalised reference, event fields or similar descriptions. A transfer detector pairs opposite-signed transactions of similar amount across your own accounts within a set time window. A keyword-rule categoriser, configured in one file, tags each transaction and suggests its type. A ledger-rebuild service re-derives each account from its statement files and checks the derived opening balance against the recorded one. It then carries existing categories and notes over to the rebuilt transactions. Alongside the Laravel core, a second Laravel service reads Google Sheets through the Drive and Sheets APIs, finds the header row automatically and parses the expense and income blocks that sit side by side. A Google Apps Script job, run on a daily trigger, scans Gmail for alerts from an allow-list of providers. It parses each provider's alert format, skips messages it has already recorded and appends the transactions to a separate spreadsheet for each year.
Technical Stack
The technologies and tools we used to build this solution
Frontend
Backend
APIs & Libraries
Infrastructure
Key Features Implemented
Core functionality and technical implementations
Per-Bank Parsers Behind an Abstract Parser
A shared base class handles dates, amounts and text cleanup, and each bank's parser handles its own PDF, CSV or Excel layout. Anything unrecognised falls back to a generic parser.
Transaction De-Duplication
Imports are checked against a content hash and then against same-amount transactions within a day either side, matched by reference, event fields or description similarity.
Internal-Transfer Detection
Opposite-signed transactions of similar amount on your own accounts, within a set time window, are paired as one transfer instead of being counted as spending and income.
Auto-Categorisation & Ledger Rebuild
Keyword rules tag each transaction and suggest its type. A rebuild re-derives each account from its statements, checks the opening balance and keeps manual categories and notes.
Google Sheets Snapshot Parser
Spreadsheets are read through the Sheets and Drive APIs. The parser finds the header row automatically and reads the expense and income blocks that share the same rows.
Gmail Alert Scanning Into Yearly Sheets
A daily Apps Script trigger scans Gmail for alerts from allow-listed providers, parses amount, currency, reference and channel, and appends new rows to a spreadsheet for each year.
Results & Impact
What this project achieved
- Statements from several banks and wallets, in PDF, CSV or Excel, are imported into one consistent transaction format
- Transactions repeated across overlapping statements are caught on import instead of being counted twice
- Moving money between your own accounts is recorded as a transfer, not double-counted as spending and income
- Parser fixes can be applied by rebuilding the ledger, without losing categories or notes added by hand
- Hand-maintained spreadsheets are imported even when their header row is not at the top
- Transaction-alert emails are collected into yearly spreadsheets automatically, and messages already recorded are skipped
Related Case Studies
Explore more of our technical work