docs(33): create phase plans for currency system
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>
This commit is contained in:
255
.planning/phases/33-currency-system/33-06-PLAN.md
Normal file
255
.planning/phases/33-currency-system/33-06-PLAN.md
Normal file
@@ -0,0 +1,255 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user