LedgerMap · Contra entries audit
Code audit · 15/10/2026 · main @ 1410594 (Release 0.2.0)

How LedgerMap finds contra entries

A contra entry is a transfer between two of the party's own bank accounts. The money leaves one bank and arrives at the other. Miracle must book it once. This page explains every way the app finds one, what the user sees, and where it can go wrong.

Sources read: server/contra.go, server/contra_suggest.go, frontend/src/lib/book-contra.ts, use-book-contra.ts, use-in-miracle.ts, contra-pairs.ts, components/contra-review.tsx, contra-pair-dialog.tsx, entry-table.tsx, statement-entries.tsx, export-step.tsx, voucher-export.ts, and the docs. All examples on this page are made up.

WAY A

Same UTR / RRN

Both statements print the same bank reference. The server pairs them.

Saved by itself
WAY B

Same amount, no reference

One bank prints no reference. Same amount, ≤ 1 day apart.

User confirms
WAY C

Found in Miracle books

The other bank's statement is not imported, but its posting is in Miracle.

In books: by itself
Misbooked: confirm
WAY D

Paired by hand

Alt+C or "Pair as contra" on any entry. Any dates.

User picks
QUESTION 1

Where can a user turn a transaction into a contra entry?

Short answer: in six places. The main place is the Contra entries step (Accept, Not contra, Unpair). From every other entry table, the user can use Alt+C or the Pair as contra chip. The Ledger box can also take another bank's ledger, but that does not make a pair. The Export step can undo an "already in Miracle" entry, and the Bank ledgers step warns that a change unpairs.
PLACE 1 · Contra entries step → "To confirm"

Accept or reject a possible contra entry

This is the first review step. It lists transfers the app is not sure about: same-amount pairs between two statements (Way B), and matches with Miracle books (Way C).

Actions
Accept (or Enter on the row) saves it. Not contra (or Delete) sets it aside. Accept all N shows when there are 2 or more.
"Why" column
Way B: "Same date" or "1 day apart", plus "Same amount on both statements". Way C: Already in Miracle or Booked against Suspense. If other postings also match, it adds "N other postings or entries match too: check it first."
Effect
Accept on Way B pairs both entries with each other's bank ledger. Not contra on Way B is permanent (saved on the server). Not contra on Way C is kept only for this tab.
contra-review.tsx · accept(), reject(), acceptAll()
  Import · Review entries
Upload›Bank ledgers›Contra entries3 left›Suggestions41 left›Suspense12 left›Review›ExportAll entries

Transfers between the party’s own bank accounts. Each one is exported once, so Miracle does not book it twice.

To confirm 2✓ Accept all 2
DateFromToAmountWhyActions
04/04/2026HDFC Current
Row 18 · NEFT DR ICIC0000123…
ICICI OD
Row 7 · BY TRANSFER
50,000.001 day apart
Same amount on both statements
✓ Accept ✕ Not contra
09/04/2026HDFC Current
Row 31 · IMPS-…-SELF
SBI CC MIRACLE
Receipt voucher · 09/04/2026
1,20,000.00Booked against Suspense A/c
Accepting exports it as a transfer; then correct that voucher in Miracle.
✓ Accept ✕ Not contra
PLACE 2 · Contra entries step → "Contra entries"

See saved pairs and break them

The second table on the same step. It lists every saved pair once, with how it was found.

"Found by" column
Same reference with the UTR/RRN (Way A). Confirmed (Way B or D, no reference). From Miracle books (Way C misbooked), with "Miracle books it against … : correct that voucher."
Actions
Unpair (Delete) for a pair. Undo for a books transfer: it clears the ledger, so the entry goes back to "To confirm".
Effect of Unpair
Both sides lose their automatic ledger. The two entries are rejected for good: they are never paired or suggested again.
contra-review.tsx · Origin, unpair() · server rejectContra()
  Contra entries
Contra entries 3
DateFromToAmountFound byAction
01/04/2026HDFC Current
Row 3
ICICI OD
Row 1
2,00,000.00Same reference
HDFCN52026040100509415
⛓ Unpair
12/04/2026ICICI OD
Row 22
HDFC Current
Row 40
75,000.00Confirmed⛓ Unpair
09/04/2026HDFC Current
Row 31
SBI CC MIRACLE
Receipt voucher · 09/04/2026
1,20,000.00From Miracle books
Miracle books it against Suspense A/c: correct that voucher.
↶ Undo

Missing a transfer? Press Alt+C on an entry in any other step to pair it with its other side.

PLACE 3 · Any entry table → "Pair as contra" chip

A small button under the narration

Shown in Suggestions, Suspense, Review and All entries. It shows only on an entry with no ledger that has at least one possible partner.

Partner rule
Opposite side (debit ↔ credit), on another bank of this import, same amount or the debit higher by a fee (≤ ₹100 and ≤ 1%). No date limit.
Keyboard
Not a Tab stop. Alt+C does the same thing.
entry-table.tsx:865 (pairable) · contra-pairs.ts contraPartners()
  Suspense
Sr.DateNarrationDebitCreditLedger
4012/04/2026TRANSFER FROM 0123XXXX
⇄ Pair as contra
75,000.00
Search ledgers…
4113/04/2026UPI/RAMESH TRADERS/…4,250.00
Search ledgers…
PLACE 4 · Alt+C → "Pair as a contra entry" pop-up

Pick the other side by hand

Alt+C works on the row, in its Ledger box, or anywhere on the page outside another text box. The pop-up lists every partner, nearest date first, with a search box.

Enter
Pairs the two. Saved on the server as a pair with no reference (manual: true). An earlier "Not contra" for the same two is forgotten.
When it cannot pair
The page says why: "already a contra entry", "a possible contra entry waiting", "Clear its ledger first", or "No credit on another bank … has the same amount".
contra-pair-dialog.tsx · contra-pairs.ts unpairable() · server acceptContra()
  Pop-up
Pair as a contra entry
HDFC Current row 40 · 12/04/2026 · TRANSFER FROM 0123XXXX · credit 75,000.00. Choose the debit on another bank that records the same transfer.
⇄
ICICI OD row 22 · 12/04/2026
NEFT-SELF-ACCOUNT · Same date
75,000.00✓
⇄
SBI CC row 9 · 02/05/2026
CHQ PAID 000412 · 20 days apart
75,000.00
↑ ↓ choose · Enter pairs · Esc cancels
PLACE 5 · Ledger box → choose another bank

A transfer by ledger only (no pair)

The Ledger box offers every ledger except the statement's own bank. So the user can type "ICICI OD" for an HDFC entry. That exports a transfer voucher, but it does not create a pair.

What then happens
The server may pair it later if the other side shares a UTR/RRN, or suggest it if the amount is the same and dates are ≤ 1 day apart. If not, the other bank's entry is exported too. See finding F4.
entry-ledger-picker.tsx:133 · voucher-export.ts:106 (only mapping.contra is left out)
  Suggestions
NarrationDebitLedger
CHQ 000418 SELF30,000.00
icici▌
ICICI OD · Bank Accounts
ICICI Lombard Ins. · Expenses
PLACE 6 · Export step and Bank ledgers step

Reverse actions

Export step: "N entries are already in Miracle and left out" with Show. Each row has Export it (or Delete). It clears the ledger and sets the posting aside, so the entry goes to Suggestions or Suspense and is exported.

Bank ledgers step: changing a PDF's bank ledger warns "Changing the bank ledger unpairs this statement’s contra entries", then dissolves its pairs and matches again.

export-step.tsx InMiracle · statement-import.tsx:641 · server dissolveStatementPairs()
  Export
212entries
3PDFs
12to suspense
9already in Miracle
6contra left out
  • 6 contra deposits are left out. The withdrawal from the other bank books each transfer.
9 entries are already in Miracle and left out, so Miracle does not book them twice.
1 is no longer found in Miracle: check it.
Show ▾
DateEntryAmountIn MiracleAction
05/04/2026HDFC Current row 22
NEFT DR AXIS…
25,000.00Axis Current
Contra voucher · 05/04/2026
⤴ Export it

Also: how a contra entry looks in the other steps

Paired entry (Way A, B, D)

Read-only. No Ledger box. Cannot be selected. Below the ledger:

Contra with ICICI OD row 1 · Ref HDFCN52026… · change it in Contra entries

Already in Miracle (Way C)

Has a ledger box (editable). Below it:

Already in Miracle · left out of the export

Misbooked, accepted (Way C)

Has a ledger box (editable). Below it:

Contra entry from Miracle books

QUESTION 2

What happens if one side is already in Miracle?

Short answer: the app reads the party's Miracle books on this PC (never sent to the server). It looks for a posting of another bank on the opposite side, same amount, at most 3 days away. If that posting is booked against this bank, Miracle already has the whole transfer: the entry is saved as "In Miracle" by itself and left out of the export. If it is booked against some other ledger (for example Suspense), it is probably a misbooked transfer: the user must confirm it, and is told to correct that voucher.

1 · Read the books

The page reads RKACCT01.DBF of every financial year in the entries' period ± 3 days, for all the party's bank ledgers. Desktop: Go backend/miracle_books.go. Browser: lib/miracle-books.ts. Needs the company folder on this PC and at least two bank ledgers. Without a folder the step says: "Connect the company folder on this PC to check its books…"

2 · Which entries are checked?

Entries with no ledger, no pair, no possible contra from another statement (Way B wins), and not set aside. Entries already accepted from the books are rechecked to keep their posting.

3 · Does a posting match?

Posting is on another bank ledger · opposite side (a debit here = that bank's deposit) · same amount, or the withdrawal higher by a fee (≤ ₹100 and ≤ 1%) · at most 3 days apart. Each posting goes to one entry only: exact amount first, then nearest date. The match counts how many other postings or entries also fit.
Booked against this bank

Already in Miracle

Only at the exact amount. Any amount (no ₹500 floor).

Nothing else matches → saved by itself as the other bank's ledger, source books, in_books = true. Not shown in To confirm.

Others match → waits in To confirm with "Already in Miracle" and "check it first".

Export: left out. Listed under "already in Miracle" with Show / Export it.

Booked against another ledger

Probably misbooked

Needs ≥ ₹500 and a narration that is not cash, ATM or card (csh, cash, atm, nwd-, atw-, pos). Fee difference allowed.

Always waits in To confirm: "Booked against Suspense A/c".

Accept → saved with the other bank's ledger and exported. The user must correct the Miracle voucher, or the transfer is booked twice.

When Miracle is later corrected, the entry becomes "In Miracle" by itself.

Booked against a bank or cash

Ignored

The posting is a real transfer with some other bank or cash. It never matches.

The entry goes on to Suggestions or Suspense as normal.

Safety rules that protect the export

Entries "in books" are never paired by the server with another statement's row (loadContraBook removes them). That would export the other row and book the transfer twice.
Nothing is auto-saved while the books are being read again. An old read could be out of date.
If an auto-save fails, the entry waits in To confirm instead of being retried silently.
The Export step marks an "in Miracle" entry whose posting the books no longer show: "no longer found in Miracle: check it."
"Export it" puts an entry back into the export, and sets its posting aside so it is not re-matched.
The server sees only the chosen ledger and the in_books flag. Company files and postings stay on the PC.
Gap: a transfer with a fee that is already in Miracle is not caught at all. It drops into Suggestions/Suspense with no hint and is exported in full. See finding F1.
QUESTION 3

What do we show as contra suggestions?

Short answer: three kinds of suggestion, plus one sure match that needs no suggestion. Only same-reference pairs (Way A) and exact "already in Miracle" matches with no rival are saved by themselves. Everything else waits for the user in To confirm, or is only offered through Alt+C.
A · Same reference
auto
B · Possible contra
To confirm
C · Miracle books
in books misbooked
D · Alt+C partners
by hand
Runs whereServer, after every bank link and entry writeServer, on statement load and recommendationsBrowser / desktop app, on this PCBrowser / desktop app
ReferenceMust share a UTR or 12-digit RRN; one side's refs all on the otherNo shared ref needed. Blocked if both print refs and none is sharedNot used (books keep no UTR)Not used
AmountEqual, or debit higher by fee (≤ ₹100, ≤ 1%)Exactly equal, ≥ ₹500In books: exact, any amount. Misbooked: fee allowed, ≥ ₹500Equal, or debit higher by fee
Dates≤ 3 days apart≤ 1 day apart≤ 3 days apartAny; nearest first
Must be uniqueYes, each other's only candidateYes, each other's only candidateOne posting per entry; rivals counted and shownNo, lists all
Cash / ATM / card excludedNot needed (no shared UTR)NoMisbooked onlyNo
Saved by itselfYesNoIn books with no rival: yes. Else noNo
Rejected howUnpair → never again (server)Not contra → never again (server)Not contra → this tab onlyEsc
Other rulesSame-statement debit + credit with same ref and amount = reversal, never paired. Copies from overlapping statements join the pair.A row with a ledger counts only if it is the other bank; at least one side must be open.Postings against a bank or cash are ignored.Both sides need no ledger, or the other's bank; at most one has one.

Order of priority

Same reference (A)→ Exact ledger match→ Possible contra (B)→ Miracle books (C)→ Rule recommendations→ Miracle history→ Suspense

An entry with a Way B suggestion is skipped by Way C. An entry with any non-bank ledger is skipped by A, B and C. Alt+C (D) is always available as a manual fallback.

What the user reads on each suggestion

1 day apart

"Same amount on both statements" (Way B)

Already in Miracle

"Accepting leaves it out of the export." + rivals note (Way C, in books, with a rival)

Booked against Suspense A/c

"Accepting exports it as a transfer; then correct that voucher in Miracle." (Way C, misbooked)

QUESTION 4

What do we show at each stage?

Short answer: contra work happens in step 3 of the step bar. Earlier steps only set it up (pairing needs a bank ledger on both PDFs). Later steps show contra entries read-only and keep undecided ones out. Export writes each transfer once.
1

Upload

Nothing about contra. New PDFs added later pair with earlier banks.

2

Bank ledgers

Linking a ledger triggers server pairing (Way A). Changing it warns and unpairs.

3

Contra entries

To confirm (B, C) and Contra entries (saved). Accept, Not contra, Unpair, Undo. Books status line.

4

Suggestions

Undecided contra entries are left out, with a note. Alt+C / chip.

5

Suspense

Same as Suggestions. Contra entries never shown as suspense.

6

Review

Paired entries under the other bank's ledger, read-only.

7

Export

Contra once (withdrawal side). In-books left out. Notes and tiles.

Step 3 · Contra entries, in detail

  • Tab count: "N left" = entries of open Way B pairs (both sides) + Way C entries to confirm.
  • Opens first when it has work left.
  • Empty state: "No transfers between the party’s banks" + Continue to suggestions.
  • To confirm: sorted by date. Enter accepts, Delete rejects.
  • Contra entries: Delete unpairs or undoes.
  • Footer: "Checking the company’s Miracle books…", "Checked the company’s Miracle books…" or the read error, with Read books again.

Steps 4–6 · Suggestions, Suspense, Review

  • Entries with an undecided contra entry stay out: "2 possible contra entries are still undecided and stay in Contra entries."
  • Paired entries: no Ledger box, no checkbox, note "Contra with … · change it in Contra entries".
  • A Review ledger with only paired entries is locked.
  • All entries: Status column shows Contra or In Miracle.

Step 7 · Export rules for contra

Pair, both in import

Exported once from the bank the money left. The deposit is left out: "N contra deposits are left out."

Pair, withdrawal outside import

The deposit is kept: "…kept, since its withdrawal is in a statement outside this import. Leave it out in Miracle if that withdrawal was already exported."

In Miracle

Left out. Counted in the tile and listed under Show. Recorded as exported on the server.

Entry states

No ledger→Possible contra (in step 3)→ Accept →Contra
No ledger→ books, no rival →In Miracle→ Export it →No ledger
Contra→ Unpair / other ledger →No ledger (never re-paired)
AUDIT

Findings

Checked against the code on main. The contra unit tests pass (frontend: 4 files, 14 tests; Go: pure-logic tests). The Go PostgreSQL tests were skipped, because TEST_DATABASE_URL was not set.

High

F1 · A transfer with a fee that is already in Miracle is exported again in full

When the books hold the transfer against this bank but the amount differs by a fee, the match is thrown away. The entry gets no hint and goes to Suggestions or Suspense. The export then writes the whole row, so Miracle gets the transfer amount a second time.

HDFC debit ₹50,010 (₹50,000 + ₹10 fee). Miracle already has "ICICI ← HDFC ₹50,000". Today: the ₹50,010 row goes to Suspense and is exported. Result: ₹50,000 booked twice.

Fix idea: keep the match. Show it in To confirm as "Already in Miracle, except a ₹10 fee", and export only the fee (or ask).

frontend/src/lib/book-contra.ts:133 · if (inBooks && other !== amount) continue;
Medium

F2 · Cash withdrawals and deposits can be suggested as contra

The docs say "Cash deposits and withdrawals are not treated as contra". But the server's amount-only suggestion (Way B) and Alt+C have no cash filter. Only the books path (Way C) has one.

ATM / cash withdrawal ₹50,000 from HDFC and cash deposit ₹50,000 into SBI the same day. Neither prints a UTR. Today: suggested as an HDFC → SBI contra. In Miracle this should go through the Cash ledger.

Fix idea: apply the same notTransfer narration test to suggestable on the server.

server/contra_suggest.go:34 suggestable() · docs/statement-import.md (point 8)
Medium

F3 · "Pair as contra" shows on many entries that are not transfers

The chip shows on any open entry that has an opposite entry of the same amount on another bank, at any date. Round amounts (₹10,000, ₹25,000) repeat often. So the chip can show on many rows, and it loses meaning.

Fix idea: show the chip only when a partner is within a few days. Keep Alt+C for any date.

frontend/src/components/entry-table.tsx:865 · frontend/src/lib/contra-pairs.ts contraPartners()
Medium

F4 · Choosing another bank in the Ledger box makes a one-sided transfer

The user can pick "ICICI OD" for an HDFC entry. The export treats it as a normal row. If ICICI's statement is in the import and its side is not paired, the ICICI entry is exported too, and the transfer is booked twice. No warning is shown.

Fix idea: when a bank ledger is chosen and a partner exists, offer "Pair as contra" at once; or warn in Export.

frontend/src/components/entry-ledger-picker.tsx:133 · frontend/src/lib/voucher-export.ts:106
Low

F5 · The step count and the "To confirm" count differ

The tab counts entries (2 per pair). The table header counts transfers (1 per pair). Two open pairs show "4 left" on the tab and "To confirm 2" on the page.

frontend/src/components/statement-entries.tsx:181
Low

F6 · All entries does not show contra suggestions or misbooked transfers as contra

An entry with a possible contra waiting shows "No ledger". An accepted misbooked transfer from the books shows "Chosen", not "Contra".

frontend/src/lib/entry-status.ts:30, :37
Low

F7 · Way B misses some real transfers

  • Account numbers count as references. If both sides print a different 12-digit number, the pair is blocked.
  • Way B needs an exact amount. A transfer with a fee and no UTR is never suggested.
  • An entry that already got a non-bank ledger by an exact match is never suggested.

Alt+C covers all three, but only if the user notices.

server/contra_suggest.go:35, :39 · contraBook.suggestionRows()
Low

F8 · "Not contra" on a books match is kept only in this tab

It is saved by entry key with the tab state, not on the server. Another PC or a new window shows the same match again. This is by design, but it is not the same as Way B, which remembers for good.

frontend/src/lib/use-book-contra.ts (notBookContra)

What works well

Amount + date alone never pairs automatically. Only a shared reference does.
Ambiguous cases (two candidates, same-statement reversals) are left for the user.
The user's own ledger choices are never overwritten by contra matching.
Overlapping statements of one account join one pair, so a transfer is not exported twice.
Rejections are remembered on the server, also after a bank ledger is linked again.
The export leaves out the deposit side and says why.

The crux

  • Where: the Contra entries step (Accept / Not contra / Unpair), Alt+C or the "Pair as contra" chip in every other step, and Export → Show → Export it.
  • Already in Miracle: read on this PC; same amount against this bank → saved by itself and left out of the export; against another ledger → user confirms and must correct Miracle.
  • Suggestions: same UTR/RRN = automatic; same amount ≤ 1 day = confirm; Miracle books = confirm unless it is clearly already there.
  • Stages: all decisions happen in step 3; later steps show contra read-only; export writes each transfer once.
  • Fix first: F1 (fee transfer already in Miracle gets double-booked), then F2 (cash suggested as contra).