/* DS1 (DESIGN-SPEC §17 — scan→ledger persistence). * * The single row-builder for a MANUAL scan's ledger_entries row. Shared * (CommonJS) so the Next scan route builds the row through it AND the Jest * suite can prove a written row is readable by the /api/ledger/mine query * (same user_id scope, same player_key = nameKey, same dedupe key). * * DATA SEMANTICS: line/book are the REAL book values the user scanned; * locked_odds/team attach only when the cache-only snapshot confirms them * (absent beats wrong). Only grade/edge/confidence/model_value are MODEL. */ const { nameKey, normalizeName } = require('./playerName'); /** The columns the dedupe UNIQUE constraint (migration 019) is keyed on, and * the onConflict target for the upsert. Kept here as the single source. */ const LEDGER_CONFLICT_COLS = 'user_id,player_key,stat,line,side,game_id'; /** Today's date in ET (YYYY-MM-DD) — the game_date the settle pass keys on. */ function gameDateET(now = new Date()) { return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', }).format(now); } const numOrNull = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null); /** * Build the ledger_entries row for a completed manual scan. * @param {{ * userId: string, * sport: string, * player: string, * stat: string, * line: number, * side: string, * book?: string | null, * team?: string | null, * lockedOdds?: string | number | null, * grade?: string | null, * edge?: number | null, * confidence?: number | null, * projection?: number | null, * now?: Date, * }} opts * @returns {Record} a row ready for `sb.from('ledger_entries').upsert(row, ...)`. */ function buildManualLedgerRow({ userId, sport, player, stat, line, side, book, team = null, lockedOdds = null, grade = null, edge = null, confidence = null, projection = null, now = new Date(), }) { const s = String(sport || '').toLowerCase(); const playerKey = nameKey(player); const gameDate = gameDateET(now); return { user_id: userId, player_key: playerKey, player_name: normalizeName(player).display || player, sport: s, stat: String(stat || '').toLowerCase(), line, side, locked_odds: lockedOdds != null ? String(lockedOdds) : null, book: book || 'draftkings', team, opponent: null, grade: grade ?? null, edge: numOrNull(edge), confidence: numOrNull(confidence), model_value: numOrNull(projection), graded_at: now.toISOString(), game_id: `manual:${s}:${gameDate}:${playerKey}`, game_date: gameDate, }; } module.exports = { buildManualLedgerRow, gameDateET, LEDGER_CONFLICT_COLS };