Files
GearBox/.planning/phases/33-currency-system/33-03-PLAN.md
Jean-Luc Makiola 7a696f39a5 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>
2026-04-13 17:58:37 +02:00

227 lines
10 KiB
Markdown

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