"""CRUD API для user_custom_pois (#254). POST /api/v1/custom-pois → 201 CustomPoiOut GET /api/v1/custom-pois → list[CustomPoiOut] PATCH /api/v1/custom-pois/{id} → CustomPoiOut DELETE /api/v1/custom-pois/{id} → 204 Auth: anonymous user_id из header X-Session-Id (workaround до #67 NextAuth). Если заголовок отсутствует — генерируем UUID и возвращаем в X-Session-Id response header. Frontend сохраняет в localStorage. """ from __future__ import annotations import logging import uuid from typing import Annotated, Any from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status from sqlalchemy.orm import Session from app.core.db import get_db from app.schemas.custom_poi import CustomPoiCreate, CustomPoiOut, CustomPoiUpdate from app.services.site_finder.custom_pois import ( create_custom_poi, delete_custom_poi, list_custom_pois, update_custom_poi, ) logger = logging.getLogger(__name__) router = APIRouter() def _resolve_session_id( x_session_id: str | None, response: Response, ) -> str: """Вернуть user_id из X-Session-Id header или сгенерировать UUID. При генерации устанавливаем X-Session-Id в response header чтобы frontend мог сохранить значение в localStorage. """ if x_session_id and x_session_id.strip(): return x_session_id.strip() generated = str(uuid.uuid4()) response.headers["X-Session-Id"] = generated logger.debug("generated session_id=%s (no X-Session-Id header)", generated) return generated @router.post("", response_model=CustomPoiOut, status_code=status.HTTP_201_CREATED) def create_poi( payload: CustomPoiCreate, db: Annotated[Session, Depends(get_db)], response: Response, x_session_id: Annotated[str | None, Header()] = None, ) -> Any: """Создать кастомную POI точку. Возвращает 201 с созданным объектом. X-Session-Id response header содержит актуальный session_id (пригодится frontend для localStorage). """ user_id = _resolve_session_id(x_session_id, response) poi = create_custom_poi(db, user_id, payload) # Echo back session_id чтобы frontend всегда имел его в response response.headers["X-Session-Id"] = user_id return poi @router.get("", response_model=list[CustomPoiOut]) def list_pois( db: Annotated[Session, Depends(get_db)], response: Response, x_session_id: Annotated[str | None, Header()] = None, parcel_cad: Annotated[ str | None, Query(description="Фильтр по кадастровому номеру участка (возвращает global + parcel POI)"), ] = None, ) -> Any: """Список кастомных POI пользователя. Если parcel_cad задан — возвращает global (parcel_cad IS NULL) + parcel-specific POI. """ user_id = _resolve_session_id(x_session_id, response) response.headers["X-Session-Id"] = user_id return list_custom_pois(db, user_id, parcel_cad) @router.patch("/{poi_id}", response_model=CustomPoiOut) def patch_poi( poi_id: int, payload: CustomPoiUpdate, db: Annotated[Session, Depends(get_db)], response: Response, x_session_id: Annotated[str | None, Header()] = None, ) -> Any: """Обновить поля кастомной POI (PATCH-style). 404 если не найдена.""" user_id = _resolve_session_id(x_session_id, response) response.headers["X-Session-Id"] = user_id poi = update_custom_poi(db, poi_id, user_id, payload) if poi is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Custom POI not found") return poi @router.delete("/{poi_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_poi( poi_id: int, db: Annotated[Session, Depends(get_db)], response: Response, x_session_id: Annotated[str | None, Header()] = None, ) -> None: """Удалить кастомную POI. 404 если не найдена.""" user_id = _resolve_session_id(x_session_id, response) deleted = delete_custom_poi(db, poi_id, user_id) if not deleted: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Custom POI not found")