deckhearth/lib/pricing/price-history.js
Randall Stillwell d0468015eb
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
feat(pricing): convoy for online prices and local stores
Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio),
valuation and sort-by-price, card detail pricing UI, and nearby store
discovery via home zip and OSM geo clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 22:47:46 -05:00

78 lines
2.2 KiB
JavaScript

import { sql } from '../sql.js';
/**
* @typedef {Object} PriceSnapshotRow
* @property {number} cardId
* @property {string} source
* @property {string} [priceType]
* @property {string} [currency]
* @property {number} amount
* @property {string | Date} [recordedAt]
*/
/**
* Upsert daily price snapshots (idempotent per card/source/type/currency/day).
*
* @param {PriceSnapshotRow[]} rows
* @returns {Promise<number>} rows written
*/
export async function recordPriceSnapshots(rows) {
if (!rows?.length) return 0;
let written = 0;
for (const row of rows) {
if (row.cardId == null || row.amount == null || Number.isNaN(Number(row.amount))) {
continue;
}
const priceType = row.priceType ?? 'market';
const currency = row.currency ?? 'USD';
const recordedAt =
row.recordedAt instanceof Date
? row.recordedAt.toISOString().slice(0, 10)
: row.recordedAt ?? new Date().toISOString().slice(0, 10);
await sql`
INSERT INTO card_price_history (card_id, source, price_type, currency, amount, recorded_at)
VALUES (
${row.cardId},
${row.source},
${priceType},
${currency},
${Number(row.amount)},
${recordedAt}
)
ON CONFLICT (card_id, source, price_type, currency, recorded_at)
DO UPDATE SET amount = EXCLUDED.amount
`;
written += 1;
}
return written;
}
/**
* @param {number} cardId
* @param {{ days?: number }} [options]
* @returns {Promise<Array<{ date: string, amount: number, source: string, priceType: string, currency: string }>>}
*/
export async function getPriceHistoryPoints(cardId, { days = 90 } = {}) {
const cappedDays = Math.min(Math.max(Number(days) || 90, 1), 365);
const result = await sql`
SELECT recorded_at, amount, source, price_type, currency
FROM card_price_history
WHERE card_id = ${cardId}
AND recorded_at >= CURRENT_DATE - ${cappedDays}::integer
ORDER BY recorded_at ASC
`;
return result.rows.map((row) => ({
date: row.recorded_at instanceof Date
? row.recorded_at.toISOString().slice(0, 10)
: String(row.recorded_at).slice(0, 10),
amount: parseFloat(row.amount),
source: row.source,
priceType: row.price_type,
currency: row.currency,
}));
}