--- phase: 33-currency-system plan: 03 type: execute wave: 2 depends_on: [01, 02] files_modified: - src/server/services/market-price.service.ts - src/server/routes/market-prices.ts - src/server/routes/exchange-rates.ts - src/server/index.ts - src/server/services/item.service.ts - src/server/services/thread.service.ts - tests/services/market-price.service.test.ts autonomous: true requirements: [D-01, D-02, D-06, D-09, D-10] must_haves: truths: - "GET /api/exchange-rates returns current exchange rates" - "GET /api/global-items/:id/prices returns market prices for a catalog item" - "POST /api/global-items/:id/prices creates/updates a market price (authenticated)" - "Item and candidate API responses include price currency context" - "Candidate update accepts foundPriceCents, foundPriceCurrency, foundPriceDate fields" artifacts: - path: "src/server/services/market-price.service.ts" provides: "CRUD operations for market prices" exports: ["getMarketPrices", "upsertMarketPrice"] - path: "src/server/routes/market-prices.ts" provides: "Market price API endpoints" - path: "src/server/routes/exchange-rates.ts" provides: "Exchange rate API endpoint" - path: "tests/services/market-price.service.test.ts" provides: "Market price service tests" min_lines: 30 key_links: - from: "src/server/routes/market-prices.ts" to: "src/server/services/market-price.service.ts" via: "route handler calls service" pattern: "getMarketPrices|upsertMarketPrice" - from: "src/server/routes/exchange-rates.ts" to: "src/server/services/currency.service.ts" via: "route handler calls getExchangeRates" pattern: "getExchangeRates" --- Create market prices API, exchange rates endpoint, and update existing item/candidate endpoints with currency context. Purpose: Server-side price infrastructure — enables clients and MCP consumers to access market prices and perform currency conversion. Output: New API endpoints for market prices and exchange rates, updated item/candidate responses with currency fields. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/33-currency-system/33-CONTEXT.md @.planning/phases/33-currency-system/33-01-SUMMARY.md From src/server/services/currency.service.ts (created in Plan 01): ```typescript export interface ExchangeRates { base: string; date: string; rates: Record; } export function getExchangeRates(): Promise; export function convertPrice(cents: number, from: string, to: string, rates: ExchangeRates): number; export const CURRENCY_MARKET_MAP: Record; export function getMarketForCurrency(currency: string): string; ``` From src/db/schema.ts (updated in Plan 01): ```typescript export const marketPrices = pgTable("market_prices", { id: serial("id").primaryKey(), globalItemId: integer("global_item_id").notNull().references(() => globalItems.id, { onDelete: "cascade" }), market: text("market").notNull(), currency: text("currency").notNull(), priceCents: integer("price_cents").notNull(), source: text("source"), createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => [unique().on(table.globalItemId, table.market, table.currency)]); ``` From src/server/routes/items.ts (existing pattern): ```typescript // Route pattern: Hono routes with zod-validator import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; ``` From src/server/index.ts (existing route registration pattern): ```typescript app.route("/api/items", itemRoutes); app.route("/api/threads", threadRoutes); // etc. ``` Task 1: Create market price service and API endpoints src/server/services/market-price.service.ts, src/server/routes/market-prices.ts, src/server/routes/exchange-rates.ts, src/server/index.ts, tests/services/market-price.service.test.ts src/server/services/global-item.service.ts, src/server/routes/global-items.ts, src/server/index.ts - getMarketPrices(db, globalItemId) returns all market prices for a global item - getMarketPricesForMarket(db, globalItemId, market) returns market-specific prices - upsertMarketPrice(db, data) creates or updates a market price (ON CONFLICT update) - GET /api/exchange-rates returns ExchangeRates JSON (public, no auth) - GET /api/global-items/:id/prices returns { marketPrices: [...], communityStats: [...] } - POST /api/global-items/:id/prices requires auth, validates with Zod, calls upsertMarketPrice Create `src/server/services/market-price.service.ts`: - `getMarketPrices(db, globalItemId)`: SELECT * FROM market_prices WHERE global_item_id = $1 ORDER BY market - `getMarketPricesForMarket(db, globalItemId, market)`: Same + AND market = $2 - `upsertMarketPrice(db, { globalItemId, market, currency, priceCents, source })`: INSERT INTO market_prices ... ON CONFLICT (global_item_id, market, currency) DO UPDATE SET price_cents = EXCLUDED.price_cents, source = EXCLUDED.source - Type `Db` follows existing pattern: `type Db = typeof prodDb` Create `src/server/routes/exchange-rates.ts`: - `GET /` (mounted at /api/exchange-rates): Call `getExchangeRates()` from currency.service, return JSON response - Public endpoint (no auth required) — follows existing pattern where GET endpoints are public Create `src/server/routes/market-prices.ts`: - `GET /global-items/:id/prices`: Call getMarketPrices(db, id), return { marketPrices } - `POST /global-items/:id/prices`: Require auth (per existing auth middleware pattern), validate body with Zod schema `{ market: z.string(), currency: z.string().max(3), priceCents: z.number().int().nonnegative(), source: z.string().optional() }`, call upsertMarketPrice Register routes in `src/server/index.ts`: - `app.route("/api/exchange-rates", exchangeRateRoutes)` - `app.route("/api/market-prices", marketPriceRoutes)` Create `tests/services/market-price.service.test.ts`: - Test getMarketPrices returns empty array for unknown item - Test upsertMarketPrice creates a new market price - Test upsertMarketPrice updates existing price on conflict - Test getMarketPricesForMarket filters by market - Use createTestDb() helper (from tests/helpers/db.ts) - src/server/services/market-price.service.ts exports getMarketPrices, getMarketPricesForMarket, upsertMarketPrice - src/server/routes/exchange-rates.ts exports a Hono app - src/server/routes/market-prices.ts exports a Hono app with GET and POST handlers - src/server/index.ts contains `app.route("/api/exchange-rates"` - src/server/index.ts contains `app.route("/api/market-prices"` - `bun test tests/services/market-price.service.test.ts` passes cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/market-price.service.test.ts Market prices API and exchange rates endpoint working with tests Task 2: Update item and candidate endpoints with currency context src/server/services/item.service.ts, src/server/services/thread.service.ts src/server/services/item.service.ts, src/server/services/thread.service.ts, src/server/routes/items.ts, src/server/routes/threads.ts Update `src/server/services/item.service.ts`: - In create/update functions: accept and persist `priceCurrency` field from request body - In getAll/getById responses: include `priceCurrency` in the SELECT column list - The existing `priceCents` fields remain unchanged — `priceCurrency` is additive Update `src/server/services/thread.service.ts`: - In candidate create/update functions: accept and persist `foundPriceCents`, `foundPriceCurrency`, `foundPriceDate` fields (per D-06, D-07) - In getThreadWithCandidates response: include `foundPriceCents`, `foundPriceCurrency`, `foundPriceDate` in the candidate SELECT - The existing candidate `priceCents` field remains unchanged Per D-09, D-10: Do NOT add conversion logic to these endpoints yet — that will be handled by the client formatter evolution in Plan 05. The server returns raw prices with currency metadata; the client handles display formatting. - src/server/services/item.service.ts create function handles priceCurrency - src/server/services/item.service.ts getAll includes priceCurrency in select - src/server/services/thread.service.ts candidate create handles foundPriceCents, foundPriceCurrency, foundPriceDate - src/server/services/thread.service.ts getThreadWithCandidates includes foundPriceCents, foundPriceCurrency, foundPriceDate - `bun test` passes (existing tests still work) cd /home/jlmak/Projects/jlmak/GearBox && bun test Item and candidate services return currency context in all responses, accept new currency fields on create/update ## Trust Boundaries | Boundary | Description | |----------|-------------| | client→server | Market price submissions (POST) — user input for price, currency, market | | server→database | SQL queries with user-provided market/currency strings | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-33-06 | Tampering | POST /api/market-prices | mitigate | Zod validation on all fields — priceCents must be non-negative integer, currency max 3 chars, market non-empty string | | T-33-07 | Elevation of Privilege | POST /api/market-prices | mitigate | Auth middleware required on POST — only authenticated users can submit prices | | T-33-08 | Injection | market-price.service.ts | mitigate | Use Drizzle ORM parameterized queries — no raw SQL string concatenation | - `bun test` passes (all existing + new tests) - Exchange rates endpoint returns valid JSON - Market prices endpoint returns array for known global item - Exchange rates and market prices APIs available - Item/candidate responses include currency context - All tests pass After completion, create `.planning/phases/33-currency-system/33-03-SUMMARY.md`