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>
227 lines
10 KiB
Markdown
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>
|