01 The problem
A real case from the local development data (ASMARK ENGINEERS, Central Bank of India). Every month the current account pays the loan ₹43,100. Usually the loan statement shows one credit. On 25/04/2023 it showed two credits.
Current account sends
Loan account receives
Today: the app does not pair these three entries automatically. It does not suggest them. You cannot pair them by hand (Alt+C) either, because the dialog compares one amount with one amount. Neither side prints a UTR/RRN reference.
02 What happens today
The app makes contra entries in four ways. All four compare one entry with one entry.
| Way | Rule today | Result for 1 → 2 |
|---|---|---|
| Automatic pair server/contra.go | Shared UTR/RRN, equal amount (or fee ≤ ₹50 and 1%), ≤ 3 days, only candidate | ✕ No pair no reference, no equal amount |
| Possible contra entry server/contra_suggest.go | Same amount, ≤ 1 day, no conflicting reference | ✕ Not suggested no entry is ₹43,100 |
| Pair by hand (Alt+C) contra-pair-dialog.tsx | Pick one entry of the same amount (± fee) | ✕ Not in the list |
| Contra from the books lib/book-contra.ts | One entry against one Miracle posting of the same amount | ✕ No match |
The only workaround books the transfer twice
The user can choose the other bank's ledger by hand on all three entries. But the export leaves out a deposit only when it is in a contra pair. So all three entries are written. Switch the views to compare.
Voucher file rows written
| Bank (file) | Ledger | Withdrawal | Deposit |
|---|
What Miracle books
| Ledger | Moved | Correct? |
|---|
03 How the app should handle it
Two terms: the single side is the one entry on one bank. The split side is the two (or more) entries on the other bank. Together they make one contra group.
Find the group
- By hand (first): Alt+C, then choose several entries. The dialog shows the total.
- Suggestion (second): the Contra step lists a group only when it is the one possible combination.
- Never saved without the user.
Save it as one pair
- All entries get the other bank's ledger (source auto, confirmed).
- All share one contra_pair number.
- Note: "Contra with Loan account rows 43, 44".
- Unpair or a ledger change breaks the whole group.
Export it once
- The withdrawal side writes the voucher, as now.
- 1 → 2: one voucher of ₹43,100. Both credits are left out.
- The books check treats both credits as one Miracle posting of ₹43,100.
Rules a group must pass
| Amount | The split side's total equals the single side. Or the debit side is higher by a transfer fee: at most ₹50 and 1% of the total. |
|---|---|
| Dates | Every split entry is at most 1 day from the single entry. We use only this 1-day window. |
| Minimum | The single side is at least ₹500. A part can be any amount (a real part was ₹97.50). |
| Banks | All split entries are on one other bank ledger of the party. |
| Kind | Not cash, ATM or card (POS). No conflicting UTR/RRN. |
| Free | Every entry has no ledger yet, or already the other bank's ledger. An entry is in one pair or group only. |
| Direction | Both: 1 debit → 2 credits, and 2 debits → 1 credit. |
| Older apps | Keep working. contra.with is already a list. Group suggestions go in a new API field. |
04 Try the new pair dialog
A working mock of the proposed Alt+C dialog. Click in the list, or press Tab to reach it. Then use ↑ ↓ to move, Space to add or remove, and Ctrl+Enter to pair. The third and fourth entries are made up for the demo.
Pair as a contra entry
Current account · 25/04/2023 · WITHDRAWAL TRANSFER · debit 43,100.00. Choose the credits on another bank that record the same transfer.
05 Shapes we support
Red dot = debit (money leaves). Green dot = credit (money arrives).
The loan example.
The same thing in reverse.
Searching all sums is slow and noisy.
Too hard to check. Not needed yet.
06 What is already ready
I read the code. The data model and the export already allow groups. Changes are needed in finding and choosing them, and in the books checks.
Many rows can share one contra_pair number. Copies from overlapping statements use this today.
contraPair{out, in []entryRef}: each side is already a list.
contra.withAlready a list. Older apps will show a group as a normal contra entry.
The rule works per entry. A deposit whose withdrawal is in the import is left out.
rejectContra already rejects every debit–credit combination.
pairable and contraPartners compare one amount. They must compare a total.
POST …/contra names one entry. Add an optional list.
joinCopies assumes one row per statement in a pair.
exportsInBooks: one Miracle posting must cover both split entries.
A new search for two-entry sums in contra_suggest.go.
Two statement credits against one Miracle posting. Do this as a follow-up.
07 Real data: what happens with correct links
We use only the 1-day rule. The test data had PDFs linked to the wrong ledgers. So I linked every statement to its correct ledger, by the account number printed in the PDF, and ran the rules again. Use Show in PDF to see a row on its page, or Open PDF to open the original file.
What "correct links" means here (click to see the links I used)
| Account (from the PDF) | Ledger | Statements |
|---|---|---|
| 5149380565 | CBI C/A 5149380565 (current account) | 11 PDFs: 2243–2252, 2254 |
| 5164197786 | CBI MACHINERY TERM LOAN 5164197786 - 43100 | 13 PDFs: 44, 2253, 2255–2258, 2260–2266 |
| 5701294918 | CBI MACHINERY TERM LOAN 5701294918 - 55000 | 2 PDFs: 45, 2259 |
- Only ASMARK ENGINEERS has PDFs of its own accounts. The test PDFs of "Patel offset" (TRIDEV POLYMERS, YES BANK…) and "MOHIT RAIYANI" (Riken and Bipinbhai Bakori's Kotak and SBI) belong to other people, so with correct data they are not in those parties at all.
- Overlapping statements of one account repeat rows. A repeated row counts as one entry, as the server already does for pairs. The cards list the other copies.
ASMARK's loan instalments: how the loan statement records them
Show as a table
| Loan | Instalments | One credit | Two credits |
|---|
What the rules leave, step by step
Combinations whose amounts add up (± fee), dated at most 1 day apart.
14 of 40 instalments arrive as two credits on the loan: interest + principal ("TRANSFER REP-INTEREST" + "O.S. DEPOSIT TRANSFER"), or overdue + the rest ("O.S DEPOSIT" + "O.S. DEPOSIT TRANSFER").
Every split instalment passes all the rules. Nothing that is not a transfer is suggested.
Here the instalment is a normal one-to-one pair, and the current account has a separate SMS-charges row the same day. The "no 1:1 pair" rule keeps these as normal pairs.
Your ₹1,564 question. The ₹1,564 SBI credit and the ₹1,564 Kotak debit (12/07/2020) both print the RRN 019419468878. Today's normal contra matching already pairs them: in the database they are contra pair #1, saved automatically. The ₹10 freecharge row is not part of that transfer. So the group rules correctly ignore it: a 1:1 pair already exists. (With correct data, these two PDFs belong in Riken Bakori's party, not MOHIT RAIYANI's, but the result is the same there.)
08 Options and recommendation
A blue border shows my recommendation.
A How groups are found
Alt+C with several entries chosen. Small work, no risk: the user chooses.
≤ 1 day, reference rule, only candidate, no 1:1 pair. Medium work. With correct links: 14 suggestions, all real, none wrong.
No real cases found. Skip.
Large work and very noisy. By hand only.
C Storage
The schema already allows groups. Only joinCopies needs a fix.
More migration and API work. No gain.
D Export — see section 9
Today's rule. No change. Matches what pairs do.
Only if the split side must match the statement line by line.
Exact on both sides. Not known if Miracle's import allows it.
E API for older installed apps
Old contraSuggestions unchanged. Optional with: [...] on accept.
Breaks older apps. No.
09 Export choices: what Miracle sees
All three choices give correct balances. They differ in how the Miracle lines compare with each bank statement.
Current account in Miracle
| Line | Amount |
|---|
Loan account in Miracle
| Line | Amount |
|---|
10 Suggested plan
by hand
Pair a group by hand
- Server: pairableGroup(single, parts); the accept endpoint takes a list; fix joinCopies.
- Frontend: multi-select in the Alt+C dialog with a running total; keyboard tests; docs/keyboard.md and the help dialog.
- Books check: one Miracle posting covers the split side.
suggest
Suggest groups in the Contra step
- Find 1↔2 and 2↔1 sums: ≤ 1 day, reference rule, only candidate, no 1:1 pair, single side ≥ ₹500 (parts any amount).
- Accept saves the group. Not contra rejects every combination.
- Run the scan in section 7 again on more parties to measure how many suggestions are real.
Contra from the books, one-to-two
- Two statement credits against one Miracle posting from another bank.
Open question: can Miracle's voucher import read one voucher with several lines? Only option D3 needs this. The recommended plan does not.
·· Words used
- Contra entry
- A transfer between two of the party's own bank accounts.
- Contra group
- One entry on one bank plus two or more entries on another bank that record one transfer.
- Single side
- The one entry of a group (here: the ₹43,100 debit).
- Split side
- The two or more entries of a group (here: the two loan credits).
- UTR / RRN
- The network reference of a NEFT/RTGS (UTR) or IMPS/UPI (RRN) transfer. When both sides print it, the app pairs them automatically.
- Transfer fee
- The sender may pay a fee in the same row. The debit can be higher by at most ₹50 and 1%.
✓ In short
- Banks sometimes record one transfer as 1 debit and 2 credits (or the reverse). Today the app cannot pair these entries. The workaround books the transfer twice.
- The database, the API list and the export already allow groups. The work is in finding and choosing groups, and in the books check.
- Phase 1: pair a group by hand with Alt+C. Phase 2: suggest groups only when one combination fits. Never save a group without the user.
- Keep "the withdrawal side writes the voucher". Ask whether Miracle accepts vouchers with several lines.