# Reference patch — 03-loyalty-rate

The answer key for how the reference absorbs this change, and the thing that makes `budget.json`
reproducible. **Never shipped to the candidate.**

1. **Overlay** `../contract-delta/MemberTier.java` and `../contract-delta/OrderRequest.java` onto
   `contract/`. Contract files are given, so nothing here is charged. `MemberTier` is a new enum
   carrying no percentage — the rates are a commercial number and belong to the platform, not to a
   contract type. The revised `OrderRequest` adds a fourth component, `tier`, and keeps the
   three-argument constructor as a convenience that supplies `MemberTier.NONE` — which is why every
   existing caller, including the whole base suite, still compiles and still passes.
2. **Add** `LoyaltyRate.java` (in this directory) to `reference/src/`. New file, so `measureChange`
   charges nothing for it.
3. **Change one line** in `reference/src/OrderBookFactory.java`, in `standardTariff()`:

   ```diff
   -        return new StandardTariff(new DistanceBandFee());
   +        return new LoyaltyRate(new StandardTariff(new DistanceBandFee()));
   ```

Measured: `reference_diff = 2` (1 added, 1 removed, one file touched), so
`diff_budget = max(3, 2 + ceil(2/2)) = 3`.

Verified after the patch: the curveball suite is 10 tests green, and the base suite is 23 tests
green. **The base suite staying green matters here** — the requirement said an order that does not
mention a tier must not notice this change, and that is a claim about the old tests, not the new
ones.

## Where the requirement landed, and where it did not

`StandardTariff` and `DistanceBandFee` were not opened: **the shelf prices and the fee bands did not
change to make room for a discount.** Nor were `OrderStage`, `OrderBook`, `Order`, `Lifecycle`,
`Stages`, `PlacedStage`, `CancelledStage`, `DeliveredStage` or `PlainStage` — the whole lifecycle has
no opinion about money beyond the two stages that already move what is charged.

**What the requirement itself cost: one wrapper over one interface.** Read `LoyaltyRate` and notice
how much of the requirement is not written down anywhere:

| The requirement said | Where it is enforced |
|---|---|
| an order with no tier behaves exactly as it did | nowhere — `MemberTier.NONE` maps to 0, and 0% of anything is 0 |
| the receipt still says what the food and the delivery cost | nowhere — `Order.charge` is the only mutator that can touch the money and it cannot reach a quote; the wrapper hands the two quoted figures straight through |
| the difference has to be readable | nowhere — `OrderTotal.adjustmentMinor()` is `quoted - charged`, so the cost of the scheme is derived rather than stored |
| the delivery is not discounted | one argument: the percentage is taken on `itemsMinor()`, not on `quotedMinor()` |
| the forty-five minute promise applies on top | nowhere — `DeliveredStage` already *subtracts* the fee rather than overwriting the total, so two adjustments compose |
| a member who cancels pays nothing | nowhere — `CancelledStage` sets the charge to 0, and 0 minus a discount was never going to be asked for |
| a member's order walks the same stages and tells the same people | nowhere — a tariff has never known what a stage is |
| there will be a fourth tier, and the rates will move | one row in a map, and one line in the platform's wiring |

Eight rules, one `EnumMap` and one subtraction.

## The composition point, which is the whole reason this is a curveball

The interesting assertion in the suite is this one:

```java
        String id = api.place(dinnerFor(MemberTier.GOLD));   // 10000 of food, 2750 of delivery
        clock.advance(Duration.ofMinutes(46));
        deliver(api, id);
        assertEquals(9000, api.total(id).totalMinor());
```

A gold member, delivered late, is charged 9000: the food less a tenth, and nothing for the delivery.
That number is only right because **both** adjustments subtract:

```java
        order.charge(money.totalMinor() - money.deliveryFeeMinor());     // DeliveredStage
        quoted.totalMinor() - discount                                  // LoyaltyRate
```

`DeliveredStage`'s javadoc already argues for this, before any discount existed: *"the two
expressions are equal today, which is exactly the kind of coincidence worth not relying on."* Had it
been written the obvious way — `charge(money.itemsMinor())`, which passes every base test — this
curveball would have silently thrown the member's discount away, and the order would have been
charged 10000. Nothing would have failed to compile and no old test would have gone red; a member
would just have been overcharged by 1000.

That is the trade to be able to name in a defence: **an adjustment must say what it takes off, not
what the answer is.** Two `charge(...)` calls that each assert the final figure cannot compose, and
which one wins depends on the order the stages happen to run in.

## What this costs without the seam

The plausible alternative is a clause inside the tariff that already exists, because "it is the
class that works the price out":

```java
    @Override
    public OrderTotal priceOf(OrderRequest request) {
        long items = request.itemsMinor();
        long delivery = fees.feeMinor(request.distanceMetres());
        long discount = 0;
        if (request.tier() == MemberTier.SILVER) {
            discount = (items * 5 + 50) / 100;
        } else if (request.tier() == MemberTier.GOLD) {
            discount = (items * 10 + 50) / 100;
        }
        return new OrderTotal(items, delivery, items + delivery - discount);
    }
```

**Measured, not asserted: 8 lines** (7 added, 1 removed) in `StandardTariff.java` — against this
patch's 2 in `OrderBookFactory.java`. Both pass all ten curveball tests.

The six extra lines are not the cost. The cost is that `StandardTariff`'s own javadoc — *"Nothing is
knocked off here, which is exactly why the seam is worth having"* — has just stopped being true, and
the class that holds what the food costs now also holds what marketing decided last Tuesday. Those
two change on completely different clocks. The fourth tier is a third `else if`; the weekend
promotion is an edit to a literal inside the class that prices every order on the platform; and a
service charge, a voucher and a festival offer are three more clauses in the same method, in an order
nobody wrote down. With the wrapper, each of those is a new file and one line of wiring, and the
order they apply in is visible at the wiring site as the order they are nested in.

## The seam question an interviewer will ask

"You used a template method for the stages and a decorator for the money. Why not the same shape for
both?"

Because they are answers to different questions, and being able to say which is which is the point of
the problem. A tariff is a **policy**: it answers *what does this cost*, it is chosen by whoever
wires the platform up, and it has no idea when it will be asked or what else is going on around it —
the caller keeps control of the sequence, which is exactly what lets one wrap another. A stage's own
work is a **step**: the skeleton in `OrderStage` decides when it runs and a stage cannot reorder it,
notify twice or skip its own timestamp. Swapping a policy changes an answer; overriding a hook
changes a step. A discount expressed as a lifecycle hook would mean the money knew what a stage was;
a stage's audience expressed as a policy would mean every stage had to remember to ask.
