Split a checkout class that prices, parses coupons, charges and emails into small parts, with the settings and the notifier passed in, and every old total kept exactly.
The checkout in the editor works, and nobody wants to touch it: one class reads two globals, cuts coupon codes out of strings, compares payment names with if, adds up the tax and the fee, and sends the receipt. Refactor it into small parts without changing a single total.
The rules (keep them exactly):
| Step | Rule |
|---|---|
| Subtotal | price times quantity, summed over the lines |
| Coupon | "" takes nothing; FLAT50 takes 50 off but never more than the subtotal; OFF10 takes 10 percent off, rounded down; any other code is rejected |
| Delivery | 40 rupees, free once the discounted amount is 500 or more |
| Tax | the tax percent of the discounted amount, rounded down |
| Payment fee | card: 2 percent of everything so far, rounded down; upi: nothing; cod: 20 rupees; any other name is rejected |
| Status | cod orders are pending, the rest paid |
For the order of two notebooks at 120 and four pens at 25, paid by UPI, the total is 397.
Build this API (Line, Settings, Receipt and a FakeNotifier are given):
NoCoupon(), FlatOff(amount) and PercentOff(percent), each with discount(subtotal), and parse_coupon(code) returning one of them. An unknown code raises ValueError("unknown coupon: GOLD"). This is the only place that reads a code string.Card(), Upi() and CashOnDelivery(), each with fee(amount) and a status attribute ("paid" or "pending"); payment_for(name) looks one up and raises ValueError("unknown payment: cheque"); register_payment(name, method) adds one.PriceCalculator(settings) with total(lines, coupon, payment): a pure calculation with no mail and no globals.CheckoutService(calculator, notifier) with place(lines, coupon_code, payment_name): prices the order, calls notifier.send("receipt: <total>") once, and returns a Receipt. A rejected order sends nothing.JavaScript and TypeScript use camelCase: parseCoupon, paymentFor, registerPayment, and new Settings({ taxPercent: 10 }). Errors are Errors with the same messages.
The tests check every old total, the calculator with plain numbers and with two different settings side by side, the service with a fake notifier, a new coupon and a new payment method written inside the tests and plugged in, and that no comparison against a payment name is left in your code.
Write your code, then run 11 example tests and 3 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and the runtime restarts, your code stays.