"""Per-ticker validation + historical-price endpoints. These power the dashboard's "Add a position" form. Neither endpoint persists holdings — they wrap the existing Yahoo chart fetcher and optionally seed anonymous ticker_universe (validate only). Both endpoints are gated behind ``require_paid`` so they match the rest of the import surface. """ from __future__ import annotations from datetime import datetime, timezone import httpx from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.auth import require_auth from app.db import get_session, utcnow from app.logging import get_logger from app.models import Quote as QuoteModel from app.services.access import require_paid from app.services.market import ( UA, YAHOO_CHART, Quote, _yahoo_range_covering, fetch_yahoo, ) from app.services.ticker_universe import upsert_tickers log = get_logger("ticker_validate") router = APIRouter(dependencies=[Depends(require_auth)]) @router.get( "/ticker/validate", dependencies=[Depends(require_paid)], ) async def validate_ticker( symbol: str, session: AsyncSession = Depends(get_session), ) -> dict: """Live quote for one ticker. Returns ``{ok: true, symbol, price, currency, as_of}`` on success or ``{ok: false, error}`` when the symbol isn't recognised. Seeds ticker_universe + writes a Quote row as a side-effect on success so the dashboard's /api/universe call picks it up on the next refresh.""" symbol = symbol.strip().upper()[:32] if not symbol: return {"ok": False, "error": "symbol required"} async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client: quote = await fetch_yahoo(client, symbol, symbol, "") if quote.error or quote.price is None: log.info("ticker.validate.miss", symbol=symbol, error=(quote.error or "no price")[:120]) return {"ok": False, "error": "Symbol not recognised"} # Side-effect: seed the universe + write the quote so /api/universe # has data on the next minute-cycle refresh. await upsert_tickers(session, [symbol]) session.add(QuoteModel( symbol=quote.symbol, source=quote.source, label=quote.label, group_name="universe", price=quote.price, currency=quote.currency, as_of=quote.as_of, changes=quote.changes or None, error=None, fetched_at=utcnow(), )) await session.commit() return { "ok": True, "symbol": quote.symbol, "price": quote.price, "currency": quote.currency, "as_of": quote.as_of, }