Skip to main content

What this feature is

Dispensaries is the user’s list of the shops they buy from. A user can save a shop by name — with address, phone, website, email, receipt domains and up to five menu URLs — favorite it, edit it, delete it, and open a detail screen showing what they have spent there and every order they have placed there. Shops also appear without being typed: saving an order or importing a Gmail receipt creates the shop automatically. The Shopping Agent is the Pro feature that reads a saved shop’s live menu. From a shop with menu URLs, the user confirms a scan on an explicit consent screen, watches its progress, and lands on a results screen where every product is either matched to a strain in the catalog or offered for research. On top of that menu, a per-shopper pass produces personalized picks from the user’s own favorites, dislikes and stash. The same matching engine backs the Safari extension, which highlights products on real dispensary sites.

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, so grep 'DSH-088' finds the assertion. Rows whose coverage predates the programme are cited by ID and file; the test is found by its plain-English name in that file. A few behaviors landed in wave 3 under a defect id rather than a case id — those cite BUG-nnn, which is likewise the literal prefix of the test title.

The behavior contract

Saving a shop

One shop, one identity

Finding a saved shop

Editing, renaming and visiting

Deleting a shop

Shop detail, spend and order history

Receipt domains

Shops created for you

Running a scan

Keeping and re-opening a scan’s results

Reading the menu

Personalized picks

Researching unmatched products

The Safari extension

What the live tier proves end-to-end

Tier C signs in as a real, disposable +clerk_test identity, drives the deployed staging Convex deployment and the production Hono API, and deletes everything it creates. The free lane spends nothing; the paid lane is opt-in.
  • The whole saved-shop lifecycle works on the deployed backend — create, read, favorite, edit, rename, archive-on-delete and re-add (DSH-161, DSH-164).
  • The deployed validators reject exactly what the client rejects, and duplicate prevention behaves on a real deployment as it does in memory (DSH-162, DSH-163).
  • One deployed identity cannot touch another’s shops, and the Revyl launch-smoke fixture seeds idempotently on staging (DSH-165, DSH-166).
  • The scan endpoint’s gates are real: an absent or unverified Clerk token is refused, and a free identity is stopped at the entitlement gate before any credit is spent — measured with a deliberately invalid body so the status names the gate reached (DSH-168, DSH-167).
  • A scan result’s storage lifecycle holds on the deployed backend — create, read, delete, the active-result quota, and a source-run binding that cannot be forged (DSH-176, DSH-177, DSH-178).
  • A real cold scan of a known-good menu completes and decodes on the results screen, a second identical scan is served from cache, two concurrent identical scans collapse into one run, a menu with no products yields an empty menu, and a failing scan surfaces an error and releases its lease (DSH-169, DSH-170, DSH-171, DSH-172, DSH-175).
  • Queueing discovery research reaches the real research pipeline (DSH-179), and a Gmail receipt really does create the shop it came from (DSH-181).

Deliberate gaps & conditions

  • The paid lane is opt-in and skipped by default. DSH-169–172, DSH-175 and DSH-179 each spend Firecrawl credits or real research runs and take minutes; they run only when TIER_C_PAID is set deliberately. A default Tier C run reports them as skipped, which is not evidence they pass.
  • Which gate the deployed API stops a Tier C identity at is probed, not pinned. That is a deployment fact that has already moved once — first the auth-realm split, then the subscription gate flipping from bypass to enforce — so DSH-167 measures it and reports it rather than asserting a status a config change would falsify.
  • Age-gated dispensary sites are not scanned in CI. DSH-173 and DSH-174 are deferred: they depend on a live third party, are non-deterministic, and the interactive scraping mode is materially more expensive.
  • /extension/match is not exercised on staging. DSH-183 would require flipping a feature flag on a deployment shared with every other suite, so the endpoint is proven at Tier B only.
  • Two behaviors are contracted here but proven in the Orders and Gmail suites to avoid asserting the same rule against two copies of it: order-driven shop auto-creation on staging (DSH-180 → ORD-205) and the live email-domain endpoint (DSH-182 → GML-226).
  • Two P3 rows are deliberately unbuilt: an over-long search query (DSH-014, one validator string) and “data export includes dispensaries” (DSH-047, already covered structurally by the export-policy test EXP-001).
  • The Shopping Agent is still flag-gated off in production builds. Everything above is proven, but the feature is reachable today only in development and on staging — which is why the live tier is the only place the end-to-end decode is observed.
  • The stored-result delete path has no UI. It is contracted and tested (DSH-076, DSH-176) but nothing in the app calls it; results expire instead.
  • 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 saved-shop or scan-storage rule gets its Tier A case in apps/mobile/convex/tests/dispensaries.featuretests.test.ts. A new form, hook or screen-state rule goes in the co-located *.featuretests.test.ts next to its helper (dispensaryForm, useShoppingAgent, shoppingAgentTransformer, useMenuProducts) or in apps/mobile/test/ui/features/Dispensaries.test.tsx / ShoppingAgent.test.tsx for rendering; API and pipeline rules go in apps/api/src/routes/shopping/__tests__/shopping-routes.featuretests.test.ts or packages/trigger/src/tasks/shopping-menu-scan.featuretests.test.ts. Reach for apps/mobile/test/integration/suites/dispensaries.integration.test.ts only when the claim is about the deployed backend — and put anything that spends money behind the paid-lane gate. Then add the row to the master matrix; see Mobile Feature Test Platform for naming, tags, and the tier decision.