levelmax World 5 Explicit Design
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. B
L
Level 5.B ~25 min Target: PHPStan max

Boss: a returns desk

Returning all three mugs from a discounted order refunds £5.01 on a £5.00 order. Not much, until it's every discounted order for a year. Finance wants the penny back. The returns page has already been redesigned around better types. The returns desk behind it hasn't.

ReturnsDesk::quote() works out refunds in float pounds, finds orders by asking a global container, decides whether the 30 days are up with whatever new DateTime() says today is, and ignores a SKU it doesn't recognise. None of it is new. This is World 5 in one method.

Your checklist

Level Ask of ReturnsDesk
5.1 Value objects Does it take an OrderReference, so normalising and checking happen once, before it's called?
5.2 Money Is every amount Money in integer pence, with the discount allocated so the shares add up?
5.3 Immutable all the way down Are dates DateTimeImmutable, and is every modify() result used?
5.4 Dependency injection Does it ask for orders and the time in its constructor, rather than fetching them?
5.5 Small interfaces Does it depend on the smallest interface it needs, so a test's fake has one method?

The tests use a one-method fake for the orders and a stopped clock, so the desk has to accept both. ShopServices passes the real DatabaseOrderRepository, which implements the big OrderRepository. Both fit, if you pick the right interface.

CHALLENGE

The returns desk quotes refunds as floats, so returning all three mugs from a discounted order gives back a penny more than the customer paid. It fetches orders from a global container, reads the real time through a mutable DateTime, and quietly ignores SKUs that aren't on the order.

Bring src/ReturnsDesk.php up to World 5 standards, so the tests pass and PHPStan is clean at max. ReturnsController and ShopServices (both read only) are already written against the result:

  • quote() takes an OrderReference and returns Money in the order's currency
  • each line's share of the discount comes from Money::allocate(), by line price, so a whole order refunds exactly what was paid
  • the constructor takes the smallest interface that can find an order, and a Clock
  • a SKU that isn't on the order throws an InvalidArgumentException, like an unknown order
Running on PHP 8.3…

Run the tests, run PHPStan, or submit for both. Ctrl/⌘ + Enter submits from the editor.