Finance automation · MVP
Bank Statement Automation
A controlled pipeline that turns PDF bank statements into reviewed, de-duplicated entries in Finmap.
One line at a time
A bank PDF is not data yet
A statement line is a date, an amount and a paragraph the bank wrote for itself. Somebody used to turn that into an entry by hand, every month. Pick a line and see what the pipeline does instead.
The statement, as it was exported
Pick a line
What Finmap receives
One incoming line, two operations
1 line → 2 operations- Account
- Main · UA…3099
- Amount
- 35 000.00 UAH
- Category
- Legal services
- Counterparty
- Meridian Foods
- External id
- …7c4a1f:main
- Account
- Main · UA…3099
- Amount
- 7 000.00 UAH
- Category
- ПДВ
- Project
- ПДВ
- External id
- …7c4a1f:pdv
- The VAT is a fixed sixth of the gross, so the two amounts never round apart.
- It splits on the account, not on the wording — a line that forgets to mention ПДВ is split anyway.
A transfer. It never touches profit
1 line → 1 transfer- From
- Main · UA…3099
- To
- Deposit · UA…1543
- Amount
- 120 000.00 UAH
- Effect on the month
- None
- Both IBANs are in the own-accounts registry, so it is a transfer before any category is considered.
- Typed by hand this is two entries, an expense and an income, and the month reads busier than it was.
An expense, against the person who was paid
1 line → 1 expense- Account
- Main · UA…3099
- Amount
- 36 250.00 UAH
- Category
- Payroll
- Employee
- Петренко І. М.
- A keyword rule on the stem of “зарплата” sets the payroll category, whichever way the bank spelled the rest.
- The name is matched against the employee list pulled from Finmap, so the expense lands on a person.
An expense, with the bank already named
1 line → 1 expense- Account
- Main · UA…3099
- Amount
- 1 480.00 UAH
- Category
- Bank services
- Counterparty
- Bank commission
- Six spellings of a bank charge sit in the mapping file and all of them land on one category.
- Rules live in a YAML file rather than in the code, which makes a new one a two-line change.
Not an expense. Money set aside
1 line → 1 transfer- From
- Main · UA…3099
- To
- ПДВ account
- Amount
- 54 300.00 UAH
- Matched in
- Payment plan · 42%
- The payment plan is matched on date, amount, counterparty and legal entity.
- ПДВ is money set aside rather than spent, so it is a transfer — and a 42% match waits for a person in the draft.
It lands, and it says it does not know
1 line → 1 expense, flagged- Account
- Main · UA…3099
- Amount
- 9 750.00 UAH
- Category
- Fallback
- Tag
- unmapped
- No keyword rule, no tax match, nothing in the payment plan, no counterparty anyone has seen before.
- It still lands, in a fallback category and carrying an unmapped tag, so it is one filter away rather than lost.
A parser per bank format: Kredo, Raiffeisen, the operational export and the Mono payroll sheet, with a generic fallback behind them. Amounts, names and account tails here are stand-ins.
Why it is safe to press twice
Import the same statement again. Nothing doubles
A period extended by two days, a run from a second machine, a file dragged in twice — by hand every one of those doubles the month, and somebody finds it weeks later. Here nothing doubles, because every operation carries an identity of its own.
Import it again
- 01First import28 March, the statement as it stood
- Lines read
- 16
- Already there
- 0
- Written
- 16
Sixteen operations written.
- 02Re-exported file30 March, same period plus two days
- Lines read
- 32
- Already there
- 16
- Written
- 16
The overlap is recognised. Only the new days are written.
- 03The very same fileDragged in again, on purpose
- Lines read
- 32
- Already there
- 32
- Written
- 0
Read in full, written nowhere.
- 01Inside the batchTwo exports in one folder overlap. The second copy is marked before anything is sent.
- 02Against what was postedEvery published operation keeps its identity locally, with the date it went — so a re-exported period is caught and dated.
- 03At the far endIf both miss, Finmap refuses the identity itself — and that refusal is read as success, not an error.
Ten things, hashed. Same line, same identity, forever.
- 01bank
- 02account IBAN
- 03booking time
- 04amount
- 05currency
- 06direction
- 07counterparty IBAN
- 08document number
- 09purpose
- 10recipe version
IBANs and wording are normalised first, so spacing and case cannot make one line look like two.
Every run also checks the statement’s own arithmetic — opening balance plus everything that moved, against the closing balance — and a difference is a warning on the draft rather than a block.
What it changed
The same statement, typed and imported
The bank sends the same PDF either way. What changes is how many decisions a person makes by hand, and what happens when the file arrives twice.
| What changes | By handOne line at a time, into Finmap | Through the pipelineA draft first, then published |
|---|---|---|
| Getting a statement in | By handEvery line read off a PDF and retyped, for each account and each month | Through the pipelineThe PDF is parsed and every line arrives as a draft operation |
| Deciding what a line is | By handA category chosen from memory, and differently depending on who is doing it | Through the pipelineKeyword rules, tax detection and the payment plan decide — and each says why |
| Money moved between own accounts | By handEntered twice, as an expense and as an income, and the month looks busier than it was | Through the pipelineRecognised as a transfer from the account registry, so it never reaches profit |
| VAT inside a client payment | By handSplit by hand at the end of the quarter, or not split at all | Through the pipelineOne incoming line becomes net income and a ПДВ line, five sixths and one sixth |
| The same period imported twice | By handDoubled, and found weeks later by whoever notices the balance | Through the pipelineCaught at three levels before anything is written, with the date of the original |
| Checking nothing was lost | By handThe closing balance is compared by hand, when there is time | Through the pipelineOpening plus movements is checked against closing on every run |
| A line nobody has a rule for | By handGuessed at, or parked in a note that outlives the month | Through the pipelineLands in a fallback category with an unmapped tag, waiting for a person |