Skip to main content
Data Extraction & Fintech

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

Financial Data ExtractionPDF ParsingData De-DuplicationGoogle Workspace AutomationLaravel DevelopmentRegression Testing

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

BladeAlpine.jsTailwind CSSGoogle Sheets

Backend

Laravel 13Laravel 12PHP 8.3Google Apps Script

APIs & Libraries

smalot/pdfparserLaravel ExcelGoogle Sheets & Drive APIGmail (GmailApp)

Infrastructure

MySQLTime-Based TriggersPHPUnit Regression SuitePer-Year Spreadsheets

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

Have a Similar Project in Mind?

We'd love to discuss how we can bring your vision to life with the same level of technical excellence.