What this feature is
Achievements turn what a user already does — buying, stashing, favoriting, ranking, reading notebooks — into badges. There are ten tracks of four tiers each, from Core Collection and Spending Milestones through Activity Streaks and Notebook Mastery, with each card carrying a rarity (Common, Rare, Epic, Legendary) worth XP toward a level and an identity title. Users see them in the Achievements Hub, on a detail screen where up to five can be pinned to their profile, in the profile’s “Active Quests” strip, and as a celebration that fires the moment something unlocks. Nothing here is entered by hand: every number is derived from the user’s own orders, stash, collection and notebooks, and the same derived numbers power the lifetime totals on the Stats tab.How to read the contract
Every case ID below is a live test. Cases built during the feature-test programme carry their ID in the test title, sogrep 'ACH-045' finds the assertion. A
handful of rows point at coverage that predates the programme; those are cited by
ID and file, and the test is found by its plain-English name in that file.
The behavior contract
Earning a badge
Activity streaks
Unlocking exactly once
Derived numbers always reconcile with source data
Backfilling a user whose history predates the projection
Who can see and change what
Reading achievements in the app
Celebrating an unlock
What the live tier proves end-to-end
Tier C runs against the deployed staging backend. Because thresholds are one-shot, each case uses a brand-new, really-signed-in identity and deletes it afterwards.- A real order placed through the deployed backend produces a real unlock (ACH-140).
- A spending threshold crosses on cent-precise real money with no float drift, and the teardown that reverses it settles to a clean zero (ACH-141).
- Shops whose names differ only by casing, whitespace or trailing punctuation collapse to one dispensary under the production identity rules (ACH-142).
- Five real stash writes unlock the first Stash Manager tier and open a one-day streak (ACH-143); three distinct notebook views unlock the first Notebook Mastery tier while a repeat view is ignored (ACH-145).
- All five public achievement functions accept the literal payloads the app sends (ACH-146), and deleting the source orders leaves the unlock standing (ACH-147).
- The celebration feed comes back newest-first with one shared batch id per engine run, and a pin round-trips and normalizes (ACH-148, ACH-149).
- Ten parallel writes yield one consistent evaluation, not ten (ACH-151).
- No client-writable unlock surface exists on the deployed backend, and the deployed validator rejects a pin without an owner key (ACH-154, ACH-085C).
Deliberate gaps & conditions
- The Daily Stories track is a deliberate product quarantine, not a bug. All ten tracks remain defined in the rules so no earned badge is ever rewritten, but there is no story producer, the feature flag is hard-disabled, and the track is excluded from the visible set — nine tracks and thirty-six cards. ACH-016 proves the thresholds are correct; ACH-017 proves the product path cannot reach them; ACH-018 proves the Hub hides the track so completion is not permanently capped.
- There is no steady-state drift detector. ACH-070 is blocked because the thing it would test does not exist. Drift is caught at backfill time (ACH-065) and by the differential in STATS-001, not by a scheduled reconciliation.
- The backfill has no client-callable entry point, so ACH-152 cannot run a real backfill against staging. Backfill correctness is proved at Tier A instead (ACH-060 – ACH-069, STATS-010).
- The recent-unlocks limit clamp landed after ACH-090 was written; the row is
buildable now and is the one remaining
deferredcase in this area. - Engine latency has no budget test (ACH-150). Asserting a p95 would need ten repeat runs on a fresh identity each time, and a flaky timing gate is worse signal than none.
- Cross-feature chains are proved once, in the owning suite. The order → stash → achievement chain and account-deletion-clears-achievements are asserted in the orders and users integration suites rather than duplicated here (ACH-144, ACH-153).
- Tier C skips cleanly without credentials, naming exactly what is missing, and is
never part of
pnpm test,pnpm check, or a cached Turbo run.
Where to extend
A new track, threshold, streak rule, or projection counter gets its Tier A case inapps/mobile/convex/tests/achievements.featuretests.test.ts (metrics and streaks),
achievements.featuretests.part2.test.ts (engine, backfill, auth), or
apps/mobile/convex/tests/statsProjection.test.ts (anything a lifetime number depends
on). A new hook argument or rendered state goes in the co-located
useAchievementsV2.featuretests.test.tsx / achievements-v2.featuretests.test.ts, or in
apps/mobile/test/ui/features/AchievementsHub.test.tsx /
AchievementsCelebration.test.tsx. Reach for
apps/mobile/test/integration/suites/achievements.integration.test.ts only when the
claim is about the deployed backend — and remember every threshold there needs a
fresh identity. Then add the row to the master matrix; see
Mobile Feature Test Platform for naming,
tags, and the tier decision.