# Reference patch — 03-partial-refund-window

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

**Read this one first if you are comparing it to curveballs 01 and 02.** Those two land entirely on
an existing seam (`PricingPolicy`) or entirely on the public API (`AdjacencyFinder` calls nothing
but `MovieBookingApi` itself) and cost 2 and 4 lines. This one does not, and the difference is
deliberate: `MovieBookingApi`'s own "out of scope" list names "refunds and cancellations of a
confirmed booking" explicitly. There is no seam for a capability the base contract states outright
that it does not have. Absorbing this honestly costs more, and the number below is reported as
measured rather than shopped down to look better.

1. **Add** `RefundPolicy.java` (in this directory) to `reference/src/`. New file, so
   `measureChange` charges nothing for it — the one piece of actual arithmetic this requirement
   introduces (the window comparison and the half-refund rounding) lives here and nowhere else.
2. **Add one enum constant** to `reference/src/HoldOutcome.java`:
   `HELD, EXPIRED, CONFIRMED, RELEASED` becomes `HELD, EXPIRED, CONFIRMED, RELEASED, CANCELLED`.
3. **Add one field** to `reference/src/HoldRecord.java`: `Instant confirmedAt;`, set the moment a
   hold becomes a booking and null until then.
4. **In `reference/src/Show.java`:**
   - `confirm()` records `record.confirmedAt = now;` in the `HELD` branch, and its switch
     expression gains a `case CANCELLED -> throw ...` — required by the compiler once
     `HoldOutcome` has a fifth constant, since a switch **expression** over an enum must be
     exhaustive.
   - A new method, `cancel(String holdId, Instant now, Duration fullRefundWindow)`, doing exactly
     what `confirm` and `release` already do: look up the record, check its outcome, mutate the
     seats and the record together under this show's own lock, and return a value built from the
     result. `release()` is **not touched** — a statement `switch` (unlike `confirm`'s expression
     one) does not require exhaustiveness, so an unmatched `CANCELLED` there is already the
     correct no-op with no code needed.
5. **Add one method** to `reference/src/MovieBookingSystem.java`, a one-line delegation exactly
   like curveball 02's `holdAdjacent`:

   ```diff
        @Override
        public void releaseHold(String holdId) {
            showFor(holdId).release(holdId, clock.now());
        }
   +
   +    @Override
   +    public RefundResult cancelBooking(String holdId) {
   +        return showFor(holdId).cancel(holdId, clock.now(), FULL_REFUND_WINDOW);
   +    }
   ```

6. **Add** `contract-delta/MovieBookingApi.java` (the full revised interface, plus
   `contract-delta/RefundResult.java`) to the candidate's contract. Neither is counted in the
   measured diff, the same convention as curveball 02.

`edits.json` beside this file is every edit above in machine-readable form.

Measured: `reference_diff = 26` (25 added, 1 removed across four files), so
`diff_budget = max(3, 26 + ceil(26/2)) = 39`.

Both the curveball suite (7 tests) and the base suite (20 tests) are green after the patch. **No
base assertion is invalidated** — `hold`, `confirm` and `releaseHold`'s own behaviour is unchanged;
`confirm` merely also records a timestamp nothing in the base contract observes.

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

`MovieBookingSystem.hold`, `MovieBookingSystem.confirm`, `MovieBookingSystem.releaseHold`,
`Show.tryHold`, `Show.release`, `Show.statusOf` and `Show.availableSeats` were none of them opened.
Every seat-state transition this requirement needed — `BOOKED` back to `AVAILABLE` — reused the
exact three lines `release()` already had for `HELD` back to `AVAILABLE`; cancellation is not a new
kind of seat transition, only a new *path* to a transition the design already knew how to make.

The test worth watching in a candidate's design is `windowStartsAtConfirmationNotAtHold`: a design
that measures the refund window from the hold's own creation, or from its `expiresAt`, rather than
from `Booking.confirmedAt`, gets this wrong for exactly the customer the requirement was written
for — the one who held for four and a half minutes deciding, then paid, then changed their mind a
minute later. That customer must still be inside the window, and the only correct anchor for it is
the moment of confirmation.
