Skip to main content

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

A gap listed here is a decision or a known condition, not coverage. Nothing below is evidence that the behavior works.
  • 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 in apps/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.