| 1 | from datetime import datetime, timezone |
| 2 | from uuid import uuid4 |
| 3 | |
| 4 | import httpx |
| 5 | import pytest |
| 6 | |
| 7 | from app.fact_precedence import FactSourceTier, FinancialFact, FinancialFactKey, merge_fact |
| 8 | from app.international_fundamentals import EodhdFundamentalProvider, SecEdgarFundamentalProvider, international_provider_for |
| 9 | from app.models import CompanyResearchProfile, ProvenancedValue, SourceMode |
| 10 | from app.settings import Settings |
| 11 | from app.structured_research import financial_result_history_from_facts, financial_statement_history_from_facts |
| 12 | |
| 13 | |
| 14 | def _profile(**changes): |
| 15 | values = dict(instrument_id=uuid4(), company_id=uuid4(), company_name="Microsoft Corporation", ticker="MSFT", exchange="NASDAQ", mic="XNAS", country="US", currency="USD") |
| 16 | values.update(changes) |
| 17 | return CompanyResearchProfile(**values) |
| 18 | |
| 19 | |
| 20 | def _client(responses): |
| 21 | def handler(request): |
| 22 | for suffix, payload in responses.items(): |
| 23 | if request.url.path.endswith(suffix): return httpx.Response(200, json=payload) |
| 24 | return httpx.Response(404) |
| 25 | return httpx.AsyncClient(transport=httpx.MockTransport(handler)) |
| 26 | |
| 27 | |
| 28 | @pytest.mark.asyncio |
| 29 | async def test_sec_resolves_cik_and_normalizes_explicit_companyfacts(): |
| 30 | client = _client({"company_tickers.json": {"0": {"ticker": "MSFT", "cik_str": 789019}}, "CIK0000789019.json": {"facts": {"us-gaap": { |
| 31 | "RevenueFromContractWithCustomerExcludingAssessedTax": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 100, "frame": "CY2026Q1", "accn": "a", "filed": "2026-04-25"}]}}, |
| 32 | "NetIncomeLoss": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 25, "frame": "CY2026Q1", "accn": "a"}]}}, |
| 33 | "EarningsPerShareDiluted": {"units": {"USD/shares": [{"form": "10-Q", "end": "2026-03-31", "val": 3, "frame": "CY2026Q1", "accn": "a"}]}}, |
| 34 | "Assets": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 500, "accn": "a"}]}}, |
| 35 | "NetCashProvidedByUsedInOperatingActivities": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 40, "frame": "CY2026Q1", "accn": "a"}]}}, |
| 36 | "PaymentsToAcquirePropertyPlantAndEquipment": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 8, "frame": "CY2026Q1", "accn": "a"}]}}, |
| 37 | }}}}) |
| 38 | result = await SecEdgarFundamentalProvider(Settings(), client=client).collect(_profile()) |
| 39 | assert result.verified_provider_ids == {"SEC_CIK": "0000789019"} |
| 40 | assert {(f.key.metric, f.key.period_type) for f in result.facts} >= {("revenue", "QUARTERLY"), ("pat", "QUARTERLY"), ("eps", "QUARTERLY"), ("total_assets", "AS_AT"), ("operating_cash_flow", "QUARTERLY"), ("capex", "QUARTERLY")} |
| 41 | assert all(f.source_tier == FactSourceTier.OFFICIAL_REGULATORY and f.source_mode == SourceMode.REAL for f in result.facts) |
| 42 | await client.aclose() |
| 43 | |
| 44 | |
| 45 | @pytest.mark.asyncio |
| 46 | async def test_eodhd_validates_isin_exchange_currency_and_reuses_symbol_mapping(): |
| 47 | profile = _profile(company_name="AIXTRON SE", ticker="AIXA", exchange="XETRA", mic="XFRA", country="DE", currency="EUR", isin="DE000A0WMPJ6") |
| 48 | payload = {"General": {"ISIN": "DE000A0WMPJ6", "Exchange": "F", "CurrencyCode": "EUR"}, "Financials": {"Income_Statement": {"yearly": {"2025": {"date": "2025-12-31", "Revenue": "100", "NetIncome": "20"}}, "quarterly": {"2026Q1": {"date": "2026-03-31", "Revenue": "30"}}}, "Balance_Sheet": {"yearly": {"2025": {"date": "2025-12-31", "TotalAssets": "200"}}}, "Cash_Flow": {"yearly": {"2025": {"date": "2025-12-31", "CapitalExpenditures": "5"}}}}} |
| 49 | settings = Settings(eodhd_api_key="test", eodhd_base_url="https://eod.test") |
| 50 | result = await EodhdFundamentalProvider(settings, client=_client({"fundamentals/AIXA": payload})).collect(profile) |
| 51 | assert result.verified_provider_ids == {"EODHD": "AIXA"} |
| 52 | assert {f.key.metric for f in result.facts} >= {"revenue", "pat", "total_assets", "capex"} |
| 53 | assert any(f.key.metric == "revenue" and f.key.period_type == "QUARTERLY" for f in result.facts) |
| 54 | bad = {**payload, "General": {**payload["General"], "CurrencyCode": "USD"}} |
| 55 | assert not (await EodhdFundamentalProvider(settings, client=_client({"fundamentals/AIXA": bad})).collect(profile)).facts |
| 56 | |
| 57 | |
| 58 | def test_international_precedence_and_read_projection_keep_basis_and_series_limits(): |
| 59 | profile = _profile(); now = datetime.now(timezone.utc) |
| 60 | def fact(metric, end, kind, tier, value, basis="CONSOLIDATED"): |
| 61 | return FinancialFact(FinancialFactKey(profile.instrument_id, metric, end, kind, basis), ProvenancedValue(value=value, source_url="https://source", source_name="source", source_type="TEST", retrieved_at=now), tier, "test", end, SourceMode.REAL) |
| 62 | sec = fact("revenue", "2026-03-31", "QUARTERLY", FactSourceTier.OFFICIAL_REGULATORY, 100) |
| 63 | assert merge_fact(sec, fact("revenue", "2026-03-31", "QUARTERLY", FactSourceTier.STRUCTURED_FUNDAMENTALS, 1)) is sec |
| 64 | facts = [fact("revenue", f"202{year}-12-31", "ANNUAL", FactSourceTier.OFFICIAL_REGULATORY, year) for year in range(1, 7)] |
| 65 | facts += [fact("revenue", f"2026-0{month}-28", "QUARTERLY", FactSourceTier.OFFICIAL_REGULATORY, month) for month in range(1, 6)] |
| 66 | history = financial_result_history_from_facts(facts) |
| 67 | assert len([x for x in history if x.period_type == "ANNUAL"]) == 4 |
| 68 | assert len([x for x in history if x.period_type == "QUARTERLY"]) == 4 |
| 69 | mixed = facts + [fact("total_assets", "2026-03-31", "AS_AT", FactSourceTier.OFFICIAL_REGULATORY, 10, "UNKNOWN")] |
| 70 | assert financial_statement_history_from_facts(mixed, period_type="AS_AT", metrics={"total_assets"})[0].reporting_basis == "UNKNOWN" |
| 71 | |
| 72 | |
| 73 | def test_provider_routing_keeps_india_outside_international_pipeline(): |
| 74 | assert international_provider_for(_profile(country="IN", exchange="NSE", mic="XNSE"), Settings()) is None |
| 75 | assert isinstance(international_provider_for(_profile(), Settings()), SecEdgarFundamentalProvider) |
| 76 | assert isinstance(international_provider_for(_profile(country="DE", exchange="XETRA", mic="XFRA", currency="EUR"), Settings(eodhd_api_key="x")), EodhdFundamentalProvider) |