Add consolidated disclosure statement type

Create unified disclosure statement to organize footnote disclosures
separate from primary financial statements. Disclosures are now grouped
by type (tax, debt, securities, derivatives, leases, intangibles, ma,
revenue, cash_flow) in a dedicated statement type for cleaner UI
presentation.
This commit is contained in:
2026-03-16 18:54:23 -04:00
parent a58b07456e
commit 14a7773504
16 changed files with 5679 additions and 731 deletions

View File

@@ -1,13 +1,22 @@
import { and, desc, eq, gte, lt, sql } from 'drizzle-orm';
import { db } from '@/lib/server/db';
import { filingStatementSnapshot } from '@/lib/server/db/schema';
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 ParseStatus = "ready" | "partial" | "failed";
type SnapshotSource =
| "sec_filing_summary"
| "xbrl_instance"
| "companyfacts_fallback";
type FinancialStatementKind = 'income' | 'balance' | 'cash_flow' | 'equity' | 'comprehensive_income';
type FinancialStatementKind =
| "income"
| "balance"
| "cash_flow"
| "disclosure"
| "equity"
| "comprehensive_income";
type StatementValuesByPeriod = Record<string, number | null>;
@@ -18,7 +27,7 @@ export type FilingStatementSnapshotPeriod = {
filingDate: string;
periodStart: string | null;
periodEnd: string | null;
filingType: '10-K' | '10-Q';
filingType: "10-K" | "10-Q";
periodLabel: string;
};
@@ -53,12 +62,18 @@ export type DimensionStatementSnapshotRow = {
export type FilingStatementBundle = {
periods: FilingStatementSnapshotPeriod[];
statements: Record<FinancialStatementKind, FilingFaithfulStatementSnapshotRow[]>;
statements: Record<
FinancialStatementKind,
FilingFaithfulStatementSnapshotRow[]
>;
};
export type StandardizedStatementBundle = {
periods: FilingStatementSnapshotPeriod[];
statements: Record<FinancialStatementKind, StandardizedStatementSnapshotRow[]>;
statements: Record<
FinancialStatementKind,
StandardizedStatementSnapshotRow[]
>;
};
export type DimensionStatementBundle = {
@@ -70,7 +85,7 @@ export type FilingStatementSnapshotRecord = {
filing_id: number;
ticker: string;
filing_date: string;
filing_type: '10-K' | '10-Q';
filing_type: "10-K" | "10-Q";
period_end: string | null;
statement_bundle: FilingStatementBundle | null;
standardized_bundle: StandardizedStatementBundle | null;
@@ -86,7 +101,7 @@ export type UpsertFilingStatementSnapshotInput = {
filing_id: number;
ticker: string;
filing_date: string;
filing_type: '10-K' | '10-Q';
filing_type: "10-K" | "10-Q";
period_end: string | null;
statement_bundle: FilingStatementBundle | null;
standardized_bundle: StandardizedStatementBundle | null;
@@ -96,7 +111,9 @@ export type UpsertFilingStatementSnapshotInput = {
source: SnapshotSource;
};
function toSnapshotRecord(row: FilingStatementSnapshotRow): FilingStatementSnapshotRecord {
function toSnapshotRecord(
row: FilingStatementSnapshotRow,
): FilingStatementSnapshotRecord {
return {
id: row.id,
filing_id: row.filing_id,
@@ -111,7 +128,7 @@ function toSnapshotRecord(row: FilingStatementSnapshotRow): FilingStatementSnaps
parse_error: row.parse_error,
source: row.source,
created_at: row.created_at,
updated_at: row.updated_at
updated_at: row.updated_at,
};
}
@@ -131,7 +148,9 @@ export async function getFilingStatementSnapshotByFilingId(filingId: number) {
return row ? toSnapshotRecord(row) : null;
}
export async function upsertFilingStatementSnapshot(input: UpsertFilingStatementSnapshotInput) {
export async function upsertFilingStatementSnapshot(
input: UpsertFilingStatementSnapshotInput,
) {
const now = new Date().toISOString();
const [saved] = await db
@@ -149,7 +168,7 @@ export async function upsertFilingStatementSnapshot(input: UpsertFilingStatement
parse_error: input.parse_error,
source: input.source,
created_at: now,
updated_at: now
updated_at: now,
})
.onConflictDoUpdate({
target: filingStatementSnapshot.filing_id,
@@ -164,8 +183,8 @@ export async function upsertFilingStatementSnapshot(input: UpsertFilingStatement
parse_status: input.parse_status,
parse_error: input.parse_error,
source: input.source,
updated_at: now
}
updated_at: now,
},
})
.returning();
@@ -174,16 +193,20 @@ export async function upsertFilingStatementSnapshot(input: UpsertFilingStatement
export async function listFilingStatementSnapshotsByTicker(input: {
ticker: string;
window: '10y' | 'all';
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())];
const constraints = [
eq(filingStatementSnapshot.ticker, input.ticker.trim().toUpperCase()),
];
if (input.window === '10y') {
constraints.push(gte(filingStatementSnapshot.filing_date, tenYearsAgoIso()));
if (input.window === "10y") {
constraints.push(
gte(filingStatementSnapshot.filing_date, tenYearsAgoIso()),
);
}
if (cursorId && Number.isFinite(cursorId) && cursorId > 0) {
@@ -194,18 +217,21 @@ export async function listFilingStatementSnapshotsByTicker(input: {
.select()
.from(filingStatementSnapshot)
.where(and(...constraints))
.orderBy(desc(filingStatementSnapshot.filing_date), desc(filingStatementSnapshot.id))
.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 ?? '')
? String(usedRows[usedRows.length - 1]?.id ?? "")
: null;
return {
snapshots: usedRows.map(toSnapshotRecord),
nextCursor
nextCursor,
};
}
@@ -213,18 +239,21 @@ export async function countFilingStatementSnapshotStatuses(ticker: string) {
const rows = await db
.select({
status: filingStatementSnapshot.parse_status,
count: sql<string>`count(*)`
count: sql<string>`count(*)`,
})
.from(filingStatementSnapshot)
.where(eq(filingStatementSnapshot.ticker, ticker.trim().toUpperCase()))
.groupBy(filingStatementSnapshot.parse_status);
return rows.reduce<Record<ParseStatus, number>>((acc, row) => {
acc[row.status] = Number(row.count);
return acc;
}, {
ready: 0,
partial: 0,
failed: 0
});
return rows.reduce<Record<ParseStatus, number>>(
(acc, row) => {
acc[row.status] = Number(row.count);
return acc;
},
{
ready: 0,
partial: 0,
failed: 0,
},
);
}