Background
The Customer Rewards team at a large online retailer runs the points programme: members earn points on every order and trade them for store credit. The team is moving the earning rules out of a spreadsheet and into this service. The ledger, tiers and order import already exist; the function that works out how many points an order earns has not been written yet, and the import has a few rough edges of its own.
How the system works
Order export
CSV, one order line per row: orderId,memberId,category,amount.
orderId,memberId,category,amount
O-1,M-7,grocery,12.40
O-1,M-7,ELECTRONICS,$99.99
- An optional header row (the first non-blank line, if it starts with
orderin any case) is skipped. Blank lines are ignored. Fields are trimmed. - Rows with the same
orderIdbelong to one order, in the order the ids first appear. All rows of an order must name the same member. orderId,memberIdandcategoryare required; categories are stored in upper case.amountis dollars: an optional$, 1 to 7 digits, then optionally.and 1 or 2 digits.12is 1200 cents,12.5is 1250 cents,12.34is 1234 cents.- A wrong number of fields, a missing value, a bad amount, or an order id shared by two members is a
ParseError.
Tiers
A member's tier comes from their lifetime spend (the total of all orders they've had points for, plus any starting spend):
| Tier | Lifetime spend |
|---|---|
| BASIC | below $250.00 |
| SILVER | at least $250.00 (25000 cents) |
| GOLD | at least $1,000.00 (100000 cents) |
Earning points
pointsForOrder(order, tier):
- Base points. For each line, take the whole dollars of its amount (cents are dropped line by line: $12.99 is 12) and multiply by the category's multiplier:
GROCERY2,BOOKS3, anything else 1. Categories match in any case. The base is the sum over all lines. - Tier bonus. Add
base × percent / 100, rounded down, where percent is BASIC 0, SILVER 25, GOLD 50. - Big-basket bonus. If the order's total (the exact sum of its line amounts in cents) is at least $200.00, add a flat 100 points. This bonus isn't multiplied or increased by the tier bonus.
- An order with no lines earns 0.
Examples: GROCERY $10.00 + BOOKS $20.00 + GARDEN $5.00 at BASIC → 20 + 60 + 5 = 85. ELECTRONICS $23.00 at SILVER → 23 + 5 (5.75 rounded down) = 28. GROCERY $200.00 at GOLD → 400 + 200 + 100 = 700.
The helpers categoryMultiplier, tierBonusPercent and orderTotalCents are already written.
The ledger
addMember(id, startingSpend)opens an account with 0 points; a duplicate id throwsstd::invalid_argument. Member ids are case-sensitive.applyOrder(order): the member must exist (elsestd::invalid_argument). Points are earned at the tier the member had before this order (tierBefore); then the points are added to the balance and the order total to the lifetime spend, andtierAfteris the new tier. An order id that was already applied (for any member) is a duplicate: it earns nothing, changes nothing and is reported withduplicate = true.redeem(id, points): points redeem in blocks of 500, each worth $5.00 of store credit. The request must be a positive multiple of 500 and no more than the balance; otherwise it returns 0 and changes nothing. On success it takes the points and returns the credit in cents (1000 points → 1000 cents).balance(id)andtierOf(id)throwstd::invalid_argumentfor an unknown member.
Project structure
include/
util.h ParseError, trim / toUpper / split, dollar amount parsing
member.h Tier, tier thresholds, tierFor, tierName
order.h OrderLine, Order, orderTotalCents, order export parser
points.h category multipliers, tier bonus, pointsForOrder
ledger.h OrderResult, Ledger (members, orders, redemptions)
src/ implementations of the above
tests/ unit tests (testlib.h is the test framework, runner.cpp the entry point, fixtures.h shared data)
Your task
Implement pointsForOrder in src/points.cpp as specified in "Earning points", and fix any defects that stop the test suite from passing.
- 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.