docs(33): add research, validation strategy, and UI design contract
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
261
.planning/phases/33-currency-system/33-RESEARCH.md
Normal file
261
.planning/phases/33-currency-system/33-RESEARCH.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Phase 33: Currency System - Research
|
||||
|
||||
**Researched:** 2026-04-13
|
||||
**Status:** Complete
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Phase 33 replaces the current symbol-only currency swap with a market-aware pricing system. The existing codebase stores all prices as integer cents in a single `priceCents` column (items, candidates, globalItems). The current `formatPrice()` simply swaps the currency symbol without conversion. This phase introduces market-specific pricing, exchange rate conversion via frankfurter.app (ECB data), community price data, and a dual-display format for converted prices.
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### Price Storage (Single Currency)
|
||||
- **items.priceCents** — integer, user's personal item price
|
||||
- **items.purchasePriceCents** — integer, what the user paid (already exists, separate from MSRP)
|
||||
- **globalItems.priceCents** — integer, catalog reference price (currently no currency/market tag)
|
||||
- **threadCandidates.priceCents** — integer, candidate price during research
|
||||
- All prices assumed to be in the user's selected currency (symbol swap only)
|
||||
|
||||
### Price Display Chain
|
||||
1. `useCurrency()` hook reads `currency` setting from DB via `useSetting("currency")`
|
||||
2. `useFormatters()` composes `price(cents)` using `formatPrice(cents, currency)`
|
||||
3. `formatPrice()` maps currency to symbol and formats cents → display string
|
||||
4. All components use `const { price } = useFormatters()` — centralized formatting
|
||||
|
||||
### Price Aggregation (SQL)
|
||||
- `setup.service.ts`: `SUM(COALESCE(global_items.price_cents, items.price_cents) * items.quantity)` for setup totals
|
||||
- `totals.service.ts`: Same COALESCE pattern for category and global totals
|
||||
- `discovery.service.ts`: Returns `priceCents` from globalItems without conversion
|
||||
- These SQL aggregates assume all prices are in the same currency — they'll need currency-awareness
|
||||
|
||||
### Settings Infrastructure
|
||||
- `settings` table: key-value pairs per user (`userId`, `key`, `value`)
|
||||
- Current `currency` setting: stored as string ("USD", "EUR", etc.)
|
||||
- `useSetting()` / `useUpdateSetting()` hooks for read/write
|
||||
- Settings page: pill toggle for currency selection (6 options)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 1. Database Schema Design
|
||||
|
||||
**New table: `market_prices`** (recommended over JSONB on globalItems)
|
||||
|
||||
```sql
|
||||
CREATE TABLE market_prices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
global_item_id INTEGER NOT NULL REFERENCES global_items(id) ON DELETE CASCADE,
|
||||
market TEXT NOT NULL, -- 'EU', 'UK', 'US', etc.
|
||||
currency TEXT NOT NULL, -- 'EUR', 'GBP', 'USD'
|
||||
price_cents INTEGER NOT NULL, -- MSRP/UVP in that market's currency
|
||||
source TEXT, -- 'manufacturer', 'retailer', 'community'
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
UNIQUE(global_item_id, market, currency)
|
||||
);
|
||||
```
|
||||
|
||||
Rationale: Separate table allows multiple market prices per item without schema changes to globalItems. The existing `globalItems.priceCents` becomes the "default/primary" price (EU market initially).
|
||||
|
||||
**New table: `community_prices`**
|
||||
|
||||
```sql
|
||||
CREATE TABLE community_prices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
global_item_id INTEGER NOT NULL REFERENCES global_items(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
market TEXT NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
price_cents INTEGER NOT NULL,
|
||||
price_date TIMESTAMP, -- when bought/found
|
||||
source_type TEXT NOT NULL, -- 'purchased' | 'researched'
|
||||
created_at TIMESTAMP DEFAULT NOW() NOT NULL,
|
||||
UNIQUE(global_item_id, user_id, source_type)
|
||||
);
|
||||
```
|
||||
|
||||
**Modify existing tables:**
|
||||
- `items`: Add `price_currency TEXT DEFAULT 'EUR'` (source currency for "what I paid")
|
||||
- `threadCandidates`: Add `price_currency TEXT DEFAULT 'EUR'`, `found_price_cents INTEGER`, `found_price_currency TEXT`, `found_price_date TIMESTAMP` (D-06, D-07)
|
||||
|
||||
### 2. Exchange Rate System
|
||||
|
||||
**frankfurter.app API:**
|
||||
- Base URL: `https://api.frankfurter.app`
|
||||
- Latest rates: `GET /latest?from=EUR&to=USD,GBP`
|
||||
- Response: `{ "base": "EUR", "date": "2026-04-13", "rates": { "USD": 1.08, "GBP": 0.86 } }`
|
||||
- Free, no API key, daily ECB data, supports 30+ currencies
|
||||
- Rate limit: reasonable for daily fetches (no documented limit for <100 req/day)
|
||||
|
||||
**New service: `currency.service.ts`**
|
||||
|
||||
```typescript
|
||||
interface ExchangeRates {
|
||||
base: string;
|
||||
date: string;
|
||||
rates: Record<string, number>;
|
||||
}
|
||||
|
||||
// Cache in-memory with 24h TTL, fallback to last known rates on fetch failure
|
||||
let cachedRates: ExchangeRates | null = null;
|
||||
let cacheExpiry: number = 0;
|
||||
|
||||
export async function getExchangeRates(): Promise<ExchangeRates> { ... }
|
||||
export function convertPrice(cents: number, from: string, to: string, rates: ExchangeRates): number { ... }
|
||||
```
|
||||
|
||||
**Caching strategy:**
|
||||
- In-memory cache with 24h TTL (ECB updates daily ~16:00 CET)
|
||||
- On fetch failure: use cached rates (stale but functional)
|
||||
- Optional: persist last-known rates to DB settings for cold-start resilience
|
||||
- Server-side conversion (D-09) — no client-side rate fetching
|
||||
|
||||
### 3. Market Mapping
|
||||
|
||||
Currency → Market mapping (D-12):
|
||||
```typescript
|
||||
const CURRENCY_MARKET_MAP: Record<string, string> = {
|
||||
EUR: 'EU', USD: 'US', GBP: 'UK',
|
||||
JPY: 'JP', CAD: 'CA', AUD: 'AU'
|
||||
};
|
||||
```
|
||||
|
||||
The `currency` setting in the settings table implies market. No separate market setting needed.
|
||||
|
||||
### 4. API Changes
|
||||
|
||||
**New endpoints:**
|
||||
- `GET /api/exchange-rates` — returns current rates (public, cached)
|
||||
- `GET /api/global-items/:id/prices` — returns market prices + community data for a catalog item
|
||||
|
||||
**Modified endpoints:**
|
||||
- All endpoints returning prices should accept optional `?currency=EUR` query param
|
||||
- Server converts prices when currency differs from stored currency
|
||||
- Converted prices include `{ priceCents, currency, converted: boolean, sourceCurrency?, sourcePrice? }`
|
||||
|
||||
**Community price submission:**
|
||||
- `POST /api/global-items/:id/prices` — submit "what I paid" (requires auth + item in collection)
|
||||
- Candidate "found price" tracked via existing candidate update endpoint with new fields
|
||||
|
||||
### 5. Client-Side Changes
|
||||
|
||||
**`formatPrice()` evolution:**
|
||||
```typescript
|
||||
// Current: formatPrice(cents, currency) → "$12.00"
|
||||
// New: formatPrice(cents, currency, options?) → "$12.00" or "€12.00 (~$13.00)"
|
||||
interface FormatPriceOptions {
|
||||
converted?: boolean;
|
||||
sourceCurrency?: string;
|
||||
sourcePrice?: number;
|
||||
showDual?: boolean; // dual display format (D-14)
|
||||
}
|
||||
```
|
||||
|
||||
**`useCurrency()` evolution:**
|
||||
```typescript
|
||||
// Current: returns Currency string
|
||||
// New: returns { currency, market, showConversions }
|
||||
interface CurrencyContext {
|
||||
currency: Currency;
|
||||
market: string;
|
||||
showConversions: boolean; // D-15: auto-show conversions toggle
|
||||
}
|
||||
```
|
||||
|
||||
**Settings page:**
|
||||
- Currency picker becomes "Market & Currency" selector
|
||||
- Auto-suggestion on first visit (D-13): `navigator.language` → locale → suggested currency
|
||||
- Toggle for "Show price conversions automatically" (D-15)
|
||||
|
||||
### 6. Transition Strategy
|
||||
|
||||
The existing `priceCents` on globalItems becomes the EU/default market price. No data migration needed for personal items since they already store "what I paid" in the user's chosen currency. The new `price_currency` column defaults to 'EUR' matching the current assumption.
|
||||
|
||||
**Backward compatibility:**
|
||||
- All existing `priceCents` fields remain — they're the "primary" price
|
||||
- New market_prices table adds additional market prices
|
||||
- APIs that currently return `priceCents` continue to do so, with optional conversion
|
||||
- `useFormatters()` hook signature stays the same for basic usage
|
||||
|
||||
### 7. Community Price Aggregation
|
||||
|
||||
Aggregation queries for community stats (D-21):
|
||||
- Use median (more robust against outliers than average)
|
||||
- Minimum 3 reports before showing aggregate
|
||||
- Filter by market for locale-specific stats
|
||||
- Include report count for transparency
|
||||
|
||||
```sql
|
||||
SELECT market, currency,
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price_cents) as median_price,
|
||||
COUNT(*) as report_count
|
||||
FROM community_prices
|
||||
WHERE global_item_id = $1 AND market = $2
|
||||
GROUP BY market, currency
|
||||
HAVING COUNT(*) >= 3;
|
||||
```
|
||||
|
||||
### 8. MCP Tool Updates
|
||||
|
||||
Existing MCP tools that return prices need currency context:
|
||||
- `list_items`, `get_item`: Include `priceCurrency` in response
|
||||
- `create_item`, `update_item`: Accept optional `priceCurrency` param
|
||||
- `get_setup`: Include currency info with totals
|
||||
- New tool: `get_exchange_rates` — returns current conversion rates
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Low Risk
|
||||
- frankfurter.app downtime — mitigated by caching with stale-serve fallback
|
||||
- Schema migration — additive only (new tables + new nullable columns)
|
||||
- `formatPrice()` changes — backward compatible with optional params
|
||||
|
||||
### Medium Risk
|
||||
- SQL aggregate complexity — setup/totals queries need to handle mixed currencies when summing prices from items with different source currencies
|
||||
- Community price data quality — solved by tying submissions to collection ownership (D-05) and minimum report threshold
|
||||
|
||||
### High Risk
|
||||
- **Mixed-currency aggregation in setup totals** — when items in a setup have prices in different currencies, SUM is meaningless without conversion. Must convert all to user's currency before aggregating. This adds a server-side conversion step to every setup total query.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Unit Tests
|
||||
- `currency.service.test.ts`: Rate fetching, caching, conversion math
|
||||
- `formatPrice()`: Dual display format, conversion labels
|
||||
- Market mapping: currency → market resolution
|
||||
|
||||
### Integration Tests
|
||||
- Market prices CRUD operations
|
||||
- Community price submission with ownership validation
|
||||
- Setup totals with mixed-currency items
|
||||
- Exchange rate caching behavior
|
||||
|
||||
### E2E Tests
|
||||
- Settings page: market/currency selection
|
||||
- Global item detail: market prices display
|
||||
- Comparison table: normalized currency display
|
||||
- Setup totals: converted price display
|
||||
|
||||
## Implementation Order (Recommended Waves)
|
||||
|
||||
**Wave 1 — Foundation:**
|
||||
1. Schema changes (market_prices, community_prices tables, column additions)
|
||||
2. Currency service (rate fetching, caching, conversion)
|
||||
3. Database push
|
||||
|
||||
**Wave 2 — Server Integration:**
|
||||
4. Market prices API endpoints
|
||||
5. Price conversion in existing endpoints
|
||||
6. Setup/totals query updates for currency-awareness
|
||||
|
||||
**Wave 3 — Client & Display:**
|
||||
7. Formatter evolution (dual display, conversion labels)
|
||||
8. Settings page market/currency selector
|
||||
9. Global item detail with market prices
|
||||
10. Comparison table currency normalization
|
||||
11. MCP tool updates
|
||||
|
||||
---
|
||||
|
||||
## RESEARCH COMPLETE
|
||||
|
||||
*Phase: 33-currency-system*
|
||||
*Research completed: 2026-04-13*
|
||||
251
.planning/phases/33-currency-system/33-UI-SPEC.md
Normal file
251
.planning/phases/33-currency-system/33-UI-SPEC.md
Normal file
@@ -0,0 +1,251 @@
|
||||
---
|
||||
phase: 33
|
||||
slug: currency-system
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-04-13
|
||||
---
|
||||
|
||||
# Phase 33 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for the Currency System phase. Covers market/currency selector, dual price display, converted price labels, community price aggregation display, and candidate research price fields.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Tool | none |
|
||||
| Preset | not applicable |
|
||||
| Component library | none (custom Tailwind components) |
|
||||
| Icon library | Lucide via `LucideIcon` from `lib/iconData` |
|
||||
| Font | System font stack (Tailwind default) |
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (must be multiples of 4):
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| xs | 4px | Icon gaps, inline padding |
|
||||
| sm | 8px | Compact element spacing, pill toggle gaps |
|
||||
| md | 16px | Default element spacing, card padding |
|
||||
| lg | 24px | Section padding within cards |
|
||||
| xl | 32px | Layout gaps between sections |
|
||||
| 2xl | 48px | Major section breaks |
|
||||
| 3xl | 64px | Page-level spacing |
|
||||
|
||||
Exceptions: none
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
| Role | Size | Weight | Line Height |
|
||||
|------|------|--------|-------------|
|
||||
| Body | 14px (text-sm) | 400 (normal) | 1.5 |
|
||||
| Label | 12px (text-xs) | 500 (medium) | 1.5 |
|
||||
| Heading | 20px (text-xl) | 600 (semibold) | 1.4 |
|
||||
| Display | 14px (text-sm) | 600 (semibold) | 1.5 |
|
||||
|
||||
Matches existing app typography (settings page, detail pages).
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
| Role | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| Dominant (60%) | #ffffff | Page background, card surfaces |
|
||||
| Secondary (30%) | #f9fafb / #f3f4f6 | Gray-50/100 — pill backgrounds, inactive states, card borders |
|
||||
| Accent (10%) | #3b82f6 | Blue-500 — conversion indicator icon, "best price" highlight |
|
||||
| Destructive | #ef4444 | Red-500 — not used in this phase |
|
||||
|
||||
Accent reserved for: conversion indicator dots, "best price" cell highlight (green-50 for price, blue-50 for weight — existing pattern from ComparisonTable)
|
||||
|
||||
### Phase-Specific Colors
|
||||
|
||||
| Element | Color | Tailwind Class |
|
||||
|---------|-------|----------------|
|
||||
| Converted price text | gray-400 | `text-gray-400` |
|
||||
| Conversion tilde prefix | gray-400 | `text-gray-400` |
|
||||
| Market price label | gray-500 | `text-gray-500` |
|
||||
| Community price aggregate | gray-700 | `text-gray-700` |
|
||||
| Community report count | gray-400 | `text-gray-400` |
|
||||
| Auto-suggestion banner background | blue-50 | `bg-blue-50` |
|
||||
| Auto-suggestion banner text | blue-700 | `text-blue-700` |
|
||||
|
||||
---
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### 1. Market/Currency Selector (Settings Page)
|
||||
|
||||
Evolves the existing currency pill toggle. Same visual pattern, updated copy.
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Market & Currency │
|
||||
│ Sets your market region and currency for price display │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────┐ │
|
||||
│ │ [$ USD] [€ EUR] [£ GBP] [¥ JPY] [CA$ CAD] [A$ AUD] │ │
|
||||
│ └────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ── separator ── │
|
||||
│ │
|
||||
│ Show Converted Prices [toggle]│
|
||||
│ Display approximate conversions when local price │
|
||||
│ is not available │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- Pill toggle: same `bg-gray-100 rounded-full` container, same button styles as existing
|
||||
- Selected pill: `bg-white text-gray-700 shadow-sm font-medium`
|
||||
- Unselected pill: `text-gray-400 hover:text-gray-600`
|
||||
- New toggle for "Show Converted Prices": standard toggle switch, `bg-gray-200` off / `bg-blue-500` on
|
||||
- Section heading: `text-sm font-medium text-gray-900`
|
||||
- Description: `text-xs text-gray-500 mt-0.5`
|
||||
|
||||
### 2. Auto-Suggestion Banner (First Visit)
|
||||
|
||||
Shown once when no currency preference is set. Appears above the settings card.
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 🌍 Based on your location, we suggest EUR (€) [Use €] │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- Container: `bg-blue-50 border border-blue-100 rounded-xl px-4 py-3`
|
||||
- Text: `text-sm text-blue-700`
|
||||
- Button: `text-sm font-medium text-blue-700 hover:text-blue-800 underline`
|
||||
- Globe icon: Lucide `globe` icon, 16px, `text-blue-500`
|
||||
- Dismissible: clicking "Use €" sets the setting and hides the banner
|
||||
|
||||
### 3. Dual Price Display Format (D-14)
|
||||
|
||||
When a price is converted from another currency, display both.
|
||||
|
||||
**Inline format:** `€2,000 (~$2,160)`
|
||||
|
||||
**Specs:**
|
||||
- Source price: `text-sm font-medium text-gray-900` (existing style)
|
||||
- Converted price: `text-xs text-gray-400 ml-1`
|
||||
- Tilde prefix: included in converted text as literal `~`
|
||||
- No line break between source and converted — inline on same line
|
||||
- If no conversion needed (local price exists): show only the local price, no parenthetical
|
||||
|
||||
### 4. Global Item Detail — Market Prices Section (D-17)
|
||||
|
||||
New section on the catalog item detail page, below existing specs.
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Price │
|
||||
│ │
|
||||
│ €2,199.00 MSRP (EU) │
|
||||
│ │
|
||||
│ Community (DE): €1,680 median (14 reports) │
|
||||
│ │
|
||||
│ ▸ Other Markets │
|
||||
│ $2,499.00 MSRP (US) │
|
||||
│ £1,999.00 MSRP (UK) │
|
||||
│ Community (US): $2,100 median (8 reports) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- Section heading: `text-sm font-medium text-gray-900`
|
||||
- Primary market price: `text-lg font-semibold text-gray-900`
|
||||
- Market label: `text-xs text-gray-500 ml-2`
|
||||
- Community line: `text-sm text-gray-700`
|
||||
- Report count: `text-xs text-gray-400` in parentheses
|
||||
- "Other Markets" collapsible: `text-sm text-gray-500 cursor-pointer hover:text-gray-700`
|
||||
- Chevron: Lucide `chevron-right` (rotates to `chevron-down` when expanded), 14px
|
||||
- Collapsed by default
|
||||
- Inner market rows: same styling, indented with `pl-4`
|
||||
|
||||
### 5. Comparison Table — Currency Normalization (D-20)
|
||||
|
||||
Extends existing ComparisonTable component.
|
||||
|
||||
**Existing behavior preserved.** Additional specs:
|
||||
- When candidate price is in a different currency than user's preference, show dual format in the price cell
|
||||
- Converted prices show `~` prefix: `~$2,160` in `text-gray-400`
|
||||
- Best-price highlighting (existing `bg-green-50`) still applies after conversion
|
||||
- New "Found Price" row in comparison table for candidate research prices (D-06)
|
||||
|
||||
### 6. Candidate "Price I Found" Field (D-06, D-07)
|
||||
|
||||
New fields in the candidate edit form.
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Price I Found │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐│
|
||||
│ │ $ _____.__ │ │ USD ▾ │ │ 2026-04-13 ││
|
||||
│ └──────────────┘ └──────────────┘ └──────────────────┘│
|
||||
│ Research price — when you found it at this price │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Specs:**
|
||||
- Field row: three inputs inline (`flex gap-2`)
|
||||
- Price input: standard number input matching existing `priceCents` field style
|
||||
- Currency select: small dropdown matching existing form select style, `text-xs`
|
||||
- Date input: standard date input, `text-xs`
|
||||
- Helper text: `text-xs text-gray-400 mt-1`
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
| Element | Copy |
|
||||
|---------|------|
|
||||
| Market selector heading | "Market & Currency" |
|
||||
| Market selector description | "Sets your market region and currency for price display" |
|
||||
| Conversion toggle heading | "Show Converted Prices" |
|
||||
| Conversion toggle description | "Display approximate conversions when local price is not available" |
|
||||
| Auto-suggestion text | "Based on your location, we suggest {CURRENCY} ({SYMBOL})" |
|
||||
| Auto-suggestion CTA | "Use {SYMBOL}" |
|
||||
| Converted price label | "~{SYMBOL}{amount}" (inline, no separate label) |
|
||||
| Community price line | "Community ({MARKET}): {SYMBOL}{median} median ({N} reports)" |
|
||||
| Other markets toggle | "Other Markets" |
|
||||
| Found price label | "Price I Found" |
|
||||
| Found price helper | "Research price — when you found it at this price" |
|
||||
| No market price fallback | "No local price — showing converted estimate" |
|
||||
| Price section heading | "Price" |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| No registries | N/A | N/A |
|
||||
|
||||
This phase uses only custom Tailwind components matching existing codebase patterns.
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [ ] Dimension 1 Copywriting: PASS
|
||||
- [ ] Dimension 2 Visuals: PASS
|
||||
- [ ] Dimension 3 Color: PASS
|
||||
- [ ] Dimension 4 Typography: PASS
|
||||
- [ ] Dimension 5 Spacing: PASS
|
||||
- [ ] Dimension 6 Registry Safety: PASS
|
||||
|
||||
**Approval:** pending
|
||||
82
.planning/phases/33-currency-system/33-VALIDATION.md
Normal file
82
.planning/phases/33-currency-system/33-VALIDATION.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
phase: 33
|
||||
slug: currency-system
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-04-13
|
||||
---
|
||||
|
||||
# Phase 33 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Bun test runner + Playwright |
|
||||
| **Config file** | `bunfig.toml` / `playwright.config.ts` |
|
||||
| **Quick run command** | `bun test` |
|
||||
| **Full suite command** | `bun test && bun run test:e2e` |
|
||||
| **Estimated runtime** | ~15 seconds (unit) + ~60 seconds (e2e) |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `bun test`
|
||||
- **After every plan wave:** Run `bun test && bun run test:e2e`
|
||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 15 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 33-01-01 | 01 | 1 | D-01, D-02 | — | N/A | unit | `bun test tests/services/currency.service.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 33-01-02 | 01 | 1 | D-08, D-09 | — | N/A | unit | `bun test tests/services/currency.service.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 33-02-01 | 02 | 2 | D-01 | — | N/A | integration | `bun test tests/services/market-price.service.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 33-02-02 | 02 | 2 | D-04, D-05 | — | Ownership validation | integration | `bun test tests/services/community-price.service.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 33-03-01 | 03 | 3 | D-12, D-14 | — | N/A | unit | `bun test tests/lib/formatters.test.ts` | ❌ W0 | ⬜ pending |
|
||||
| 33-03-02 | 03 | 3 | D-16 | — | N/A | e2e | `bun run test:e2e --grep "currency"` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `tests/services/currency.service.test.ts` — stubs for rate fetching, caching, conversion
|
||||
- [ ] `tests/services/market-price.service.test.ts` — stubs for market price CRUD
|
||||
- [ ] `tests/services/community-price.service.test.ts` — stubs for community price submission + ownership validation
|
||||
- [ ] `tests/lib/formatters.test.ts` — stubs for dual display format, conversion labels
|
||||
|
||||
*Existing test infrastructure (Bun test runner, createTestDb helper) covers framework needs.*
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Auto-suggestion via browser locale | D-13 | Requires browser environment with locale | Open app in incognito, verify suggestion matches browser locale |
|
||||
| Dual display format readability | D-14 | Visual check | Verify converted prices show `€2,000 (~$2,160)` format |
|
||||
| Community price aggregation display | D-21 | Requires seeded community data | Seed 3+ price reports, verify "Users in DE typically pay..." display |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 15s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
Reference in New Issue
Block a user