"""Analytics endpoints for the Sverdlovsk dashboard.""" from __future__ import annotations import re from typing import Annotated, Any, Literal from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.core.db import get_db from app.schemas.complex_buildings import ComplexBuilding from app.schemas.recommend import RecommendMixInput, RecommendMixOutput from app.services import analytics_queries as q router = APIRouter() DEV_ID_RE = re.compile(r"^\d+_\d+$") def _validate_dev(dev_id: str) -> str: if not DEV_ID_RE.match(dev_id): raise HTTPException(status_code=400, detail="developer_id must match \\d+_\\d+") return dev_id # ---- Sverdlovsk region ------------------------------------------------------ @router.get("/sverdl/market-pulse") def sverdl_market_pulse(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]: """Monthly series Jan 2025 → present: total square (тыс м²), sold%, avg price.""" return q.market_pulse(db, region_code=66) @router.get("/sverdl/quartirography") def sverdl_quartirography( db: Annotated[Session, Depends(get_db)], source: Literal["portfolio", "deals"] = "portfolio", ) -> list[dict[str, Any]]: """Что строится (portfolio) vs что покупают (deals) по сегментам.""" return q.quartirography(db, source=source, region_id=66) @router.get("/sverdl/pipeline") def sverdl_pipeline(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]: """По году ввода: total / sold% / unsold% / unopened%.""" return q.pipeline_by_year(db, region_code=66) @router.get("/sverdl/districts") def sverdl_districts(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]: """ЕКБ-районы: ЖК-count, flat-count, area, цены.""" return q.districts(db) @router.get("/sverdl/yandex-listings") def sverdl_yandex_listings(db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]: """Снимок активных новостроек Яндекс.Недвижимости.""" return q.yandex_listings(db) # ---- Developers ------------------------------------------------------------- @router.get("/developers/top") def developers_top( db: Annotated[Session, Depends(get_db)], limit: Annotated[int, Query(ge=1, le=50)] = 15, ) -> list[dict[str, Any]]: """Топ-девелоперы Свердл по объёму с Δ sold% за всю доступную историю.""" return q.top_developers(db, region_code=66, limit=limit) @router.get("/developers/history") def developers_history( db: Annotated[Session, Depends(get_db)], ids: Annotated[str, Query(min_length=1)], ) -> list[dict[str, Any]]: """Comma-separated developer_ids → time series sold_perc.""" dev_ids = [_validate_dev(i.strip()) for i in ids.split(",") if i.strip()] if not dev_ids: raise HTTPException(status_code=400, detail="ids must contain at least one developer_id") return q.developer_history(db, developer_ids=dev_ids, region_code=66) @router.get("/developers/{developer_id}") def developer_detail( db: Annotated[Session, Depends(get_db)], developer_id: str, ) -> dict[str, Any]: dev = _validate_dev(developer_id) result = q.developer_detail(db, developer_id=dev) if result is None: raise HTTPException(status_code=404, detail="developer not found") return result @router.get("/developers/{developer_id}/portfolio") def developer_portfolio( db: Annotated[Session, Depends(get_db)], developer_id: str, ) -> list[dict[str, Any]]: dev = _validate_dev(developer_id) return q.developer_portfolio(db, developer_id=dev) # ---- Per-object drill-in (sale_graph / sales_agg / infra / photos) ---------- @router.get("/object/{obj_id}/sale_graph") def object_sale_graph( db: Annotated[Session, Depends(get_db)], obj_id: int, type_filter: Annotated[str | None, Query(alias="type")] = None, ) -> list[dict[str, Any]]: """Time-series продаж per-ЖК. type=apartments|parking, без фильтра — обе серии.""" return q.object_sale_graph(db, obj_id=obj_id, type_filter=type_filter) @router.get("/object/{obj_id}/sales_agg") def object_sales_agg( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> list[dict[str, Any]]: """3 строки: apartments + nonliv + parking (total / realised / sold%).""" return q.object_sales_agg(db, obj_id=obj_id) @router.get("/object/{obj_id}/infrastructure") def object_infrastructure( db: Annotated[Session, Depends(get_db)], obj_id: int, category: Annotated[str | None, Query()] = None, max_distance: Annotated[int, Query(ge=0, le=10000)] = 5000, ) -> list[dict[str, Any]]: """POI вокруг ЖК. Фильтр по category (Спорт/Образование/...) и radius.""" return q.object_infrastructure(db, obj_id=obj_id, category=category, max_distance=max_distance) @router.get("/object/{obj_id}/photos") def object_photos( db: Annotated[Session, Depends(get_db)], obj_id: int, limit: Annotated[int, Query(ge=1, le=500)] = 100, ) -> list[dict[str, Any]]: """Фото-метаданные с photo_url, отсортированные по period_dt DESC.""" return q.object_photos(db, obj_id=obj_id, limit=limit) @router.get("/object/{obj_id}") def object_detail( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> dict[str, Any]: """Базовая инфа объекта из domrf_kn_objects (имя, адрес, координаты, девелопер).""" result = q.object_detail(db, obj_id=obj_id) if result is None: raise HTTPException(status_code=404, detail="object not found") return result @router.get("/object/{obj_id}/full") def object_full_detail( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> dict[str, Any]: """Extended object info — все 30+ новых полей (22begh: specs/accessibility/metro/scores).""" result = q.object_full_detail(db, obj_id=obj_id) if result is None: raise HTTPException(status_code=404, detail="object not found") return result @router.get("/object/{obj_id}/quartirography") def object_quartirography( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> list[dict[str, Any]]: """Квартирография ЖК: группировка flats по комнатности с count/area/price.""" return q.object_flats_quartirography(db, obj_id=obj_id) @router.get("/object/{obj_id}/checks") def object_checks( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> list[dict[str, Any]]: """6 проверок «Проверено на наш.дом.рф» (22f).""" return q.object_obj_checks(db, obj_id=obj_id) @router.get("/object/{obj_id}/documents") def object_documents( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> list[dict[str, Any]]: """PDF документы из domrf_kn_documents (22i): декларации, разрешения, отчётность.""" return q.object_documents(db, obj_id=obj_id) @router.get("/object/{obj_id}/buildings", response_model=list[ComplexBuilding]) def object_buildings( db: Annotated[Session, Depends(get_db)], obj_id: int, ) -> list[dict[str, Any]]: """Список кадастровых зданий ЖК из cad_buildings.""" return q.complex_buildings(db, obj_id=obj_id) # ---- PRINZIP-specific ------------------------------------------------------- @router.get("/prinzip/insights") def prinzip_insights() -> dict[str, Any]: """Static recommendations + benchmarks (from PRINZIP_Strategy_Apr27).""" return q.prinzip_insights() @router.get("/prinzip/districts") def prinzip_districts(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]: return q.prinzip_district_distribution(db, developer_id="6208_0") @router.get("/prinzip/objects") def prinzip_objects(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]: """Список 28 ЖК PRINZIP с агрегатами sales_agg + sparkline-данными для UI таблицы.""" return q.prinzip_objects_with_velocity(db) # Funnel-эндпойнты (CRM-данные) перенесены в backend/app/api/v1/admin_leads.py: # /api/v1/admin/leads/funnel/{monthly,by-source,by-object} с X-Admin-Token. # ---- Recommender (Уровень 1, rule-based) ----------------------------------- @router.post("/recommend/mix", response_model=RecommendMixOutput) def recommend_mix( payload: RecommendMixInput, db: Annotated[Session, Depends(get_db)], ) -> dict[str, Any]: """Rule-based квартирография для участка в указанном районе ЕКБ. На вход — district_name (8 районов ЕКБ), опционально area_total_m2 и target_class. На выход — распределение по 5 бакетам комнатности с ожидаемыми ценами/площадями/выручкой и список comparable ЖК. """ return q.recommend_mix( db, district_name=payload.district_name, area_total_m2=payload.area_total_m2, target_class=payload.target_class, months_window=payload.months_window, price_factor=payload.price_factor, target_months=payload.target_months, )