LedgerMap · INF-79 · branch t3/2d4a705b · 16/10/2026

Several bank statements for one bank ledger

What changed, why it is safe, and what you will see in the app.

In short

1. What changed for the user

Before
March.pdf→HDFC 1234
April.pdf→HDFC 1234

April could not use HDFC. The app said “Already connected to another PDF — choose another”. You had to run a second import.

Now
March.pdf↘
HDFC 1234
April.pdf↗

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

DateNarrationDebitBalance
01/03NEFT ACME+500.001,500.00
30/03RENT300.001,200.00
31/03SMS CHARGES10.001,190.00

April.pdf

DateNarrationDebitBalance
30/03RENT300.001,200.00
31/03SMS CHARGES10.001,190.00
02/04UPI SHOP90.001,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:

  1. Both PDFs are connected to the same bank ledger.
  2. Same date.
  3. Same side (debit or credit) and same amount.
  4. Same reference (UTR, cheque no.). If the bank prints no reference, the same narration (spaces do not matter).
  5. 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 hasApril.pdf hasResult
2 × “SMS ₹10” on 31/032 × “SMS ₹10” on 31/032 entries both of April’s are copies
2 × “SMS ₹10” on 31/033 × “SMS ₹10” on 31/033 entries 2 copies + 1 new
—2 × “SMS ₹10” on 31/032 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:

CheckWhy
Every entry of that day has the same side and amount in both PDFsIt is the same day, fully printed in both.
Both PDFs print the day’s closing balance, and it is the sameThe 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:

PlaceWhat happens
Review steps (Contra, Suggestions, Suspense, Review)The entry shows once. You choose its ledger once.
All entriesListed 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 booksEach 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 entriesBefore, 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:

HDFC 1234 is also connected to March.pdf. Entries both PDFs print come in once.

Review: a box above every step

It shows only when a bank has more than one PDF. Nothing in it stops you.

HDFC 1234 has several PDFs in this import. 1 thing to check.
ⓘ2 entries of April.pdf are also in March.pdf and come in once.
⚠The balance does not carry on from April.pdf to June.pdf: April.pdf ends on 30/04/2026 (balance 1,100.00 CR), and June.pdf starts on 01/06/2026 (balance 900.00 CR). Entries between them may be missing: add the statement for those days.

Tables now name the PDF

Sr. no.BankDateNarration
2HDFC 1234 · March.pdf30/03/2026RENT
3HDFC 1234 · March.pdf31/03/2026SMS CHARGES
3HDFC 1234 · April.pdf02/04/2026UPI SHOP

6. The four messages, and when they appear

MessageWhenWhat to do
Came in onceA PDF repeats entries of another PDF.Nothing. This is information.
Balance gapThe 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 matchOn 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-alikeAn 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.

WeaknessFix
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:

WeaknessFix
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)

9. Limits

10. How it was checked

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

RoundFoundFix
1A 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.
2A 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.
3Server grouped two different withdrawals as copies. False “no match” warning with three PDFs.Server also needs same narration and balance. Compare copy groups.
4False gap warning when one PDF is inside another.Compare with the PDF that reaches latest.
5A race: a new choice could be deleted by the cleanup.Lock the copies until the cleanup ends.
6No findings.
7After Fable’s fixes: a third PDF printing an entry the “other way” was not linked.The transaction is also filed under its second wording.
8No findings.
9After 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

FileWhat it does
frontend/src/lib/statement-copies.tsNew. Finds copies and checks the balance chain.
frontend/src/lib/use-statement-entries.tsHides copies, one entry per transaction for the books check, Sr. no. per PDF.
frontend/src/components/same-bank-notes.tsxNew. The box in the review.
frontend/src/components/statement-import.tsxRemoves the one-PDF rule; adds the “also connected” note.
frontend/src/lib/voucher-export.tsA transfer paired with a copy is still left out once.
server/contra_suggest.goCopies count as one candidate for possible contra entries.
docs/statement-import.md and othersUpdated docs.