Skip to main content

What this feature is

The stash is the shelf: what a person currently owns, in the units they own it in. Jars arrive three ways — added by hand from a strain page, added in bulk from an order’s line items, or merged in automatically when an order is confirmed. Each jar is a line keyed by strain, product type and unit, so a second eighth of the same flower tops up the existing line while a cartridge of the same strain stays separate. From the Stash tab a person adjusts quantities, marks a jar empty when they have finished it, archives one they no longer want to track, or permanently deletes a hand-entered mistake; the same actions work across a multi-select. Every one of those writes lands in a searchable history timeline, and the timeline plus the current shelf feed the Stats cards — consumption velocity, restock prediction, cost per gram, and the stash hero forecast.
Archiving is not consumption. Emptying and archiving make the identical state change — quantity 0, inactive — but they record different events, and only stash_marked_empty counts toward consumption analytics. Archiving a mis-entered 28 g line used to report 28 g smoked; it no longer does.

The behavior contract

Adding a jar

Adding a whole order’s worth at once

Changing quantities

Consuming

Emptying, archiving, and permanent deletion

Bulk actions from the selection bar

Reading the shelf

History and the timeline

Stats cards and forecasts

The order-to-stash merge contract

Propagation to the rest of the app

Ownership, export, and deletion

Scale and concurrency

What the live tier proves end-to-end

Tier C signs in as a real Clerk identity against the deployed staging Convex deployment and the production Hono API, then cleans up after itself.
  • An app-shaped add round-trips through the deployed backend, and the deployed validators accept the argument shapes all five write mutations are called with from the app (STASH-300, STASH-301).
  • The real scheduler runs: an add lands its stash_added history row, and syncs the strain into the collection as owned (STASH-302, STASH-303).
  • Strain enrichment resolves against the real Hono API for both the slug path and the name path (STASH-307).
  • Every layer reports the same grams for the same shelf — stash stats, lifetime stats and the restock forecast agree on the deployed data, including millilitres (STASH-308).
  • A large real archive pages to exhaustion without duplicates or gaps, and a server-side search finds a match that lies beyond the first page (STASH-309, STASH-310).
  • Cross-user isolation holds on the real deployment — one account is refused another’s stash rows (STASH-313).
  • The teardown contract works: the fixture reset returns a seeded test account to zero remaining rows, which is what makes the rest of the suite repeatable (STASH-312).

Deliberate gaps & conditions

Two rows are deliberately not built. Four Tier C rows are proven in another area’s suite and must not be rebuilt here: achievement metrics incrementing after an add (STASH-304 → the achievements suite), the order-confirm merge end to end (STASH-305 → the orders suite), a stash notebook accepting sourceStashItemIds (STASH-306 → the notebooks suite), and extension slugs matching the shelf (STASH-314 → the dispensaries suite). The order-merge idempotency row is likewise owned by the order post-save suite (STASH-133).
STASH-131 must never be inverted. It sits two tests away from STASH-132, shares a fixture, and reads almost identically — but STASH-132 was correctly inverted to assert that the merge does sync the collection, while STASH-131 pins the correct behavior that the merge does not emit per-item stash_added events. Inverting both ships a gram double-count. STASH-131 carries a do-not-invert fence in the file.
A 500-id bulk retire is a recorded ceiling, not an endorsement. STASH-052 exists to document that no array cap is enforced on that path today, unlike permanent deletion which caps at 50. If a cap is added, that case is where it gets asserted.

Where to extend

A new backend rule — a validator, an ownership check, an event write — gets a case in apps/mobile/convex/tests/stash.featuretests.test.ts for the write surface, or stash.featuretests.part2.test.ts for history, analytics, propagation and scale. A new hook argument or optimistic update gets a co-located *.featuretests.test.ts(x) next to its source; a new screen state gets a jest-expo suite under apps/mobile/test/ui/features/. Anything that only fails against a real deployment — a validator drift, the scheduler actually firing, pagination over a real archive — goes in apps/mobile/test/integration/suites/stash.integration.test.ts, scoped to its own test user and cleaned up with the fixture reset. See Mobile Feature Test Platform for tier selection, naming, and the Tier C isolation rules.