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)
This commit is contained in:
parent
a2e7d59af2
commit
9e1beb7495
7 changed files with 447 additions and 138 deletions
|
|
@ -1,8 +1,7 @@
|
|||
"""API routes for listing decisions (like/dislike)."""
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from api.auth import User, get_current_user
|
||||
|
|
@ -18,11 +17,10 @@ decision_router = APIRouter(prefix="/api/decisions", tags=["decisions"])
|
|||
|
||||
class SetDecisionRequest(BaseModel):
|
||||
decision: str = Field(description="'liked' or 'disliked'")
|
||||
listing_type: str = Field(default="RENT", description="'RENT' or 'BUY'")
|
||||
listing_type: str = Field(description="'RENT' or 'BUY'")
|
||||
|
||||
|
||||
class DecisionResponse(BaseModel):
|
||||
id: int
|
||||
listing_id: int
|
||||
listing_type: str
|
||||
decision: str
|
||||
|
|
@ -35,19 +33,30 @@ def _get_user_id(user: User) -> int:
|
|||
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 a decision (like/dislike) on a listing."""
|
||||
"""Set or update a like/dislike decision for a listing."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = DecisionRepository(engine)
|
||||
try:
|
||||
|
|
@ -60,14 +69,7 @@ async def set_decision(
|
|||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return DecisionResponse(
|
||||
id=result.id, # type: ignore[arg-type]
|
||||
listing_id=result.listing_id,
|
||||
listing_type=result.listing_type,
|
||||
decision=result.decision,
|
||||
created_at=result.created_at.isoformat(),
|
||||
updated_at=result.updated_at.isoformat(),
|
||||
)
|
||||
return _to_response(result)
|
||||
|
||||
|
||||
@decision_router.get("", response_model=list[DecisionResponse])
|
||||
|
|
@ -77,35 +79,25 @@ async def get_decisions(
|
|||
"""Get all decisions for the current user."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = DecisionRepository(engine)
|
||||
decisions = decision_service.get_decisions(repo, user_id=user_id)
|
||||
return [
|
||||
DecisionResponse(
|
||||
id=d.id, # type: ignore[arg-type]
|
||||
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(),
|
||||
)
|
||||
for d in decisions
|
||||
]
|
||||
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 = "RENT",
|
||||
listing_type: str = Query(..., description="RENT or BUY"),
|
||||
) -> dict[str, bool]:
|
||||
"""Remove a decision (back to neutral)."""
|
||||
"""Remove a decision (un-like/un-dislike)."""
|
||||
user_id = _get_user_id(user)
|
||||
repo = DecisionRepository(engine)
|
||||
deleted = decision_service.clear_decision(
|
||||
repo,
|
||||
user_id=user_id,
|
||||
listing_id=listing_id,
|
||||
listing_type=listing_type,
|
||||
)
|
||||
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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue