Skip to content

Sprint 8: rewards & coupons — claim, validate, redeem with points burn - #22

Merged
hynding merged 14 commits into
mainfrom
sprint-8-rewards-coupons
Jul 9, 2026
Merged

Sprint 8: rewards & coupons — claim, validate, redeem with points burn#22
hynding merged 14 commits into
mainfrom
sprint-8-rewards-coupons

Conversation

@hynding

@hynding hynding commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the Sprint 8 plan (docs/superpowers/plans/2026-07-09-sprint-8-rewards-coupons.md) — the burn side of Sprint 7's points economy, closing the earn/burn loop and delivering the "coupon/promo-code generation and validation" v1.x roadmap slice:

  • Rewards config plane: new reward Strapi content type — generated (codePrefix + 10-char code, ambiguity-free 32-char alphabet) or static shared codes, optional pointsPrice, expiry window, per-user claim limit, inventory cap; lifecycle validation incl. per-project slug and static-code uniqueness
  • Coupons runtime (migration 0007, pure additive): one coupons row per claim — the row is the claim record, so per-user and inventory counts derive from COUNT(*) with no drift
  • Atomic claims: pg_advisory_xact_lock per reward (plus per user when priced — one user racing two priced rewards cannot overspend), counts/balance read under the locks, decideClaim as the sole gate, wallet debit + coupon insert in one transaction; race-tested with exact-count assertions (inventory boundary, balance boundary, parallel redeems)
  • Checkout lifecycle (sk-only): POST /v1/coupons/validate (read-only, always 200) and POST /v1/coupons/redeem (conditional UPDATE + FOR UPDATE SKIP LOCKED; shared static codes consume oldest-unredeemed; response slug resolved from the consumed claim)
  • SDK: listRewards(), claimReward(slug), server-side validateCoupon/redeemCoupon; claim failures surface as typed PromoceanApiError (branch on insufficient_points vs claim_limit_reached)
  • Widget + demo: <RewardsStore/> (balance, claim buttons, inline code + copy, server-authoritative repeat claims); demo earn→claim→redeem flow incl. an sk-backed coupon checkout form; new rewards-loop e2e

Behavior notes for existing consumers

  • Wallet wire change: recent[].source gains 'redemption'older SDK/contracts versions will fail zod-parsing a wallet containing a redemption entry. Semver-minor contracts/sdk bump (changeset included); consumers must upgrade before points-spending exists in their project.
  • DELETE /v1/users/:userId counts gain coupons (additive); erasure removes coupon rows.
  • Four new error codes, all HTTP 409: reward_unavailable, claim_limit_reached, insufficient_points, already_redeemed.
  • staticCode never appears in pk-accessible responses — codes are revealed only by claiming (else the wallet debit is bypassable). Enforced by schema shape and asserted per-entry in tests.
  • Kill-switch semantics: enabled: false blocks new claims only; to stop redemption of outstanding codes, set endsAt in the past (expiry is evaluated at validate/redeem time — claimed codes die with the campaign).
  • pk claim surface follows the established track() trust model (any pk holder can claim as any userId) — privacy note added to the README.
  • Existing dev volumes: the seed skips when projects exist, so pre-Sprint-8 stacks won't gain the two demo rewards — docker compose --profile stack down -v to reseed.

Test plan

  • Full turbo 27/27 green post fix-wave (contracts 67, core 85, adapter-db 63 Testcontainers incl. the three concurrency races, adapter-strapi 47, sdk 41, widgets 30, api 165)
  • Fresh-seeded compose stack: all 5 Playwright specs green incl. the new rewards loop (free static claim → insufficient-points state → earn 250 via seeded rules+bonuses → priced claim debits 100 → sk validate/redeem → re-redeem 409 → erasure counts coupons)
  • Live sk hand-curl transcript: claim → validate(claimed) → redeem(200) → re-redeem(409 already_redeemed) → erase(coupons:1)
  • CMS lifecycle rules live-verified against a booted Strapi (incl. stash-revert regression checks on both fix waves)

Review notes

Nine tasks through the full implement→review gate cycle (two task fix waves: cms slug-uniqueness populate fix 97fc48e, widgets repeat-claim fix cf3a579) plus a final whole-branch review — verdict ready to merge after fix wave ac33cf6 (codePrefix capped at 54 so generated codes always fit the 64-char validate/redeem bound — a plan-defect catch; per-project static-code uniqueness; redeem response from the consumed claim; descriptive misconfiguration guard; doc corrections). Follow-ups filed: #20 (cms admin-session verification + legacy duplicate-staticCode scan), #21 (widgets test hardening).

🤖 Generated with Claude Code

hynding and others added 14 commits July 9, 2026 08:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… coverage (migration 0007)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Once claimed, a reward row previously replaced the claim button with the
code+copy UI permanently, foreclosing legitimate repeat claims allowed by
perUserLimit > 1. The row now shows both the most recently issued code and
the claim button, still subject to the same disabled rules recomputed from
refetched data; exceeding the per-user limit surfaces via the server's 409
claim_limit_reached through the existing inline error mapping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ess, redeem slug from consumed claim, docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hynding
hynding merged commit 5418a8b into main Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant