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" | "disclosure" | "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< FinancialStatementKind, FilingFaithfulStatementSnapshotRow[] >; }; export type StandardizedStatementBundle = { periods: FilingStatementSnapshotPeriod[]; statements: Record< FinancialStatementKind, StandardizedStatementSnapshotRow[] >; }; export type DimensionStatementBundle = { statements: Record; }; 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; }; 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); } 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, }; } 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, }, ); }