A small CLI that reconciles a system's internal transactions against one or more bank statement CSVs over a date range. It reports exact matches, likely matches with amount discrepancies, and unmatched rows on either side.
go build -o reco .Requires Go 1.25+ (see go.mod). No external dependencies — standard library only.
reco -system system.csv -bank bca.csv,bni.csv -start 2024-01-01 -end 2024-01-31
reco -greedy -maxDiff 10000 -system system.csv -bank bca.csv,bni.csv -start 2024-01-01 -end 2024-01-31| Flag | Required | Description |
|---|---|---|
-system |
yes | Path to the internal system transactions CSV. |
-bank |
yes | Comma-separated list of bank statement CSV paths. |
-start |
yes | Start date, inclusive (YYYY-MM-DD). |
-end |
yes | End date, inclusive (YYYY-MM-DD). |
-greedy |
no | Enable Phase 2 nearest-amount matching (off by default). |
-maxDiff |
no | Max allowed amount difference for a greedy match (default 0). |
The tool prints a summary to stdout and writes a reconciliation_<unix>.csv
file in the working directory. Without -greedy it runs exact-match only; any
non-exact transaction is reported as unmatched.
-maxDiff is a strict threshold: a greedy match is only recorded when the
amount difference is <= maxDiff. The default 0 means zero tolerance — so
-greedy on its own suggests nothing, and you must pass -maxDiff N (e.g.
-maxDiff 10000) to allow discrepancies up to N. This prevents a large
transaction from being force-matched to a far-off bank row.
System CSV — columns: trxID, amount, type, transactionTime
trxID,amount,type,transactionTime
S1,100.00,CREDIT,2024-01-02T09:00:00Zamountis always positive; direction comes fromtype(DEBITorCREDIT).transactionTimeaccepts RFC3339,YYYY-MM-DD HH:MM:SS, orYYYY-MM-DD.
Bank CSV — columns: unique_identifier, amount, date
unique_identifier,amount,date
B100,100.00,2024-01-02
B101,-50.00,2024-01-03amountis signed (negative for debits).- The bank name is derived from the file name (
bca.csv→bca).
Sample files live in testdata/.
An interactive version with code pointers is in docs/how-it-works.html.
Both sides are first filtered to the [start, end] window (calendar day,
inclusive). System amounts are signed via the type column so they compare
directly against the already-signed bank amounts. Both phases run off one
shared per-day index — bank rows bucketed by day, each day's rows sorted by
amount — so lookups are O(log n), not a full scan.
- Phase 1 — exact match. For each system transaction, binary-search its day's bank rows for the same signed amount and claim the first unused match → Matched pairs.
- Phase 2 — discrepancy match (only with
-greedy). Remaining system transactions are paired with the nearest-amount unused bank row on the same day, found by binary search over the sorted rows. A pair is only recorded when the amount difference is within-maxDiff; otherwise the transaction stays unmatched and the bank row is left free for a closer one. Kept pairs become Recommended (likely matches needing an adjustment), each carrying the absolute amount difference. - Leftovers. Anything still unpaired is reported as unmatched — system transactions missing from the banks, and bank rows missing from the system (grouped by bank).
The stdout summary reports counts of processed, matched, recommended, and unmatched transactions plus the total discrepancy, followed by the unmatched rows on each side.
A CSV (reconciliation_<unix>.csv) is also written with one row per matched and
recommended pair: Type, TrxID/BankID, Date, Amount, Difference.
| File | Responsibility |
|---|---|
main.go |
CLI flag parsing and orchestration. |
model.go |
Transaction, BankRow, Pair, BankDay types. |
parse.go |
CSV reading and parsing. |
reconcile.go |
Matching logic, summary, and CSV export. |
testdata/ holds small hand-written fixtures (see testdata/README.md).
testdata/gen generates large synthetic datasets for performance testing:
go run ./testdata/gen -n 1000000 -days 3 -out testdata/largeThis produces ~3M system + ~3M bank rows. The matching is O((S+B)·log B), so
a 3M-row set reconciles in seconds. The generated CSVs are tracked with Git LFS
(see .gitattributes).
