Background
The Retail Returns team owns the service that decides whether a customer may return items from an order and how much they get back. Category managers configure return rules as a JSON policy file, and the service evaluates every return request against it. After a recent refactor, the service and its test suite are in bad shape.
How the system works
Policy file
{
"default": {"returnable": true, "windowDays": 30, "requiresUnopened": false, "restockingFeePct": 0},
"rules": [
{"category": "Electronics", "windowDays": 15, "restockingFeePct": 15},
{"category": "Appliances", "windowDays": 45, "restockingFeePct": 12.5}
]
}
- Every category rule must have a
category. Any other field it omits is taken fromdefault. - Category matching is case-insensitive and ignores surrounding whitespace. Categories with no matching rule use the default rule.
restockingFeePctmay be fractional (e.g.12.5).- The service has its own small JSON parser (
json.cpp). It supports objects, arrays, strings (escapes\" \\ \/ \n \t),true/false/null, and numbers that may be negative and/or fractional (-3,12.5). Malformed input throwsjson::ParseError.
Evaluating a return request
The service checks a request in this order. The first failing check decides the outcome:
| # | Check | Outcome |
|---|---|---|
| 1 | order id exists | ORDER_NOT_FOUND |
| 2 | order belongs to the requesting customer | NOT_OWNER |
| 3 | the SKU is on the order | ITEM_NOT_FOUND |
| 4 | 0 < quantity <= purchased − already returned | INVALID_QUANTITY |
| 5 | request date is not before the purchase date | INVALID_DATE |
| 6 | the category is returnable | NON_RETURNABLE |
| 7 | days since purchase <= windowDays (the last day of the window is allowed; a same-day return is day 0) | WINDOW_EXPIRED |
| 8 | if the item was opened, the rule must not require it to be unopened | MUST_BE_UNOPENED |
If every check passes, the outcome is APPROVED and:
gross = paid unit price × quantity. The paid price is what the customer actually paid, after discounts. It isn't the catalog list price.- If the item was opened:
restockingFee = restockingFeePct % of gross, rounded half up to the nearest cent. Otherwise the fee is 0. refund = gross − restockingFee.
All money is in integer cents.
State
ReturnService keeps the orders in memory. An approved return adds to that line's returned quantity and to the customer's refund total. A rejected request changes nothing. Customers may return a line in several requests, but never more units in total than they bought.
Project structure
include/
json.h tiny JSON value + parser
date_util.h ISO date parsing, day arithmetic
money.h cents helpers (percentages, formatting)
order.h Order, OrderLine, ReturnRequest, Decision, Outcome
policy.h PolicyRule, PolicyBook (loaded from JSON)
return_engine.h evaluateReturn: pure eligibility + refund calculation
return_service.h ReturnService: order store, ownership, state updates
src/ implementations of the above
tests/ unit tests (testlib.h is the test framework, runner.cpp the entry point)
Your task
The QA team and customers have reported problems since the last release:
- The project no longer builds.
- Customers returning electronics on the last day of the return window are turned away.
- Refunds for opened appliances are a few cents to a few dollars off.
- Customers who bought discounted items say their restocking fees are too high.
- Some customers have returned more units than they bought by splitting a return into several requests.
Find and fix the defects so that all tests pass.
- Do not modify files in
tests/. - Keep your changes minimal. Fix the defects; don't rewrite modules.
- Additional hidden tests check the behaviour described in this README.