deckhearth/.convoys/pricing-online-local.md
Randall Stillwell d0468015eb
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
feat(pricing): convoy for online prices and local stores
Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio),
valuation and sort-by-price, card detail pricing UI, and nearby store
discovery via home zip and OSM geo clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 22:47:46 -05:00

320 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: pricing-online-local
classification: feature
success_metric: Users see honest online prices (MTG/Pokémon/Lorcana) with sync timestamps, can sort collections by price with correct foil-aware totals, and authenticated users with a saved zip see nearby game stores — all on $0 APIs.
skip: []
status: implementers-complete
created: 2026-09-08
branch: convoy/pricing-online-local
design_direction:
version: 1
locked_at: 2026-09-08
extension: incremental
parent_system: liquid-glass
product_type: consumer TCG collection + pricing utility
pattern: authenticated app — card detail tabs + collection list + settings form
style: Liquid Glass — warm ember accent on glass; honesty-first pricing metadata
stack: nextjs-pages-router | react-18 | tailwind | css-vars
generator: skipped
generator_reason: Established Liquid Glass DS; incremental pricing surfaces only
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
---
# Convoy: Pricing — Online + Local Near Me (Free APIs Only)
**Branch:** `convoy/pricing-online-local`
**Constraint:** **$0 recurring** — no Google Places, no paid pricing tiers, no scraping.
**Goal:** Wire honest online pricing + gentle local store discovery.
---
## Why
Collection valuation, card detail, and purchase flows today show incomplete or wrong prices: Pokémon/Lorcana sync writes null prices, MTG totals use foil `market_price` for nonfoil rows, sort-by-price UI is inert, and Price History / Purchase tabs are stubs. Users need trustworthy online estimates and optional local store discovery without paid APIs.
## Scope
**In:** Scryfall (MTG), TCGdex (Pokémon), Foilio (Lorcana) ingest; foil-aware valuation; sort-by-price; pricing + history APIs; Purchase / Price History tabs; home zip + nearby stores (Nominatim + Overpass).
**Out:** Paid aggregators, scraping, in-store inventory/prices, buylist data, affiliate links.
## Roles invoked
1. Conductor ✓
2. IA Architect ✓
3. UI Designer ✓ (incremental — skipped ui-ux-pro-max)
4. UX Reviewer ✓
5. Security Auditor ✓ (planning phase)
6. Architect ✓ → **Human gate 1** ← you are here
7. Implementers (13 briefs, `/multitask` where safe)
8. Audit fan-out (reviewer + security + design-system + a11y)
## Todos
- [x] Planning roles (IA, UX, UI, Security, Architect)
- [ ] Human gate 1 — plan approval
- [ ] Phase 0 implementers (Briefs 1–7)
- [ ] Phase 1 implementers (Briefs 8–9)
- [ ] Phase 2 implementers (Briefs 10–13)
- [ ] Audit fan-out + merge
---
## Free-only stack (recommended)
### Online pricing by game
| Game | Source | Cost | Key? | Notes |
|------|--------|------|------|-------|
| **MTG** | [Scryfall](https://scryfall.com/docs/api) bulk + card API | Free | No | Already integrated. `prices.usd` / `usd_foil`. Polite rate limits (~10 req/s). |
| **Pokemon** | [TCGdex](https://tcgdex.dev) REST | Free | **No** | Catalog + embedded `pricing.tcgplayer` (USD) + `pricing.cardmarket` (EUR). Hourly/daily updates. |
| **Lorcana** | [Foilio feed](https://foilio.net/developers) `GET /api/prices/feed` | Free | **No** | JSON, CORS, cached 1h. EUR/USD as reported; match on `card_id` / name+set. |
| **Fallback links** | TCGPlayer + eBay search URLs | Free | No | Already on card detail — keep for buy-outbound. |
**Optional signup (still $0):**
- [TCG Price Lookup](https://tcgpricelookup.com/tcg-api) — **10,000 req/month free** with API key (multi-game). Good for on-demand card detail if TCGdex/Foilio miss a card.
**Do NOT use for this convoy:**
- tcgapi.dev beyond 100/day (too tight for bulk sync)
- Google Places (paid after credits)
- Scraping TCGPlayer / Cardmarket pages
### Local “near me” (free)
| Step | Source | Cost |
|------|--------|------|
| Zip → lat/lng | [Nominatim](https://nominatim.org/release-docs/develop/api/Search/) (OSM) | Free — **1 req/s max**, cache forever per zip |
| Stores in radius | [Overpass API](https://wiki.openstreetmap.org/wiki/Overpass_API) `shop=games` | Free — cache 7 days per `(lat,lng,radius)` |
Show: name, address, distance, `website` / `phone` tags, OpenStreetMap link. **No scraped in-store prices.**
---
## Decisions (planning round)
| # | Question | Decision |
| --- | --- | --- |
| A | Nearby stores auth | **Auth required**; zip from saved `home_zip` only — no anonymous geocode relay (Security sev-3) |
| B | Local stores placement | Subsection in **Purchase tab**, not a fourth tab |
| C | Zip input | Manual zip in Preferences; geolocation **nice-to-have** later |
| D | Online pricing visibility | Public (matches existing `/api/cards/[id]`) |
| E | Price History empty state | Show tab with honest empty copy until snapshots accumulate |
| F | Collection sort | **Server-side** `sort=price` on paginated APIs |
| G | Settings placement | Home zip under **Preferences** panel |
| H | Currency v1 | USD primary; respect `preferred_currency` where data exists |
---
## Design direction
Incremental extension of Liquid Glass — no new token files. Prices use `.gradient-text-gold` / `var(--accent-flame)`; staleness uses neutral tertiary pills (not error red); glass on section containers only (never per-card grid). See full typography/spacing/badge spec in planning transcript; repo tokens win on conflict.
**Surfaces:** Purchase tab price summary, Price History chart panel, staleness badge, collection sort control, settings zip field, nearby stores list.
---
## IA
### Affected routes
**Pages:** `/card/[id]` [modified], `/settings` [modified], `/my-cards` [modified], `/collection/[identifier]` [modified], `/collections` [impacted], `/profile` [impacted], `/cards` [impacted]
**API:** `GET /api/cards/[id]/pricing` [new], `GET /api/cards/[id]/price-history` [new], `GET /api/stores/nearby` [new], `GET|PUT /api/user/settings` [modified], `GET /api/cards/owned` [modified], bulk import jobs [impacted]
### User flow
```mermaid
flowchart LR
A[Settings: save zip] --> B[Card Detail]
B --> C[Purchase tab]
C --> D[Online NM price]
C --> E[Buy links]
C --> F[Stores near you]
G[My Collection] --> H[Sort by Price]
```
### Screen inventory
| Screen | Path | New/modified | Notes |
| --- | --- | --- | --- |
| Card Detail — Purchase | `/card/[id]` | Modified | DB prices, staleness badge, outbound links, local stores subsection |
| Card Detail — Price History | `/card/[id]` | Modified | Snapshot chart from sync-stored history |
| Settings — zip | `/settings` | Modified | Home ZIP in Preferences |
| My Collection | `/my-cards` | Modified | Sort by price + corrected estimated value |
| List Detail | `/collection/[identifier]` | Modified | Wire existing sort dropdown |
---
## UX
**Reuse:** `CardDetailView`, `useCardDetail`, `CollectionPageView`, `useCollectionView`, `StatCard`, `GlassSurface`, `Input`, `Button`, settings page patterns.
**Key patterns:** DB-first pricing (no optimistic price UI); staleness badge with text date (not color-only); server-side sort; zip saves with explicit Settings Save button; local stores below purchase links.
**A11y handoff:** Tab roles on card detail tabs; `aria-live` for pricing load; zip validation via `Input error`; 4.5:1 contrast on badge text; link purpose text on all outbound URLs.
**Anti-patterns:** No “live” pricing copy; no per-card glass; no client-only sort across pages; no auto GPS on load.
---
## Security (planning)
| Check | Status | Notes |
| --- | --- | --- |
| Auth boundary | ⚠️ | Pricing public OK; **nearby stores auth required** |
| Authorization (IDOR) | ⚠️ | Zip settings owner-scoped; no arbitrary zip query |
| Input / injection | ⚠️ | Overpass fixed-template only; validate zip + radius |
| Secrets exposure | ✅ | Core path keyless; server-only optional key |
| Dependencies | ⚠️ | Validate upstream JSON; native fetch preferred |
**Top pre-merge fixes:** Overpass QL injection (sev 4), geocode relay without auth (sev 3), bulk sync admin gate (sev 3).
Re-audit with file:line citations after implementer PR.
---
## Architecture
### Decomposition
| Brief # | Title | Phase | Depends on | Est. PR |
| --- | --- | --- | --- | --- |
| 1 | Pricing schema migration | 0 | — | ~120 LOC |
| 2 | Pricing foundation lib | 0 | 1 | ~280 LOC |
| 3 | MTG bulk sync + snapshots | 0 | 2 | ~180 LOC |
| 4 | TCGdex Pokémon pricing | 0 | 2 | ~320 LOC |
| 5 | Foilio Lorcana pricing | 0 | 2 | ~280 LOC |
| 6 | Foil-aware valuation APIs | 0 | 2 | ~150 LOC |
| 7 | Sort by price wiring | 0 | 2, 6 | ~220 LOC |
| 8 | Card pricing + history APIs | 1 | 2 | ~350 LOC |
| 9 | Card detail pricing UI | 1 | 8 | ~380 LOC |
| 10 | Geo clients + DB cache | 2 | 1 | ~300 LOC |
| 11 | Stores nearby API | 2 | 10 | ~200 LOC |
| 12 | Settings home zip | 2 | 1 | ~120 LOC |
| 13 | Local stores card detail UI | 2 | 9, 11, 12 | ~200 LOC |
**Brief files:** `.convoys/pricing-online-local/brief-*.md`
### Schema diff (migration `1783000000001_add-pricing-schema.js`)
- `cards.prices_updated_at TIMESTAMPTZ`
- `card_price_history` (daily snapshots, UNIQUE per card/day/source/type/currency)
- `users.home_zip VARCHAR(10)`
- `geo_zip_cache`, `geo_stores_cache` (DB-backed, 7d TTL on stores)
### slice_dependencies (multitask-ready)
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files: [migrations/1783000000001_add-pricing-schema.js, docs/SCHEMA_MAP.md]
- brief: 2
depends_on: [1]
files: [lib/pricing/card-price.js, lib/pricing/price-history.js, lib/pricing/collection-valuation.js, lib/pricing/outbound-links.js]
- brief: 3
depends_on: [2]
files: [lib/card-import/bulk-sync.js]
- brief: 4
depends_on: [2]
files: [lib/pricing/tcgdex-client.js, lib/card-import/pokemon-bulk.js]
- brief: 5
depends_on: [2]
files: [lib/pricing/foilio-client.js, lib/card-import/lorcana-bulk.js]
- brief: 6
depends_on: [2]
files: [pages/api/user/stats.js, pages/api/collections/[identifier].js]
- brief: 7
depends_on: [2, 6]
files: [pages/api/cards/owned.js, pages/api/collections/[identifier]/cards.js, lib/use-collection-view.js, components/CollectionPageView.js, pages/my-cards.js]
- brief: 8
depends_on: [2]
files: [lib/pricing/pricing-service.js, pages/api/cards/[id]/pricing.js, pages/api/cards/[id]/price-history.js]
- brief: 9
depends_on: [8]
files: [components/card-detail/CardDetailPurchaseTab.js, components/card-detail/CardDetailPriceHistoryTab.js, components/CardDetailView.js, lib/use-card-detail.js]
- brief: 10
depends_on: [1]
files: [lib/pricing/nominatim.js, lib/pricing/overpass.js, lib/pricing/geo-cache.js]
- brief: 11
depends_on: [10]
files: [pages/api/stores/nearby.js, lib/rate-limit.js]
- brief: 12
depends_on: [1]
files: [pages/settings.js, pages/api/user/settings.js]
- brief: 13
depends_on: [9, 11, 12]
files: [components/card-detail/CardDetailLocalStores.js, components/CardDetailView.js, lib/use-card-detail.js]
```
**Parallelization:** After Brief 1 → Brief 2 → **parallel** Briefs 3, 4, 5, 6 → Brief 7 → Brief 8 → Brief 9 → **parallel** Briefs 10, 12 → Brief 11 → Brief 13.
### Risks
| Risk | Mitigation |
| --- | --- |
| TCGdex ID mismatch | Match set_code + card_number; log unmatched |
| Foilio card_id drift | Fallback name+set match |
| Nominatim ToS | DB cache forever; 1 req/s; server-only |
| Overpass load | 7d cache; auth + 1/hour/user |
| Valuation on lists (no is_foil) | `COALESCE(current_price, market_price)` + tooltip |
---
## Phase checklist
### Phase 0
- [ ] Pokemon bulk → TCGdex prices
- [ ] Lorcana bulk → Foilio feed
- [ ] Fix MTG foil valuation bug
- [ ] Sort by Price wired
- [ ] Price snapshots on sync
### Phase 1
- [ ] `GET /api/cards/[id]/pricing`
- [ ] Purchase tab + staleness badge
- [ ] Price History tab
### Phase 2
- [ ] Settings home zip
- [ ] `GET /api/stores/nearby` (auth-gated)
- [ ] Local stores on card detail
---
## Env vars
```bash
TCGDEX_BASE_URL=https://api.tcgdex.net/v2/en
FOILIO_PRICES_FEED_URL=https://foilio.net/api/prices/feed?game=lorcana
NOMINATIM_USER_AGENT=DeckHearth/1.0 (contact@stillwell.cloud)
OVERPASS_URL=https://overpass-api.de/api/interpreter
TCG_PRICE_LOOKUP_API_KEY= # optional
PRICING_LOCAL_ENABLED=true
```
---
## Gentleness rules
| API | Rule |
|-----|------|
| Scryfall bulk | One download per sync; User-Agent set |
| TCGdex | Batch by set; sleep 100ms between pages |
| Foilio | One feed fetch/hour |
| Nominatim | Max 1 req/s; cache zip→coords forever |
| Overpass | Max 1 query/user/hour; 7d cache |
---
**Architecture complete. 13 briefs created. Estimated PRs: 13. Awaiting human gate 1 (plan approval) before implementers run.**