FIFO Inventory: a case study in refusing a default
How an accountant got tired of a tool pricing stock wrong, and built the costing engine he actually wanted.
Case study · a story about a number that fought back
The premise is embarrassing: I trusted a tool to tell me what my stock was worth, and it was wrong. Not a rounding error: a whole different number.
I’m an accountant. I work in cost layers: the idea that the stock sitting in a warehouse isn’t one amorphous blob, but a stack of purchases made at different prices over time. When you sell something, you don’t pull from the blob; you pull from the oldest layer first. That’s FIFO.
The problem
The software I was using had a costing model baked in. It looked right, mostly. But on the layers it couldn’t represent (a partial sale, a return, a transfer between warehouses), it quietly did something else. The stock was worth more than it actually was. And here’s the kicker:
That’s not a bug report. That’s a materiality problem: the thing accountants train their whole careers to catch. I couldn’t walk away from it, because walking away meant the number stayed wrong.
What “right” actually requires
Costing isn’t hard, it’s fussy. The rules:
- Every purchase creates a layer with a cost.
- Every sale consumes from the oldest layer first, in order.
- Money is not a float. A cent is a cent. If the arithmetic can drift by
0.00000001, that’s a bug in a ledger, not a rounding convenience. - Every journal that leaves the system has to agree with the layers that produced it.
That last rule mattered most. If I was going to push cost of goods sold to Xero, the number had to reconcile, top to bottom, no unexplained variance.
The build
I wrote the engine in TypeScript, on top of Postgres. The cost layers live in their own table, the source of truth. Every inventory movement (receive, sell, adjust, transfer) writes a transaction, and COGS is derived by walking the layers, oldest first.
The math stays in decimal.js. Thirty seconds of setup, but it’s the difference between “trust me, it rounds correctly” and “it is exactly this number, because integers-of-cents don’t drift.”
┌─────────────────────────────────────────────────────┐
│ layer 1 · 100 units @ $10.00 → consumed first │
│ layer 2 · 50 units @ $12.50 → next │
│ layer 3 · 80 units @ $11.00 → behind those │
└─────────────────────────────────────────────────────┘
Sell 120 units → 100 from layer 1, 20 from layer 2. COGS is (100 × 10.00) + (20 × 12.50). No averaging. No opting into the default. Just the rule, applied exactly.
The part I actually want to tell you
This wasn’t a “fun weekend project.” It was the answer to a specific, personal frustration: a number I was responsible for was being computed wrong, and no one was ever going to tell me. Building the tool didn’t remove the tedium of costing; it removed the fear that the tedium was hiding a mistake.
Software that gets money wrong isn’t a minor bug. It’s the kind of thing that quietly erodes trust in every number it touches. If you work with numbers, you know the feeling. If you don’t, this is why your accountant sometimes looks tired.
It ships COGS to Xero on a schedule now, reconciled. And every time it posts a journal, I get the same small satisfaction: that number is right. I checked.