Amirali YaghoutiSenior Software Engineer

business Case study

Loyalty + Wallet Ledger

Loyalty balances are money. In a gold and jewellery business that money arrives from several directions: a deposit against a piece, credit from a trade-in, a promotional balance from a campaign. Every defect in the code that moves it is a real amount owed to or taken from a customer. So I built it as a domain first and proved it 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 owed six months ago. At that point nobody can reconstruct how the number was reached. Jewellery makes it worse in two ways. Customer value here is not one kind of thing: a deposit against a specific piece is not spendable elsewhere, a trade-in credit may be, a promotional balance expires. Collapsing them into one number makes the arithmetic easy and the questions unanswerable, and those questions are exactly the ones that come up in a dispute. And the goods are priced by the gram, so floating-point arithmetic is not a rounding error, it is a shortfall: 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. It refuses floats rather than silently rounding them, and it rounds toward zero in both directions, so an earn and its clawback cancel exactly.
  • An append-only wallet ledger where every movement is an entry with its own type and a balance is the sum of those entries, never a stored column. Nothing is edited or deleted; a correction is a reversal that names the entry it undoes, which is what keeps the history honest.
  • Separate buckets for cash, promotional credit and refund credit, typed and queryable rather than encoded in a note field, because the three differ in tax treatment, legal status and expiry rules and cannot share a balance. Store-issued credit is spent first, so the shop never refunds its own promotion as though it were the customer's money.
  • A point lifecycle where points are minted unspendable and stay that way until the order that earned them becomes irrevocable. Cancelled and expired points are kept distinct: expired points were genuinely earned and aged out, cancelled points never were.
  • 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.
  • An audit trail that falls out of the design rather than a separate log. The entries are the record, so any balance reads as a sequence of movements with causes, which is what makes a customer conversation about a balance possible.
  • A mutation checker that plants known money defects one at a time and fails if any survives the test suite.

Technical approach

  • I built and proved the domain 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 structure where the answer to how did we get here exists at all.
  • I modelled the bucket separation instead of leaving it to convention. Promotional credit that expires and cash credit that does not are different things, and a single balance column forces them to pretend otherwise. A category you cannot query is a category you will get wrong.
  • 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 same rule closes cancellation cycling and cash-on-delivery refusal farming, because there is nothing spendable to run off with.
  • Every exclusive hold carries a deadline, so a redemption whose process dies cannot leave a customer's points reserved indefinitely: unusable, absent from the visible balance and impossible to explain.
  • 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. Two further guards turned out to be deliberately redundant; rather than delete or pretend, the redundancy is asserted explicitly and the checker reports it as a finding if the layering ever changes. And customer value became explainable: any balance can be walked back through the entries that produced it, which is the property that matters when a customer disagrees with a number.

Commercial value

A loyalty system that cannot explain a balance becomes a support burden and eventually a dispute. In jewellery retail the balance conversations are high-value and infrequent, which means they are exactly the ones you cannot afford to lose, and an auditable ledger is what makes the shop's position defensible. Building the ledger properly first is cheaper than reconstructing it after the first month of live balances.

implementation-brief.readme

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"
  corrections: "a reversal naming the entry it undoes;
                never an edit or delete"
  buckets: "cash / promotional / refund, typed and queryable;
            separated by tax, legal status and expiry;
            store-issued credit spent first"
  points: "minted unspendable until the order is irrevocable;
           cancelled and expired kept distinct"
  earn_basis: "post-discount, pre-VAT, pre-shipping, typed
               exclusions"
  audit: "the entries are the record; no separate log"
  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. Modelling value categories explicitly costs more up front and prevents the class of bug where a deposit against one item quietly becomes spendable against another. Money bugs found after go-live are not bugs, they are liabilities.