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:
308
.planning/phases/33-currency-system/33-05-PLAN.md
Normal file
308
.planning/phases/33-currency-system/33-05-PLAN.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
phase: 33-currency-system
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [01, 03]
|
||||
files_modified:
|
||||
- src/client/lib/formatters.ts
|
||||
- src/client/hooks/useCurrency.ts
|
||||
- src/client/hooks/useFormatters.ts
|
||||
- src/client/hooks/useExchangeRates.ts
|
||||
- src/client/routes/settings.tsx
|
||||
autonomous: true
|
||||
requirements: [D-10, D-11, D-12, D-13, D-14, D-15, D-16]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Currency picker in settings now implies market selection"
|
||||
- "Settings page has a 'Show Converted Prices' toggle"
|
||||
- "formatPrice supports dual display format: source price + converted in parentheses"
|
||||
- "Converted prices always show ~ prefix to indicate approximation"
|
||||
- "useCurrency returns currency, market, and showConversions flag"
|
||||
- "Auto-suggestion appears on first visit based on browser locale"
|
||||
artifacts:
|
||||
- path: "src/client/lib/formatters.ts"
|
||||
provides: "Extended formatPrice with dual display and conversion options"
|
||||
exports: ["formatPrice", "formatDualPrice"]
|
||||
- path: "src/client/hooks/useCurrency.ts"
|
||||
provides: "Market-aware currency hook"
|
||||
exports: ["useCurrency"]
|
||||
- path: "src/client/hooks/useExchangeRates.ts"
|
||||
provides: "React Query hook for exchange rates"
|
||||
exports: ["useExchangeRates"]
|
||||
- path: "src/client/routes/settings.tsx"
|
||||
provides: "Updated settings page with market/currency selector and conversion toggle"
|
||||
key_links:
|
||||
- from: "src/client/hooks/useFormatters.ts"
|
||||
to: "src/client/lib/formatters.ts"
|
||||
via: "formatPrice import"
|
||||
pattern: "formatPrice|formatDualPrice"
|
||||
- from: "src/client/hooks/useExchangeRates.ts"
|
||||
to: "/api/exchange-rates"
|
||||
via: "React Query fetch"
|
||||
pattern: "exchange-rates"
|
||||
- from: "src/client/hooks/useCurrency.ts"
|
||||
to: "src/client/hooks/useSettings.ts"
|
||||
via: "useSetting('currency')"
|
||||
pattern: "useSetting"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Evolve the client-side price formatting, currency hook, and settings UI to support market-aware pricing with dual display.
|
||||
|
||||
Purpose: User-facing currency system — market/currency selector, auto-suggestion, conversion toggle, and dual price display format.
|
||||
Output: Updated formatters, enhanced currency hook, new exchange rates hook, redesigned settings currency section.
|
||||
</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
|
||||
|
||||
<interfaces>
|
||||
From src/client/lib/formatters.ts (current):
|
||||
```typescript
|
||||
export type Currency = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD";
|
||||
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
|
||||
```
|
||||
|
||||
From src/client/hooks/useCurrency.ts (current):
|
||||
```typescript
|
||||
export function useCurrency(): Currency;
|
||||
```
|
||||
|
||||
From src/client/hooks/useFormatters.ts (current):
|
||||
```typescript
|
||||
export function useFormatters(): {
|
||||
weight: (grams: number | null) => string;
|
||||
price: (cents: number | null) => string;
|
||||
unit: WeightUnit;
|
||||
currency: Currency;
|
||||
};
|
||||
```
|
||||
|
||||
From src/client/hooks/useSettings.ts (pattern):
|
||||
```typescript
|
||||
export function useSetting(key: string): { data: string | undefined, ... };
|
||||
export function useUpdateSetting(): UseMutationResult<...>;
|
||||
```
|
||||
|
||||
From src/client/lib/api.ts:
|
||||
```typescript
|
||||
export function apiGet<T>(path: string): Promise<T>;
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Extend formatPrice with dual display and create exchange rates hook</name>
|
||||
<files>src/client/lib/formatters.ts, src/client/hooks/useExchangeRates.ts</files>
|
||||
<read_first>src/client/lib/formatters.ts, src/client/hooks/useFormatters.ts, src/client/lib/api.ts</read_first>
|
||||
<action>
|
||||
Update `src/client/lib/formatters.ts`:
|
||||
|
||||
Per D-14: Add `formatDualPrice` function:
|
||||
```typescript
|
||||
export interface DualPriceOptions {
|
||||
sourceCents: number;
|
||||
sourceCurrency: Currency;
|
||||
targetCurrency: Currency;
|
||||
convertedCents: number;
|
||||
}
|
||||
|
||||
export function formatDualPrice(options: DualPriceOptions): { source: string; converted: string } {
|
||||
const source = formatPrice(options.sourceCents, options.sourceCurrency);
|
||||
const converted = `~${formatPrice(options.convertedCents, options.targetCurrency)}`;
|
||||
return { source, converted };
|
||||
}
|
||||
```
|
||||
|
||||
Per D-11: The `~` prefix on converted prices indicates approximation. The `converted` string is always prefixed with `~`.
|
||||
|
||||
Keep existing `formatPrice` unchanged for backward compatibility — all existing callers continue to work. `formatDualPrice` is additive.
|
||||
|
||||
Create `src/client/hooks/useExchangeRates.ts`:
|
||||
```typescript
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiGet } from "../lib/api";
|
||||
|
||||
interface ExchangeRates {
|
||||
base: string;
|
||||
date: string;
|
||||
rates: Record<string, number>;
|
||||
}
|
||||
|
||||
export function useExchangeRates() {
|
||||
return useQuery({
|
||||
queryKey: ["exchange-rates"],
|
||||
queryFn: () => apiGet<ExchangeRates>("/api/exchange-rates"),
|
||||
staleTime: 1000 * 60 * 60, // 1 hour client-side stale time
|
||||
gcTime: 1000 * 60 * 60 * 24, // 24 hour garbage collection
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function convertClientPrice(
|
||||
cents: number,
|
||||
from: string,
|
||||
to: string,
|
||||
rates: Record<string, number>,
|
||||
): number {
|
||||
if (from === to) return cents;
|
||||
const fromRate = rates[from] ?? 1;
|
||||
const toRate = rates[to] ?? 1;
|
||||
return Math.round((cents / fromRate) * toRate);
|
||||
}
|
||||
```
|
||||
|
||||
This provides both a React Query hook for components and a pure conversion function that mirrors the server-side logic.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- src/client/lib/formatters.ts exports formatDualPrice alongside existing formatPrice
|
||||
- formatDualPrice returns { source: "€2,000.00", converted: "~$2,160.00" } format
|
||||
- Existing formatPrice function unchanged (backward compatible)
|
||||
- src/client/hooks/useExchangeRates.ts exports useExchangeRates and convertClientPrice
|
||||
- useExchangeRates fetches from /api/exchange-rates with 1h stale time
|
||||
- convertClientPrice(1000, "EUR", "EUR", rates) returns 1000
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && grep -c "formatDualPrice\|useExchangeRates\|convertClientPrice" src/client/lib/formatters.ts src/client/hooks/useExchangeRates.ts</automated>
|
||||
</verify>
|
||||
<done>Dual price display format and exchange rate hook available for all components</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Evolve useCurrency hook and update settings page with market selector + conversion toggle</name>
|
||||
<files>src/client/hooks/useCurrency.ts, src/client/hooks/useFormatters.ts, src/client/routes/settings.tsx</files>
|
||||
<read_first>src/client/hooks/useCurrency.ts, src/client/hooks/useFormatters.ts, src/client/routes/settings.tsx, src/client/hooks/useSettings.ts</read_first>
|
||||
<action>
|
||||
Per D-12: Update `src/client/hooks/useCurrency.ts`:
|
||||
```typescript
|
||||
import type { Currency } from "../lib/formatters";
|
||||
import { useSetting } from "./useSettings";
|
||||
|
||||
const VALID_CURRENCIES: Currency[] = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"];
|
||||
|
||||
const CURRENCY_MARKET_MAP: Record<string, string> = {
|
||||
EUR: "EU", USD: "US", GBP: "UK", JPY: "JP", CAD: "CA", AUD: "AU",
|
||||
};
|
||||
|
||||
export interface CurrencyContext {
|
||||
currency: Currency;
|
||||
market: string;
|
||||
showConversions: boolean;
|
||||
}
|
||||
|
||||
export function useCurrency(): CurrencyContext {
|
||||
const { data: currencyData } = useSetting("currency");
|
||||
const { data: showConversionsData } = useSetting("showConversions");
|
||||
|
||||
const currency: Currency = (currencyData && VALID_CURRENCIES.includes(currencyData as Currency))
|
||||
? (currencyData as Currency)
|
||||
: "USD";
|
||||
|
||||
return {
|
||||
currency,
|
||||
market: CURRENCY_MARKET_MAP[currency] ?? currency,
|
||||
showConversions: showConversionsData === "true",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
IMPORTANT: The return type changes from `Currency` to `CurrencyContext`. Update `src/client/hooks/useFormatters.ts` to destructure correctly:
|
||||
```typescript
|
||||
export function useFormatters() {
|
||||
const unit = useWeightUnit();
|
||||
const { currency } = useCurrency(); // Destructure currency from CurrencyContext
|
||||
return {
|
||||
weight: (grams: number | null) => formatWeight(grams, unit),
|
||||
price: (cents: number | null) => formatPrice(cents, currency),
|
||||
unit,
|
||||
currency,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Also update ALL other files that call `useCurrency()` and expect a plain `Currency` string — search with `grep -rn "useCurrency()" src/client/` and update each to destructure `{ currency }` or `{ currency, market, showConversions }` as needed. The settings.tsx file is the primary consumer beyond useFormatters.
|
||||
|
||||
Per D-13, D-15, D-16: Update `src/client/routes/settings.tsx`:
|
||||
|
||||
1. Change the "Currency" section heading to "Market & Currency" per UI-SPEC
|
||||
2. Change description to "Sets your market region and currency for price display"
|
||||
3. Keep the same pill toggle pattern for currency selection (same bg-gray-100 rounded-full container)
|
||||
4. Add a new "Show Converted Prices" toggle below the currency picker, separated by `border-t border-gray-100`:
|
||||
- Heading: "Show Converted Prices" (text-sm font-medium text-gray-900)
|
||||
- Description: "Display approximate conversions when local price is not available" (text-xs text-gray-500)
|
||||
- Toggle: A simple button/switch that saves `showConversions` setting as "true"/"false" using updateSetting.mutate({ key: "showConversions", value: "true"/"false" })
|
||||
- Toggle styles: `w-10 h-5 rounded-full` container, `bg-gray-200` when off, `bg-blue-500` when on, inner circle `w-4 h-4 rounded-full bg-white shadow-sm` translated right when on
|
||||
|
||||
5. Per D-13: Add auto-suggestion banner above the settings card (only shown when no currency setting exists):
|
||||
- Detect suggested currency from `navigator.language`: parse locale (e.g., "de-DE" → EUR, "en-US" → USD, "en-GB" → GBP, "ja-JP" → JPY, "fr-CA" → CAD, "en-AU" → AUD)
|
||||
- Banner: `bg-blue-50 border border-blue-100 rounded-xl px-4 py-3 mb-4 flex items-center justify-between`
|
||||
- Text: LucideIcon "globe" (16px, text-blue-500) + "Based on your location, we suggest {CURRENCY} ({SYMBOL})" (text-sm text-blue-700)
|
||||
- CTA: "Use {SYMBOL}" button (text-sm font-medium text-blue-700 hover:text-blue-800 underline) that saves the currency setting and hides the banner
|
||||
- Use useState for banner visibility, default to showing when `useSetting("currency").data` is undefined
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- src/client/hooks/useCurrency.ts exports CurrencyContext interface
|
||||
- useCurrency() returns { currency, market, showConversions } object
|
||||
- src/client/hooks/useFormatters.ts destructures { currency } from useCurrency()
|
||||
- settings.tsx heading reads "Market & Currency"
|
||||
- settings.tsx has "Show Converted Prices" toggle that persists to settings
|
||||
- settings.tsx has auto-suggestion banner using navigator.language when no currency set
|
||||
- All existing components that call useCurrency() still compile (no type errors from return type change)
|
||||
- `bun run build` succeeds
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run build</automated>
|
||||
</verify>
|
||||
<done>Market/currency selector with auto-suggestion, conversion toggle, and updated currency hook deployed</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser→app | navigator.language used for auto-suggestion — untrusted but low risk (suggestion only) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-33-13 | Spoofing | settings.tsx auto-suggestion | accept | navigator.language is a suggestion only — user explicitly confirms by clicking "Use". No security impact if spoofed. |
|
||||
| T-33-14 | Tampering | useCurrency hook | mitigate | Currency value validated against VALID_CURRENCIES allowlist — invalid values fall back to "USD" |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `bun run build` succeeds (no TypeScript errors)
|
||||
- Settings page shows Market & Currency with pill toggle
|
||||
- Settings page shows Show Converted Prices toggle
|
||||
- Auto-suggestion banner appears when no currency setting exists
|
||||
- useCurrency() returns CurrencyContext object in all consumers
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Market/currency selector in settings
|
||||
- Conversion toggle in settings
|
||||
- Auto-suggestion based on locale
|
||||
- Dual price format available in formatter
|
||||
- Exchange rates hook ready for components
|
||||
- All existing price displays still work (backward compatible)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/33-currency-system/33-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user