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>
13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 33-currency-system | 01 | execute | 1 |
|
true |
|
|
Purpose: Foundation layer — all other plans depend on these tables and the conversion service. Output: New DB tables (market_prices, community_prices), new columns on items/candidates, currency service with rate fetching/caching/conversion.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/33-currency-system/33-CONTEXT.md @.planning/phases/33-currency-system/33-RESEARCH.md From src/db/schema.ts: ```typescript // Pattern: pgTable with serial id, references, timestamps export const globalItems = pgTable("global_items", { id: serial("id").primaryKey(), brand: text("brand").notNull(), model: text("model").notNull(), priceCents: integer("price_cents"), // ... }, (table) => [unique().on(table.brand, table.model)]);export const items = pgTable("items", { id: serial("id").primaryKey(), priceCents: integer("price_cents"), purchasePriceCents: integer("purchase_price_cents"), globalItemId: integer("global_item_id").references(() => globalItems.id), // ... });
export const threadCandidates = pgTable("thread_candidates", { id: serial("id").primaryKey(), priceCents: integer("price_cents"), globalItemId: integer("global_item_id").references(() => globalItems.id), // ... });
export const settings = pgTable("settings", { userId: integer("user_id").notNull().references(() => users.id), key: text("key").notNull(), value: text("value").notNull(), }, (table) => [primaryKey({ columns: [table.userId, table.key] })]);
From src/shared/schemas.ts:
```typescript
export const createItemSchema = z.object({
priceCents: z.number().int().nonnegative().optional(),
purchasePriceCents: z.number().int().nonnegative().optional(),
// ...
});
export const createCandidateSchema = z.object({
priceCents: z.number().int().nonnegative().optional(),
// ...
});
Per D-04, D-05: Add communityPrices pgTable with: id (serial PK), globalItemId (integer FK to globalItems ON DELETE CASCADE), userId (integer FK to users), market (text NOT NULL), currency (text NOT NULL), priceCents (integer NOT NULL), priceDate (timestamp nullable), sourceType (text NOT NULL — 'purchased' or 'researched'), createdAt (timestamp defaultNow). Unique constraint on (globalItemId, userId, sourceType).
Per D-03: Add priceCurrency column to items table: priceCurrency: text("price_currency").default("EUR").
Per D-06, D-07: Add to threadCandidates table: foundPriceCents: integer("found_price_cents"), foundPriceCurrency: text("found_price_currency"), foundPriceDate: timestamp("found_price_date").
Update src/shared/schemas.ts:
createItemSchema: addpriceCurrency: z.string().max(3).optional()updateItemSchema: inherits via.partial()createCandidateSchema: addfoundPriceCents: z.number().int().nonnegative().optional(),foundPriceCurrency: z.string().max(3).optional(),foundPriceDate: z.string().datetime().optional()updateCandidateSchema: inherits via.partial()
Update src/shared/types.ts if it has manual type definitions — if types are inferred from Drizzle/Zod, no changes needed.
<acceptance_criteria>
- src/db/schema.ts contains export const marketPrices = pgTable("market_prices"
- src/db/schema.ts contains export const communityPrices = pgTable("community_prices"
- src/db/schema.ts items table contains priceCurrency: text("price_currency")
- src/db/schema.ts threadCandidates table contains foundPriceCents: integer("found_price_cents")
- src/db/schema.ts threadCandidates table contains foundPriceCurrency: text("found_price_currency")
- src/db/schema.ts threadCandidates table contains foundPriceDate: timestamp("found_price_date")
- src/shared/schemas.ts createItemSchema contains priceCurrency
- src/shared/schemas.ts createCandidateSchema contains foundPriceCents
- marketPrices has unique constraint on globalItemId + market + currency
- communityPrices has unique constraint on globalItemId + userId + sourceType
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && grep -c "marketPrices|communityPrices|priceCurrency|foundPriceCents" src/db/schema.ts
Both new tables defined in schema with all columns and constraints, existing tables have new columns, Zod schemas updated
export interface ExchangeRates {
base: string;
date: string;
rates: Record<string, number>;
}
export const CURRENCY_MARKET_MAP: Record<string, string> = {
EUR: "EU", USD: "US", GBP: "UK", JPY: "JP", CAD: "CA", AUD: "AU",
};
export function getMarketForCurrency(currency: string): string {
return CURRENCY_MARKET_MAP[currency] ?? currency;
}
Per D-08, D-09: Implement getExchangeRates():
- Fetch from
https://api.frankfurter.app/latest?from=EUR - Parse response:
{ base: "EUR", date: "2026-04-13", rates: { USD: 1.08, GBP: 0.86, ... } } - Cache in module-level variables:
let cachedRates: ExchangeRates | null = null; let cacheExpiry = 0; - Cache TTL: 24 hours (86400000ms)
- On fetch failure: return cached rates if available, throw if no cache
- Always include base currency in rates:
rates.EUR = 1(self-reference for conversion math)
Implement convertPrice(cents: number, from: string, to: string, rates: ExchangeRates): number:
- If
from === to, return cents unchanged - Convert
fromto EUR base:centsInEur = cents / rates[from] - Convert EUR to
to:result = centsInEur * rates[to] - Return
Math.round(result)(integer cents)
Export a resetCache() function for testing.
Create tests/services/currency.service.test.ts:
- Test convertPrice with known rates: EUR→USD, USD→EUR, same currency
- Test getExchangeRates caching (mock fetch)
- Test CURRENCY_MARKET_MAP entries
- Test getMarketForCurrency
<acceptance_criteria>
- src/server/services/currency.service.ts exports getExchangeRates, convertPrice, CURRENCY_MARKET_MAP, getMarketForCurrency
- convertPrice(1000, "EUR", "EUR", rates) returns 1000
- convertPrice(1000, "EUR", "USD", {base:"EUR",date:"",rates:{EUR:1,USD:1.08}}) returns 1080
- tests/services/currency.service.test.ts exists with at least 4 test cases
bun test tests/services/currency.service.test.tspasses </acceptance_criteria> cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/currency.service.test.ts Currency service with rate fetching, 24h caching, conversion math, and market mapping — all tested
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| server→frankfurter.app | External API for exchange rates — untrusted data |
| client→server | Price currency values from user input |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-33-01 | Tampering | currency.service.ts | mitigate | Validate exchange rate response shape before caching — reject if rates are missing or negative |
| T-33-02 | Spoofing | schema.ts priceCurrency | mitigate | Zod validation on priceCurrency field limits to max 3 chars; server validates against known currency list |
| T-33-03 | Denial of Service | currency.service.ts | mitigate | Cache rates for 24h; stale-serve on fetch failure; no user-triggered fetches |
| T-33-04 | Information Disclosure | community_prices | accept | Community prices are intentionally public aggregate data — no PII beyond userId which is already public in profiles |
| </threat_model> |
<success_criteria>
- New tables (market_prices, community_prices) defined in schema
- Existing tables extended with currency/date columns
- Currency service fetches, caches, and converts prices
- All tests pass </success_criteria>