wrongmove/api/decision_routes.py
Viktor Barzin 9e1beb7495
Add listing decisions (like/dislike) backend with detail endpoint
- ListingDecision model with unique constraint on (user_id, listing_id, listing_type)
- Alembic migration for listingdecision table
- DecisionRepository with dialect-aware upsert (MySQL/SQLite)
- DecisionService with input validation
- Decision API routes: PUT/GET/DELETE on /api/decisions
- GET /api/listing/{id}/detail endpoint extracting full property info from additional_info
- Add listing ID to GeoJSON feature properties
- Decision filtering on GeoJSON stream endpoint (decision_filter param)
2026-02-21 15:49:10 +00:00

103 lines
3.3 KiB
Python

import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from api.auth import User, get_current_user
from database import engine
from repositories.decision_repository import DecisionRepository
from repositories.user_repository import UserRepository
from services import decision_service
logger = logging.getLogger("uvicorn")
decision_router = APIRouter(prefix="/api/decisions", tags=["decisions"])
class SetDecisionRequest(BaseModel):
decision: str = Field(description="'liked' or 'disliked'")
listing_type: str = Field(description="'RENT' or 'BUY'")
class DecisionResponse(BaseModel):
listing_id: int
listing_type: str
decision: str
created_at: str
updated_at: str
def _get_user_id(user: User) -> int:
"""Resolve auth User to database user ID."""
user_repo = UserRepository(engine)
db_user = user_repo.get_user_by_email(user.email)
if db_user is None:
# Auto-create user on first decision interaction
db_user = user_repo.create_user(user.email)
if db_user.id is None:
raise HTTPException(status_code=500, detail="Failed to create user")
return db_user.id
def _to_response(d: decision_service.ListingDecision) -> DecisionResponse:
return DecisionResponse(
listing_id=d.listing_id,
listing_type=d.listing_type,
decision=d.decision,
created_at=d.created_at.isoformat(),
updated_at=d.updated_at.isoformat(),
)
@decision_router.put("/{listing_id}", response_model=DecisionResponse)
async def set_decision(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
body: SetDecisionRequest,
) -> DecisionResponse:
"""Set or update a like/dislike decision for a listing."""
user_id = _get_user_id(user)
repo = DecisionRepository(engine)
try:
result = decision_service.set_decision(
repo,
user_id=user_id,
listing_id=listing_id,
listing_type=body.listing_type,
decision=body.decision,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return _to_response(result)
@decision_router.get("", response_model=list[DecisionResponse])
async def get_decisions(
user: Annotated[User, Depends(get_current_user)],
) -> list[DecisionResponse]:
"""Get all decisions for the current user."""
user_id = _get_user_id(user)
repo = DecisionRepository(engine)
decisions = decision_service.get_user_decisions(repo, user_id)
return [_to_response(d) for d in decisions]
@decision_router.delete("/{listing_id}")
async def delete_decision(
user: Annotated[User, Depends(get_current_user)],
listing_id: int,
listing_type: str = Query(..., description="RENT or BUY"),
) -> dict[str, bool]:
"""Remove a decision (un-like/un-dislike)."""
user_id = _get_user_id(user)
repo = DecisionRepository(engine)
try:
deleted = decision_service.remove_decision(
repo, user_id, listing_id, listing_type
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if not deleted:
raise HTTPException(status_code=404, detail="Decision not found")
return {"success": True}