---
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.
