What this feature is
A user connects their Google account and lets the app find dispensary receipts in their mailbox. There are three ways in: a one-time historical import that scans an archive going back as far as they choose, a Pro-only Quick Sync that picks up only what has arrived since last time, and a broader search that escalates a Quick Sync into a full six-month keyword scan. A scan produces candidate emails grouped by shop; the user picks which shops and which emails to import. Importing parses each selected receipt into a review draft — nothing becomes an order until the user reviews it, except drafts that are fully resolved, which they can confirm in one tap. They can cancel a running import, resume a failed one, retry only the receipts that failed, and disconnect Gmail at any time, which revokes access and purges the scan data while leaving every order they already saved.This page is the contract, not a coverage report. Every line below is true of the code
on
main today and is enforced by the cited tests. Behaviors that are deliberately
unproven live in Deliberate gaps, never in the tables.The behavior contract
Connecting Gmail
Disconnecting and reconnecting
Choosing what to scan
Scanning the mailbox
Deciding what is a receipt
Reviewing and selecting candidates
Importing selected receipts
Turning a draft into an order
Confirming safe orders in one tap
Quick Sync
Cancelling, resuming, and retrying
The pipeline’s callback into the app
Quotas, privacy, and data lifecycle
The wizard
What the live tier proves end-to-end
The Gmail live lane runs against one real, shared mailbox with a per-user pipeline queue, so it is serialized and the disconnect cases run last.- The connection is real and correctly scoped, and the deployed backend can mint a Google token for it — the whole lane fails fast if not — GML-200, GML-201.
- A narrow scan completes end to end, its candidate ledger landing before the scan declares itself complete — GML-202, GML-203.
- Classification behaves on real mail: personal-domain candidates are decided without an AI call, splitting into genuine receipts and vetoed lifecycle mail, while generic candidates do go through AI — GML-205, GML-206.
- Import produces a draft, not an order. One selected receipt yields one draft; the user’s commit then creates one order with every line item, classified by job kind and firing its post-save effects — GML-207, GML-208, GML-209, GML-224, GML-225.
- Nothing imports twice. Re-scanning the same window imports nothing new, one-tap confirm commits only fully-resolved drafts and is idempotent, and a discarded draft leaves no order behind — GML-212, GML-210, GML-211, ORD-228.
- A non-receipt stays a non-receipt. A marketing or review-request email yields the empty outcome instead of a fabricated order — GML-213.
- Interruption is survivable. Concurrent scans collapse onto one run, cancelling mid-import stops draft creation, resuming after a failure produces a fresh generation, and retry re-imports only what failed — GML-218, GML-219, GML-220, GML-221.
- Quick Sync tells the truth about itself: a first sync uses the fallback window, and a refused sync is distinguishable from a started one, naming the job that blocked it — GML-216, GML-231.
Deliberate gaps & conditions
- Disconnect and reconnect are opt-in. GML-222 and GML-223 run only with
TIER_C_GMAIL_ALLOW_DISCONNECT=1, because disconnecting revokes the grant on the shared mailbox and reconnecting may need an interactive OAuth flow. They run last when enabled. Skipping is the deliberate default, not a config gap. - Two incremental-sync cases cannot run on this mailbox. GML-215 (the cursor advancing) and GML-217 (a second sync finding nothing) need a sync whose discovery is empty. Starting a sync has no switch to restrict it to the user’s own shops, so it always also searches the crowdsourced domain layer and discovery is never empty here. Un-skipping needs a product change and a pipeline deploy, not an environment variable — and the cursor must never be hand-reset to force it.
- The metered import ceiling cannot be observed live. Staging is structurally unmetered, so GML-228 has no observable form there; GML-023 is the only place that contract is asserted.
- The full two-thousand-candidate archive scan stays manual. GML-204 is a twenty-minute run on a shared pipeline queue; the chunking contract it would exercise is proven deterministically by GML-085.
- The purge cron is not run live. GML-229 needs an aged fixture plus a cron wait; the purge behavior itself is proven by GML-065 and GML-066.
- Nothing becomes an order without review — by design. The only exception is one-tap confirm, and it is restricted to drafts where every strain resolved (GML-037).
- The subject veto is a heuristic over the subject line only. It is deliberately positioned upstream of the parser rather than replacing it: the parser’s own monetary-evidence and cancelled-purchase gates remain the second and third layers, and are specified under Orders & Receipts.
Where to extend
A new Gmail behavior gets its test in the tier that can see it: backend logic, validators, ownership, and webhook handling inapps/mobile/convex/tests/gmail.featuretests.test.ts;
pipeline-task behavior in the co-located
packages/trigger/src/tasks/gmail/*.featuretests.test.ts beside the task, or in
receipt-subject.test.ts for what counts as a receipt subject; hook and wizard contracts in
the co-located *.featuretests.test.ts(x) or in
apps/mobile/test/ui/features/GmailWizard.test.tsx; real-mailbox journeys in
apps/mobile/test/integration/suites/gmail.integration.test.ts.
Anything added to the live suite must respect its ordering: the lane is serialized on one
shared mailbox, and the disconnect cases stay last.
Give the case a GML- id, add its matrix row, and follow the tier and naming rules on
Mobile Feature Test Platform.