---
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"
---

<objective>
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.
</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-01-SUMMARY.md

<interfaces>
From src/server/services/currency.service.ts (created in Plan 01):
```typescript
export interface ExchangeRates {
  base: string;
  date: string;
  rates: Record<string, number>;
}
export function getExchangeRates(): Promise<ExchangeRates>;
export function convertPrice(cents: number, from: string, to: string, rates: ExchangeRates): number;
export const CURRENCY_MARKET_MAP: Record<string, string>;
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.
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create market price service and API endpoints</name>
  <files>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</files>
  <read_first>src/server/services/global-item.service.ts, src/server/routes/global-items.ts, src/server/index.ts</read_first>
  <behavior>
    - 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
  </behavior>
  <action>
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)
  </action>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/market-price.service.test.ts</automated>
  </verify>
  <done>Market prices API and exchange rates endpoint working with tests</done>
</task>

<task type="auto">
  <name>Task 2: Update item and candidate endpoints with currency context</name>
  <files>src/server/services/item.service.ts, src/server/services/thread.service.ts</files>
  <read_first>src/server/services/item.service.ts, src/server/services/thread.service.ts, src/server/routes/items.ts, src/server/routes/threads.ts</read_first>
  <action>
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.
  </action>
  <acceptance_criteria>
    - 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)
  </acceptance_criteria>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test</automated>
  </verify>
  <done>Item and candidate services return currency context in all responses, accept new currency fields on create/update</done>
</task>

</tasks>

<threat_model>
## 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 |
</threat_model>

<verification>
- `bun test` passes (all existing + new tests)
- Exchange rates endpoint returns valid JSON
- Market prices endpoint returns array for known global item
</verification>

<success_criteria>
- Exchange rates and market prices APIs available
- Item/candidate responses include currency context
- All tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/33-currency-system/33-03-SUMMARY.md`
</output>
