Files
signal-platform/tests/unit/test_sec_facts_parser.py
dennisthiessenandClaude Opus 5 8453b87290
Deploy / lint (push) Successful in 10s
Deploy / test (push) Successful in 1m20s
Deploy / deploy (push) Successful in 37s
fix(sec): compose total debt across the styles filers actually tag
total_debt read LongTermDebt, else LongTermDebtNoncurrent/Current, plus one of
ShortTermBorrowings/CommercialPaper. That misses two whole tagging styles, and
it feeds net_debt -> net_debt_to_ebitda -> the categorical leverage read, so the
misses were not absences but confident wrong answers: Coca-Cola scored on 0.25bn
of commercial paper against ~39bn of debt, Verizon on 21.78bn of current
maturities against ~165bn, AT&T and Exxon produced no value at all against 134bn
and 33bn tagged. Measured over 19 large caps and 14 REITs, 11 were wrong or
absent and the rest are unchanged.

Each concept's span is now respected. LongTermDebt already includes current
maturities (Apple tags all three: 71.34 + 11.01 = 82.30), so only true
short-term borrowing is added. LongTermDebtAndCapitalLeaseObligations — what KO,
HD, T, XOM and CVX tag, and nothing read before — is noncurrent and takes a
current complement, and DebtCurrent *is* that whole complement rather than an
addition to it.

The REIT branch needed disambiguating: NotesPayable is not the same line across
issuers. MAA tags NotesPayable 5.66bn = UnsecuredDebt 5.30bn + SecuredDebt
0.36bn exactly, so there it is the total and adding the secured side
double-counts; EQR tags it alongside a larger SecuredDebt, where it is only the
unsecured component. UnsecuredDebt's presence separates them.

A component alone is no longer reported as a total. Chevron tags full debt only
in its 10-K, so its 10-Q carried 0.40bn of short-term borrowing; Boston
Properties tags SecuredDebt 4.28bn against ~15bn real. net_debt needs both sides
and yields nothing when either is missing, so None costs a leverage read where
the fragment produced a confidently wrong one.

Snapshots are immutable, so this corrects new filings only; stored history needs
scripts/reparse_fundamentals.py, which cannot complete until the EQR/931182
collision is retired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Lo99z3jWqu9X9ueBU3Z3D
2026-08-21 17:44:18 +02:00

626 lines
27 KiB
Python

"""Tests for the companyfacts -> snapshot parser, on a realistic Apple-shaped
fixture (the structure verified by live probe)."""
from __future__ import annotations
import os
from datetime import date, datetime, timezone
import pytest
from app.services.sec_facts_parser import (
Fact,
FilingMeta,
_compose_cash,
_compose_debt,
_fiscal_context,
_select_shares,
parse_snapshots,
)
UTC = timezone.utc
def _dur(start, end, val, accn, fy=2026, fp="Q2"):
return {"start": start, "end": end, "val": val, "fy": fy, "fp": fp, "accn": accn, "form": "10-Q"}
def _inst(end, val, accn, fy=2026, fp="Q2"):
return {"end": end, "val": val, "fy": fy, "fp": fp, "accn": accn, "form": "10-Q"}
COMPANYFACTS = {
"cik": 320193,
"facts": {
"us-gaap": {
"RevenueFromContractWithCustomerExcludingAssessedTax": {"units": {"USD": [
_dur("2025-09-28", "2025-12-27", 143756, "A", fp="Q1"), # Q1 discrete == YTD
_dur("2025-09-28", "2026-03-28", 254940, "B"), # Q2 YTD (181d) <- want this
_dur("2025-12-28", "2026-03-28", 111184, "B"), # Q2 discrete (90d)
]}},
"NetIncomeLoss": {"units": {"USD": [_dur("2025-09-28", "2026-03-28", 40000, "B")]}},
# only a discrete-length fact for Q2 -> must be null, not the discrete
"OperatingIncomeLoss": {"units": {"USD": [_dur("2025-12-28", "2026-03-28", 30000, "B")]}},
"EarningsPerShareDiluted": {"units": {"USD/shares": [_dur("2025-09-28", "2026-03-28", 2.55, "B")]}},
"CashAndCashEquivalentsAtCarryingValue": {"units": {"USD": [_inst("2026-03-28", 30000, "B")]}},
"MarketableSecuritiesCurrent": {"units": {"USD": [_inst("2026-03-28", 20000, "B")]}},
"LongTermDebtNoncurrent": {"units": {"USD": [_inst("2026-03-28", 80000, "B")]}},
"LongTermDebtCurrent": {"units": {"USD": [_inst("2026-03-28", 10000, "B")]}},
},
"dei": {
"EntityCommonStockSharesOutstanding": {"units": {"shares": [
{"end": "2026-04-17", "val": 14687356000, "fy": 2026, "fp": "Q2", "accn": "B", "form": "10-Q"},
]}},
},
},
}
FILINGS = {
"A": FilingMeta(date(2025, 12, 27), date(2026, 1, 30), datetime(2026, 1, 30, 11, 1, tzinfo=UTC), "10-Q"),
"B": FilingMeta(date(2026, 3, 28), date(2026, 5, 1), datetime(2026, 5, 1, 10, 1, tzinfo=UTC), "10-Q"),
}
def _by_accn(rows):
return {r.accession: r for r in rows}
def test_parses_ytd_not_discrete_and_cover_date_shares():
res = parse_snapshots(COMPANYFACTS, FILINGS, {"A", "B"})
assert not res.skipped_filings and not res.field_issues
b = _by_accn(res.rows)["B"]
assert (b.cik, b.fiscal_year, b.fiscal_period) == ("0000320193", 2026, "Q2")
assert b.period_end == date(2026, 3, 28)
assert b.period_start == date(2025, 9, 28) # YTD start (fiscal-year start)
assert b.revenue == 254940 # the 6-month YTD, NOT the 111184 discrete
assert b.net_income == 40000
assert b.operating_income is None # only a discrete-length fact existed -> null
assert b.diluted_eps == 2.55
# cash + first-present ST investment (MarketableSecuritiesCurrent), each once
assert b.cash_and_st_investments == 50000
# long-term parts summed (no aggregate, no short-term)
assert b.total_debt == 90000
assert b.shares_outstanding == 14687356000
assert b.shares_outstanding_date == date(2026, 4, 17) # cover date != period_end
assert b.filed_date == date(2026, 5, 1)
assert b.accepted_at == datetime(2026, 5, 1, 10, 1, tzinfo=UTC)
def test_q1_discrete_is_the_ytd():
res = parse_snapshots(COMPANYFACTS, FILINGS, {"A"})
a = _by_accn(res.rows)["A"]
assert a.fiscal_period == "Q1"
assert a.revenue == 143756 # Q1 YTD == Q1 discrete
assert a.period_start == date(2025, 9, 28)
def test_skips_filing_without_usable_period():
cf = {
"cik": 320193,
"facts": {"us-gaap": {"NetIncomeLoss": {"units": {"USD": [
{"start": "2025-09-28", "end": "2026-03-28", "val": 1, "fy": 2026, "fp": "H1", "accn": "X", "form": "10-Q"},
]}}}},
}
filings = {"X": FilingMeta(date(2026, 3, 28), date(2026, 5, 1), datetime(2026, 5, 1, tzinfo=UTC), "10-Q")}
res = parse_snapshots(cf, filings, {"X"})
assert res.rows == []
assert res.skipped_filings == [{"accession": "X", "reason": "no usable period identity"}]
def test_missing_accession_is_skipped():
res = parse_snapshots(COMPANYFACTS, FILINGS, {"NOPE"})
assert res.rows == []
assert res.skipped_filings == [{"accession": "NOPE", "reason": "no facts or filing metadata"}]
def test_debt_prefers_aggregate_over_parts():
rd = date(2026, 3, 28)
facts = [
Fact("us-gaap", "LongTermDebt", "USD", None, rd, 95000, 2026, "Q2"),
Fact("us-gaap", "LongTermDebtNoncurrent", "USD", None, rd, 80000, 2026, "Q2"),
Fact("us-gaap", "LongTermDebtCurrent", "USD", None, rd, 10000, 2026, "Q2"),
Fact("us-gaap", "CommercialPaper", "USD", None, rd, 5000, 2026, "Q2"),
]
# aggregate (95000) used, parts ignored; + one short-term pick (5000)
assert _compose_debt(facts, rd) == 100000
def test_cash_picks_one_st_investment_source():
rd = date(2026, 3, 28)
facts = [
Fact("us-gaap", "CashAndCashEquivalentsAtCarryingValue", "USD", None, rd, 30000, 2026, "Q2"),
Fact("us-gaap", "ShortTermInvestments", "USD", None, rd, 15000, 2026, "Q2"),
Fact("us-gaap", "MarketableSecuritiesCurrent", "USD", None, rd, 20000, 2026, "Q2"),
]
# ShortTermInvestments is first in priority -> 30000 + 15000 (not both ST tags)
assert _compose_cash(facts, rd) == 45000
RD = date(2026, 3, 28)
def _dei(end, val):
return Fact("dei", "EntityCommonStockSharesOutstanding", "shares", None, end, val, 2026, "Q2")
def _gaap_shares(end, val):
return Fact("us-gaap", "CommonStockSharesOutstanding", "shares", None, end, val, 2026, "Q2")
def test_shares_prefers_dei_cover_page():
facts = [_dei(date(2026, 4, 17), 100), _gaap_shares(RD, 999)]
assert _select_shares(facts, RD) == (100.0, date(2026, 4, 17), False)
def test_shares_falls_back_to_usgaap_at_report_date():
# Alphabet case: no dei fact; us-gaap current + a prior comparative.
facts = [_gaap_shares(date(2025, 12, 31), 888), _gaap_shares(RD, 12116)]
assert _select_shares(facts, RD) == (12116.0, RD, False) # comparative excluded
def test_shares_conflict_returns_null_ambiguous():
facts = [_dei(RD, 100), _dei(RD, 200)] # two differing consolidated values
assert _select_shares(facts, RD) == (None, None, True)
def test_shares_never_uses_weighted_average():
facts = [Fact("us-gaap", "WeightedAverageNumberOfDilutedSharesOutstanding", "shares", None, RD, 5, 2026, "Q2")]
assert _select_shares(facts, RD) == (None, None, False) # not a shares source
def test_conflicting_fiscal_context_is_rejected():
facts = [
Fact("us-gaap", "Revenues", "USD", date(2025, 9, 28), RD, 1, 2026, "Q2"),
Fact("us-gaap", "NetIncomeLoss", "USD", date(2025, 9, 28), RD, 2, 2025, "Q3"),
] # 1-1 tie between two contexts at reportDate
assert _fiscal_context(facts, RD) == (None, None)
# a clear majority wins
facts.append(Fact("us-gaap", "OperatingIncomeLoss", "USD", date(2025, 9, 28), RD, 3, 2026, "Q2"))
assert _fiscal_context(facts, RD) == (2026, "Q2")
def test_conflicting_context_skips_row():
cf = {"cik": 1, "facts": {"us-gaap": {
"Revenues": {"units": {"USD": [_dur("2025-09-28", "2026-03-28", 1, "B", fp="Q2")]}},
"NetIncomeLoss": {"units": {"USD": [_dur("2025-09-28", "2026-03-28", 2, "B", fy=2025, fp="Q3")]}},
}}}
filings = {"B": FilingMeta(RD, date(2026, 5, 1), datetime(2026, 5, 1, tzinfo=UTC), "10-Q")}
res = parse_snapshots(cf, filings, {"B"})
assert res.rows == []
assert res.skipped_filings == [{"accession": "B", "reason": "no usable period identity"}]
def test_foreign_taxonomy_and_malformed_facts_ignored():
cf = {"cik": 1, "facts": {
"us-gaap": {
"Revenues": {"units": {"USD": [_dur("2025-09-28", "2026-03-28", 500, "B")]}},
"NetIncomeLoss": {"units": {"USD": [
{"start": "2025-09-28", "end": "2026-03-28", "val": None, "fy": 2026, "fp": "Q2", "accn": "B"},
]}},
"OperatingIncomeLoss": {"units": {"USD": [
{"start": "2025-09-28", "end": "2026-03-28", "val": float("nan"), "fy": 2026, "fp": "Q2", "accn": "B"},
]}},
},
"acme": {"RevenueFromContractWithCustomerExcludingAssessedTax": {"units": {"USD": [
_dur("2025-09-28", "2026-03-28", 99999, "B"), # custom taxonomy — must be ignored
]}}},
}}
filings = {"B": FilingMeta(RD, date(2026, 5, 1), datetime(2026, 5, 1, tzinfo=UTC), "10-Q")}
res = parse_snapshots(cf, filings, {"B"})
assert res.rows[0].revenue == 500 # us-gaap Revenues, not the acme concept
assert res.rows[0].net_income is None # val None ignored
assert res.rows[0].operating_income is None # NaN ignored
def test_ambiguous_shares_produces_row_plus_note():
cf = {"cik": 1, "facts": {
"us-gaap": {"Revenues": {"units": {"USD": [_dur("2025-09-28", "2026-03-28", 500, "B")]}}},
"dei": {"EntityCommonStockSharesOutstanding": {"units": {"shares": [
{"end": "2026-04-17", "val": 100, "fy": 2026, "fp": "Q2", "accn": "B"},
{"end": "2026-04-17", "val": 200, "fy": 2026, "fp": "Q2", "accn": "B"},
]}}},
}}
filings = {"B": FilingMeta(RD, date(2026, 5, 1), datetime(2026, 5, 1, tzinfo=UTC), "10-Q")}
res = parse_snapshots(cf, filings, {"B"})
assert len(res.rows) == 1 and res.rows[0].shares_outstanding is None # row kept, shares null
assert res.field_issues == [{"accession": "B", "reason": "ambiguous shares outstanding"}]
assert res.skipped_filings == [] # a field issue is NOT a skipped filing
# Opt-in live check against real Apple companyfacts. Skips unless SEC_LIVE=1 and a
# real SEC_USER_AGENT are set (network + fair-access contact email).
@pytest.mark.skipif(
not (os.environ.get("SEC_LIVE") and os.environ.get("SEC_USER_AGENT")),
reason="set SEC_LIVE=1 + SEC_USER_AGENT to run the live SEC parser check",
)
async def test_live_apple_parse_invariants():
from app.services.sec_client import SecClient
def _dt(s):
return datetime.fromisoformat(s.replace("Z", "+00:00")) if s else None
async with SecClient(user_agent=os.environ["SEC_USER_AGENT"]) as c:
cf = await c.companyfacts(320193)
sub = await c.submissions(320193, include_history=False)
filings = {
f["accession"]: FilingMeta(
date.fromisoformat(f["report_date"]),
date.fromisoformat(f["filing_date"]),
_dt(f["acceptance_datetime"]),
f["form"],
)
for f in sub["filings"]
if f["report_date"] and f["filing_date"] and f["acceptance_datetime"]
}
rows = parse_snapshots(cf, filings, set(filings)).rows
assert len(rows) > 20
assert all(r.period_end and r.fiscal_year and r.fiscal_period for r in rows)
# YTD revenue is non-decreasing within a fiscal year
by_fy: dict[int, list] = {}
for r in rows:
if r.revenue is not None:
by_fy.setdefault(r.fiscal_year, []).append((r.fiscal_period, r.revenue))
order = {"Q1": 1, "Q2": 2, "Q3": 3, "FY": 4}
for fy, series in by_fy.items():
series.sort(key=lambda x: order[x[0]])
vals = [v for _, v in series]
assert vals == sorted(vals), f"YTD revenue not monotonic in FY{fy}: {series}"
# shares cover-date differs from period_end
latest = max(rows, key=lambda r: r.period_end)
assert latest.shares_outstanding_date != latest.period_end
# -- revenue concept coverage (A5 parity findings) ---------------------------
def _one_filing(concepts: dict, *, start: str, end: str, fp: str):
"""A single 10-Q whose facts are the given {concept: value} at one YTD span."""
facts = {
name: {"units": {"USD": [_dur(start, end, val, "X", fp=fp)]}}
for name, val in concepts.items()
}
companyfacts = {"cik": 19617, "facts": {"us-gaap": facts}}
filings = {
"X": FilingMeta(
date.fromisoformat(end), date(2026, 5, 1), datetime(2026, 5, 1, 10, tzinfo=UTC), "10-Q"
)
}
return parse_snapshots(companyfacts, filings, {"X"})
def test_revenue_reads_banks_total_revenue_tag():
# JPM/GS/WFC tag RevenuesNetOfInterestExpense in every 10-Q and never (or
# only annually) `Revenues` -- previously null, so revenue growth was too.
res = _one_filing(
{"RevenuesNetOfInterestExpense": 49836}, start="2026-01-01", end="2026-03-31", fp="Q1"
)
assert res.rows[0].revenue == 49836
def test_revenue_reads_including_assessed_tax_variant():
# ARE/KHC tag only the Including variant.
res = _one_filing(
{"RevenueFromContractWithCustomerIncludingAssessedTax": 671},
start="2026-01-01", end="2026-03-31", fp="Q1",
)
assert res.rows[0].revenue == 671
def test_revenue_concept_priority_is_unchanged_by_the_added_tags():
# The new entries are appended, so any issuer that already resolved keeps
# the same concept -- only issuers that resolved to nothing gain a value.
res = _one_filing(
{
"RevenueFromContractWithCustomerExcludingAssessedTax": 100,
"RevenueFromContractWithCustomerIncludingAssessedTax": 110,
"RevenuesNetOfInterestExpense": 120,
"Revenues": 130,
},
start="2026-01-01", end="2026-03-31", fp="Q1",
)
assert res.rows[0].revenue == 100
def test_four_four_five_q3_ytd_span_is_accepted():
# A 12/12/12/16-week filer's YTD-Q3 is 36 weeks = 251 days (COST 2026 Q3),
# which missed the old 20-day tolerance around 273 by ~2 and dropped Q3
# every year -- breaking the quarter chain and nulling TTM and YoY.
res = _one_filing(
{"RevenueFromContractWithCustomerExcludingAssessedTax": 207431},
start="2025-09-01", end="2026-05-10", fp="Q3",
)
assert (date(2026, 5, 10) - date(2025, 9, 1)).days == 251
assert res.rows[0].revenue == 207431
def test_eps_falls_back_to_continuing_operations_variant():
# REG tags only this variant on every filing; FCX tags it in its 10-K while
# using EarningsPerShareDiluted in its 10-Qs.
companyfacts = {
"cik": 910606,
"facts": {"us-gaap": {"IncomeLossFromContinuingOperationsPerDilutedShare": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 1.81, "X", fp="Q1")]}
}}},
}
filings = {"X": FilingMeta(date(2026, 3, 31), date(2026, 5, 1),
datetime(2026, 5, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"})
assert res.rows[0].diluted_eps == 1.81
def test_eps_concept_priority_is_unchanged_by_the_added_tag():
companyfacts = {
"cik": 831259,
"facts": {"us-gaap": {
"EarningsPerShareDiluted": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 0.61, "X", fp="Q1")]}},
"IncomeLossFromContinuingOperationsPerDilutedShare": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 0.75, "X", fp="Q1")]}},
}},
}
filings = {"X": FilingMeta(date(2026, 3, 31), date(2026, 5, 1),
datetime(2026, 5, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"})
assert res.rows[0].diluted_eps == 0.61
# -- period identity from the fiscal calendar, not SEC's fy/fp ---------------
from app.services.sec_facts_parser import _period_identity # noqa: E402
def _meta(end: str, form: str = "10-Q") -> FilingMeta:
d = date.fromisoformat(end)
return FilingMeta(d, d, datetime(d.year, d.month, d.day, tzinfo=UTC), form)
def test_a_10q_is_never_labelled_fy():
# BXP: a 10-Q for period end 2026-03-31 carried fy/fp saying "2026 FY", which
# collided with the real annual row and measured a 90-day fact against the
# 365-day FY expectation.
fy, fp = _period_identity(_meta("2026-03-31"), "1231")
assert (fy, fp) == (2026, "Q1")
def test_december_filer_years_do_not_collide():
# FRT: two 10-Ks, ending 2024-12-31 and 2025-12-31, both labelled "2024 FY".
assert _period_identity(_meta("2024-12-31", "10-K"), "1231") == (2024, "FY")
assert _period_identity(_meta("2025-12-31", "10-K"), "1231") == (2025, "FY")
def test_january_year_end_groups_its_quarters():
# CRM/CRWD/WDAY: the year ending 2026-01-31 and its own quarters must share a
# fiscal year, and must not collide with the year ending 2025-01-31.
assert _period_identity(_meta("2026-01-31", "10-K"), "0131") == (2026, "FY")
assert _period_identity(_meta("2025-01-31", "10-K"), "0131") == (2025, "FY")
assert _period_identity(_meta("2025-04-30"), "0131") == (2026, "Q1")
assert _period_identity(_meta("2025-07-31"), "0131") == (2026, "Q2")
assert _period_identity(_meta("2025-10-31"), "0131") == (2026, "Q3")
def test_mid_year_end_orders_correctly():
# STX: the year ending 2025-06-27 was labelled "2027 FY" and sorted after
# quarters that precede it.
assert _period_identity(_meta("2025-06-27", "10-K"), "0627") == (2025, "FY")
assert _period_identity(_meta("2025-10-03"), "0627") == (2026, "Q1")
assert _period_identity(_meta("2026-01-02"), "0627") == (2026, "Q2")
assert _period_identity(_meta("2026-04-03"), "0627") == (2026, "Q3")
def test_four_four_five_quarters_place_correctly():
# COST: a 12/12/12/16-week year leaves Q3 112 days from the year end, not 91.
assert _period_identity(_meta("2025-11-23"), "0830") == (2026, "Q1")
assert _period_identity(_meta("2026-02-15"), "0830") == (2026, "Q2")
assert _period_identity(_meta("2026-05-10"), "0830") == (2026, "Q3")
assert _period_identity(_meta("2026-08-30", "10-K"), "0830") == (2026, "FY")
def test_year_end_crossing_january_still_groups_one_year():
# DPZ (fiscalYearEnd 0102): the label shifts by one against Domino's own
# naming, which is fine -- a year and its quarters must simply agree.
year, _ = _period_identity(_meta("2025-12-28", "10-K"), "0102")
assert (year, "FY") == _period_identity(_meta("2025-12-28", "10-K"), "0102")
assert _period_identity(_meta("2025-03-23"), "0102") == (year, "Q1")
assert _period_identity(_meta("2025-06-15"), "0102") == (year, "Q2")
assert _period_identity(_meta("2025-09-07"), "0102") == (year, "Q3")
def test_missing_fiscal_calendar_falls_back_to_filing_context():
assert _period_identity(_meta("2026-03-31"), None) == (None, None)
# ...and parse_snapshots then uses the fy/fp path, preserving old behaviour.
res = parse_snapshots(COMPANYFACTS, FILINGS, {"B"})
assert (res.rows[0].fiscal_year, res.rows[0].fiscal_period) == (2026, "Q2")
def test_eps_falls_back_to_basic_only_when_no_diluted_variant_exists():
# PPL's 2026 Q1 tags no diluted EPS at all, only basic -- one missing period
# broke the quarter chain and nulled TTM.
companyfacts = {
"cik": 922224,
"facts": {"us-gaap": {"EarningsPerShareBasic": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 0.60, "X", fp="Q1")]}
}}},
}
filings = {"X": FilingMeta(date(2026, 3, 31), date(2026, 5, 1),
datetime(2026, 5, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
assert res.rows[0].diluted_eps == 0.60
def test_diluted_still_wins_over_basic_when_both_present():
companyfacts = {
"cik": 320193,
"facts": {"us-gaap": {
"EarningsPerShareDiluted": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 1.36, "X", fp="Q1")]}},
"EarningsPerShareBasic": {
"units": {"USD/shares": [_dur("2026-01-01", "2026-03-31", 1.40, "X", fp="Q1")]}},
}},
}
filings = {"X": FilingMeta(date(2026, 3, 31), date(2026, 5, 1),
datetime(2026, 5, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
assert res.rows[0].diluted_eps == 1.36
def test_weighted_average_shares_prefers_the_shortest_span():
# A 10-Q carries both the quarter's average and the YTD one. The shorter
# window sits closer to the current count, which is what market cap wants.
companyfacts = {
"cik": 1326801,
"facts": {"us-gaap": {"WeightedAverageNumberOfDilutedSharesOutstanding": {
"units": {"shares": [
_dur("2026-01-01", "2026-09-30", 2_600_000_000, "X", fp="Q3"), # YTD
_dur("2026-07-01", "2026-09-30", 2_564_000_000, "X", fp="Q3"), # quarter
]}
}}},
}
filings = {"X": FilingMeta(date(2026, 9, 30), date(2026, 11, 1),
datetime(2026, 11, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
assert res.rows[0].weighted_avg_diluted_shares == 2_564_000_000
def test_weighted_average_shares_falls_back_to_the_basic_and_diluted_concept():
companyfacts = {
"cik": 1326801,
"facts": {"us-gaap": {"WeightedAverageNumberOfSharesOutstandingBasicAndDiluted": {
"units": {"shares": [_dur("2026-07-01", "2026-09-30", 500_000, "X", fp="Q3")]}
}}},
}
filings = {"X": FilingMeta(date(2026, 9, 30), date(2026, 11, 1),
datetime(2026, 11, 1, 10, tzinfo=UTC), "10-Q")}
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
assert res.rows[0].weighted_avg_diluted_shares == 500_000
# -- fiscal year end resolution (submissions.fiscalYearEnd is unreliable) -----
from app.services.sec_facts_parser import resolve_fiscal_year_end # noqa: E402
def test_the_issuers_own_10k_overrides_a_wrong_declared_year_end():
# Franklin Resources declares 1231 while every 10-K ends 09-30. Trusting the
# declaration labelled its fiscal Q2 (Mar) as Q1, colliding with the real
# fiscal Q1 (Dec) and destroying the quarter chain.
filings = {
"K": _meta("2025-09-30", "10-K"),
"Q": _meta("2025-12-31"),
}
assert resolve_fiscal_year_end(filings, "1231") == "0930"
def test_declared_year_end_is_used_when_no_annual_filing_is_present():
assert resolve_fiscal_year_end({"Q": _meta("2026-03-31")}, "1231") == "1231"
assert resolve_fiscal_year_end({}, None) is None
def test_a_wrong_declared_year_end_no_longer_collides_two_periods():
"""End to end: BEN's Dec and Mar quarters must land on distinct keys."""
def _q(accn, start, end, val):
return _dur(start, end, val, accn, fp="Q1")
companyfacts = {
"cik": 38777,
"facts": {"us-gaap": {"Revenues": {"units": {"USD": [
_q("Q1", "2025-10-01", "2025-12-31", 2327), # fiscal Q1
_q("Q2", "2025-10-01", "2026-03-31", 4622), # fiscal Q2 YTD
]}}}},
}
filings = {
"K": _meta("2025-09-30", "10-K"),
"Q1": _meta("2025-12-31"),
"Q2": _meta("2026-03-31"),
}
res = parse_snapshots(companyfacts, filings, {"Q1", "Q2"}, fiscal_year_end="1231")
keys = {(r.fiscal_year, r.fiscal_period) for r in res.rows}
assert len(keys) == 2, f"periods collided on one key: {keys}"
assert keys == {(2026, "Q1"), (2026, "Q2")}
# --- debt composition across the tagging styles large filers actually use ----
# Values are the real shapes measured 2026-08; before this composition, seven of
# nineteen sampled large caps carried a materially wrong or absent total_debt.
_RD = date(2026, 3, 28)
def _f(concept, val):
return Fact("us-gaap", concept, "USD", None, _RD, val, 2026, "Q2")
def test_debt_from_a_noncurrent_lease_aggregate_adds_its_current_side():
"""KO/HD/T/XOM/CVX tag LongTermDebtAndCapitalLeaseObligations, which nothing
read before — AT&T reported no debt at all against 134bn tagged."""
facts = [_f("LongTermDebtAndCapitalLeaseObligations", 134_630), _f("DebtCurrent", 9_320)]
assert _compose_debt(facts, _RD) == 143_950
def test_debt_current_is_the_whole_current_side_not_an_addition():
"""DebtCurrent already spans short-term borrowing AND current maturities, so
adding commercial paper on top would count it twice."""
facts = [
_f("LongTermDebtNoncurrent", 22_840),
_f("DebtCurrent", 11_300),
_f("LongTermDebtCurrent", 6_460),
_f("CommercialPaper", 4_840),
]
assert _compose_debt(facts, _RD) == 34_140
def test_debt_falls_back_to_the_split_current_parts():
facts = [
_f("LongTermDebtNoncurrent", 36_890),
_f("LongTermDebtCurrent", 3_900),
_f("ShortTermBorrowings", 10_670),
]
assert _compose_debt(facts, _RD) == 51_460
def test_notes_payable_is_the_unsecured_side_when_nothing_names_it():
"""Realty Income and VMRK tag a secured and an unsecured side, no aggregate."""
facts = [_f("NotesPayable", 25_090), _f("SecuredDebt", 40), _f("CommercialPaper", 1_400)]
assert _compose_debt(facts, _RD) == 26_530
def test_an_explicit_unsecured_side_wins_over_notes_payable():
"""MAA tags NotesPayable 5.66bn = UnsecuredDebt 5.30bn + SecuredDebt 0.36bn, so
NotesPayable is the total there and adding SecuredDebt to it double-counts.
Preferring the explicit unsecured side reproduces the total either way."""
facts = [_f("NotesPayable", 5_660), _f("UnsecuredDebt", 5_300), _f("SecuredDebt", 360)]
assert _compose_debt(facts, _RD) == 5_660
def test_one_side_of_a_reits_debt_is_not_a_total():
"""Boston Properties tags SecuredDebt 4.28bn and commercial paper against ~15bn
of real debt; Ventas the same shape. Composing from one side invents a total."""
assert _compose_debt([_f("SecuredDebt", 4_280), _f("CommercialPaper", 750)], _RD) is None
assert _compose_debt([_f("UnsecuredDebt", 5_300)], _RD) is None
def test_current_maturities_alone_are_not_a_total():
"""LongTermDebtCurrent used to stand in for the whole long-term side, which
reports the slice due within a year as if it were the debt."""
assert _compose_debt([_f("LongTermDebtCurrent", 6_460)], _RD) is None
def test_an_aggregate_beats_the_reit_parts():
"""AvalonBay tags all three; summing the parts would understate the total."""
facts = [_f("LongTermDebt", 9_020), _f("SecuredDebt", 700), _f("UnsecuredDebt", 7_410),
_f("CommercialPaper", 920)]
assert _compose_debt(facts, _RD) == 9_940
def test_a_short_term_only_filing_reports_no_total_at_all():
"""Chevron tags its full debt only in the 10-K, so a 10-Q carries 0.40bn of
short-term borrowing alone — reporting that as *total* debt reads as a
near-unlevered issuer carrying 50bn. None costs a leverage read; the partial
value produces a confidently wrong one."""
assert _compose_debt([_f("ShortTermBorrowings", 401)], _RD) is None
def test_no_debt_facts_at_all_is_still_none():
assert _compose_debt([_f("CashAndCashEquivalentsAtCarryingValue", 100)], _RD) is None