How a purchase earns points
Every time a sale closes, the till sends it to a Lambda Function URL and the earn function takes over. It finds the member, reads the rules, works out how many points the sale earns, and decides whether to do nothing, add the points, add the points and announce a reward, or park the sale for a staff member to check. The whole decision is plain Python. No model. No guessing. Every number lives in the rules doc, where the owner can change it without a deploy.
Key takeaways
- The earn function runs on each sale, posted from the till to a Lambda Function URL.
- The rules doc holds every number — points per dollar, bonus items, and the reward tiers.
- Four moves per sale: skip, add points, add and announce a reward, or hold for review.
- The member’s balance is updated with a safe one-at-a-time write so two sales can’t clash.
- The earn function never calls a model. Points are money, so the math is fully predictable.
The decision flow, per sale
The rules doc: points-per-dollar isn’t magic, it’s in the doc
The rules doc reads in plain prose: “Earn 1 point per dollar spent. Beans and gift cards earn double points. Reward tiers: 100 points = a free coffee, 250 points = a free lunch, 500 points = $25 off. Members can be away 60 days before they count as lapsing.” The earn function reads these numbers from the doc (mirrored to S3 so it doesn’t hit Drive on every sale) and does the arithmetic. Nothing is hard-coded; the owner can change a tier or add a bonus item by editing the doc, and the next sale picks up the change.
Per-item overrides are easy too. The rules doc can name specific product codes that earn a flat bonus (“each bag of beans: +6 points”) or a multiplier (“all whole-bean products: 2× points”). The till sends the line items, so the earn function can match them against the bonus list and add the extra points. This is how a shop runs a “double points weekend” without a developer — add a line to the doc on Friday, remove it on Monday.
Four moves, always
Every sale, every time, lands in exactly one of four buckets. The names are simple on purpose.
- Skip. The sale has no member attached and no phone to match against — a walk-in who isn’t in the program. Do nothing. Most anonymous sales are skips, and that’s fine.
- Add points. The normal case. The sale earns points, the new balance doesn’t cross a reward tier, so add the points and update the balance. Write a row to the points ledger so the change is recorded.
- Add + reward. The points push the balance over a reward tier. Add the points and email the customer: “You’ve earned a free coffee — show this at the counter.” The reward is now claimable at the redeem desk. Write the ledger row and the reward-earned note.
- Hold for review. Something looks off. The basket is many times bigger than this member ever buys (a possible mistyped total), or the sale is a refund that would push the balance below zero. Park it on a small review list for a staff member to approve or reject. Nothing is added until a human looks. This is the rare case, but it’s the one that keeps a typo from handing someone 5,000 points.
Balance writes that can’t clash
On a busy morning the same customer might somehow trigger two sales seconds apart, or a sale and a redemption might land at the same moment. If both read the old balance and both wrote, one change would be lost. The earn function avoids this by updating the balance with a safe one-at-a-time write — it tells DynamoDB “add 24 points to whatever the balance is right now,” rather than reading the number and writing back a total it computed. DynamoDB applies these one at a time, so no points go missing even when two things happen at once. Redemptions in Part 4 use the same trick in reverse.
Every move also writes a row to the loy-ledger table: (member, sale_id, points_change, reason, timestamp). The balance is the running total; the ledger is the history. If a balance ever looks wrong, you can replay the ledger and see exactly how it got there — which sale, which bonus, which redemption. That’s what makes every change reversible.
Why the earn path uses no model
The earn function could call a model to, say, write a more charming reward email or guess whether a basket is suspicious. It doesn’t. Two reasons. First, points are money — a customer who earns 24 points must always earn exactly 24 points, and a model in that loop introduces variance nobody wants near a balance. Second, this runs on every single sale, so a model call would cost money thousands of times a month for no gain. The math is a few lines of plain Python and zero surprises.
Bedrock fires elsewhere — once a month, to write the owner the plain-English program summary covered in Part 6. Not on the earn path. The earn function reads a doc, does arithmetic, updates a balance, and writes a ledger row.
Next post: how a reward gets redeemed — how staff look a customer up, what the confirm step does, and the guardrails that stop a reward going out by mistake.
All posts