--- convoy: scanner-mobile-checkout brief_number: 2 depends_on: [] recommended_model: composer-2.5-fast model_tier: fast files: - lib/use-scanner-queue.js - lib/scanner-session.js - test/lib/scanner-session.test.js - test/lib/use-scanner-queue.test.js cross_brief_commitments: - brief: 4 description: | Export `clearScannerCartStorage()` from `scanner-session.js`. Brief 4 calls it when the user confirms "Leave" on the back-guard modal (D1/D7). Queue hook exposes `hydrateFromStorage` on mount — Brief 4 does not reimplement hydration. --- # Brief 2: Cart model — stop auto-route + sessionStorage ## Goal (1 sentence) Reverse rebuild D3 auto-route: identifies enqueue locally only, auto-select new rows, and persist cart + selection in `sessionStorage` (D7). ## Files in scope (do not edit anything else) - `lib/use-scanner-queue.js` - `lib/scanner-session.js` - `test/lib/scanner-session.test.js` - `test/lib/use-scanner-queue.test.js` (new) ## Conventions to follow - **No new API routes.** Commit paths stay `addScannedCardToOwned` / `addScannedCardToCollection` from `lib/scanner-route-api.js` (already verified). - **Do not use** `SCANNER_SESSION_STORAGE_KEY` / `localStorage` for cart rows (UX anti-pattern). Add a separate key in `scanner-session.js`: ```js export const SCANNER_CART_STORAGE_KEY = 'deckhearth:scanner-cart'; ``` - Serialize `selectedCards` as `number[]` (Set is not JSON-safe). - Keep `mergeScannedCardEntry` unchanged — cart entry shape already includes `processed: false` and `confidence` from `buildScannedCardPayload`. - `sessionDestination` param may remain on the hook signature for backward compat but **must not** trigger network I/O in `handleCardScanned`. ## Implementation shape (verified against `use-scanner-queue.js`) **Remove auto-route** — delete the block after `setScannedCards(nextQueue)`: ```js // DELETE lines ~148-168 (sessionDestination guard + routeScannedCardToDestination) ``` **Replace `handleCardScanned` with enqueue-only:** ```js const handleCardScanned = (cardData) => { setAutoRouteError(null); const { cardEntry, scannedCards: nextQueue } = mergeScannedCardEntry( scannedCards, cardData, scanDefaults ); setScannedCards(nextQueue); setSelectedCards((prev) => new Set([...prev, cardEntry.id])); persistCart(nextQueue, new Set([...selectedCards, cardEntry.id])); }; ``` Add `loadScannerCart()` / `saveScannerCart({ scannedCards, selectedCardIds })` / `clearScannerCartStorage()` in `scanner-session.js`. Hydrate on mount in `useScannerQueue` via `useState` initializer + `useEffect` debounced save on `[scannedCards, selectedCards]` changes. **Checkout helpers** (used by Brief 4 footer CTAs): ```js const commitSelectedToOwned = async () => handleBulkAction('owned'); const commitSelectedToCollection = async (collectionId) => handleBulkAction('collection', collectionId); ``` Export these plus `unprocessedCount` derived helper: `scannedCards.filter(c => !c.processed).length`. After successful `handleBulkAction`, remove committed rows from `scannedCards` entirely (not `processed: true`) per D1/D6 — cart holds only uncommitted items. ## Acceptance criteria - [ ] `handleCardScanned` performs zero `fetch` calls (no `routeScannedCardToDestination`) - [ ] New enqueue auto-adds card `id` to `selectedCards` - [ ] Cart + selection survive `sessionStorage` round-trip within a tab (`test/lib/scanner-session.test.js`) - [ ] `test/lib/use-scanner-queue.test.js`: mock `scanner-route-api` and assert `handleCardScanned` never calls route helpers; assert `handleBulkAction('owned')` calls `addScannedCardToOwned` once per selected card - [ ] `clearScannerCartStorage()` clears persisted state - [ ] Existing `test/lib/scanner-session.test.js` cases still pass - [ ] No scope expansion ## Rationale (≤3 sentences) Cart semantics are pure client state and can land before any UI work. Separating sessionStorage from the existing `localStorage` destination prefs avoids D7/localStorage anti-pattern. Removing committed rows instead of flipping `processed` simplifies the checkout sheet list.