business Case study
Loyalty + Wallet Ledger
Loyalty balances are money. Every defect in the code that moves them is a real amount owed to or taken from a customer, which is why this was built as a domain first, proven on its own, before a single WordPress hook or database table existed.
The business problem
Most loyalty implementations store a balance in a column and add to it. That is fine until the first refund, the first clawback, or the first argument about what a customer was actually owed six months ago, at which point there is no way to reconstruct how the number was reached. Floating-point arithmetic makes it worse: an earn and its reversal stop cancelling exactly, and the error is permanent.
What I delivered
- A Money type in exact integer minor units of Rial, which refuses floats rather than silently rounding them, and rounds toward zero in both directions so an earn and its clawback cancel exactly.
- An append-only wallet ledger where a balance is the sum of its entries and never a stored column, so any balance can be reconstructed and explained.
- Separate buckets for cash, promotional credit and refund credit, because the three differ in tax treatment, legal status and expiry rules and cannot share a balance.
- A point lifecycle where points are minted unspendable and stay that way until the order that earned them becomes irrevocable.
- An explicit earn basis: post-discount, pre-VAT, pre-shipping, with typed exclusions, so what an order is worth for earning is defined in one place.
- A mutation checker that plants known money defects one at a time and fails if any survives the test suite.
Technical approach
- The domain was built and proven completely before any persistence, endpoint, hook or cron existed. Nothing here is installable, and that was the point: money logic should be correct before it is reachable.
- Balances are derived, never stored. That is more expensive to read and it is the only way to answer why a balance is what it is.
- Bucket separation is modelled, not conventional. Promotional credit that expires and cash credit that does not are different things, and a single balance column forces them to pretend otherwise.
- Points mint unspendable because the alternative is a customer spending points from an order that is later refunded, which is a real loss that is hard to recover.
- The mutation checker exists because a green test suite proves the code passes its tests, not that the tests would notice a defect. It plants rounding-up on an earn, spending cash before promotional credit, minting points already spendable, and redeeming an expired grant.
Result and evidence
The mutation checker has already changed this code four times: two guards were unreachable dead code, one was a silent clamp that hid the bug it appeared to prevent, and one reported the wrong cause. Each of those would have passed review and passed the tests.
Commercial value
A loyalty system that cannot explain a balance becomes a support burden and eventually a dispute. Building the ledger properly first is cheaper than reconstructing it after the first month of live balances.
Readable implementation brief
implementation_brief {
project: "Loyalty + Wallet Ledger"
status: "pure domain; no schema, hook, endpoint or cron"
money: "integer Rial minor units; floats refused, not rounded;
rounds toward zero so earn and clawback cancel"
ledger: "append-only; balance = sum(entries), never a column"
buckets: "cash / promotional / refund, separated by tax,
legal status and expiry"
points: "minted unspendable until the order is irrevocable"
earn_basis: "post-discount, pre-VAT, pre-shipping, typed
exclusions"
proof: "mutation-check.py plants known money defects;
it has corrected this code four times"
}What this project shows
The mutation checker is what I would want a reviewer to look at. Writing tests is table stakes; proving the tests would fail on a real defect is the part almost nobody does.
Refusing to build the WordPress layer until the domain was proven took discipline and was clearly right. Money bugs found after go-live are not bugs, they are liabilities.