Several bank statements for one bank ledger
What changed, why it is safe, and what you will see in the app.
- Before: one bank ledger could take only one PDF per import.
- Now: many PDFs can share one bank ledger (for example March and April of HDFC).
- The risk: two PDFs that overlap print the same entries. If both are exported, Miracle books them twice.
- The fix: the app finds those repeated entries and brings each one in once.
- The app also warns you when a statement seems to be missing, or when two PDFs may belong to two accounts.
1. What changed for the user
April could not use HDFC. The app said “Already connected to another PDF — choose another”. You had to run a second import.
Both PDFs connect to HDFC in one import. You review all HDFC entries together.
2. The danger: overlapping statements
Banks often give statements that overlap. For example, you download March on 31 March, and then April from 30 March. The two days in the middle are in both PDFs.
March.pdf
| Date | Narration | Debit | Balance |
|---|---|---|---|
| 01/03 | NEFT ACME | +500.00 | 1,500.00 |
| 30/03 | RENT | 300.00 | 1,200.00 |
| 31/03 | SMS CHARGES | 10.00 | 1,190.00 |
April.pdf
| Date | Narration | Debit | Balance |
|---|---|---|---|
| 30/03 | RENT | 300.00 | 1,200.00 |
| 31/03 | SMS CHARGES | 10.00 | 1,190.00 |
| 02/04 | UPI SHOP | 90.00 | 1,100.00 |
amber rows are in both PDFs. If we only removed the old rule, the rent would go to Miracle twice.
This is why the old rule existed. Removing it safely needed the work below.
3. How the app finds a repeated entry
An entry in a later PDF is a copy of an entry in an earlier PDF when all of these are true:
- Both PDFs are connected to the same bank ledger.
- Same date.
- Same side (debit or credit) and same amount.
- Same reference (UTR, cheque no.). If the bank prints no reference, the same narration (spaces do not matter).
- If both rows print a balance, it is the same balance. The running balance is the strongest proof that two rows are one transaction.
One important exception
Two equal entries inside one PDF are never copies of each other. A bank can really charge ₹10 twice on one day. So the app counts:
| March.pdf has | April.pdf has | Result |
|---|---|---|
| 2 × “SMS ₹10” on 31/03 | 2 × “SMS ₹10” on 31/03 | 2 entries both of April’s are copies |
| 2 × “SMS ₹10” on 31/03 | 3 × “SMS ₹10” on 31/03 | 3 entries 2 copies + 1 new |
| — | 2 × “SMS ₹10” on 31/03 | 2 entries nothing removed |
The same PDF uploaded twice is all copies, so it adds nothing.
When two downloads print the same day differently
Two downloads of one account can print an entry differently (the UTR as a reference in one, inside the narration in the other), or print a day’s entries in another order (so the balance beside each row differs). The app then compares the whole day:
| Check | Why |
|---|---|
| Every entry of that day has the same side and amount in both PDFs | It is the same day, fully printed in both. |
| Both PDFs print the day’s closing balance, and it is the same | The account’s balance at the end of a day does not depend on row order. Without it, two accounts’ equal ₹500 charges on one day could look like one, so the day is not compared as a whole. |
If both hold, the day’s still-unmatched entries are copies, matched by side and amount. If not, nothing is merged, and a warning says how many entries did not match. A PDF covering only one day is never compared as a whole: its print order, and so its closing balance, cannot be told.
4. What happens to a copy
All copies are one transaction. It comes in once when any PDF’s date range brings it in. The app shows the copy that already has a saved ledger or contra pair (if any), otherwise the first one. The other copies are left out of everything:
| Place | What happens |
|---|---|
| Review steps (Contra, Suggestions, Suspense, Review) | The entry shows once. You choose its ledger once. |
| All entries | Listed once, under the first PDF that brings it in. |
| Export (Miracle voucher file) | Written once. no double booking |
| Contra (bank-to-bank transfers) | A transfer pairs with one copy only. A deposit paired with the other copy is still left out of the export, as before. |
| Check against Miracle’s books | Each Miracle posting matches one entry, never two copies. Before, two copies could fight for one posting, and the loser looked “not booked” and was written again. |
| Server: possible contra entries | Before, two copies looked like two candidates, so no transfer was suggested. Now copies count as one, and rejecting a suggestion covers every copy. |
5. What you will see in the app
Bank ledgers step
The ledger you already used is still in the list and is still suggested. A short note says what sharing does:
Review: a box above every step
It shows only when a bank has more than one PDF. Nothing in it stops you.
Tables now name the PDF
| Sr. no. | Bank | Date | Narration |
|---|---|---|---|
| 2 | HDFC 1234 · March.pdf | 30/03/2026 | RENT |
| 3 | HDFC 1234 · March.pdf | 31/03/2026 | SMS CHARGES |
| 3 | HDFC 1234 · April.pdf | 02/04/2026 | UPI SHOP |
- Sr. no. counts each PDF’s own rows. Before, it counted across the whole bank.
- The All entries switcher lists each PDF, not each bank.
- Alt+Enter (statement around an entry) shows only that entry’s PDF, so the Balance column does not jump between PDFs.
6. The four messages, and when they appear
| Message | When | What to do |
|---|---|---|
| Came in once | A PDF repeats entries of another PDF. | Nothing. This is information. |
| Balance gap | The balance at the end of one PDF is not the balance at the start of the next one. | A statement for the days between is probably missing. Add it. |
| No match | On days both PDFs print, each PDF has entries the other does not. The message gives how many and the first date. If nothing matches at all, it may be two accounts. | Press the button to open that PDF in All entries. If two rows are the same transaction, map one to suspense or leave it out. |
| Look-alike | An entry matches another PDF’s entry on everything except the balance. Both are kept. | Open All entries and check it. If it is a real repeat, map one of them to suspense or leave it out. |
7. Second opinion: Fable’s design review
I asked Claude Fable to review the plan and the code. It found 10 weaknesses. All were fixed, then OpenQodex reviewed the result again.
| Weakness | Fix |
|---|---|
| Same entry printed differently (reference vs. narration) was booked twice, and no warning showed when other rows matched. | Whole-day comparison (above). Warning counts unmatched entries on shared days. |
| Rows of a day in another order gave different balances, so both copies were kept. | Whole-day comparison uses the day’s closing balance. |
| Miracle’s last day: one posting could hide a different entry in each PDF, so both of two equal entries came in. | One posting now hides one transaction, with all its copies. |
| Clearing a copy’s old ledger told the server “rejected”, and the server re-matched it. | New ?copy=1 option: no rejection, no re-match. Copies already holding the new ledger are left alone. |
| The shown copy could switch to one with an automatic match after you saved. | Your own choices rank above automatic matches; the shown copy stays put. |
| Books check skipped a PDF with no exported entries. | Checks every PDF of a bank with any exported entry. |
| Opening-balance rows caused false warnings. | Rows without an amount are ignored. |
| Server compared balance text (“1200.0” ≠ “1200.00”). | Compares numbers; a missing balance is unknown. |
| Contra partner and pop-up lost hidden copies. | Resolved to the shown copy; the pop-up says how many rows are listed elsewhere. |
| Warnings had no way to reach the entries; no keyboard tests. | Button “Show April.pdf in All entries”; rows say “Also in March.pdf”; keyboard tests. |
Not changed: Fable also recommends storing choices per transaction on the server (copy groups). That stays the recommended follow-up.
7b. Third opinion: a fresh Fable review
A new Fable reviewer, with no memory of the first, checked the code after the first round of fixes. It found problems the first review missed, and some that the fixes introduced:
| Weakness | Fix |
|---|---|
| Hidden copies were never recorded as exported. A later import of that PDF alone could book them again if it printed them differently. | Saving the voucher files now records every copy with its entry, and lists every PDF of the import. |
| The whole-day match could merge two different transactions when balances were missing (e.g. two accounts on one ledger, each with a ₹500 charge). | A day is compared as a whole only when both PDFs print its closing balance. |
| Server contra suggestions could point at an old statement outside the import, so accepting them could book a transfer twice. | The server now offers the copy inside the import. |
| A typed “From” date on one PDF could bring back an entry that Miracle’s last day left out in another. | A transaction left out by a Miracle posting stays out of every PDF. |
| A failed clean-up could leave two different ledgers on two copies, and a reload could silently show the old one. | The review box now warns: “another ledger saved in another PDF … the ledger shown is used”. |
| One-day PDFs printed newest first gave a wrong day-end balance. | One-day PDFs are not compared as a whole. |
| The copy work ran on every screen update; a React rule was broken. | Computed only when its inputs change; the rule is fixed. |
Checked and not changed: the pop-up count was already correct. Stale automatic matches on hidden copies belong to the server copy-groups follow-up.
8. Decisions I made (please check)
- Different balance = keep both, and warn. I chose not to remove an entry when its balance differs. Removing a real transaction is worse than showing a warning. You answer the question in the issue with this; change it if you prefer.
- Which copy is shown: the one that already has a saved choice (a contra pair first), so earlier work is not lost.
- Changing a choice clears the copies’ old choices. The server stores a ledger per entry, not per transaction. So when you set or clear the ledger of the entry shown, the app removes the old ledgers its hidden copies still hold. Otherwise an old ledger could come back later. Contra pairs are never touched.
- Same PDF twice is not refused. Every entry is a copy, so it adds nothing, and the box says so.
- One voucher file per PDF stays. Copies are left out of the later file.
- Copy finding runs in the app, not on the server. It needs no new API, so older servers and older installed apps are not affected. The rule is the same as the server’s rule for entries already exported.
- The box shows in the review only, not on the Bank ledgers list. The list does not load the entries, and loading them there would slow it down.
9. Limits
- The balance check needs balances. A PDF that prints no balance on its first or last day is not checked.
- A month with no transactions cannot be uploaded, so a date gap alone is not a warning. Only a balance that does not carry on is.
- If you type different From/To dates for two PDFs of one bank, an entry comes in when any of the two ranges includes it (once).
- Design note (recommended follow-up): the server keeps one choice per entry, but the review treats copies as one transaction. The app keeps them in step (it clears copies’ old choices and locks them while it does). The fully robust fix is for the server to know copy groups and save one choice per transaction in one step. The review flagged three variations of this; all are fixed, but the server change would remove the whole class.
- The Miracle books check now uses one entry per transaction. That part is covered by the full review tests, but it has no test with real company books.
10. How it was checked
- Frontend: 530 tests pass (33 for the copy finder and date ranges, review tests with overlapping PDFs, a keyboard test for the warning button, tests that saved choices are kept and old copy choices cleared, and an export test for transfers). Lint and build are clean.
- Server: all tests pass, including the database tests, and
go vet. New tests cover copies in contra suggestions. - Not done: no test in the desktop app with real customer PDFs. Nothing is pushed or deployed.
11. Independent review (OpenQodex)
Six rounds before the first Fable review, two after it, and one after the second. Each finding was fixed and tested. The last round: Passed: no findings
| Round | Found | Fix |
|---|---|---|
| 1 | A saved choice on the hidden copy was lost. A copy outside its date range was not linked, so a transfer could be written twice. | Show the copy that holds the choice. Link every copy, in range or not. |
| 2 | A copy without a balance could merge two different transactions. A cleared choice could come back through another copy. | Remember a balance once a copy prints it. Clear copies’ old choices. |
| 3 | Server grouped two different withdrawals as copies. False “no match” warning with three PDFs. | Server also needs same narration and balance. Compare copy groups. |
| 4 | False gap warning when one PDF is inside another. | Compare with the PDF that reaches latest. |
| 5 | A race: a new choice could be deleted by the cleanup. | Lock the copies until the cleanup ends. |
| 6 | No findings. | |
| 7 | After Fable’s fixes: a third PDF printing an entry the “other way” was not linked. | The transaction is also filed under its second wording. |
| 8 | No findings. | |
| 9 | After the second Fable review’s fixes: no findings. | |
The Go linter (golangci) failed to start in every round. That is a tool problem, not a code finding.
12. Where the code is
| File | What it does |
|---|---|
frontend/src/lib/statement-copies.ts | New. Finds copies and checks the balance chain. |
frontend/src/lib/use-statement-entries.ts | Hides copies, one entry per transaction for the books check, Sr. no. per PDF. |
frontend/src/components/same-bank-notes.tsx | New. The box in the review. |
frontend/src/components/statement-import.tsx | Removes the one-PDF rule; adds the “also connected” note. |
frontend/src/lib/voucher-export.ts | A transfer paired with a copy is still left out once. |
server/contra_suggest.go | Copies count as one candidate for possible contra entries. |
docs/statement-import.md and others | Updated docs. |