The A5 cutover has been on and observed in production, so SEC Company Facts + DoltHub earnings are already the live source for `fundamental_data`. This removes everything the legacy path still occupied. Gone: the three providers and their config/env keys; the weekly `fundamental_collector` job; the cutover toggle (SEC + Dolt is now the unconditional path, so `off` can no longer silently freeze scoring inputs); the A5 parity report, whose deltas became structurally zero once the candidate builder started writing the table it compared against; and the FMP tier of universe bootstrap. Two behavioral notes: - Disabling **SEC Fundamentals Import** now stops the SEC network fetch only. The local cache refresh moved outside the job-enable check, because candidates also derive from daily closes and earnings events — freezing those on an ingestion pause would stale scoring with no fallback left to recover from. - `/ingestion/fetch?sources=fundamentals` still accepts the key and reports `skipped`; there is no per-ticker fetch any more. Migration 029 does not blanket-delete the leftover settings rows. Migrations run before the service restart, and pre-A6 code reads an absent `job_*_enabled` row as *enabled* — so the two behavior-bearing keys become tombstones pinned to safe values (hidden in Admin) and only the inert three are deleted. Removing the provider keys from the production `.env` is the matching rollout step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
2.5 KiB
Python
86 lines
2.5 KiB
Python
"""Unit tests for fundamental_service — the surviving read path.
|
|
|
|
Writes to ``fundamental_data`` are covered by test_fundamental_data_refresh.py;
|
|
this file only guards the lookup used by the router and scoring.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
|
|
from app.database import Base
|
|
from app.exceptions import NotFoundError
|
|
from app.models.fundamental import FundamentalData
|
|
from app.models.ticker import Ticker
|
|
from app.services import fundamental_service
|
|
|
|
# Use a dedicated engine so commit/refresh work without conflicting
|
|
# with the conftest transactional session.
|
|
_engine = create_async_engine("sqlite+aiosqlite://", echo=False)
|
|
_session_factory = async_sessionmaker(_engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
async def _setup_tables():
|
|
async with _engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
yield
|
|
async with _engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.drop_all)
|
|
|
|
|
|
@pytest.fixture
|
|
async def session() -> AsyncSession:
|
|
async with _session_factory() as s:
|
|
yield s
|
|
|
|
|
|
@pytest.fixture
|
|
async def ticker(session: AsyncSession) -> Ticker:
|
|
"""Create a test ticker."""
|
|
t = Ticker(symbol="AAPL")
|
|
session.add(t)
|
|
await session.commit()
|
|
await session.refresh(t)
|
|
return t
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_fundamental_returns_the_cached_row(
|
|
session: AsyncSession, ticker: Ticker
|
|
):
|
|
fields = {"pe_ratio": "split guard applied"}
|
|
session.add(
|
|
FundamentalData(
|
|
ticker_id=ticker.id,
|
|
pe_ratio=None,
|
|
market_cap=1_000_000.0,
|
|
fetched_at=datetime.now(timezone.utc),
|
|
unavailable_fields_json=json.dumps(fields),
|
|
)
|
|
)
|
|
await session.commit()
|
|
|
|
record = await fundamental_service.get_fundamental(session, symbol="aapl")
|
|
|
|
assert record is not None
|
|
assert record.market_cap == 1_000_000.0
|
|
assert json.loads(record.unavailable_fields_json) == fields
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_fundamental_returns_none_without_a_cached_row(
|
|
session: AsyncSession, ticker: Ticker
|
|
):
|
|
assert await fundamental_service.get_fundamental(session, symbol="AAPL") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_fundamental_rejects_an_unknown_symbol(session: AsyncSession):
|
|
with pytest.raises(NotFoundError):
|
|
await fundamental_service.get_fundamental(session, symbol="NOPE")
|