Bugnik
← All problems

14Loyalty Points Calculator

C++EasyFeatureSuggested time: 60 min

Loyalty points calculator (implement pointsForOrder; tiers, redemptions)

Sign in to start

Before you start

You'll get more out of this problem if you're comfortable with:

Language

  • Range-based for loops over vectors of structs
  • Integer arithmetic: division that rounds down, percentages with whole numbers

Libraries and frameworks

  • std::map, std::set and the headers that declare them

Build and test tools

  • Reading g++ errors that suggest a missing #include
  • Running the tests and filtering them by name

Reasoning

  • Turning a written spec into a small function, step by step
  • Money as integer cents, including amounts like 12.5

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 order in any case) is skipped. Blank lines are ignored. Fields are trimmed.
  • Rows with the same orderId belong to one order, in the order the ids first appear. All rows of an order must name the same member.
  • orderId, memberId and category are required; categories are stored in upper case.
  • amount is dollars: an optional $, 1 to 7 digits, then optionally . and 1 or 2 digits. 12 is 1200 cents, 12.5 is 1250 cents, 12.34 is 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):

TierLifetime spend
BASICbelow $250.00
SILVERat least $250.00 (25000 cents)
GOLDat least $1,000.00 (100000 cents)

Earning points

pointsForOrder(order, tier):

  1. 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: GROCERY 2, BOOKS 3, anything else 1. Categories match in any case. The base is the sum over all lines.
  2. Tier bonus. Add base × percent / 100, rounded down, where percent is BASIC 0, SILVER 25, GOLD 50.
  3. 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.
  4. 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 throws std::invalid_argument. Member ids are case-sensitive.
  • applyOrder(order): the member must exist (else std::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, and tierAfter is the new tier. An order id that was already applied (for any member) is a duplicate: it earns nothing, changes nothing and is reported with duplicate = 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) and tierOf(id) throw std::invalid_argument for 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.