Skip to main content
When Your Ledger Only Records Direction, Loans Look Like Revenue
saasfinancedata-migration+3

When Your Ledger Only Records Direction, Loans Look Like Revenue

A finance ledger that tags every row as just INCOME or EXPENSE will eventually count capital injections and loan proceeds as earnings. Here's the trap, how we found it in our own ops hub, and the migration we used to fix it safely.

The number that was quietly wrong

If your accounting ledger tags each transaction only by which way the money moved — INCOME or EXPENSE — your profit-and-loss report is going to lie to you eventually. Not because of a bug in the summing, but because of a category error baked into the schema: direction is not the same thing as what the money economically is.

Money coming in isn’t automatically revenue. A loan lands in your account as an inflow. So does a capital contribution from a founder. So does a client refund going the other way. If the only fact you’ve recorded is the arrow, a P&L that sums by arrow will happily report your loan proceeds as income and your capital injection as earnings. The report isn’t broken. It’s faithfully summing a field that never carried the meaning you needed.

That’s exactly what we found in our own internal operations and finance hub.

What direction-only typing actually does to a report

Our ledger (the internal running list of every money movement) tagged each row with direction only. The P&L endpoint summed by direction as if INCOME meant “earned” and EXPENSE meant “spent.” For routine client payments and vendor bills, that reads correctly, which is exactly why this kind of trap survives casual review — most rows behave, so the totals look plausible.

The rows that don’t behave are the ones that matter most to a founder reading the statement: capital contributions and loan movements. Those are financing, not operations. Counting them as income inflates revenue; counting a loan repayment as an expense understates profit. The headline numbers drift in ways that are hard to spot precisely because nothing errors out.

There was a second, separate leak we caught in the same pass. A deleted test account still had $100 that reports were pulling into rupee totals — because those aggregate reports didn’t explicitly respect the account’s delete flag. Soft-deleting a record only hides it from the places that actually check. Any aggregate that doesn’t filter the delete flag keeps counting it.

Two axes, not one

The fix is a schema fix, not a report patch. Direction and category are two different axes and need two different fields. We added a proper TransactionKind: CLIENT_INCOME, OTHER_INCOME, REFUND, EXPENSE, CAPITAL, LOAN, TRANSFER, and OPENING_BALANCE. Now a row can say both “money came in” and “this is a loan, not revenue,” and the report can tell the difference.

The new reports endpoint derives income, expenses, clients, categories and cash by kind, worked back from the real account balance rather than trusting a naive directional sum.

Backfilling meaning you can’t fully infer

The interesting part is the old data. Most historical rows can be classified by rule in a migration — a vendor payment is an EXPENSE, and so on. But some rows carry meaning no rule can recover reliably: one capital contribution, two loan rows, and a few refunds that were only identifiable by their description and exact amount.

For those, a loose heuristic is the wrong tool. A heuristic that’s 90% right on financial records is a quiet corruption waiting to happen, and an UPDATE against a production ledger is a one-shot, irreversible move. So we wrote the ambiguous cases as an explicit SQL script that:

  • asserts exact row counts before touching anything — if the data doesn’t match what we expect going in, it stops rather than guessing;
  • is designed to be rehearsed — run BEGIN ... ROLLBACK against a real backup, inspect the diff, confirm it matches expectations, and only then flip to COMMIT in production.

That turns an irreversible one-shot update into something you can dry-run until the result is exactly what you intended.

The takeaway for anyone running their own books

Two things to check in your own system:

  1. Does your ledger record what each transaction is, or only which way it moved? If it’s only direction, your income statement will eventually swallow loans and capital as revenue or spend. Add a category axis separate from direction.
  2. Do your aggregate reports explicitly filter soft-deletes? If deletion is a flag rather than a real removal, every total that forgets to check the flag keeps counting the ghost.

And when you’re assigning meaning to historical data you can’t fully infer, prefer a count-asserted, rehearsable script over a clever heuristic. The goal isn’t to be clever once — it’s to be able to check your work before it becomes permanent.

Have a project in mind?

Fixed price after a paid discovery — no hourly billing. A real engineer reads every enquiry, and we reply within 24 hours.