# Phase 33: Currency System - Context **Gathered:** 2026-04-13 **Status:** Ready for planning ## Phase Boundary Replace the placeholder currency symbol swap with a real market-aware pricing system. Users select their market (tied to currency), see market-specific UVP/MSRP prices, community "what I paid" data filtered by locale, and approximate conversions as a labeled fallback when local prices don't exist. Includes community price submissions, candidate research prices, and purchase date tracking. ## Implementation Decisions ### Data Model & Source Currency - **D-01:** Prices are market-specific, NOT simple exchange rate conversions. A €2,000 bike in Germany may be £2,200 in the UK and $3,100 in the US — these are independent market prices - **D-02:** Catalog items (globalItems) can have multiple market prices — UVP/MSRP stored per market/currency. Start with EU/DE prices as the primary market - **D-03:** Personal items store "what I paid" in the user's currency, auto-tagged with their market - **D-04:** Community price data is locale-tagged — German users see aggregate data from German submissions, UK users from UK submissions - **D-05:** Price submissions tied to collection ownership — you can only report a price for items you have in your collection (you actually bought it). Captured automatically when adding to collection - **D-06:** Candidate items in research threads can also have a "price I found it for" field — research-quality price data during comparison, valuable even before purchase - **D-07:** Both "what I paid" and "price I found it for" include a date field (when bought / when found) for temporal context and data aging ### Conversion Strategy - **D-08:** Exchange rates sourced from ECB via frankfurter.app — free, daily updates, no API key, covers EUR/USD/GBP/JPY/CAD/AUD and ~30 more - **D-09:** Server-side conversion — server fetches rates daily, caches them, returns converted prices in API responses. MCP/API consumers also get conversion - **D-10:** Conversion is a FALLBACK, not the default — when a local market price exists, show that. Only convert when no local price is available - **D-11:** Converted prices are always clearly labeled as approximate — never presented as real market prices ### User Experience & Display - **D-12:** Currency picker = market picker. Selecting EUR implies EU market, GBP implies UK market, USD implies US market. Simplifies settings — one choice drives both currency display and market data filtering - **D-13:** Auto-suggestion on first visit based on browser locale and IP geolocation. User can change anytime in settings - **D-14:** Converted prices use dual display format: `€2,000 (~£1,720)` — source price prominent, converted in parentheses. Makes it clear what's real vs. approximate - **D-15:** Global setting to auto-activate conversion (show converted prices by default) OR per-price toggle. User controls whether they see conversions automatically - **D-16:** Existing currency picker in settings page evolves to be the market/currency selector with the auto-suggestion behavior ### Catalog & Sharing Implications - **D-17:** Catalog detail page: user's market UVP shown prominently + community average for their market. Collapsible "Other markets" section shows prices from other regions - **D-18:** Shared setups (card level): show viewer's market MSRP if available, otherwise converted price - **D-19:** Shared setups (detail level): full breakdown — owner's actual price, MSRP per market, community averages, conversion info - **D-20:** Comparison tables (thread candidates): normalize all candidates to user's currency for apples-to-apples comparison. Converted prices marked with ~. Users can add their own researched price via "price I found it for" - **D-21:** Community price aggregation shows per-market stats: "Users in DE typically pay €1,600 (12 reports)" ### Claude's Discretion - Rate caching strategy (how long to cache, fallback when ECB is unreachable) - Schema design for market prices table (separate table vs. JSONB on globalItems) - Aggregation queries for community price stats (median vs. average, minimum report count threshold) - How to handle the transition from the current simple `priceCents` integer to the richer model ## Canonical References **Downstream agents MUST read these before planning or implementing.** No external specs — requirements fully captured in decisions above. ### Existing Implementation (to be replaced/extended) - `src/client/lib/formatters.ts` — Current `formatPrice()` with symbol-only swap, `Currency` type (lines 24-45) - `src/client/hooks/useCurrency.ts` — Current `useCurrency()` hook reading from settings (6 currencies) - `src/client/hooks/useFormatters.ts` — `useFormatters()` hook composing weight + price formatters - `src/client/routes/settings.tsx` — Currency pill picker UI (lines 254-280) - `src/db/schema.ts` — `priceCents: integer` on items, candidates, globalItems - `src/shared/schemas.ts` — Zod schemas with `priceCents` fields - `src/server/services/setup.service.ts` — Setup totals computed via SQL SUM on price_cents - `src/server/services/discovery.service.ts` — Discovery feed price queries - `src/client/components/ComparisonTable.tsx` — Candidate price comparison display - `src/client/lib/impactDeltas.ts` — Price delta calculations for setup impact preview ## Existing Code Insights ### Reusable Assets - `useFormatters()` hook: Central price formatting — all price displays go through this. Extend rather than replace - `useCurrency()` hook: Already reads currency preference from settings DB — extend to also imply market - Settings page currency picker: Existing UI to evolve into market/currency selector - `formatPrice()`: Needs to support dual display format and conversion annotations ### Established Patterns - Prices stored as integer cents (`priceCents`) throughout the codebase (items, candidates, globalItems, setup aggregates) - COALESCE merge for reference items — global base + personal overlay. Currency data needs to work with this pattern - SQL aggregates for setup totals — computed on read, not stored. Currency conversion needs to integrate with these queries - Settings stored via `useSetting()` hook / settings table — currency/market preference fits this pattern ### Integration Points - `src/db/schema.ts`: New market prices table, modify items/candidates for source currency + date fields - `src/server/services/`: New currency service for rate fetching, caching, conversion - `src/server/services/setup.service.ts`: Setup total queries need currency-aware aggregation - `src/server/services/discovery.service.ts`: Feed prices need market-awareness - `src/client/lib/formatters.ts`: Dual display format, conversion labeling - `src/client/hooks/useCurrency.ts`: Evolve to market-aware hook - `src/client/routes/settings.tsx`: Market/currency selector with auto-suggestion - `src/server/mcp/`: MCP tools need currency-aware price responses - `src/client/components/ComparisonTable.tsx`: Normalized currency display - `src/client/routes/global-items/$globalItemId.tsx`: Market prices + community data display ## Specific Ideas - The existing currency picker (pill toggle in settings) becomes the market selector — same UI pattern but with market implications - Auto-suggestion uses browser locale first, IP geolocation as fallback — suggest on first visit, respect manual override - Community price display: "Users in DE typically pay €1,600 (12 reports)" — locale-filtered aggregation - Candidate "price I found it for" is research-quality data — valuable even pre-purchase, should be treated as community data too - Purchase date on price submissions enables data aging — prices from years ago are less relevant - Primary market is EU/DE — start seeding UVP data for European manufacturers ## Deferred Ideas None — discussion stayed within phase scope. --- *Phase: 33-currency-system* *Context gathered: 2026-04-13*