# Ledger & Reconciliation

> Payment reconciliation for Indonesian finance teams: QRIS, virtual account and e-wallet payments on a balanced ledger, matched to provider settlements and bank statements with reasons.

Every rupiah traced, every gap explained.

- URL: https://weaveward.com/work/payment-ledger-reconciliation
- Year: 2026
- Industry: Finance
- Capabilities: Business systems, Web apps
- Technologies: TypeScript, Node.js, PGlite (PostgreSQL), Drizzle ORM, Fastify, React, Vite, fast-check

## The challenge

Indonesian businesses take payments through QRIS, virtual accounts at several banks, e-wallets and card gateways, and providers settle in batches, net of fees, sometimes days later. Finance teams reconcile orders, provider settlements and bank statements by hand in spreadsheets, so partial payments, duplicates, refunds and late settlements can take days to resolve.

The work needs more than a match rate. Every figure in a cash report has to lead back to the journal that produced it, and every correction has to leave a trail that an auditor can follow.

## What we built

Ledger & Reconciliation connects payment events, a double-entry ledger and statement matching in one TypeScript product, with a workbench where the finance team confirms what the engine suggests.

- **A balanced, append-only ledger.** Accounts and journals in whole rupiah, where every journal's postings sum to zero, a correction is a reversal entry rather than an edit, and any balance can be read as of a point in time
- **Verified payment webhooks.** Events from a QRIS acquirer, a bank virtual account and an e-wallet are checked for an HMAC signature and a fresh timestamp, de-duplicated and applied through a payment state machine
- **Explainable matching.** Provider settlement and bank statement CSVs are matched against the ledger by exact reference, deterministic rules and fee-aware amount and date windows, and many-to-one batch settlements are solved as a bounded subset-sum. Every match records its reason and a confidence
- **A reconciliation workbench.** Statement lines sit on one side and ledger items on the other, with suggested matches and their reasons in between, next to an exceptions inbox and a journal explorer
- **Exceptions closed by journal.** Unmatched lines, amount mismatches, duplicates and late settlements are assigned and resolved, and each resolution posts an adjusting journal
- **Reports that open their journals.** Daily cash position, fee (MDR) analysis per channel and ageing of unreconciled items, with every figure traceable to the journals behind it

## How it works

### Payments into the ledger

Each provider's webhook is verified, stored once and applied by a state machine that accepts events in any order. The payment's state change, its journal and an outbox message commit in one transaction, and the transactional outbox then publishes the ledger event.

### Statements into matches

Settlement reports and bank statements arrive as CSV files and are matched in tiers: exact reference first, then deterministic rules, then amount and date windows net of each channel's fees. A bank credit that covers a batch of payments is explained as a bounded subset-sum within a tolerance, and every match carries its reason and confidence into the workbench.

### Exceptions into journals

Whatever stays unmatched lands in the exception queue as unmatched, amount mismatch, duplicate or late. A person assigns and resolves each item, and the resolution posts an adjusting journal, so the ledger and the reports stay in step.

### Reports from postings

Balances are derived from postings, so any balance can be read as of a point in time. The daily cash position, the fee analysis per channel and the ageing of unreconciled items are built from the same postings, and each figure opens the journals behind it.

## Engineering insights

Money rules are enforced where they cannot be skipped: in the database, at the webhook boundary and in property-based tests.

- **The database refuses an unbalanced journal.** Amounts are whole rupiah integers, never floating point, and a journal whose postings do not sum to zero is rejected by the application and again by the database. Corrections are reversal entries, and an idempotency key makes a retried posting return the first journal instead of posting twice
- **A webhook is a claim until verified.** Each event needs a valid HMAC signature and a timestamp inside a tolerance window, and it is stored once under its event ID, because the window alone would still let a replay through while it is open. The state machine then accepts events in any order, so a late expiry cannot undo a confirmed payment
- **One transaction, then the outbox.** A payment's state change, its journal and its outbox message commit together, so downstream systems never hear of a payment the ledger does not hold
- **Batches solved as a bounded search.** Many-to-one settlements are explained as a subset-sum over the candidate items, bounded in size and within a tolerance, so the search stays small. The answer reaches the workbench as a suggested match with its reason and confidence
- **Properties, not examples.** Property-based tests with fast-check generate payment histories and assert that every journal balances, that balances equal the sum of postings at any point in time, that a reversal restores balances and that matching never uses a statement line twice

## Design system

Ledger precise is calm, numerical and auditable, made for finance staff who spend month-end in dense tables: navy ink on a paper ground stays restful through long sessions, and each status colour means one thing only, so attention goes straight to what needs a decision.

- **Colour.** Matched mint, suggested amber and exception red each mark one status, and red marks exceptions only, never a negative amount. Mint and amber sit just above WCAG's 3:1 on paper, enough for icons and markers but not for text, so every status pairs its colour with a navy label
- **Typography.** Inter with tabular figures on every amount, so digits align down a column, with amounts right-aligned in whole rupiah. References such as virtual account numbers, provider transaction IDs and journal numbers are set in a monospace face, so long identifiers can be compared character by character
- **Components.** A workbench that sets statement lines beside ledger items, match details that show the reason and confidence, an exception queue whose resolution posts an adjusting journal, and report tables in which every figure opens the journals behind it
- **Traceability.** Every number on screen is traceable to its journal, which is the trail an auditor follows

## Where this fits

Any business that takes payments through several channels can start from the same foundation: a ledger that cannot be unbalanced and matching that explains itself. Explore our [business systems](/services/business-systems) and [web apps](/services/web-apps), or [talk to us about your reconciliation process](/contact).
