2066 lines
74 KiB
TypeScript
2066 lines
74 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import {
|
|
__financialTaxonomyInternals,
|
|
getCompanyFinancialTaxonomy
|
|
} from './financial-taxonomy';
|
|
import type { FilingTaxonomySnapshotRecord } from './repos/filing-taxonomy';
|
|
import type {
|
|
FinancialStatementKind,
|
|
FinancialStatementPeriod,
|
|
StructuredKpiRow,
|
|
TaxonomyFactRow,
|
|
TaxonomyStatementRow
|
|
} from '@/lib/types';
|
|
|
|
function createRow(input: {
|
|
key?: string;
|
|
label?: string;
|
|
conceptKey?: string;
|
|
qname?: string;
|
|
localName?: string;
|
|
statement?: FinancialStatementKind;
|
|
roleUri?: string | null;
|
|
order?: number;
|
|
depth?: number;
|
|
hasDimensions?: boolean;
|
|
values: Record<string, number | null>;
|
|
sourceFactIds?: number[];
|
|
unit?: string | null;
|
|
}): TaxonomyStatementRow {
|
|
const localName = input.localName ?? 'RevenueFromContractWithCustomerExcludingAssessedTax';
|
|
const conceptKey = input.conceptKey ?? `http://fasb.org/us-gaap/2024#${localName}`;
|
|
const qname = input.qname ?? `us-gaap:${localName}`;
|
|
|
|
return {
|
|
key: input.key ?? conceptKey,
|
|
label: input.label ?? localName,
|
|
conceptKey,
|
|
qname,
|
|
namespaceUri: 'http://fasb.org/us-gaap/2024',
|
|
localName,
|
|
isExtension: false,
|
|
statement: input.statement ?? 'income',
|
|
roleUri: input.roleUri ?? input.statement ?? 'income',
|
|
order: input.order ?? 1,
|
|
depth: input.depth ?? 0,
|
|
parentKey: null,
|
|
values: input.values,
|
|
units: Object.fromEntries(Object.keys(input.values).map((periodId) => [periodId, input.unit ?? 'iso4217:USD'])),
|
|
hasDimensions: input.hasDimensions ?? false,
|
|
sourceFactIds: input.sourceFactIds ?? [1]
|
|
};
|
|
}
|
|
|
|
function createSnapshot(input: {
|
|
filingId: number;
|
|
filingType: '10-K' | '10-Q';
|
|
filingDate: string;
|
|
periods: Array<{
|
|
id: string;
|
|
periodStart: string | null;
|
|
periodEnd: string;
|
|
periodLabel: string;
|
|
}>;
|
|
statement: FinancialStatementKind;
|
|
rows?: TaxonomyStatementRow[];
|
|
}) {
|
|
const defaultRow = createRow({
|
|
statement: input.statement,
|
|
values: Object.fromEntries(input.periods.map((period, index) => [period.id, 100 + index]))
|
|
});
|
|
const faithfulRows = {
|
|
income: input.statement === 'income' ? (input.rows ?? [defaultRow]) : [],
|
|
balance: input.statement === 'balance' ? (input.rows ?? [{ ...defaultRow, statement: 'balance' }]) : [],
|
|
cash_flow: input.statement === 'cash_flow' ? (input.rows ?? [{ ...defaultRow, statement: 'cash_flow' }]) : [],
|
|
equity: [],
|
|
comprehensive_income: []
|
|
} satisfies FilingTaxonomySnapshotRecord['faithful_rows'];
|
|
|
|
return {
|
|
id: input.filingId,
|
|
filing_id: input.filingId,
|
|
ticker: 'MSFT',
|
|
filing_date: input.filingDate,
|
|
filing_type: input.filingType,
|
|
parse_status: 'ready',
|
|
parse_error: null,
|
|
source: 'xbrl_instance',
|
|
parser_engine: 'fiscal-xbrl',
|
|
parser_version: '0.1.0',
|
|
taxonomy_regime: 'us-gaap',
|
|
fiscal_pack: 'core',
|
|
periods: input.periods.map((period) => ({
|
|
id: period.id,
|
|
filingId: input.filingId,
|
|
accessionNumber: `0000-${input.filingId}`,
|
|
filingDate: input.filingDate,
|
|
periodStart: period.periodStart,
|
|
periodEnd: period.periodEnd,
|
|
filingType: input.filingType,
|
|
periodLabel: period.periodLabel
|
|
})),
|
|
faithful_rows: faithfulRows,
|
|
statement_rows: faithfulRows,
|
|
surface_rows: {
|
|
income: [],
|
|
balance: [],
|
|
cash_flow: [],
|
|
equity: [],
|
|
comprehensive_income: []
|
|
},
|
|
detail_rows: {
|
|
income: {},
|
|
balance: {},
|
|
cash_flow: {},
|
|
equity: {},
|
|
comprehensive_income: {}
|
|
},
|
|
kpi_rows: [],
|
|
derived_metrics: null,
|
|
validation_result: null,
|
|
normalization_summary: null,
|
|
facts_count: 0,
|
|
concepts_count: 0,
|
|
dimensions_count: 0,
|
|
created_at: input.filingDate,
|
|
updated_at: input.filingDate
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
}
|
|
|
|
function createPeriod(input: {
|
|
id: string;
|
|
filingId: number;
|
|
filingDate: string;
|
|
periodEnd: string;
|
|
periodStart?: string | null;
|
|
filingType?: '10-K' | '10-Q';
|
|
}): FinancialStatementPeriod {
|
|
return {
|
|
id: input.id,
|
|
filingId: input.filingId,
|
|
accessionNumber: `0000-${input.filingId}`,
|
|
filingDate: input.filingDate,
|
|
periodStart: input.periodStart ?? null,
|
|
periodEnd: input.periodEnd,
|
|
filingType: input.filingType ?? '10-Q',
|
|
periodLabel: 'Test period'
|
|
};
|
|
}
|
|
|
|
function createDimensionFact(input: {
|
|
filingId: number;
|
|
filingDate: string;
|
|
conceptKey: string;
|
|
qname: string;
|
|
localName: string;
|
|
periodEnd: string;
|
|
value: number;
|
|
axis?: string;
|
|
member?: string;
|
|
}): TaxonomyFactRow {
|
|
return {
|
|
id: input.filingId,
|
|
snapshotId: input.filingId,
|
|
filingId: input.filingId,
|
|
filingDate: input.filingDate,
|
|
statement: 'income',
|
|
roleUri: 'income',
|
|
conceptKey: input.conceptKey,
|
|
qname: input.qname,
|
|
namespaceUri: 'http://fasb.org/us-gaap/2024',
|
|
localName: input.localName,
|
|
value: input.value,
|
|
contextId: `ctx-${input.filingId}`,
|
|
unit: 'iso4217:USD',
|
|
decimals: null,
|
|
periodStart: '2025-01-01',
|
|
periodEnd: input.periodEnd,
|
|
periodInstant: null,
|
|
dimensions: [{
|
|
axis: input.axis ?? 'srt:ProductOrServiceAxis',
|
|
member: input.member ?? 'msft:CloudMember'
|
|
}],
|
|
isDimensionless: false,
|
|
sourceFile: null
|
|
};
|
|
}
|
|
|
|
function createFact(input: {
|
|
id?: number;
|
|
filingId: number;
|
|
filingDate: string;
|
|
statement?: FinancialStatementKind;
|
|
conceptKey?: string;
|
|
qname?: string;
|
|
localName: string;
|
|
periodEnd: string;
|
|
periodStart?: string | null;
|
|
periodInstant?: string | null;
|
|
value: number;
|
|
unit?: string | null;
|
|
}): TaxonomyFactRow {
|
|
const conceptKey = input.conceptKey ?? `http://fasb.org/us-gaap/2024#${input.localName}`;
|
|
const qname = input.qname ?? `us-gaap:${input.localName}`;
|
|
|
|
return {
|
|
id: input.id ?? input.filingId,
|
|
snapshotId: input.filingId,
|
|
filingId: input.filingId,
|
|
filingDate: input.filingDate,
|
|
statement: input.statement ?? 'income',
|
|
roleUri: input.statement ?? 'income',
|
|
conceptKey,
|
|
qname,
|
|
namespaceUri: 'http://fasb.org/us-gaap/2024',
|
|
localName: input.localName,
|
|
value: input.value,
|
|
contextId: `ctx-${input.filingId}-${input.localName}`,
|
|
unit: input.unit ?? 'iso4217:USD',
|
|
decimals: null,
|
|
periodStart: input.periodStart ?? null,
|
|
periodEnd: input.periodEnd,
|
|
periodInstant: input.periodInstant ?? null,
|
|
dimensions: [],
|
|
isDimensionless: true,
|
|
sourceFile: null
|
|
};
|
|
}
|
|
|
|
function createKpiRow(input: {
|
|
key: string;
|
|
values: Record<string, number | null>;
|
|
provenanceType?: StructuredKpiRow['provenanceType'];
|
|
sourceConcepts?: string[];
|
|
sourceFactIds?: number[];
|
|
}): StructuredKpiRow {
|
|
return {
|
|
key: input.key,
|
|
label: input.key,
|
|
category: 'operating_kpi',
|
|
unit: 'percent',
|
|
order: 10,
|
|
segment: null,
|
|
axis: null,
|
|
member: null,
|
|
values: input.values,
|
|
sourceConcepts: input.sourceConcepts ?? [],
|
|
sourceFactIds: input.sourceFactIds ?? [],
|
|
provenanceType: input.provenanceType ?? 'taxonomy',
|
|
hasDimensions: false
|
|
};
|
|
}
|
|
|
|
function findRow(rows: ReturnType<typeof __financialTaxonomyInternals.buildStandardizedRows>, key: string) {
|
|
const row = rows.find((entry) => entry.key === key);
|
|
expect(row).toBeDefined();
|
|
return row!;
|
|
}
|
|
|
|
function findPeriodId(periods: FinancialStatementPeriod[], periodEnd: string) {
|
|
const period = periods.find((entry) => entry.periodEnd === periodEnd);
|
|
expect(period).toBeDefined();
|
|
return period!.id;
|
|
}
|
|
|
|
function findStandardizedResponseRow(
|
|
response: Awaited<ReturnType<typeof getCompanyFinancialTaxonomy>>,
|
|
key: string
|
|
) {
|
|
const row = response.statementRows?.standardized.find((entry) => entry.key === key);
|
|
expect(row).toBeDefined();
|
|
return row!;
|
|
}
|
|
|
|
describe('financial taxonomy internals', () => {
|
|
it('selects the primary quarter duration for 10-Q income statements', () => {
|
|
const snapshot = createSnapshot({
|
|
filingId: 1,
|
|
filingType: '10-Q',
|
|
filingDate: '2026-01-28',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'instant', periodStart: null, periodEnd: '2025-12-31', periodLabel: 'Instant' },
|
|
{ id: 'quarter', periodStart: '2025-10-01', periodEnd: '2025-12-31', periodLabel: '2025-10-01 to 2025-12-31' },
|
|
{ id: 'ytd', periodStart: '2025-07-01', periodEnd: '2025-12-31', periodLabel: '2025-07-01 to 2025-12-31' }
|
|
]
|
|
});
|
|
|
|
const selection = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([snapshot], 'income', 'quarterly');
|
|
|
|
expect(selection.periods).toHaveLength(1);
|
|
expect(selection.periods[0]?.id).toBe('quarter');
|
|
});
|
|
|
|
it('selects the latest instant for balance sheets', () => {
|
|
const snapshot = createSnapshot({
|
|
filingId: 2,
|
|
filingType: '10-K',
|
|
filingDate: '2025-07-30',
|
|
statement: 'balance',
|
|
periods: [
|
|
{ id: 'prior', periodStart: null, periodEnd: '2024-06-30', periodLabel: 'Instant' },
|
|
{ id: 'current', periodStart: null, periodEnd: '2025-06-30', periodLabel: 'Instant' }
|
|
]
|
|
});
|
|
|
|
const selection = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([snapshot], 'balance', 'annual');
|
|
|
|
expect(selection.periods).toHaveLength(1);
|
|
expect(selection.periods[0]?.id).toBe('current');
|
|
});
|
|
|
|
it('builds one reporting period per filing for the selected statement', () => {
|
|
const annual = createSnapshot({
|
|
filingId: 10,
|
|
filingType: '10-K',
|
|
filingDate: '2025-07-30',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'annual', periodStart: '2024-07-01', periodEnd: '2025-06-30', periodLabel: '2024-07-01 to 2025-06-30' },
|
|
{ id: 'quarter', periodStart: '2025-04-01', periodEnd: '2025-06-30', periodLabel: '2025-04-01 to 2025-06-30' }
|
|
]
|
|
});
|
|
const quarterly = createSnapshot({
|
|
filingId: 11,
|
|
filingType: '10-Q',
|
|
filingDate: '2025-10-29',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'instant', periodStart: null, periodEnd: '2025-09-30', periodLabel: 'Instant' },
|
|
{ id: 'quarter', periodStart: '2025-07-01', periodEnd: '2025-09-30', periodLabel: '2025-07-01 to 2025-09-30' },
|
|
{ id: 'ytd', periodStart: '2025-01-01', periodEnd: '2025-09-30', periodLabel: '2025-01-01 to 2025-09-30' }
|
|
]
|
|
});
|
|
|
|
const annualPeriods = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([annual, quarterly], 'income', 'annual').periods;
|
|
const quarterlyPeriods = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([annual, quarterly], 'income', 'quarterly').periods;
|
|
|
|
expect(annualPeriods.map((period) => period.id)).toEqual(['annual']);
|
|
expect(quarterlyPeriods.map((period) => period.id)).toEqual(['quarter']);
|
|
});
|
|
|
|
it('ignores future-dated fallback note periods for annual income selection', () => {
|
|
const snapshot = createSnapshot({
|
|
filingId: 12,
|
|
filingType: '10-K',
|
|
filingDate: '2025-11-05',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'annual', periodStart: '2024-09-30', periodEnd: '2025-09-28', periodLabel: '2024-09-30 to 2025-09-28' },
|
|
{ id: 'future-note', periodStart: '2026-09-28', periodEnd: '2027-09-26', periodLabel: '2026-09-28 to 2027-09-26' }
|
|
],
|
|
rows: [
|
|
createRow({
|
|
localName: 'Revenues',
|
|
label: 'Revenues',
|
|
values: { annual: 44_284_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CostOfSales',
|
|
label: 'Cost of Sales',
|
|
values: { annual: 19_738_000_000 },
|
|
order: 2
|
|
}),
|
|
createRow({
|
|
localName: 'EffectiveIncomeTaxRateContinuingOperations',
|
|
label: 'Effective Income Tax Rate Continuing Operations',
|
|
roleUri: null,
|
|
order: Number.MAX_SAFE_INTEGER,
|
|
values: { 'future-note': 0.16 },
|
|
unit: 'pure'
|
|
})
|
|
]
|
|
});
|
|
|
|
const selection = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([snapshot], 'income', 'annual');
|
|
|
|
expect(selection.periods).toHaveLength(1);
|
|
expect(selection.periods[0]?.id).toBe('annual');
|
|
});
|
|
|
|
it('prefers broader presented-row coverage over sparser later annual periods', () => {
|
|
const snapshot = createSnapshot({
|
|
filingId: 13,
|
|
filingType: '10-K',
|
|
filingDate: '2025-07-30',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'primary', periodStart: '2024-07-01', periodEnd: '2025-06-30', periodLabel: '2024-07-01 to 2025-06-30' },
|
|
{ id: 'sparse-later', periodStart: '2024-07-16', periodEnd: '2025-07-15', periodLabel: '2024-07-16 to 2025-07-15' }
|
|
],
|
|
rows: [
|
|
createRow({
|
|
localName: 'Revenues',
|
|
label: 'Revenues',
|
|
values: { primary: 245_122_000_000, 'sparse-later': 246_000_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'OperatingIncomeLoss',
|
|
label: 'Operating Income',
|
|
values: { primary: 109_433_000_000 },
|
|
order: 2
|
|
}),
|
|
createRow({
|
|
localName: 'NetIncomeLoss',
|
|
label: 'Net Income',
|
|
values: { primary: 88_136_000_000 },
|
|
order: 3
|
|
})
|
|
]
|
|
});
|
|
|
|
const selection = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([snapshot], 'income', 'annual');
|
|
|
|
expect(selection.periods).toHaveLength(1);
|
|
expect(selection.periods[0]?.id).toBe('primary');
|
|
});
|
|
|
|
it('falls back to plausible non-presented periods when no presented rows exist', () => {
|
|
const snapshot = createSnapshot({
|
|
filingId: 14,
|
|
filingType: '10-K',
|
|
filingDate: '2025-11-05',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'annual', periodStart: '2024-09-30', periodEnd: '2025-09-28', periodLabel: '2024-09-30 to 2025-09-28' },
|
|
{ id: 'future-note', periodStart: '2026-09-28', periodEnd: '2027-09-26', periodLabel: '2026-09-28 to 2027-09-26' }
|
|
],
|
|
rows: [
|
|
createRow({
|
|
localName: 'Revenues',
|
|
label: 'Revenues',
|
|
roleUri: null,
|
|
order: Number.MAX_SAFE_INTEGER,
|
|
values: { annual: 44_284_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'EffectiveIncomeTaxRateContinuingOperations',
|
|
label: 'Effective Income Tax Rate Continuing Operations',
|
|
roleUri: null,
|
|
order: Number.MAX_SAFE_INTEGER,
|
|
values: { 'future-note': 0.16 },
|
|
unit: 'pure'
|
|
})
|
|
]
|
|
});
|
|
|
|
const selection = __financialTaxonomyInternals.selectPrimaryPeriodsByCadence([snapshot], 'income', 'annual');
|
|
|
|
expect(selection.periods).toHaveLength(1);
|
|
expect(selection.periods[0]?.id).toBe('annual');
|
|
});
|
|
|
|
it('maps overlapping GAAP aliases into one standardized COGS row while preserving faithful rows', () => {
|
|
const period2024 = createPeriod({
|
|
id: '2024-q4',
|
|
filingId: 30,
|
|
filingDate: '2025-01-29',
|
|
periodEnd: '2024-12-31'
|
|
});
|
|
const period2025 = createPeriod({
|
|
id: '2025-q4',
|
|
filingId: 31,
|
|
filingDate: '2026-01-28',
|
|
periodEnd: '2025-12-31'
|
|
});
|
|
|
|
const faithfulRows = __financialTaxonomyInternals.buildRows([
|
|
createSnapshot({
|
|
filingId: 30,
|
|
filingType: '10-Q',
|
|
filingDate: '2025-01-29',
|
|
statement: 'income',
|
|
periods: [{
|
|
id: '2024-q4',
|
|
periodStart: '2024-10-01',
|
|
periodEnd: '2024-12-31',
|
|
periodLabel: '2024-10-01 to 2024-12-31'
|
|
}],
|
|
rows: [
|
|
createRow({
|
|
localName: 'CostOfRevenue',
|
|
label: 'Cost of Revenue',
|
|
values: { '2024-q4': 45_000 },
|
|
sourceFactIds: [101]
|
|
})
|
|
]
|
|
}),
|
|
createSnapshot({
|
|
filingId: 31,
|
|
filingType: '10-Q',
|
|
filingDate: '2026-01-28',
|
|
statement: 'income',
|
|
periods: [{
|
|
id: '2025-q4',
|
|
periodStart: '2025-10-01',
|
|
periodEnd: '2025-12-31',
|
|
periodLabel: '2025-10-01 to 2025-12-31'
|
|
}],
|
|
rows: [
|
|
createRow({
|
|
localName: 'CostOfGoodsSold',
|
|
label: 'Cost of Goods Sold',
|
|
values: { '2025-q4': 48_000 },
|
|
sourceFactIds: [202]
|
|
})
|
|
]
|
|
})
|
|
], 'income', new Set(['2024-q4', '2025-q4']));
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows(
|
|
{
|
|
rows: faithfulRows,
|
|
statement: 'income',
|
|
periods: [period2024, period2025],
|
|
facts: []
|
|
}
|
|
);
|
|
|
|
expect(faithfulRows).toHaveLength(2);
|
|
|
|
const cogs = standardizedRows.find((row) => row.key === 'cost_of_revenue');
|
|
expect(cogs).toBeDefined();
|
|
expect(cogs?.values['2024-q4']).toBe(45_000);
|
|
expect(cogs?.values['2025-q4']).toBe(48_000);
|
|
expect(cogs?.sourceConcepts).toEqual([
|
|
'us-gaap:CostOfGoodsSold',
|
|
'us-gaap:CostOfRevenue'
|
|
]);
|
|
expect(cogs?.sourceRowKeys).toHaveLength(2);
|
|
});
|
|
|
|
it('aggregates standardized dimension drill-down across mapped source concepts', () => {
|
|
const period2024 = createPeriod({
|
|
id: '2024-q4',
|
|
filingId: 40,
|
|
filingDate: '2025-01-29',
|
|
periodEnd: '2024-12-31'
|
|
});
|
|
const period2025 = createPeriod({
|
|
id: '2025-q4',
|
|
filingId: 41,
|
|
filingDate: '2026-01-28',
|
|
periodEnd: '2025-12-31'
|
|
});
|
|
const faithfulRows = [
|
|
createRow({
|
|
localName: 'CostOfRevenue',
|
|
label: 'Cost of Revenue',
|
|
values: { '2024-q4': 45_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CostOfGoodsSold',
|
|
label: 'Cost of Goods Sold',
|
|
values: { '2025-q4': 48_000 }
|
|
})
|
|
];
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows(
|
|
{
|
|
rows: faithfulRows,
|
|
statement: 'income',
|
|
periods: [period2024, period2025],
|
|
facts: []
|
|
}
|
|
);
|
|
|
|
const breakdown = __financialTaxonomyInternals.buildDimensionBreakdown([
|
|
createDimensionFact({
|
|
filingId: 40,
|
|
filingDate: '2025-01-29',
|
|
conceptKey: faithfulRows[0].key,
|
|
qname: faithfulRows[0].qname,
|
|
localName: faithfulRows[0].localName,
|
|
periodEnd: '2024-12-31',
|
|
value: 20_000,
|
|
member: 'msft:ProductivityMember'
|
|
}),
|
|
createDimensionFact({
|
|
filingId: 41,
|
|
filingDate: '2026-01-28',
|
|
conceptKey: faithfulRows[1].key,
|
|
qname: faithfulRows[1].qname,
|
|
localName: faithfulRows[1].localName,
|
|
periodEnd: '2025-12-31',
|
|
value: 28_000,
|
|
member: 'msft:IntelligentCloudMember'
|
|
})
|
|
], [period2024, period2025], faithfulRows, standardizedRows);
|
|
|
|
const cogs = breakdown?.['cost_of_revenue'] ?? [];
|
|
expect(cogs).toHaveLength(2);
|
|
expect(cogs.map((row) => row.sourceLabel)).toEqual([
|
|
'Cost of Revenue',
|
|
'Cost of Goods Sold'
|
|
]);
|
|
});
|
|
|
|
it('prefers exact Microsoft income aliases over pro forma and reconciliation rows', () => {
|
|
const period = createPeriod({
|
|
id: '2024-fy',
|
|
filingId: 80,
|
|
filingDate: '2024-07-30',
|
|
periodStart: '2023-07-01',
|
|
periodEnd: '2024-06-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'income',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
localName: 'BusinessAcquisitionsProFormaRevenue',
|
|
label: 'Business Acquisitions Pro Forma Revenue',
|
|
values: { '2024-fy': 247_442_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'RevenueFromContractWithCustomerExcludingAssessedTax',
|
|
label: 'Revenue From Contract With Customer Excluding Assessed Tax',
|
|
values: { '2024-fy': 245_122_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'NonoperatingIncomeExpense',
|
|
label: 'Nonoperating Income Expense',
|
|
values: { '2024-fy': -1_646_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'SellingAndMarketingExpense',
|
|
label: 'Selling And Marketing Expense',
|
|
values: { '2024-fy': 24_456_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'GeneralAndAdministrativeExpense',
|
|
label: 'General And Administrative Expense',
|
|
values: { '2024-fy': 7_609_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'OperatingIncomeLoss',
|
|
label: 'Operating Income Loss',
|
|
values: { '2024-fy': 109_433_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'BusinessAcquisitionsProFormaNetIncomeLoss',
|
|
label: 'Business Acquisitions Pro Forma Net Income Loss',
|
|
values: { '2024-fy': 88_308_000_000 },
|
|
hasDimensions: true
|
|
}),
|
|
createRow({
|
|
localName: 'NetIncomeLoss',
|
|
label: 'Net Income Loss',
|
|
values: { '2024-fy': 88_136_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CurrentIncomeTaxExpenseBenefit',
|
|
label: 'Current Income Tax Expense Benefit',
|
|
values: { '2024-fy': 24_389_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'IncomeTaxExpenseBenefit',
|
|
label: 'Income Tax Expense Benefit',
|
|
values: { '2024-fy': 19_651_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'IncomeLossFromContinuingOperationsBeforeIncomeTaxesExtraordinaryItemsNoncontrollingInterest',
|
|
label: 'Income Loss From Continuing Operations Before Income Taxes Extraordinary Items Noncontrolling Interest',
|
|
values: { '2024-fy': 107_787_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'EffectiveIncomeTaxRateReconciliationInterestIncomeExpense',
|
|
label: 'Effective Income Tax Rate Reconciliation Interest Income Expense',
|
|
values: { '2024-fy': 0.011 },
|
|
unit: 'pure'
|
|
}),
|
|
createRow({
|
|
localName: 'InvestmentIncomeNet',
|
|
label: 'Investment Income Net',
|
|
values: { '2024-fy': 3_157_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CostOfGoodsAndServicesSold',
|
|
label: 'Cost Of Goods And Services Sold',
|
|
values: { '2024-fy': 74_114_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
const revenue = findRow(standardizedRows, 'revenue');
|
|
expect(revenue.values['2024-fy']).toBe(245_122_000_000);
|
|
expect(revenue.resolvedSourceRowKeys['2024-fy']).toContain('RevenueFromContractWithCustomerExcludingAssessedTax');
|
|
|
|
const operatingIncome = findRow(standardizedRows, 'operating_income');
|
|
expect(operatingIncome.values['2024-fy']).toBe(109_433_000_000);
|
|
expect(operatingIncome.resolvedSourceRowKeys['2024-fy']).toContain('OperatingIncomeLoss');
|
|
|
|
const sga = findRow(standardizedRows, 'selling_general_and_administrative');
|
|
expect(sga.values['2024-fy']).toBe(32_065_000_000);
|
|
expect(sga.formulaKey).toBe('selling_general_and_administrative');
|
|
|
|
const netIncome = findRow(standardizedRows, 'net_income');
|
|
expect(netIncome.values['2024-fy']).toBe(88_136_000_000);
|
|
expect(netIncome.resolvedSourceRowKeys['2024-fy']).toContain('NetIncomeLoss');
|
|
|
|
const taxExpense = findRow(standardizedRows, 'income_tax_expense');
|
|
expect(taxExpense.values['2024-fy']).toBe(19_651_000_000);
|
|
expect(taxExpense.resolvedSourceRowKeys['2024-fy']).toContain('IncomeTaxExpenseBenefit');
|
|
|
|
const pretaxIncome = findRow(standardizedRows, 'pretax_income');
|
|
expect(pretaxIncome.values['2024-fy']).toBe(107_787_000_000);
|
|
|
|
const interestIncome = findRow(standardizedRows, 'interest_income');
|
|
expect(interestIncome.values['2024-fy']).toBe(3_157_000_000);
|
|
expect(interestIncome.resolvedSourceRowKeys['2024-fy']).toContain('InvestmentIncomeNet');
|
|
|
|
const cogs = findRow(standardizedRows, 'cost_of_revenue');
|
|
expect(cogs.label).toBe('Cost of Sales');
|
|
expect(cogs.values['2024-fy']).toBe(74_114_000_000);
|
|
|
|
expect(standardizedRows.some((row) => row.key.includes('BusinessAcquisitionsProFormaRevenue'))).toBe(true);
|
|
expect(standardizedRows.some((row) => row.key.includes('BusinessAcquisitionsProFormaNetIncomeLoss'))).toBe(true);
|
|
});
|
|
|
|
it('merges detail rows across taxonomy-version concept drift for Microsoft residuals', () => {
|
|
const aggregated = __financialTaxonomyInternals.aggregateDetailRows({
|
|
statement: 'income',
|
|
selectedPeriodIds: new Set(['2024-fy', '2025-fy']),
|
|
snapshots: [
|
|
{
|
|
...createSnapshot({
|
|
filingId: 90,
|
|
filingType: '10-K',
|
|
filingDate: '2024-07-30',
|
|
statement: 'income',
|
|
periods: [{
|
|
id: '2024-fy',
|
|
periodStart: '2023-07-01',
|
|
periodEnd: '2024-06-30',
|
|
periodLabel: 'FY24'
|
|
}]
|
|
}),
|
|
detail_rows: {
|
|
income: {
|
|
unmapped: [
|
|
{
|
|
key: 'http://fasb.org/us-gaap/2024#AdvertisingExpense',
|
|
parentSurfaceKey: 'unmapped',
|
|
label: 'Advertising Expense',
|
|
conceptKey: 'http://fasb.org/us-gaap/2024#AdvertisingExpense',
|
|
qname: 'us-gaap:AdvertisingExpense',
|
|
namespaceUri: 'http://fasb.org/us-gaap/2024',
|
|
localName: 'AdvertisingExpense',
|
|
unit: 'iso4217:USD',
|
|
values: { '2024-fy': 1_500_000_000 },
|
|
sourceFactIds: [101],
|
|
isExtension: false,
|
|
dimensionsSummary: [],
|
|
residualFlag: true
|
|
},
|
|
{
|
|
key: 'https://xbrl.microsoft.com/2024#BusinessAcquisitionsProFormaRevenue',
|
|
parentSurfaceKey: 'unmapped',
|
|
label: 'Business Acquisitions Pro Forma Revenue',
|
|
conceptKey: 'https://xbrl.microsoft.com/2024#BusinessAcquisitionsProFormaRevenue',
|
|
qname: 'msft:BusinessAcquisitionsProFormaRevenue',
|
|
namespaceUri: 'https://xbrl.microsoft.com/2024',
|
|
localName: 'BusinessAcquisitionsProFormaRevenue',
|
|
unit: 'iso4217:USD',
|
|
values: { '2024-fy': 247_442_000_000 },
|
|
sourceFactIds: [102],
|
|
isExtension: true,
|
|
dimensionsSummary: [],
|
|
residualFlag: true
|
|
}
|
|
]
|
|
},
|
|
balance: {},
|
|
cash_flow: {},
|
|
equity: {},
|
|
comprehensive_income: {}
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord,
|
|
{
|
|
...createSnapshot({
|
|
filingId: 91,
|
|
filingType: '10-K',
|
|
filingDate: '2025-07-30',
|
|
statement: 'income',
|
|
periods: [{
|
|
id: '2025-fy',
|
|
periodStart: '2024-07-01',
|
|
periodEnd: '2025-06-30',
|
|
periodLabel: 'FY25'
|
|
}]
|
|
}),
|
|
detail_rows: {
|
|
income: {
|
|
unmapped: [
|
|
{
|
|
key: 'http://fasb.org/us-gaap/2025#AdvertisingExpense',
|
|
parentSurfaceKey: 'unmapped',
|
|
label: 'Advertising Expense',
|
|
conceptKey: 'http://fasb.org/us-gaap/2025#AdvertisingExpense',
|
|
qname: 'us-gaap:AdvertisingExpense',
|
|
namespaceUri: 'http://fasb.org/us-gaap/2025',
|
|
localName: 'AdvertisingExpense',
|
|
unit: 'iso4217:USD',
|
|
values: { '2025-fy': 1_700_000_000 },
|
|
sourceFactIds: [201],
|
|
isExtension: false,
|
|
dimensionsSummary: [],
|
|
residualFlag: true
|
|
},
|
|
{
|
|
key: 'https://xbrl.microsoft.com/2025#BusinessAcquisitionsProFormaRevenue',
|
|
parentSurfaceKey: 'unmapped',
|
|
label: 'Business Acquisitions Pro Forma Revenue',
|
|
conceptKey: 'https://xbrl.microsoft.com/2025#BusinessAcquisitionsProFormaRevenue',
|
|
qname: 'msft:BusinessAcquisitionsProFormaRevenue',
|
|
namespaceUri: 'https://xbrl.microsoft.com/2025',
|
|
localName: 'BusinessAcquisitionsProFormaRevenue',
|
|
unit: 'iso4217:USD',
|
|
values: { '2025-fy': 219_790_000_000 },
|
|
sourceFactIds: [202],
|
|
isExtension: true,
|
|
dimensionsSummary: [],
|
|
residualFlag: true
|
|
}
|
|
]
|
|
},
|
|
balance: {},
|
|
cash_flow: {},
|
|
equity: {},
|
|
comprehensive_income: {}
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord
|
|
]
|
|
});
|
|
|
|
const unmapped = aggregated.unmapped ?? [];
|
|
expect(unmapped).toHaveLength(2);
|
|
|
|
const advertising = unmapped.find((row) => row.label === 'Advertising Expense');
|
|
expect(advertising?.values).toEqual({
|
|
'2024-fy': 1_500_000_000,
|
|
'2025-fy': 1_700_000_000
|
|
});
|
|
|
|
const proFormaRevenue = unmapped.find((row) => row.label === 'Business Acquisitions Pro Forma Revenue');
|
|
expect(proFormaRevenue?.values).toEqual({
|
|
'2024-fy': 247_442_000_000,
|
|
'2025-fy': 219_790_000_000
|
|
});
|
|
});
|
|
|
|
it('resolves CASY-style income gaps with ordered fallbacks and tighter interest income exclusions', () => {
|
|
const period = createPeriod({
|
|
id: '2025-fy',
|
|
filingId: 81,
|
|
filingDate: '2025-06-23',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'income',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
localName: 'Revenues',
|
|
label: 'Revenues',
|
|
values: { '2025-fy': 15_940_899_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CostOfGoodsAndServiceExcludingDepreciationDepletionAndAmortization',
|
|
label: 'Cost Of Goods And Service Excluding Depreciation Depletion And Amortization',
|
|
values: { '2025-fy': 12_188_496_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'OperatingExpenses',
|
|
label: 'Operating Expenses',
|
|
values: { '2025-fy': 2_552_356_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'CostOfGoodsAndServicesSoldDepreciationAndAmortization',
|
|
label: 'Cost Of Goods And Services Sold Depreciation And Amortization',
|
|
values: { '2025-fy': 403_647_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'NetIncomeLoss',
|
|
label: 'Net Income Loss',
|
|
values: { '2025-fy': 546_520_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'IncomeTaxExpenseBenefit',
|
|
label: 'Income Tax Expense Benefit',
|
|
values: { '2025-fy': 165_929_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'InterestExpense',
|
|
label: 'Interest Expense',
|
|
values: { '2025-fy': 83_951_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'InterestIncomeExpenseNet',
|
|
label: 'Interest Income Expense Net',
|
|
values: { '2025-fy': 13_102_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'EffectiveIncomeTaxRateContinuingOperations',
|
|
label: 'Effective Income Tax Rate Continuing Operations',
|
|
values: { '2025-fy': 0.233 },
|
|
unit: 'pure'
|
|
})
|
|
]
|
|
});
|
|
|
|
expect(findRow(standardizedRows, 'cost_of_revenue').values['2025-fy']).toBe(12_188_496_000);
|
|
expect(findRow(standardizedRows, 'gross_profit').values['2025-fy']).toBe(3_752_403_000);
|
|
expect(findRow(standardizedRows, 'selling_general_and_administrative').values['2025-fy']).toBe(2_552_356_000);
|
|
expect(findRow(standardizedRows, 'pretax_income').values['2025-fy']).toBe(712_449_000);
|
|
expect(findRow(standardizedRows, 'operating_income').values['2025-fy']).toBe(796_400_000);
|
|
expect(findRow(standardizedRows, 'depreciation_and_amortization').values['2025-fy']).toBe(403_647_000);
|
|
expect(findRow(standardizedRows, 'depreciation_and_amortization_expenses').values['2025-fy']).toBe(403_647_000);
|
|
expect(findRow(standardizedRows, 'ebitda').values['2025-fy']).toBe(1_200_047_000);
|
|
expect(findRow(standardizedRows, 'effective_tax_rate').values['2025-fy']).toBe(0.233);
|
|
expect(standardizedRows.some((row) => row.key === 'interest_income')).toBe(false);
|
|
});
|
|
|
|
it('keeps EBITDA formula-only instead of mapping it directly from operating income', () => {
|
|
const period = createPeriod({
|
|
id: '2024-fy',
|
|
filingId: 90,
|
|
filingDate: '2024-07-30',
|
|
periodStart: '2023-07-01',
|
|
periodEnd: '2024-06-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'income',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
localName: 'OperatingIncomeLoss',
|
|
label: 'Operating Income Loss',
|
|
values: { '2024-fy': 109_433_000_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'DepreciationDepletionAndAmortization',
|
|
label: 'Depreciation Depletion And Amortization',
|
|
values: { '2024-fy': 22_287_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
const ebitda = findRow(standardizedRows, 'ebitda');
|
|
expect(ebitda.values['2024-fy']).toBe(131_720_000_000);
|
|
expect(ebitda.formulaKey).toBe('ebitda');
|
|
expect(ebitda.resolvedSourceRowKeys['2024-fy']).toBeNull();
|
|
expect(ebitda.sourceRowKeys).toHaveLength(0);
|
|
});
|
|
|
|
it('uses balance template ordering and sections, with equity falling back to assets minus liabilities', () => {
|
|
const period = createPeriod({
|
|
id: '2025-balance',
|
|
filingId: 95,
|
|
filingDate: '2025-07-30',
|
|
periodEnd: '2025-06-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'balance',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'Assets',
|
|
label: 'Assets',
|
|
values: { '2025-balance': 619_003_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'Liabilities',
|
|
label: 'Liabilities',
|
|
values: { '2025-balance': 275_524_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'AssetsCurrent',
|
|
label: 'Assets Current',
|
|
values: { '2025-balance': 191_131_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
const currentAssets = findRow(standardizedRows, 'current_assets');
|
|
const totalAssets = findRow(standardizedRows, 'total_assets');
|
|
const totalLiabilities = findRow(standardizedRows, 'total_liabilities');
|
|
const totalEquity = findRow(standardizedRows, 'total_equity');
|
|
|
|
expect(currentAssets.category).toBe('assets');
|
|
expect(totalLiabilities.category).toBe('liabilities');
|
|
expect(totalEquity.category).toBe('equity');
|
|
expect(totalEquity.values['2025-balance']).toBe(343_479_000_000);
|
|
expect(totalEquity.formulaKey).toBe('total_equity');
|
|
expect(currentAssets.order).toBeLessThan(totalAssets.order);
|
|
expect(totalAssets.order).toBeLessThan(totalLiabilities.order);
|
|
expect(totalLiabilities.order).toBeLessThan(totalEquity.order);
|
|
});
|
|
|
|
it('maps cash flow capex and computes free cash flow from the template formula', () => {
|
|
const period = createPeriod({
|
|
id: '2024-cf',
|
|
filingId: 100,
|
|
filingDate: '2024-07-30',
|
|
periodStart: '2023-07-01',
|
|
periodEnd: '2024-06-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'cash_flow',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'NetCashProvidedByUsedInOperatingActivities',
|
|
label: 'Net Cash Provided By Used In Operating Activities',
|
|
values: { '2024-cf': 118_548_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'PaymentsToAcquirePropertyPlantAndEquipment',
|
|
label: 'Payments To Acquire Property Plant And Equipment',
|
|
values: { '2024-cf': 46_937_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
const capex = findRow(standardizedRows, 'capital_expenditures');
|
|
expect(capex.values['2024-cf']).toBe(-46_937_000_000);
|
|
|
|
const freeCashFlow = findRow(standardizedRows, 'free_cash_flow');
|
|
expect(freeCashFlow.values['2024-cf']).toBe(71_611_000_000);
|
|
expect(freeCashFlow.formulaKey).toBe('free_cash_flow');
|
|
});
|
|
|
|
it('prefers dimensionless facts over dimensioned or label-only rows for canonical balance rows', () => {
|
|
const period = createPeriod({
|
|
id: '2025-balance',
|
|
filingId: 110,
|
|
filingDate: '2025-02-05',
|
|
periodEnd: '2024-12-31',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'balance',
|
|
periods: [period],
|
|
rows: [
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'Assets',
|
|
label: 'Assets',
|
|
values: { '2025-balance': 8_700_000_000 },
|
|
hasDimensions: true
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'IntangibleAssetsGrossExcludingGoodwill',
|
|
label: 'Intangible Assets Gross Excluding Goodwill',
|
|
values: { '2025-balance': 2_793_000_000 }
|
|
})
|
|
],
|
|
facts: [
|
|
createFact({
|
|
filingId: 110,
|
|
filingDate: '2025-02-05',
|
|
statement: 'balance',
|
|
localName: 'Assets',
|
|
periodEnd: '2024-12-31',
|
|
periodInstant: '2024-12-31',
|
|
value: 450_256_000_000
|
|
}),
|
|
createFact({
|
|
id: 111,
|
|
filingId: 110,
|
|
filingDate: '2025-02-05',
|
|
statement: 'balance',
|
|
localName: 'Goodwill',
|
|
periodEnd: '2024-12-31',
|
|
periodInstant: '2024-12-31',
|
|
value: 31_885_000_000
|
|
})
|
|
]
|
|
});
|
|
|
|
const totalAssets = findRow(standardizedRows, 'total_assets');
|
|
expect(totalAssets.values['2025-balance']).toBe(450_256_000_000);
|
|
expect(totalAssets.resolvedSourceRowKeys['2025-balance']).toContain('Assets');
|
|
expect(standardizedRows.some((row) => row.key === 'other:http://fasb.org/us-gaap/2024#Assets')).toBe(false);
|
|
|
|
const goodwill = findRow(standardizedRows, 'goodwill');
|
|
expect(goodwill.values['2025-balance']).toBe(31_885_000_000);
|
|
expect(goodwill.resolvedSourceRowKeys['2025-balance']).toContain('Goodwill');
|
|
});
|
|
|
|
it('uses alias priority for Apple-style long-term investments and unearned revenue rows', () => {
|
|
const period = createPeriod({
|
|
id: '2025-balance',
|
|
filingId: 120,
|
|
filingDate: '2025-10-31',
|
|
periodEnd: '2025-09-27',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'balance',
|
|
periods: [period],
|
|
rows: [
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'AvailableForSaleSecuritiesDebtSecurities',
|
|
label: 'Available For Sale Securities Debt Securities',
|
|
values: { '2025-balance': 98_027_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'AvailableForSaleSecuritiesDebtMaturitiesSingleMaturityDate',
|
|
label: 'Available For Sale Securities Debt Maturities Single Maturity Date',
|
|
values: { '2025-balance': 77_723_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'ContractWithCustomerLiability',
|
|
label: 'Contract With Customer Liability',
|
|
values: { '2025-balance': 13_700_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'ContractWithCustomerLiabilityCurrent',
|
|
label: 'Contract With Customer Liability Current',
|
|
values: { '2025-balance': 9_055_000_000 }
|
|
})
|
|
],
|
|
facts: []
|
|
});
|
|
|
|
const longTermInvestments = findRow(standardizedRows, 'long_term_investments');
|
|
expect(longTermInvestments.values['2025-balance']).toBe(77_723_000_000);
|
|
expect(longTermInvestments.resolvedSourceRowKeys['2025-balance']).toContain('AvailableForSaleSecuritiesDebtMaturitiesSingleMaturityDate');
|
|
|
|
const unearnedRevenue = findRow(standardizedRows, 'unearned_revenue');
|
|
expect(unearnedRevenue.values['2025-balance']).toBe(9_055_000_000);
|
|
expect(unearnedRevenue.resolvedSourceRowKeys['2025-balance']).toContain('ContractWithCustomerLiabilityCurrent');
|
|
});
|
|
|
|
it('maps WMS extension aliases and direct effective tax rate values', () => {
|
|
const period = createPeriod({
|
|
id: '2025-fy',
|
|
filingId: 130,
|
|
filingDate: '2025-05-15',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'income',
|
|
periods: [period],
|
|
rows: [
|
|
createRow({
|
|
localName: 'CostOfGoodsAndServicesSold',
|
|
label: 'Cost Of Goods And Services Sold',
|
|
values: { '2025-fy': 340_800_000 }
|
|
}),
|
|
createRow({
|
|
localName: 'SellingGeneralAndAdministrativeExpenseEmployeeStockOptionPlanSpecialDividendCompensation',
|
|
label: 'Selling General And Administrative Expense Employee Stock Option Plan Special Dividend Compensation',
|
|
values: { '2025-fy': 0 }
|
|
})
|
|
],
|
|
facts: [
|
|
createFact({
|
|
filingId: 130,
|
|
filingDate: '2025-05-15',
|
|
localName: 'CostOfGoodsSoldExcludingEmployeeStockOptionPlanSpecialDividendCompensation',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
value: 1_810_004_000
|
|
}),
|
|
createFact({
|
|
id: 131,
|
|
filingId: 130,
|
|
filingDate: '2025-05-15',
|
|
localName: 'SellingGeneralAndAdministrativeExpenseExcludingEmployeeStockOptionPlanSpecialDividendCompensation',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
value: 380_378_000
|
|
}),
|
|
createFact({
|
|
id: 132,
|
|
filingId: 130,
|
|
filingDate: '2025-05-15',
|
|
localName: 'EffectiveIncomeTaxRateContinuingOperations',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
value: 0.239,
|
|
unit: 'pure'
|
|
}),
|
|
createFact({
|
|
id: 133,
|
|
filingId: 130,
|
|
filingDate: '2025-05-15',
|
|
localName: 'InterestIncomeExpenseNonoperatingNet',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
value: -91_803_000
|
|
})
|
|
]
|
|
});
|
|
|
|
expect(findRow(standardizedRows, 'cost_of_revenue').values['2025-fy']).toBe(1_810_004_000);
|
|
expect(findRow(standardizedRows, 'selling_general_and_administrative').values['2025-fy']).toBe(380_378_000);
|
|
expect(findRow(standardizedRows, 'effective_tax_rate').values['2025-fy']).toBe(0.239);
|
|
expect(findRow(standardizedRows, 'interest_expense').values['2025-fy']).toBe(91_803_000);
|
|
});
|
|
|
|
it('inverts Fiscal.ai cash flow outflows and working-capital changes', () => {
|
|
const period = createPeriod({
|
|
id: '2025-cf',
|
|
filingId: 140,
|
|
filingDate: '2025-05-15',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'cash_flow',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'NetCashProvidedByUsedInOperatingActivities',
|
|
label: 'Net Cash Provided By Used In Operating Activities',
|
|
values: { '2025-cf': 1_000_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'PaymentsToAcquirePropertyPlantAndEquipment',
|
|
label: 'Payments To Acquire Property Plant And Equipment',
|
|
values: { '2025-cf': 250_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'IncreaseDecreaseInReceivables',
|
|
label: 'Increase Decrease In Receivables',
|
|
values: { '2025-cf': -37_487_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'IncreaseDecreaseInInventories',
|
|
label: 'Increase Decrease In Inventories',
|
|
values: { '2025-cf': 15_749_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'PaymentsForRepurchaseOfCommonStock',
|
|
label: 'Payments For Repurchase Of Common Stock',
|
|
values: { '2025-cf': 100_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'PaymentsOfDividends',
|
|
label: 'Payments Of Dividends',
|
|
values: { '2025-cf': 40_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'OtherInvestingActivitiesNet',
|
|
label: 'Other Investing Activities Net',
|
|
values: { '2025-cf': 12_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
expect(findRow(standardizedRows, 'changes_trade_receivables').values['2025-cf']).toBe(37_487_000);
|
|
expect(findRow(standardizedRows, 'changes_inventories').values['2025-cf']).toBe(-15_749_000);
|
|
expect(findRow(standardizedRows, 'capital_expenditures').values['2025-cf']).toBe(-250_000_000);
|
|
expect(findRow(standardizedRows, 'share_repurchases').values['2025-cf']).toBe(-100_000_000);
|
|
expect(findRow(standardizedRows, 'dividends_paid').values['2025-cf']).toBe(-40_000_000);
|
|
expect(findRow(standardizedRows, 'other_investing_activities').values['2025-cf']).toBe(-12_000_000);
|
|
expect(findRow(standardizedRows, 'free_cash_flow').values['2025-cf']).toBe(750_000_000);
|
|
});
|
|
|
|
it('aggregates multiple matching component rows for template rows that require it', () => {
|
|
const period = createPeriod({
|
|
id: '2025-cf',
|
|
filingId: 150,
|
|
filingDate: '2025-05-15',
|
|
periodStart: '2024-04-01',
|
|
periodEnd: '2025-03-31',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const standardizedRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'cash_flow',
|
|
periods: [period],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'IncreaseDecreaseInOtherOperatingAssets',
|
|
label: 'Increase Decrease In Other Operating Assets',
|
|
values: { '2025-cf': 25_000_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'cash_flow',
|
|
localName: 'IncreaseDecreaseInOtherOperatingLiabilities',
|
|
label: 'Increase Decrease In Other Operating Liabilities',
|
|
values: { '2025-cf': -40_000_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
const otherOperating = findRow(standardizedRows, 'changes_other_operating_activities');
|
|
expect(otherOperating.values['2025-cf']).toBe(15_000_000);
|
|
expect(otherOperating.resolvedSourceRowKeys['2025-cf']).toBeNull();
|
|
expect(otherOperating.sourceRowKeys).toHaveLength(2);
|
|
});
|
|
|
|
it('aggregates CASY balance and cash-flow component rows from dimensionless facts', () => {
|
|
const balancePeriod = createPeriod({
|
|
id: '2025-balance',
|
|
filingId: 151,
|
|
filingDate: '2025-06-23',
|
|
periodEnd: '2025-04-30',
|
|
filingType: '10-K'
|
|
});
|
|
const cashFlowPeriod = createPeriod({
|
|
id: '2025-cf',
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
filingType: '10-K'
|
|
});
|
|
|
|
const balanceRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'balance',
|
|
periods: [balancePeriod],
|
|
facts: [],
|
|
rows: [
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'PropertyPlantAndEquipmentAndFinanceLeaseRightOfUseAssetAfterAccumulatedDepreciationAndAmortization',
|
|
label: 'Property Plant And Equipment And Finance Lease Right Of Use Asset After Accumulated Depreciation And Amortization',
|
|
values: { '2025-balance': 5_413_244_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'EmployeeRelatedLiabilitiesCurrent',
|
|
label: 'Employee Related Liabilities Current',
|
|
values: { '2025-balance': 80_633_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'OtherLiabilitiesCurrent',
|
|
label: 'Other Liabilities Current',
|
|
values: { '2025-balance': 189_870_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'AccruedPropertyTaxes',
|
|
qname: 'caseys:AccruedPropertyTaxes',
|
|
conceptKey: 'http://www.caseys.com/20250430#AccruedPropertyTaxes',
|
|
label: 'Accrued Property Taxes',
|
|
values: { '2025-balance': 59_843_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'DeferredIncomeTaxLiabilitiesNet',
|
|
label: 'Deferred Income Tax Liabilities Net',
|
|
values: { '2025-balance': 646_905_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'OtherLiabilitiesNoncurrent',
|
|
label: 'Other Liabilities Noncurrent',
|
|
values: { '2025-balance': 69_380_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'LiabilitiesCurrent',
|
|
label: 'Liabilities Current',
|
|
values: { '2025-balance': 1_101_693_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'OperatingLeaseLiability',
|
|
label: 'Operating Lease Liability',
|
|
values: { '2025-balance': 449_354_000 }
|
|
}),
|
|
createRow({
|
|
statement: 'balance',
|
|
localName: 'FinanceLeaseLiability',
|
|
label: 'Finance Lease Liability',
|
|
values: { '2025-balance': 108_920_000 }
|
|
})
|
|
]
|
|
});
|
|
|
|
expect(findRow(balanceRows, 'property_plant_equipment').values['2025-balance']).toBe(5_413_244_000);
|
|
expect(findRow(balanceRows, 'accrued_liabilities').values['2025-balance']).toBe(330_346_000);
|
|
expect(findRow(balanceRows, 'other_long_term_liabilities').values['2025-balance']).toBe(69_380_000);
|
|
expect(findRow(balanceRows, 'total_current_liabilities').values['2025-balance']).toBe(1_101_693_000);
|
|
expect(findRow(balanceRows, 'leases').values['2025-balance']).toBe(558_274_000);
|
|
|
|
const cashFlowRows = __financialTaxonomyInternals.buildStandardizedRows({
|
|
statement: 'cash_flow',
|
|
periods: [cashFlowPeriod],
|
|
facts: [
|
|
createFact({
|
|
id: 2001,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'income',
|
|
localName: 'IncreaseDecreaseInDeferredIncomeTaxes',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: -59_958_000
|
|
}),
|
|
createFact({
|
|
id: 2002,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'cash_flow',
|
|
localName: 'OtherNoncashIncomeExpense',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: 4_054_000
|
|
}),
|
|
createFact({
|
|
id: 2003,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'income',
|
|
localName: 'IncreaseDecreaseInIncomeTaxes',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: -84_000
|
|
}),
|
|
createFact({
|
|
id: 2004,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'income',
|
|
localName: 'IncreaseDecreaseInIncomeTaxesReceivable',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: -15_460_000
|
|
}),
|
|
createFact({
|
|
id: 2005,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'balance',
|
|
localName: 'IncreaseDecreaseInAccruedLiabilities',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: 21_525_000
|
|
}),
|
|
createFact({
|
|
id: 2006,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'income',
|
|
localName: 'IncreaseDecreaseInPrepaidExpense',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: -3_658_000
|
|
}),
|
|
createFact({
|
|
id: 2007,
|
|
filingId: 152,
|
|
filingDate: '2025-06-23',
|
|
statement: 'cash_flow',
|
|
localName: 'ProceedsFromSaleOfPropertyPlantAndEquipment',
|
|
periodStart: '2024-05-01',
|
|
periodEnd: '2025-04-30',
|
|
value: 18_805_000
|
|
})
|
|
],
|
|
rows: []
|
|
});
|
|
|
|
expect(findRow(cashFlowRows, 'changes_income_taxes_payable').values['2025-cf']).toBe(-84_000);
|
|
expect(findRow(cashFlowRows, 'changes_accrued_expenses').values['2025-cf']).toBe(21_525_000);
|
|
expect(findRow(cashFlowRows, 'other_adjustments').values['2025-cf']).toBe(55_904_000);
|
|
expect(findRow(cashFlowRows, 'changes_other_operating_activities').values['2025-cf']).toBe(63_616_000);
|
|
expect(findRow(cashFlowRows, 'proceeds_from_sale_of_property_plant_and_equipment').values['2025-cf']).toBe(18_805_000);
|
|
});
|
|
|
|
it('matches local MSFT annual income regression on exact period-end dates', async () => {
|
|
const response = await getCompanyFinancialTaxonomy({
|
|
ticker: 'MSFT',
|
|
surfaceKind: 'income_statement',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const period2024 = findPeriodId(response.periods, '2024-06-30');
|
|
|
|
expect(response.periods.map((period) => period.periodEnd)).toEqual([
|
|
'2022-06-30',
|
|
'2023-06-30',
|
|
'2024-06-30',
|
|
'2025-06-30'
|
|
]);
|
|
expect(findStandardizedResponseRow(response, 'revenue').values[period2024]).toBe(245_122_000_000);
|
|
expect(findStandardizedResponseRow(response, 'operating_income').values[period2024]).toBe(109_433_000_000);
|
|
expect(findStandardizedResponseRow(response, 'net_income').values[period2024]).toBe(88_136_000_000);
|
|
expect(findStandardizedResponseRow(response, 'pretax_income').values[period2024]).toBe(107_787_000_000);
|
|
expect(findStandardizedResponseRow(response, 'income_tax_expense').values[period2024]).toBe(19_651_000_000);
|
|
expect(findStandardizedResponseRow(response, 'effective_tax_rate').values[period2024]).toBe(0.182);
|
|
expect(findStandardizedResponseRow(response, 'revenue').templateSection).toBe('statement');
|
|
});
|
|
|
|
it('matches local QCOM annual income regression on exact period-end dates', async () => {
|
|
const response = await getCompanyFinancialTaxonomy({
|
|
ticker: 'QCOM',
|
|
surfaceKind: 'income_statement',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const period2024 = findPeriodId(response.periods, '2024-09-29');
|
|
const period2025 = findPeriodId(response.periods, '2025-09-28');
|
|
|
|
expect(response.periods.map((period) => period.periodEnd)).toEqual([
|
|
'2022-09-25',
|
|
'2023-09-24',
|
|
'2024-09-29',
|
|
'2025-09-28'
|
|
]);
|
|
expect(response.periods.some((period) => period.periodEnd === '2026-09-28')).toBe(false);
|
|
expect(response.periods.some((period) => period.periodEnd === '2027-09-26')).toBe(false);
|
|
expect(findStandardizedResponseRow(response, 'revenue').values[period2024]).toBe(38_962_000_000);
|
|
expect(findStandardizedResponseRow(response, 'cost_of_revenue').values[period2024]).toBe(17_060_000_000);
|
|
expect(findStandardizedResponseRow(response, 'gross_profit').values[period2024]).toBe(21_902_000_000);
|
|
expect(findStandardizedResponseRow(response, 'selling_general_and_administrative').values[period2024]).toBe(2_759_000_000);
|
|
expect(findStandardizedResponseRow(response, 'research_and_development').values[period2024]).toBe(8_893_000_000);
|
|
expect(findStandardizedResponseRow(response, 'pretax_income').values[period2024]).toBe(10_336_000_000);
|
|
expect(findStandardizedResponseRow(response, 'ebitda').values[period2024]).toBe(11_777_000_000);
|
|
expect(findStandardizedResponseRow(response, 'effective_tax_rate').values[period2024]).toBe(0.02);
|
|
expect(findStandardizedResponseRow(response, 'revenue').values[period2025]).toBe(44_284_000_000);
|
|
expect(findStandardizedResponseRow(response, 'income_tax_expense').values[period2025]).toBe(7_122_000_000);
|
|
expect(findStandardizedResponseRow(response, 'net_income').values[period2025]).toBe(5_541_000_000);
|
|
});
|
|
|
|
it('matches local MSFT annual balance regression on the June 30, 2025 balance sheet', async () => {
|
|
const response = await getCompanyFinancialTaxonomy({
|
|
ticker: 'MSFT',
|
|
surfaceKind: 'balance_sheet',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const period2025 = findPeriodId(response.periods, '2025-06-30');
|
|
|
|
expect(findStandardizedResponseRow(response, 'total_assets').values[period2025]).toBe(619_003_000_000);
|
|
expect(findStandardizedResponseRow(response, 'total_liabilities').values[period2025]).toBe(275_524_000_000);
|
|
expect(findStandardizedResponseRow(response, 'total_equity').values[period2025]).toBe(343_479_000_000);
|
|
expect(findStandardizedResponseRow(response, 'total_assets').templateSection).toBe('assets');
|
|
expect(findStandardizedResponseRow(response, 'total_liabilities').templateSection).toBe('liabilities');
|
|
expect(findStandardizedResponseRow(response, 'total_equity').templateSection).toBe('equity');
|
|
});
|
|
|
|
it('matches local MSFT annual cash flow regression on the June 30, 2025 filing period', async () => {
|
|
const response = await getCompanyFinancialTaxonomy({
|
|
ticker: 'MSFT',
|
|
surfaceKind: 'cash_flow_statement',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const period2025 = findPeriodId(response.periods, '2025-06-30');
|
|
|
|
expect(findStandardizedResponseRow(response, 'depreciation_and_amortization').values[period2025]).toBe(34_153_000_000);
|
|
expect(findStandardizedResponseRow(response, 'stock_based_compensation').values[period2025]).toBe(11_974_000_000);
|
|
expect(findStandardizedResponseRow(response, 'free_cash_flow').values[period2025]).toBe(71_611_000_000);
|
|
expect(findStandardizedResponseRow(response, 'operating_cash_flow').templateSection).toBe('operating');
|
|
expect(findStandardizedResponseRow(response, 'free_cash_flow').templateSection).toBe('free_cash_flow');
|
|
});
|
|
|
|
it('matches local CASY annual income regression on the April 30, 2025 period', async () => {
|
|
const response = await getCompanyFinancialTaxonomy({
|
|
ticker: 'CASY',
|
|
surfaceKind: 'income_statement',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const period2025 = findPeriodId(response.periods, '2025-04-30');
|
|
|
|
expect(findStandardizedResponseRow(response, 'revenue').values[period2025]).toBe(15_940_899_000);
|
|
expect(findStandardizedResponseRow(response, 'cost_of_revenue').values[period2025]).toBe(12_188_496_000);
|
|
expect(findStandardizedResponseRow(response, 'gross_profit').values[period2025]).toBe(3_752_403_000);
|
|
expect(findStandardizedResponseRow(response, 'selling_general_and_administrative').values[period2025]).toBe(2_552_356_000);
|
|
expect(findStandardizedResponseRow(response, 'operating_income').values[period2025]).toBe(796_400_000);
|
|
expect(findStandardizedResponseRow(response, 'pretax_income').values[period2025]).toBe(712_449_000);
|
|
expect(findStandardizedResponseRow(response, 'income_tax_expense').values[period2025]).toBe(165_929_000);
|
|
expect(findStandardizedResponseRow(response, 'net_income').values[period2025]).toBe(546_520_000);
|
|
expect(findStandardizedResponseRow(response, 'ebitda').values[period2025]).toBe(1_200_047_000);
|
|
expect(findStandardizedResponseRow(response, 'effective_tax_rate').values[period2025]).toBe(0.233);
|
|
});
|
|
|
|
it('matches local CASY annual balance and cash-flow regression on the April 30, 2025 period', async () => {
|
|
const balance = await getCompanyFinancialTaxonomy({
|
|
ticker: 'CASY',
|
|
surfaceKind: 'balance_sheet',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
const cash = await getCompanyFinancialTaxonomy({
|
|
ticker: 'CASY',
|
|
surfaceKind: 'cash_flow_statement',
|
|
cadence: 'annual',
|
|
includeDimensions: false,
|
|
includeFacts: false,
|
|
queuedSync: false,
|
|
v3Enabled: true
|
|
});
|
|
|
|
const balancePeriod2025 = findPeriodId(balance.periods, '2025-04-30');
|
|
const cashPeriod2025 = findPeriodId(cash.periods, '2025-04-30');
|
|
|
|
expect(findStandardizedResponseRow(balance, 'total_assets').values[balancePeriod2025]).toBe(8_208_118_000);
|
|
expect(findStandardizedResponseRow(balance, 'total_liabilities').values[balancePeriod2025]).toBe(4_699_448_000);
|
|
expect(findStandardizedResponseRow(balance, 'total_equity').values[balancePeriod2025]).toBe(3_508_670_000);
|
|
expect(findStandardizedResponseRow(balance, 'cash_and_equivalents').values[balancePeriod2025]).toBe(326_662_000);
|
|
expect(findStandardizedResponseRow(balance, 'accrued_liabilities').values[balancePeriod2025]).toBe(330_731_000);
|
|
expect(findStandardizedResponseRow(balance, 'other_long_term_liabilities').values[balancePeriod2025]).toBe(121_485_000);
|
|
|
|
expect(findStandardizedResponseRow(cash, 'operating_cash_flow').values[cashPeriod2025]).toBe(1_090_854_000);
|
|
expect(findStandardizedResponseRow(cash, 'capital_expenditures').values[cashPeriod2025]).toBe(-506_224_000);
|
|
expect(findStandardizedResponseRow(cash, 'free_cash_flow').values[cashPeriod2025]).toBe(584_630_000);
|
|
expect(findStandardizedResponseRow(cash, 'acquisitions').values[cashPeriod2025]).toBe(-1_239_249_000);
|
|
expect(findStandardizedResponseRow(cash, 'long_term_debt_issued').values[cashPeriod2025]).toBe(1_100_000_000);
|
|
expect(findStandardizedResponseRow(cash, 'debt_repaid').values[cashPeriod2025]).toBe(-239_492_000);
|
|
expect(findStandardizedResponseRow(cash, 'dividends_paid').values[cashPeriod2025]).toBe(-72_309_000);
|
|
expect(findStandardizedResponseRow(cash, 'financing_cash_flow').values[cashPeriod2025]).toBe(755_994_000);
|
|
expect(findStandardizedResponseRow(cash, 'changes_income_taxes_payable').values[cashPeriod2025]).toBe(-84_000);
|
|
expect(findStandardizedResponseRow(cash, 'changes_accrued_expenses').values[cashPeriod2025]).toBe(21_525_000);
|
|
expect(findStandardizedResponseRow(cash, 'other_adjustments').values[cashPeriod2025]).toBe(55_904_000);
|
|
});
|
|
|
|
it('merges KPI rows by priority without overwriting higher-priority periods', () => {
|
|
const merged = __financialTaxonomyInternals.mergeStructuredKpiRowsByPriority([
|
|
[
|
|
createKpiRow({
|
|
key: 'loan_growth',
|
|
values: { p1: 0.12 },
|
|
sourceConcepts: ['us-gaap:LoansReceivableNetReportedAmount'],
|
|
sourceFactIds: [1]
|
|
})
|
|
],
|
|
[
|
|
createKpiRow({
|
|
key: 'loan_growth',
|
|
values: { p1: 0.11, p2: 0.09 },
|
|
sourceConcepts: ['us-gaap:FinancingReceivableRecordedInvestment'],
|
|
sourceFactIds: [2]
|
|
})
|
|
],
|
|
[
|
|
createKpiRow({
|
|
key: 'loan_growth',
|
|
values: { p2: 0.08, p3: 0.07 },
|
|
provenanceType: 'structured_note',
|
|
sourceFactIds: [3]
|
|
})
|
|
]
|
|
]);
|
|
|
|
expect(merged).toHaveLength(1);
|
|
expect(merged[0]?.values).toEqual({ p1: 0.12, p2: 0.09, p3: 0.07 });
|
|
expect(merged[0]?.sourceConcepts).toEqual([
|
|
'us-gaap:FinancingReceivableRecordedInvestment',
|
|
'us-gaap:LoansReceivableNetReportedAmount'
|
|
]);
|
|
expect(merged[0]?.sourceFactIds).toEqual([1, 2, 3]);
|
|
expect(merged[0]?.provenanceType).toBe('taxonomy');
|
|
});
|
|
|
|
it('builds faithful rows when persisted statement rows are missing sourceFactIds', () => {
|
|
const malformedSnapshot = {
|
|
...createSnapshot({
|
|
filingId: 19,
|
|
filingType: '10-K',
|
|
filingDate: '2026-02-20',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2025-fy', periodStart: '2025-01-01', periodEnd: '2025-12-31', periodLabel: '2025 FY' }
|
|
]
|
|
}),
|
|
statement_rows: {
|
|
income: [{
|
|
...createRow({
|
|
key: 'revenue',
|
|
label: 'Revenue',
|
|
statement: 'income',
|
|
values: { '2025-fy': 123_000_000 }
|
|
}),
|
|
sourceFactIds: undefined
|
|
} as unknown as TaxonomyStatementRow],
|
|
balance: [],
|
|
cash_flow: [],
|
|
equity: [],
|
|
comprehensive_income: []
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
const rows = __financialTaxonomyInternals.buildRows(
|
|
[malformedSnapshot],
|
|
'income',
|
|
new Set(['2025-fy'])
|
|
);
|
|
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0]?.key).toBe('revenue');
|
|
expect(rows[0]?.sourceFactIds).toEqual([]);
|
|
});
|
|
|
|
it('aggregates persisted surface rows when legacy snapshots are missing source arrays', () => {
|
|
const snapshot = {
|
|
...createSnapshot({
|
|
filingId: 20,
|
|
filingType: '10-K',
|
|
filingDate: '2026-02-21',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2025-fy', periodStart: '2025-01-01', periodEnd: '2025-12-31', periodLabel: '2025 FY' }
|
|
]
|
|
}),
|
|
surface_rows: {
|
|
income: [{
|
|
key: 'revenue',
|
|
label: 'Revenue',
|
|
category: 'revenue',
|
|
templateSection: 'statement',
|
|
order: 10,
|
|
unit: 'currency',
|
|
values: { '2025-fy': 123_000_000 },
|
|
sourceConcepts: undefined,
|
|
sourceRowKeys: undefined,
|
|
sourceFactIds: undefined,
|
|
formulaKey: null,
|
|
hasDimensions: false,
|
|
resolvedSourceRowKeys: {},
|
|
statement: 'income',
|
|
detailCount: 0,
|
|
resolutionMethod: 'direct',
|
|
confidence: 'high',
|
|
warningCodes: []
|
|
} as unknown as FilingTaxonomySnapshotRecord['surface_rows']['income'][number]],
|
|
balance: [],
|
|
cash_flow: [],
|
|
equity: [],
|
|
comprehensive_income: []
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
const rows = __financialTaxonomyInternals.aggregateSurfaceRows({
|
|
snapshots: [snapshot],
|
|
statement: 'income',
|
|
selectedPeriodIds: new Set(['2025-fy'])
|
|
});
|
|
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0]).toMatchObject({
|
|
key: 'revenue',
|
|
sourceConcepts: [],
|
|
sourceRowKeys: [],
|
|
sourceFactIds: []
|
|
});
|
|
});
|
|
|
|
it('aggregates persisted detail rows when legacy snapshots are missing dimension arrays', () => {
|
|
const snapshot = {
|
|
...createSnapshot({
|
|
filingId: 21,
|
|
filingType: '10-K',
|
|
filingDate: '2026-02-22',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2025-fy', periodStart: '2025-01-01', periodEnd: '2025-12-31', periodLabel: '2025 FY' }
|
|
]
|
|
}),
|
|
detail_rows: {
|
|
income: {
|
|
revenue: [{
|
|
key: 'revenue_detail',
|
|
parentSurfaceKey: 'revenue',
|
|
label: 'Revenue Detail',
|
|
conceptKey: 'us-gaap:RevenueDetail',
|
|
qname: 'us-gaap:RevenueDetail',
|
|
namespaceUri: 'http://fasb.org/us-gaap/2024',
|
|
localName: 'RevenueDetail',
|
|
unit: 'iso4217:USD',
|
|
values: { '2025-fy': 123_000_000 },
|
|
sourceFactIds: undefined,
|
|
isExtension: false,
|
|
dimensionsSummary: undefined,
|
|
residualFlag: false
|
|
} as unknown as FilingTaxonomySnapshotRecord['detail_rows']['income'][string][number]]
|
|
},
|
|
balance: {},
|
|
cash_flow: {},
|
|
equity: {},
|
|
comprehensive_income: {}
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
const rows = __financialTaxonomyInternals.aggregateDetailRows({
|
|
snapshots: [snapshot],
|
|
statement: 'income',
|
|
selectedPeriodIds: new Set(['2025-fy'])
|
|
});
|
|
|
|
expect(rows.revenue).toHaveLength(1);
|
|
expect(rows.revenue?.[0]).toMatchObject({
|
|
key: 'revenue_detail',
|
|
sourceFactIds: [],
|
|
dimensionsSummary: []
|
|
});
|
|
});
|
|
|
|
it('builds normalization metadata from snapshot fiscal pack and counts', () => {
|
|
const snapshot = {
|
|
...createSnapshot({
|
|
filingId: 15,
|
|
filingType: '10-Q',
|
|
filingDate: '2026-01-28',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: 'quarter', periodStart: '2025-10-01', periodEnd: '2025-12-31', periodLabel: '2025-10-01 to 2025-12-31' }
|
|
]
|
|
}),
|
|
parser_version: '0.1.0',
|
|
fiscal_pack: 'bank_lender',
|
|
normalization_summary: {
|
|
surfaceRowCount: 5,
|
|
detailRowCount: 3,
|
|
kpiRowCount: 2,
|
|
unmappedRowCount: 4,
|
|
materialUnmappedRowCount: 1,
|
|
warnings: []
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
expect(__financialTaxonomyInternals.buildNormalizationMetadata([snapshot])).toEqual({
|
|
parserEngine: 'fiscal-xbrl',
|
|
regime: 'us-gaap',
|
|
fiscalPack: 'bank_lender',
|
|
parserVersion: '0.1.0',
|
|
surfaceRowCount: 5,
|
|
detailRowCount: 3,
|
|
kpiRowCount: 2,
|
|
unmappedRowCount: 4,
|
|
materialUnmappedRowCount: 1,
|
|
warnings: []
|
|
});
|
|
});
|
|
|
|
it('aggregates normalization counts and warning codes across snapshots while using the latest parser identity', () => {
|
|
const olderSnapshot = {
|
|
...createSnapshot({
|
|
filingId: 17,
|
|
filingType: '10-K',
|
|
filingDate: '2025-02-13',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2024-fy', periodStart: '2024-01-01', periodEnd: '2024-12-31', periodLabel: '2024 FY' }
|
|
]
|
|
}),
|
|
parser_engine: 'fiscal-xbrl',
|
|
parser_version: '0.9.0',
|
|
fiscal_pack: 'core',
|
|
normalization_summary: {
|
|
surfaceRowCount: 4,
|
|
detailRowCount: 2,
|
|
kpiRowCount: 1,
|
|
unmappedRowCount: 3,
|
|
materialUnmappedRowCount: 1,
|
|
warnings: ['balance_residual_detected', 'income_sparse_mapping']
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
const latestSnapshot = {
|
|
...createSnapshot({
|
|
filingId: 18,
|
|
filingType: '10-Q',
|
|
filingDate: '2026-02-13',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2025-q4', periodStart: '2025-10-01', periodEnd: '2025-12-31', periodLabel: '2025 Q4' }
|
|
]
|
|
}),
|
|
parser_engine: 'fiscal-xbrl',
|
|
parser_version: '1.1.0',
|
|
fiscal_pack: 'bank_lender',
|
|
normalization_summary: {
|
|
surfaceRowCount: 6,
|
|
detailRowCount: 5,
|
|
kpiRowCount: 4,
|
|
unmappedRowCount: 2,
|
|
materialUnmappedRowCount: 0,
|
|
warnings: ['income_sparse_mapping', 'unmapped_cash_flow_bridge']
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
expect(__financialTaxonomyInternals.buildNormalizationMetadata([olderSnapshot, latestSnapshot])).toEqual({
|
|
parserEngine: 'fiscal-xbrl',
|
|
regime: 'us-gaap',
|
|
fiscalPack: 'bank_lender',
|
|
parserVersion: '1.1.0',
|
|
surfaceRowCount: 10,
|
|
detailRowCount: 7,
|
|
kpiRowCount: 5,
|
|
unmappedRowCount: 5,
|
|
materialUnmappedRowCount: 1,
|
|
warnings: [
|
|
'balance_residual_detected',
|
|
'income_sparse_mapping',
|
|
'unmapped_cash_flow_bridge'
|
|
]
|
|
});
|
|
});
|
|
|
|
it('retains pinned income surface rows even when they are intentionally null', () => {
|
|
const snapshot = {
|
|
...createSnapshot({
|
|
filingId: 16,
|
|
filingType: '10-K',
|
|
filingDate: '2026-02-13',
|
|
statement: 'income',
|
|
periods: [
|
|
{ id: '2025-fy', periodStart: '2025-01-01', periodEnd: '2025-12-31', periodLabel: '2025 FY' }
|
|
]
|
|
}),
|
|
fiscal_pack: 'bank_lender',
|
|
surface_rows: {
|
|
income: [
|
|
{
|
|
key: 'revenue',
|
|
label: 'Revenue',
|
|
category: 'surface',
|
|
templateSection: 'surface',
|
|
order: 10,
|
|
unit: 'currency',
|
|
values: { '2025-fy': 100_000_000 },
|
|
sourceConcepts: ['us-gaap:TotalNetRevenues'],
|
|
sourceRowKeys: ['revenue'],
|
|
sourceFactIds: [1],
|
|
formulaKey: null,
|
|
hasDimensions: false,
|
|
resolvedSourceRowKeys: { '2025-fy': 'revenue' },
|
|
statement: 'income',
|
|
detailCount: 0,
|
|
resolutionMethod: 'direct',
|
|
confidence: 'high',
|
|
warningCodes: []
|
|
},
|
|
{
|
|
key: 'gross_profit',
|
|
label: 'Gross Profit',
|
|
category: 'surface',
|
|
templateSection: 'surface',
|
|
order: 20,
|
|
unit: 'currency',
|
|
values: { '2025-fy': null },
|
|
sourceConcepts: [],
|
|
sourceRowKeys: [],
|
|
sourceFactIds: [],
|
|
formulaKey: null,
|
|
hasDimensions: false,
|
|
resolvedSourceRowKeys: { '2025-fy': null },
|
|
statement: 'income',
|
|
detailCount: 0,
|
|
resolutionMethod: 'not_meaningful',
|
|
confidence: 'low',
|
|
warningCodes: ['gross_profit_not_meaningful_bank_pack']
|
|
},
|
|
{
|
|
key: 'selling_general_and_administrative',
|
|
label: 'SG&A',
|
|
category: 'surface',
|
|
templateSection: 'surface',
|
|
order: 31,
|
|
unit: 'currency',
|
|
values: { '2025-fy': null },
|
|
sourceConcepts: [],
|
|
sourceRowKeys: [],
|
|
sourceFactIds: [],
|
|
formulaKey: null,
|
|
hasDimensions: false,
|
|
resolvedSourceRowKeys: { '2025-fy': null },
|
|
statement: 'income',
|
|
detailCount: 0,
|
|
resolutionMethod: 'not_meaningful',
|
|
confidence: 'low',
|
|
warningCodes: ['selling_general_and_administrative_not_meaningful_bank_pack']
|
|
}
|
|
],
|
|
balance: [],
|
|
cash_flow: [],
|
|
equity: [],
|
|
comprehensive_income: []
|
|
}
|
|
} satisfies FilingTaxonomySnapshotRecord;
|
|
|
|
const rows = __financialTaxonomyInternals.aggregateSurfaceRows({
|
|
snapshots: [snapshot],
|
|
statement: 'income',
|
|
selectedPeriodIds: new Set(['2025-fy'])
|
|
});
|
|
|
|
const grossProfit = rows.find((row) => row.key === 'gross_profit');
|
|
const sga = rows.find((row) => row.key === 'selling_general_and_administrative');
|
|
expect(grossProfit).toBeDefined();
|
|
expect(grossProfit?.values['2025-fy']).toBeNull();
|
|
expect(grossProfit?.resolutionMethod).toBe('not_meaningful');
|
|
expect(grossProfit?.warningCodes).toEqual(['gross_profit_not_meaningful_bank_pack']);
|
|
expect(sga).toBeDefined();
|
|
expect(sga?.values['2025-fy']).toBeNull();
|
|
expect(sga?.resolutionMethod).toBe('not_meaningful');
|
|
expect(sga?.warningCodes).toEqual(['selling_general_and_administrative_not_meaningful_bank_pack']);
|
|
});
|
|
});
|