79 lines
2.2 KiB
JavaScript
79 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,
|
||
|
|
}));
|
||
|
|
}
|