6 plans across 3 waves covering market-aware pricing, exchange rates, community price data, and currency-normalized display. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
---
|
|
phase: 33-currency-system
|
|
plan: 06
|
|
type: execute
|
|
wave: 3
|
|
depends_on: [03, 04, 05]
|
|
files_modified:
|
|
- src/client/routes/global-items/$globalItemId.tsx
|
|
- src/client/components/ComparisonTable.tsx
|
|
- src/client/components/SetupCard.tsx
|
|
- src/client/hooks/useGlobalItems.ts
|
|
- src/server/mcp/tools/index.ts
|
|
autonomous: true
|
|
requirements: [D-17, D-18, D-19, D-20, D-21]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Global item detail page shows market prices section with user's market MSRP prominent"
|
|
- "Global item detail page shows community price stats for user's market"
|
|
- "Global item detail has collapsible 'Other Markets' section"
|
|
- "Comparison table normalizes candidate prices to user's currency"
|
|
- "Converted prices in comparison table marked with ~ prefix"
|
|
- "SetupCard displays prices with correct currency symbol"
|
|
- "MCP tools include currency context in price responses"
|
|
artifacts:
|
|
- path: "src/client/routes/global-items/$globalItemId.tsx"
|
|
provides: "Market prices section on catalog detail page"
|
|
contains: "marketPrices"
|
|
- path: "src/client/components/ComparisonTable.tsx"
|
|
provides: "Currency-normalized comparison with conversion labels"
|
|
contains: "convertClientPrice"
|
|
key_links:
|
|
- from: "src/client/routes/global-items/$globalItemId.tsx"
|
|
to: "/api/market-prices/global-items/:id/prices"
|
|
via: "React Query fetch for market prices"
|
|
pattern: "market-prices"
|
|
- from: "src/client/components/ComparisonTable.tsx"
|
|
to: "src/client/hooks/useExchangeRates.ts"
|
|
via: "useExchangeRates + convertClientPrice"
|
|
pattern: "useExchangeRates|convertClientPrice"
|
|
---
|
|
|
|
<objective>
|
|
Integrate market-aware pricing into catalog detail pages, comparison tables, setup cards, and MCP tools.
|
|
|
|
Purpose: User-facing display of market prices, community data, and currency-normalized comparisons — the visible payoff of the currency system.
|
|
Output: Updated global item detail with market prices, comparison table with conversion, setup card with currency, MCP tools with currency context.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/33-currency-system/33-CONTEXT.md
|
|
@.planning/phases/33-currency-system/33-UI-SPEC.md
|
|
@.planning/phases/33-currency-system/33-05-SUMMARY.md
|
|
|
|
<interfaces>
|
|
From src/client/hooks/useExchangeRates.ts (Plan 05):
|
|
```typescript
|
|
export function useExchangeRates(): UseQueryResult<ExchangeRates>;
|
|
export function convertClientPrice(cents: number, from: string, to: string, rates: Record<string, number>): number;
|
|
```
|
|
|
|
From src/client/hooks/useCurrency.ts (Plan 05):
|
|
```typescript
|
|
export interface CurrencyContext {
|
|
currency: Currency;
|
|
market: string;
|
|
showConversions: boolean;
|
|
}
|
|
export function useCurrency(): CurrencyContext;
|
|
```
|
|
|
|
From src/client/lib/formatters.ts (Plan 05):
|
|
```typescript
|
|
export function formatDualPrice(options: DualPriceOptions): { source: string; converted: string };
|
|
```
|
|
|
|
From src/client/hooks/useGlobalItems.ts (existing):
|
|
```typescript
|
|
export function useGlobalItem(id: number): UseQueryResult<GlobalItem>;
|
|
```
|
|
|
|
From src/client/components/ComparisonTable.tsx (existing):
|
|
```typescript
|
|
interface ComparisonTableProps {
|
|
candidates: CandidateWithCategory[];
|
|
resolvedCandidateId: number | null;
|
|
deltas?: Record<number, CandidateDelta>;
|
|
}
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add market prices section to global item detail page</name>
|
|
<files>src/client/routes/global-items/$globalItemId.tsx, src/client/hooks/useGlobalItems.ts</files>
|
|
<read_first>src/client/routes/global-items/$globalItemId.tsx, src/client/hooks/useGlobalItems.ts, src/client/lib/api.ts</read_first>
|
|
<action>
|
|
Per D-17: Add a "Price" section to the global item detail page.
|
|
|
|
First, add a new hook in `src/client/hooks/useGlobalItems.ts`:
|
|
```typescript
|
|
export function useGlobalItemPrices(globalItemId: number) {
|
|
return useQuery({
|
|
queryKey: ["global-item-prices", globalItemId],
|
|
queryFn: () => apiGet<{
|
|
marketPrices: Array<{ market: string; currency: string; priceCents: number; source: string | null }>;
|
|
}>(`/api/market-prices/global-items/${globalItemId}/prices`),
|
|
enabled: globalItemId > 0,
|
|
});
|
|
}
|
|
|
|
export function useGlobalItemCommunityStats(globalItemId: number) {
|
|
return useQuery({
|
|
queryKey: ["global-item-community-stats", globalItemId],
|
|
queryFn: () => apiGet<Array<{ market: string; currency: string; medianPrice: number; reportCount: number }>>(`/api/community-prices/${globalItemId}`),
|
|
enabled: globalItemId > 0,
|
|
});
|
|
}
|
|
```
|
|
|
|
Then update `src/client/routes/global-items/$globalItemId.tsx`:
|
|
|
|
Add a `MarketPricesSection` component within the detail page:
|
|
- Uses `useCurrency()` to get `{ currency, market }`
|
|
- Uses `useGlobalItemPrices(id)` and `useGlobalItemCommunityStats(id)`
|
|
- Uses `useExchangeRates()` for conversion when needed
|
|
|
|
Layout per UI-SPEC section 4:
|
|
1. Section heading: "Price" (`text-sm font-medium text-gray-900`)
|
|
2. User's market MSRP shown prominently: find marketPrice where market matches user's market
|
|
- If found: `text-lg font-semibold text-gray-900` + "MSRP ({MARKET})" label in `text-xs text-gray-500 ml-2`
|
|
- If not found but other markets exist: show converted price from nearest market with dual display format using `formatDualPrice`
|
|
3. Community stats for user's market: filter communityStats where market matches
|
|
- Per D-21: "Community ({MARKET}): {SYMBOL}{median} median ({N} reports)" in `text-sm text-gray-700` with report count in `text-xs text-gray-400`
|
|
- Only show if reportCount >= 3 (server already filters, but handle empty gracefully)
|
|
4. Collapsible "Other Markets" section:
|
|
- Use useState for expanded state, default collapsed
|
|
- Toggle: "Other Markets" text with Lucide `chevron-right`/`chevron-down` icon (14px)
|
|
- Style: `text-sm text-gray-500 cursor-pointer hover:text-gray-700`
|
|
- Inner rows: same price/label styling, indented with `pl-4`
|
|
- Show all market prices except user's market
|
|
- Show community stats for other markets
|
|
|
|
Place this section below the existing weight/price display area in the detail page.
|
|
</action>
|
|
<acceptance_criteria>
|
|
- src/client/hooks/useGlobalItems.ts exports useGlobalItemPrices and useGlobalItemCommunityStats
|
|
- src/client/routes/global-items/$globalItemId.tsx contains a MarketPricesSection component
|
|
- User's market MSRP shown prominently with market label
|
|
- Community stats displayed as "Community ({MARKET}): {median} median ({N} reports)"
|
|
- "Other Markets" section is collapsible and collapsed by default
|
|
- `bun run build` succeeds
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run build && grep -c "MarketPricesSection\|useGlobalItemPrices\|useGlobalItemCommunityStats" src/client/routes/global-items/\$globalItemId.tsx src/client/hooks/useGlobalItems.ts</automated>
|
|
</verify>
|
|
<done>Global item detail page shows market prices with user's market MSRP, community stats, and collapsible other markets</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Update ComparisonTable with currency normalization and update MCP tools</name>
|
|
<files>src/client/components/ComparisonTable.tsx, src/client/components/SetupCard.tsx, src/server/mcp/tools/index.ts</files>
|
|
<read_first>src/client/components/ComparisonTable.tsx, src/client/components/SetupCard.tsx, src/server/mcp/tools/index.ts</read_first>
|
|
<action>
|
|
Per D-20: Update `src/client/components/ComparisonTable.tsx`:
|
|
- Import `useCurrency` (for user's preferred currency), `useExchangeRates`, `convertClientPrice` from hooks
|
|
- In the price rendering section:
|
|
1. Check if candidate has a different currency than user's preference (via `priceCurrency` field on candidate if available, otherwise assume same currency)
|
|
2. If different currency: convert using `convertClientPrice(candidate.priceCents, candidate.priceCurrency, userCurrency, rates)`
|
|
3. Display converted price with `~` prefix in `text-gray-400`: e.g., `~$2,160` instead of plain `$2,160`
|
|
4. Best-price highlighting (`bg-green-50`) should apply based on converted amounts for apples-to-apples comparison
|
|
- Add a new "Found Price" row (per D-06) in the ATTRIBUTE_ROWS array:
|
|
- Key: "foundPrice", Label: "Found Price"
|
|
- Render: show candidate.foundPriceCents formatted with candidate.foundPriceCurrency if available, else "—"
|
|
- Include date if available: `text-xs text-gray-400` below the price
|
|
- Note: The CandidateWithCategory interface may need extending. If the API doesn't yet return foundPriceCents/foundPriceCurrency on candidates, check the thread service response and update the interface to match.
|
|
|
|
Per D-18: Update `src/client/components/SetupCard.tsx`:
|
|
- If SetupCard shows a price total, ensure it uses `useFormatters().price()` which now uses the correct currency
|
|
- This should already work if the component uses `useFormatters()` — verify and adjust if it uses hardcoded "$" or similar
|
|
|
|
Per MCP tool updates: Update `src/server/mcp/tools/index.ts`:
|
|
- In `list_items` and `get_item` tool responses: include `priceCurrency` field alongside `priceCents`
|
|
- In `get_setup` tool response: include currency info with totals
|
|
- Add a new tool `get_exchange_rates`:
|
|
- Description: "Get current exchange rates for currency conversion"
|
|
- No parameters required
|
|
- Returns: `{ base, date, rates }` from getExchangeRates()
|
|
- In `create_item` and `update_item` tools: accept optional `priceCurrency` parameter
|
|
- In `add_candidate` and `update_candidate` tools: accept optional `foundPriceCents`, `foundPriceCurrency`, `foundPriceDate` parameters
|
|
- Follow existing MCP tool patterns for parameter/response structure
|
|
</action>
|
|
<acceptance_criteria>
|
|
- ComparisonTable.tsx imports useExchangeRates and convertClientPrice
|
|
- ComparisonTable price cells show ~ prefix when price is converted from different currency
|
|
- ComparisonTable has "Found Price" row for candidate research prices
|
|
- SetupCard uses useFormatters().price() for currency-aware display
|
|
- MCP tools/index.ts contains get_exchange_rates tool definition
|
|
- MCP list_items and get_item responses include priceCurrency
|
|
- `bun run build` succeeds
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run build && grep -c "convertClientPrice\|foundPrice\|get_exchange_rates\|priceCurrency" src/client/components/ComparisonTable.tsx src/server/mcp/tools/index.ts</automated>
|
|
</verify>
|
|
<done>Comparison table normalizes currencies, MCP tools include currency context, setup cards display correct currency</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| server→client | Market prices and exchange rates served to public clients |
|
|
| MCP client→server | MCP tool invocations with currency parameters |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-33-15 | Tampering | ComparisonTable conversion | accept | Client-side conversion uses server-provided rates — worst case is stale rates, not a security issue |
|
|
| T-33-16 | Information Disclosure | market prices display | accept | Market prices are intentionally public data — MSRP is not sensitive |
|
|
| T-33-17 | Tampering | MCP priceCurrency param | mitigate | MCP tools validate priceCurrency against known currency list before persisting |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `bun run build` succeeds (no TypeScript errors)
|
|
- Global item detail shows market prices section
|
|
- ComparisonTable normalizes prices to user's currency
|
|
- MCP get_exchange_rates tool returns rates
|
|
- All existing tests pass: `bun test`
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Catalog detail page shows market prices + community data
|
|
- Comparison table normalizes and labels converted prices
|
|
- Setup cards show correct currency
|
|
- MCP tools expose currency data and exchange rates
|
|
- Full build succeeds
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/33-currency-system/33-06-SUMMARY.md`
|
|
</output>
|