Ledger

What you can do

Loyalty points, warehouse stock with batches and expiry, mutual settlements, budget limits - four things companies usually build four times, on one register

One engine, four familiar problems

This is a platform block, not a feature of an application, because these look like different problems and are the same one.

What the business calls it What it actually is
Loyalty points, bonuses, internal currency A balance per person, earned and spent
Warehouse stock A balance per item per location, received and shipped
Mutual settlements with a counterparty A balance per counterparty, invoiced and paid
A budget or a limit A balance per cost centre, allocated and committed

Build them separately and you write the same concurrency bugs four times. Here they are the same register with different assets on it.

Points and internal currency

The complete loyalty mechanic, without a loyalty product:

  • Earn and spend, with every movement carrying its reason, so a customer asking "where did these come from" gets an answer instead of an apology.
  • Batches with an expiry. Points earned in March expire before points earned in June, and spending draws down the right ones in the right order - which is the part home-made implementations get wrong first.
  • Clawback. A cancelled order takes back exactly the points it granted, including when they have been partially spent since.
  • A shop to spend them in, with the cart holding a reservation and not a deduction, so an abandoned basket costs nobody anything.

Stock

Stock is the same primitive with a different key: a balance per item, per warehouse, per whatever else you need to split it by.

  • Receipts, shipments, transfers and write-offs as movements, so the current quantity is derived from them instead of typed in after a count.
  • Batches - by delivery, by production date, by expiry - so "what do we have" and "what expires first" are the same question asked differently.
  • Reservations against orders, which is what makes "40 in stock, 30 already promised" an honest answer instead of a promise you cannot keep.
  • A discrepancy is visible. When a physical count disagrees with the register, the register can say exactly which movements produced its figure.

Settlements and limits

  • What a counterparty owes, moved by invoices and payments instead of recalculated, so an aged balance is explainable line by line.
  • Budgets and limits, where the useful number is not what was spent but what is still available: allocated, minus committed to approvals that have not completed, minus actually spent.

Documents: carts, orders, bookings

A reservation on its own is a held amount on one account. Real work groups them: a cart is several reservations that succeed or fail together, an order is a set that converts to movements at the moment it is confirmed, a booking holds a thing for a while and then either happens or does not.

That grouping lives in a document layer above the register, declared as part of an application instead of built into the ledger. The separation keeps the register usable for the next case: the ledger knows about balances and holds, and it does not need to learn what a cart is in order for you to have one.

How it reaches the rest of the platform

  • A process moves it. An approval that completes, a shipment that is confirmed, a return that is accepted - a transition can post the movement, so the balance changes as part of the work, not as a separate act of data entry somebody has to remember.
  • Reports read it. Movements and balances reach analytics through a dataset bridge, so "points issued by month" or "stock turnover by warehouse" are ordinary questions, not an export.
  • Pages show it. A balance on a portal page or an app screen is a widget bound to the same data, not a bespoke screen written per application.

Next

Under the hood - what makes this correct when two people act at the same time, which is the only interesting question about a register of balances.