import { and, desc, eq, gte, lt, sql } from 'drizzle-orm'; import { db } from '@/lib/server/db'; import { filingStatementSnapshot } from '@/lib/server/db/schema'; type FilingStatementSnapshotRow = typeof filingStatementSnapshot.$inferSelect; type ParseStatus = 'ready' | 'partial' | 'failed'; type SnapshotSource = 'sec_filing_summary' | 'xbrl_instance' | 'companyfacts_fallback'; type FinancialStatementKind = 'income' | 'balance' | 'cash_flow' | 'equity' | 'comprehensive_income'; type StatementValuesByPeriod = Record; export type FilingStatementSnapshotPeriod = { id: string; filingId: number; accessionNumber: string; filingDate: string; periodStart: string | null; periodEnd: string | null; filingType: '10-K' | '10-Q'; periodLabel: string; }; export type FilingFaithfulStatementSnapshotRow = { key: string; label: string; concept: string | null; order: number; depth: number; isSubtotal: boolean; values: StatementValuesByPeriod; }; export type StandardizedStatementSnapshotRow = { key: string; label: string; concept: string; category: string; sourceConcepts: string[]; values: StatementValuesByPeriod; }; export type DimensionStatementSnapshotRow = { rowKey: string; concept: string | null; periodId: string; axis: string; member: string; value: number | null; unit: string | null; }; export type FilingStatementBundle = { periods: FilingStatementSnapshotPeriod[]; statements: Record; }; export type StandardizedStatementBundle = { periods: FilingStatementSnapshotPeriod[]; statements: Record; }; export type DimensionStatementBundle = { statements: Record; }; export type FilingStatementSnapshotRecord = { id: number; filing_id: number; ticker: string; filing_date: string; filing_type: '10-K' | '10-Q'; period_end: string | null; statement_bundle: FilingStatementBundle | null; standardized_bundle: StandardizedStatementBundle | null; dimension_bundle: DimensionStatementBundle | null; parse_status: ParseStatus; parse_error: string | null; source: SnapshotSource; created_at: string; updated_at: string; }; export type UpsertFilingStatementSnapshotInput = { filing_id: number; ticker: string; filing_date: string; filing_type: '10-K' | '10-Q'; period_end: string | null; statement_bundle: FilingStatementBundle | null; standardized_bundle: StandardizedStatementBundle | null; dimension_bundle: DimensionStatementBundle | null; parse_status: ParseStatus; parse_error: string | null; source: SnapshotSource; }; function toSnapshotRecord(row: FilingStatementSnapshotRow): FilingStatementSnapshotRecord { return { id: row.id, filing_id: row.filing_id, ticker: row.ticker, filing_date: row.filing_date, filing_type: row.filing_type, period_end: row.period_end, statement_bundle: row.statement_bundle ?? null, standardized_bundle: row.standardized_bundle ?? null, dimension_bundle: row.dimension_bundle ?? null, parse_status: row.parse_status, parse_error: row.parse_error, source: row.source, created_at: row.created_at, updated_at: row.updated_at }; } function tenYearsAgoIso() { const date = new Date(); date.setUTCFullYear(date.getUTCFullYear() - 10); return date.toISOString().slice(0, 10); } export async function getFilingStatementSnapshotByFilingId(filingId: number) { const [row] = await db .select() .from(filingStatementSnapshot) .where(eq(filingStatementSnapshot.filing_id, filingId)) .limit(1); return row ? toSnapshotRecord(row) : null; } export async function upsertFilingStatementSnapshot(input: UpsertFilingStatementSnapshotInput) { const now = new Date().toISOString(); const [saved] = await db .insert(filingStatementSnapshot) .values({ filing_id: input.filing_id, ticker: input.ticker, filing_date: input.filing_date, filing_type: input.filing_type, period_end: input.period_end, statement_bundle: input.statement_bundle, standardized_bundle: input.standardized_bundle, dimension_bundle: input.dimension_bundle, parse_status: input.parse_status, parse_error: input.parse_error, source: input.source, created_at: now, updated_at: now }) .onConflictDoUpdate({ target: filingStatementSnapshot.filing_id, set: { ticker: input.ticker, filing_date: input.filing_date, filing_type: input.filing_type, period_end: input.period_end, statement_bundle: input.statement_bundle, standardized_bundle: input.standardized_bundle, dimension_bundle: input.dimension_bundle, parse_status: input.parse_status, parse_error: input.parse_error, source: input.source, updated_at: now } }) .returning(); return toSnapshotRecord(saved); } export async function listFilingStatementSnapshotsByTicker(input: { ticker: string; window: '10y' | 'all'; limit?: number; cursor?: string | null; }) { const safeLimit = Math.min(Math.max(Math.trunc(input.limit ?? 40), 1), 120); const cursorId = input.cursor ? Number.parseInt(input.cursor, 10) : null; const constraints = [eq(filingStatementSnapshot.ticker, input.ticker.trim().toUpperCase())]; if (input.window === '10y') { constraints.push(gte(filingStatementSnapshot.filing_date, tenYearsAgoIso())); } if (cursorId && Number.isFinite(cursorId) && cursorId > 0) { constraints.push(lt(filingStatementSnapshot.id, cursorId)); } const rows = await db .select() .from(filingStatementSnapshot) .where(and(...constraints)) .orderBy(desc(filingStatementSnapshot.filing_date), desc(filingStatementSnapshot.id)) .limit(safeLimit + 1); const hasMore = rows.length > safeLimit; const usedRows = hasMore ? rows.slice(0, safeLimit) : rows; const nextCursor = hasMore ? String(usedRows[usedRows.length - 1]?.id ?? '') : null; return { snapshots: usedRows.map(toSnapshotRecord), nextCursor }; } export async function countFilingStatementSnapshotStatuses(ticker: string) { const rows = await db .select({ status: filingStatementSnapshot.parse_status, count: sql`count(*)` }) .from(filingStatementSnapshot) .where(eq(filingStatementSnapshot.ticker, ticker.trim().toUpperCase())) .groupBy(filingStatementSnapshot.parse_status); return rows.reduce>((acc, row) => { acc[row.status] = Number(row.count); return acc; }, { ready: 0, partial: 0, failed: 0 }); }