Skip to main content

Debug Loyalty Points System

Premium

You're handed the engine behind a retail loyalty program. It runs, it prints monthly statements, and the statements look plausible. Some of them are wrong, and support has the tickets to prove it. You're asked to find the bugs and fix them.

Starter code and solution
Python 3.11+ (standard library)
Download code

What's in the codebase

  • engine.py. Earning, redeeming, expiry, and tier calculation. record_purchase, active_batches, balance_on, lifetime_points, redeem, and tier_for.
  • models.py. Customer, Transaction, PointBatch, and Redemption. Points are tracked in batches so they can expire together.
  • statement.py. Renders a month's statement for a customer.
  • scenarios.py. The accounts and activity behind five support tickets, replayed in date order. This file is read-only reproduction data.
  • main.py. Prints every scenario, or one at a time with python3 main.py 3.

There is no test suite. The deliverable is correct statements, and the output is deterministic, so you can diff a run against the previous one as you work.

The complaints

Four customers have open tickets:

"My March statement says my balance is 4890.6900000000005 points. What is .6900000000005 of a point?"

"I've earned exactly 9,000 lifetime points, and the statement agrees, and my tier still says Silver. Gold starts at 9,000, right?"

"My statement lists purchases I never made. Who bought $200 of patio furniture on my account?"

"I redeemed 800 points in June and my July statement still says 600 points expired. What did my redemption even use, then?"

A fifth scenario is believed to be correct already. It should look exactly the same when you're finished, which makes it a regression check you get for free.

The spec

Customers earn 1.5 points per whole dollar, so a transaction is worth floor(amount × 1.5) points, and points are always whole numbers. Tiers come from lifetime points, meaning everything ever earned, even if it was later redeemed or expired: Bronze from 0, Silver at 1,500 or more, Gold at 9,000 or more, with inclusive boundaries. Redemption consumes the oldest points first, so points closest to expiry get used before they can expire. Points expire twelve months after being earned, compared by date. And each customer's history is their own.

Your task

Run it before you change anything, then work ticket by ticket. Fix each bug at its root cause rather than correcting the number on its way to the statement renderer, and keep the module structure and public functions intact.

Four tickets does not necessarily mean four bugs. One symptom can have two causes, and one cause can produce two symptoms.

What to focus on

  • Reading the spec as a specification. Each complaint maps to a sentence in the spec that the code contradicts. Finding the sentence first tells you which function to open, which is faster than reading engine.py top to bottom.
  • Separating a display problem from a data problem. A wrong number on a statement can originate in the renderer or arrive there already wrong. Check balance_on before you touch statement.py.
  • Building your own reproduction. You have deterministic output and no tests. Capturing a baseline run and diffing after each fix gives you the same feedback loop a test suite would, and it catches the fix that quietly changes a scenario you weren't working on.

Using AI on this problem

Handing an agent the whole engine and asking it to find the bugs is a poor use of it here, because several of these mistakes look like ordinary code and only become wrong when you compare them against the spec.

Give it the spec and one function at a time, and ask it where the two disagree.

Here is the spec for how points expire and how redemption chooses which points to consume, and here is the implementation. Don't propose a fix. List every place where the code's behavior would differ from the spec, and for each one give me an example of inputs that would show the difference.

Then confirm each candidate against a scenario yourself before changing anything.

Leveling signals

Mid-levelFinds and fixes the arithmetic and boundary bugs by working ticket by ticket. Fixes them in the right layer once pointed at the distinction. May miss the ones whose symptom appears on a different account than the cause.
SeniorMaps each complaint to the spec sentence it violates before opening the code. Catches that two tickets share a cause. Captures a baseline run and checks the known-good scenario after every fix.
Staff+All of that quickly, plus treats the engine as production code that computes money: names the classes of bug that keep recurring here, points out where the data model makes a wrong answer easy to write, and names what would catch the next one before a customer does.