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>
11 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 | 04 | execute | 2 |
|
|
true |
|
|
Purpose: Enable community price data (D-04, D-05, D-21) and ensure setup totals work correctly with currency metadata. Output: Community price API, aggregation queries, updated setup/totals services.
<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-01-SUMMARY.md From src/db/schema.ts (Plan 01): ```typescript export const communityPrices = pgTable("community_prices", { id: serial("id").primaryKey(), globalItemId: integer("global_item_id").notNull().references(() => globalItems.id, { onDelete: "cascade" }), userId: integer("user_id").notNull().references(() => users.id), market: text("market").notNull(), currency: text("currency").notNull(), priceCents: integer("price_cents").notNull(), priceDate: timestamp("price_date"), sourceType: text("source_type").notNull(), // 'purchased' | 'researched' createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => [unique().on(table.globalItemId, table.userId, table.sourceType)]); ```From src/server/services/setup.service.ts (existing):
export async function getAllSetups(db: Db, userId: number) { ... }
// Uses SQL: SUM(COALESCE(global_items.price_cents, items.price_cents) * items.quantity)
export async function getSetupWithItems(db: Db, userId: number, setupId: number) { ... }
From src/server/services/totals.service.ts (existing):
export async function getCategoryTotals(db: Db, userId: number) { ... }
export async function getGlobalTotals(db: Db, userId: number) { ... }
Per D-05: validateOwnership(db, userId, globalItemId):
- SELECT COUNT(*) FROM items WHERE user_id = $1 AND global_item_id = $2
- Return count > 0
Per D-04, D-05: submitCommunityPrice(db, { globalItemId, userId, market, currency, priceCents, priceDate, sourceType }):
- First call validateOwnership — if false, return null (user doesn't own this item)
- INSERT INTO community_prices ... ON CONFLICT (global_item_id, user_id, source_type) DO UPDATE SET price_cents = EXCLUDED.price_cents, price_date = EXCLUDED.price_date, market = EXCLUDED.market, currency = EXCLUDED.currency
- Return the upserted row
Per D-21: getCommunityPriceStats(db, globalItemId, market?):
- Use PostgreSQL PERCENTILE_CONT(0.5) for median calculation
- Query: SELECT market, currency, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price_cents) as median_price, COUNT() as report_count FROM community_prices WHERE global_item_id = $1 [AND market = $2] GROUP BY market, currency HAVING COUNT() >= 3
- Return array of { market, currency, medianPrice (integer cents), reportCount }
Create src/server/routes/community-prices.ts:
GET /:globalItemId: Call getCommunityPriceStats(db, id), return JSONPOST /: Require auth. Validate body:{ globalItemId: z.number().int(), market: z.string(), currency: z.string().max(3), priceCents: z.number().int().nonnegative(), priceDate: z.string().datetime().optional(), sourceType: z.enum(["purchased", "researched"]) }. Call submitCommunityPrice. Return 403 if ownership validation fails, 200 with data otherwise.
Register in src/server/index.ts: app.route("/api/community-prices", communityPriceRoutes)
Create tests/services/community-price.service.test.ts:
- Test validateOwnership returns false for non-owner
- Test validateOwnership returns true when user owns item with that globalItemId
- Test submitCommunityPrice creates a price submission
- Test submitCommunityPrice returns null when user doesn't own item
- Test getCommunityPriceStats returns empty when < 3 reports
- Use createTestDb() helper
<acceptance_criteria>
- src/server/services/community-price.service.ts exports validateOwnership, submitCommunityPrice, getCommunityPriceStats
- src/server/routes/community-prices.ts has GET and POST handlers
- src/server/index.ts contains
app.route("/api/community-prices" - getCommunityPriceStats uses PERCENTILE_CONT for median
- getCommunityPriceStats HAVING COUNT(*) >= 3
- submitCommunityPrice checks ownership before insert
bun test tests/services/community-price.service.test.tspasses </acceptance_criteria> cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/community-price.service.test.ts Community price system working with ownership validation, median aggregation with 3-report minimum
The aggregation queries in setup.service.ts and totals.service.ts should include the priceCurrency field in their response so the client can display the correct currency symbol, but the actual SUM logic does not need conversion yet (that would require the server to know the user's preferred currency during aggregation, which is a Plan 05/06 concern).
Update src/server/services/setup.service.ts:
- In
getSetupWithItems: AddpriceCurrency: items.priceCurrencyto the itemList SELECT columns - The
totalCostaggregate stays as-is (all in primary currency for now)
Update src/server/services/totals.service.ts:
- No changes needed — totals are global aggregates returned to the authenticated user
- The client formatter (Plan 05) will handle displaying in the user's preferred currency
This is intentionally minimal — the server returns raw data with currency metadata, and the client handles conversion display. Server-side conversion for aggregates would require passing the user's currency preference through every query, which adds complexity without benefit when the primary market is EUR.
<acceptance_criteria>
- src/server/services/setup.service.ts getSetupWithItems includes priceCurrency in itemList select
- Existing bun test passes — no regressions in setup or totals tests
</acceptance_criteria>
cd /home/jlmak/Projects/jlmak/GearBox && bun test
Setup and totals services return currency metadata alongside prices
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| client→server | Community price submissions — untrusted user price data |
| server→database | Ownership validation query |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-33-09 | Spoofing | POST /api/community-prices | mitigate | Auth middleware + ownership validation ensures only item owners can submit prices |
| T-33-10 | Tampering | community-price.service.ts | mitigate | Zod validation on all input fields, priceCents must be non-negative integer, currency max 3 chars |
| T-33-11 | Repudiation | community_prices | accept | Price submissions tracked with userId and createdAt — sufficient audit trail for a single-user app |
| T-33-12 | Information Disclosure | GET /api/community-prices | accept | Community price stats are intentionally public (anonymous aggregates, no individual prices exposed) |
| </threat_model> |
<success_criteria>
- Community price CRUD with ownership gate
- Aggregation with median and minimum report threshold
- Setup items include currency metadata
- All tests pass </success_criteria>