Run playwright UI tests
This commit is contained in:
@@ -1,315 +1,48 @@
|
||||
import type {
|
||||
CompanyFinancialStatementsResponse,
|
||||
DimensionBreakdownRow,
|
||||
FilingFaithfulStatementRow,
|
||||
FinancialHistoryWindow,
|
||||
FinancialStatementKind,
|
||||
FinancialStatementMode,
|
||||
FinancialStatementPeriod,
|
||||
StandardizedStatementRow
|
||||
FinancialStatementKind
|
||||
} from '@/lib/types';
|
||||
import { listFilingsRecords } from '@/lib/server/repos/filings';
|
||||
import {
|
||||
countFilingStatementSnapshotStatuses,
|
||||
type DimensionStatementSnapshotRow,
|
||||
type FilingFaithfulStatementSnapshotRow,
|
||||
type FilingStatementSnapshotRecord,
|
||||
listFilingStatementSnapshotsByTicker,
|
||||
type StandardizedStatementSnapshotRow
|
||||
} from '@/lib/server/repos/filing-statements';
|
||||
defaultFinancialSyncLimit,
|
||||
getCompanyFinancialTaxonomy
|
||||
} from '@/lib/server/financial-taxonomy';
|
||||
|
||||
type GetCompanyFinancialStatementsInput = {
|
||||
ticker: string;
|
||||
mode: FinancialStatementMode;
|
||||
statement: FinancialStatementKind;
|
||||
window: FinancialHistoryWindow;
|
||||
includeDimensions: boolean;
|
||||
includeFacts?: boolean;
|
||||
factsCursor?: string | null;
|
||||
factsLimit?: number;
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
v2Enabled: boolean;
|
||||
v2Enabled?: boolean;
|
||||
v3Enabled?: boolean;
|
||||
queuedSync: boolean;
|
||||
};
|
||||
|
||||
type FinancialStatementRowByMode = StandardizedStatementRow | FilingFaithfulStatementRow;
|
||||
|
||||
function safeTicker(input: string) {
|
||||
return input.trim().toUpperCase();
|
||||
}
|
||||
|
||||
function isFinancialForm(type: string): type is '10-K' | '10-Q' {
|
||||
return type === '10-K' || type === '10-Q';
|
||||
}
|
||||
|
||||
function rowDimensionMatcher(row: { key: string; concept: string | null }, item: DimensionStatementSnapshotRow) {
|
||||
const concept = row.concept?.toLowerCase() ?? '';
|
||||
const itemConcept = item.concept?.toLowerCase() ?? '';
|
||||
if (item.rowKey === row.key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(concept && itemConcept && concept === itemConcept);
|
||||
}
|
||||
|
||||
function periodSorter(left: FinancialStatementPeriod, right: FinancialStatementPeriod) {
|
||||
const byDate = Date.parse(left.filingDate) - Date.parse(right.filingDate);
|
||||
if (Number.isFinite(byDate) && byDate !== 0) {
|
||||
return byDate;
|
||||
}
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
function resolveDimensionPeriodId(rawPeriodId: string, periods: FinancialStatementPeriod[]) {
|
||||
const exact = periods.find((period) => period.id === rawPeriodId);
|
||||
if (exact) {
|
||||
return exact.id;
|
||||
}
|
||||
|
||||
const byDate = periods.find((period) => period.filingDate === rawPeriodId || period.periodEnd === rawPeriodId);
|
||||
return byDate?.id ?? null;
|
||||
}
|
||||
|
||||
function getRowsForSnapshot(
|
||||
snapshot: FilingStatementSnapshotRecord,
|
||||
mode: FinancialStatementMode,
|
||||
statement: FinancialStatementKind
|
||||
) {
|
||||
if (mode === 'standardized') {
|
||||
return snapshot.standardized_bundle?.statements?.[statement] ?? [];
|
||||
}
|
||||
|
||||
return snapshot.statement_bundle?.statements?.[statement] ?? [];
|
||||
}
|
||||
|
||||
function buildPeriods(
|
||||
snapshots: FilingStatementSnapshotRecord[],
|
||||
mode: FinancialStatementMode,
|
||||
statement: FinancialStatementKind
|
||||
) {
|
||||
const map = new Map<string, FinancialStatementPeriod>();
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const rows = getRowsForSnapshot(snapshot, mode, statement);
|
||||
if (rows.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourcePeriods = mode === 'standardized'
|
||||
? snapshot.standardized_bundle?.periods
|
||||
: snapshot.statement_bundle?.periods;
|
||||
|
||||
for (const period of sourcePeriods ?? []) {
|
||||
if (!map.has(period.id)) {
|
||||
map.set(period.id, {
|
||||
id: period.id,
|
||||
filingId: period.filingId,
|
||||
accessionNumber: period.accessionNumber,
|
||||
filingDate: period.filingDate,
|
||||
periodEnd: period.periodEnd,
|
||||
filingType: period.filingType,
|
||||
periodLabel: period.periodLabel
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()].sort(periodSorter);
|
||||
}
|
||||
|
||||
function buildRows(
|
||||
snapshots: FilingStatementSnapshotRecord[],
|
||||
periods: FinancialStatementPeriod[],
|
||||
mode: FinancialStatementMode,
|
||||
statement: FinancialStatementKind,
|
||||
includeDimensions: boolean
|
||||
) {
|
||||
const rowMap = new Map<string, FinancialStatementRowByMode>();
|
||||
const dimensionMap = includeDimensions
|
||||
? new Map<string, DimensionBreakdownRow[]>()
|
||||
: null;
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const rows = getRowsForSnapshot(snapshot, mode, statement);
|
||||
const dimensions = snapshot.dimension_bundle?.statements?.[statement] ?? [];
|
||||
|
||||
if (mode === 'standardized') {
|
||||
for (const sourceRow of rows as StandardizedStatementSnapshotRow[]) {
|
||||
const existing = rowMap.get(sourceRow.key) as StandardizedStatementRow | undefined;
|
||||
const hasDimensions = dimensions.some((item) => rowDimensionMatcher(sourceRow, item));
|
||||
|
||||
if (!existing) {
|
||||
rowMap.set(sourceRow.key, {
|
||||
key: sourceRow.key,
|
||||
label: sourceRow.label,
|
||||
concept: sourceRow.concept,
|
||||
category: sourceRow.category,
|
||||
sourceConcepts: [...sourceRow.sourceConcepts],
|
||||
values: { ...sourceRow.values },
|
||||
hasDimensions
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.hasDimensions = existing.hasDimensions || hasDimensions;
|
||||
for (const concept of sourceRow.sourceConcepts) {
|
||||
if (!existing.sourceConcepts.includes(concept)) {
|
||||
existing.sourceConcepts.push(concept);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [periodId, value] of Object.entries(sourceRow.values)) {
|
||||
if (!(periodId in existing.values)) {
|
||||
existing.values[periodId] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const sourceRow of rows as FilingFaithfulStatementSnapshotRow[]) {
|
||||
const rowKey = sourceRow.concept ? `concept-${sourceRow.concept.toLowerCase()}` : `label-${sourceRow.key}`;
|
||||
const existing = rowMap.get(rowKey) as FilingFaithfulStatementRow | undefined;
|
||||
const hasDimensions = dimensions.some((item) => rowDimensionMatcher(sourceRow, item));
|
||||
|
||||
if (!existing) {
|
||||
rowMap.set(rowKey, {
|
||||
key: rowKey,
|
||||
label: sourceRow.label,
|
||||
concept: sourceRow.concept,
|
||||
order: sourceRow.order,
|
||||
depth: sourceRow.depth,
|
||||
isSubtotal: sourceRow.isSubtotal,
|
||||
values: { ...sourceRow.values },
|
||||
hasDimensions
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existing.hasDimensions = existing.hasDimensions || hasDimensions;
|
||||
existing.order = Math.min(existing.order, sourceRow.order);
|
||||
existing.depth = Math.min(existing.depth, sourceRow.depth);
|
||||
existing.isSubtotal = existing.isSubtotal || sourceRow.isSubtotal;
|
||||
for (const [periodId, value] of Object.entries(sourceRow.values)) {
|
||||
if (!(periodId in existing.values)) {
|
||||
existing.values[periodId] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dimensionMap) {
|
||||
for (const item of dimensions) {
|
||||
const periodId = resolveDimensionPeriodId(item.periodId, periods);
|
||||
if (!periodId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry: DimensionBreakdownRow = {
|
||||
rowKey: item.rowKey,
|
||||
concept: item.concept,
|
||||
periodId,
|
||||
axis: item.axis,
|
||||
member: item.member,
|
||||
value: item.value,
|
||||
unit: item.unit
|
||||
};
|
||||
|
||||
const group = dimensionMap.get(item.rowKey);
|
||||
if (group) {
|
||||
group.push(entry);
|
||||
} else {
|
||||
dimensionMap.set(item.rowKey, [entry]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [...rowMap.values()].sort((a, b) => {
|
||||
const left = mode === 'standardized' ? a.label : `${(a as FilingFaithfulStatementRow).order.toString().padStart(5, '0')}::${a.label}`;
|
||||
const right = mode === 'standardized' ? b.label : `${(b as FilingFaithfulStatementRow).order.toString().padStart(5, '0')}::${b.label}`;
|
||||
return left.localeCompare(right);
|
||||
});
|
||||
|
||||
if (mode === 'standardized') {
|
||||
const standardized = rows as StandardizedStatementRow[];
|
||||
const core = standardized.filter((row) => row.category === 'core');
|
||||
const nonCore = standardized.filter((row) => row.category !== 'core');
|
||||
const orderedRows = [...core, ...nonCore];
|
||||
|
||||
return {
|
||||
rows: orderedRows,
|
||||
dimensions: dimensionMap ? Object.fromEntries(dimensionMap.entries()) : null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: rows as FilingFaithfulStatementRow[],
|
||||
dimensions: dimensionMap ? Object.fromEntries(dimensionMap.entries()) : null
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultFinancialSyncLimit(window: FinancialHistoryWindow) {
|
||||
return window === 'all' ? 120 : 60;
|
||||
}
|
||||
|
||||
export async function getCompanyFinancialStatements(input: GetCompanyFinancialStatementsInput): Promise<CompanyFinancialStatementsResponse> {
|
||||
const ticker = safeTicker(input.ticker);
|
||||
const snapshotResult = await listFilingStatementSnapshotsByTicker({
|
||||
ticker,
|
||||
window: input.window,
|
||||
limit: input.limit,
|
||||
cursor: input.cursor
|
||||
});
|
||||
|
||||
const statuses = await countFilingStatementSnapshotStatuses(ticker);
|
||||
const filings = await listFilingsRecords({
|
||||
ticker,
|
||||
limit: input.window === 'all' ? 250 : 120
|
||||
});
|
||||
|
||||
const financialFilings = filings.filter((filing) => isFinancialForm(filing.filing_type));
|
||||
const periods = buildPeriods(snapshotResult.snapshots, input.mode, input.statement);
|
||||
const rowResult = buildRows(
|
||||
snapshotResult.snapshots,
|
||||
periods,
|
||||
input.mode,
|
||||
input.statement,
|
||||
input.includeDimensions
|
||||
);
|
||||
|
||||
const latestFiling = filings[0] ?? null;
|
||||
|
||||
return {
|
||||
company: {
|
||||
ticker,
|
||||
companyName: latestFiling?.company_name ?? ticker,
|
||||
cik: latestFiling?.cik ?? null
|
||||
},
|
||||
mode: input.mode,
|
||||
export async function getCompanyFinancialStatements(
|
||||
input: GetCompanyFinancialStatementsInput
|
||||
): Promise<CompanyFinancialStatementsResponse> {
|
||||
return await getCompanyFinancialTaxonomy({
|
||||
ticker: input.ticker,
|
||||
statement: input.statement,
|
||||
window: input.window,
|
||||
periods,
|
||||
rows: rowResult.rows,
|
||||
nextCursor: snapshotResult.nextCursor,
|
||||
coverage: {
|
||||
filings: periods.length,
|
||||
rows: rowResult.rows.length,
|
||||
dimensions: rowResult.dimensions
|
||||
? Object.values(rowResult.dimensions).reduce((total, rows) => total + rows.length, 0)
|
||||
: 0
|
||||
},
|
||||
dataSourceStatus: {
|
||||
enabled: input.v2Enabled,
|
||||
hydratedFilings: statuses.ready,
|
||||
partialFilings: statuses.partial,
|
||||
failedFilings: statuses.failed,
|
||||
pendingFilings: Math.max(0, financialFilings.length - statuses.ready - statuses.partial - statuses.failed),
|
||||
queuedSync: input.queuedSync
|
||||
},
|
||||
dimensionBreakdown: rowResult.dimensions
|
||||
};
|
||||
includeDimensions: input.includeDimensions,
|
||||
includeFacts: input.includeFacts ?? false,
|
||||
factsCursor: input.factsCursor,
|
||||
factsLimit: input.factsLimit,
|
||||
cursor: input.cursor,
|
||||
limit: input.limit,
|
||||
v3Enabled: input.v3Enabled ?? input.v2Enabled ?? true,
|
||||
queuedSync: input.queuedSync
|
||||
});
|
||||
}
|
||||
|
||||
export { defaultFinancialSyncLimit };
|
||||
|
||||
export const __financialStatementsInternals = {
|
||||
buildPeriods,
|
||||
buildRows,
|
||||
defaultFinancialSyncLimit
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user