2026-02-21 13:54:38 +00:00
|
|
|
import logging
|
2026-02-23 21:30:51 +00:00
|
|
|
import time
|
2026-02-21 13:54:38 +00:00
|
|
|
from typing import Annotated
|
|
|
|
|
|
2026-02-23 21:30:51 +00:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
2026-02-21 13:54:38 +00:00
|
|
|
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'")
|
2026-02-21 15:48:02 +00:00
|
|
|
listing_type: str = Field(description="'RENT' or 'BUY'")
|
2026-02-21 13:54:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
2026-02-21 15:48:02 +00:00
|
|
|
# Auto-create user on first decision interaction
|
2026-02-21 13:54:38 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 15:48:02 +00:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-21 13:54:38 +00:00
|
|
|
@decision_router.put("/{listing_id}", response_model=DecisionResponse)
|
|
|
|
|
async def set_decision(
|
|
|
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
|
|
|
listing_id: int,
|
|
|
|
|
body: SetDecisionRequest,
|
2026-02-23 21:30:51 +00:00
|
|
|
response: Response,
|
2026-02-21 13:54:38 +00:00
|
|
|
) -> DecisionResponse:
|
2026-02-21 15:48:02 +00:00
|
|
|
"""Set or update a like/dislike decision for a listing."""
|
2026-02-23 21:30:51 +00:00
|
|
|
timings: list[str] = []
|
|
|
|
|
t0_total = time.monotonic()
|
|
|
|
|
t0 = time.monotonic()
|
2026-02-21 13:54:38 +00:00
|
|
|
user_id = _get_user_id(user)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
|
2026-02-21 13:54:38 +00:00
|
|
|
repo = DecisionRepository(engine)
|
|
|
|
|
try:
|
2026-02-23 21:30:51 +00:00
|
|
|
t0 = time.monotonic()
|
2026-02-21 13:54:38 +00:00
|
|
|
result = decision_service.set_decision(
|
|
|
|
|
repo,
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
listing_id=listing_id,
|
|
|
|
|
listing_type=body.listing_type,
|
|
|
|
|
decision=body.decision,
|
|
|
|
|
)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"upsert;dur={(time.monotonic() - t0) * 1000:.1f}")
|
2026-02-21 13:54:38 +00:00
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
|
|
|
|
|
response.headers["Server-Timing"] = ", ".join(timings)
|
2026-02-21 15:48:02 +00:00
|
|
|
return _to_response(result)
|
2026-02-21 13:54:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@decision_router.get("", response_model=list[DecisionResponse])
|
|
|
|
|
async def get_decisions(
|
|
|
|
|
user: Annotated[User, Depends(get_current_user)],
|
2026-02-23 21:30:51 +00:00
|
|
|
response: Response,
|
2026-02-21 13:54:38 +00:00
|
|
|
) -> list[DecisionResponse]:
|
|
|
|
|
"""Get all decisions for the current user."""
|
2026-02-23 21:30:51 +00:00
|
|
|
timings: list[str] = []
|
|
|
|
|
t0_total = time.monotonic()
|
|
|
|
|
t0 = time.monotonic()
|
2026-02-21 13:54:38 +00:00
|
|
|
user_id = _get_user_id(user)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
|
2026-02-21 13:54:38 +00:00
|
|
|
repo = DecisionRepository(engine)
|
2026-02-23 21:30:51 +00:00
|
|
|
t0 = time.monotonic()
|
2026-02-21 15:48:02 +00:00
|
|
|
decisions = decision_service.get_user_decisions(repo, user_id)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"fetch;dur={(time.monotonic() - t0) * 1000:.1f}")
|
|
|
|
|
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
|
|
|
|
|
response.headers["Server-Timing"] = ", ".join(timings)
|
2026-02-21 15:48:02 +00:00
|
|
|
return [_to_response(d) for d in decisions]
|
2026-02-21 13:54:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@decision_router.delete("/{listing_id}")
|
|
|
|
|
async def delete_decision(
|
|
|
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
|
|
|
listing_id: int,
|
2026-02-23 21:30:51 +00:00
|
|
|
response: Response,
|
2026-02-21 15:48:02 +00:00
|
|
|
listing_type: str = Query(..., description="RENT or BUY"),
|
2026-02-21 13:54:38 +00:00
|
|
|
) -> dict[str, bool]:
|
2026-02-21 15:48:02 +00:00
|
|
|
"""Remove a decision (un-like/un-dislike)."""
|
2026-02-23 21:30:51 +00:00
|
|
|
timings: list[str] = []
|
|
|
|
|
t0_total = time.monotonic()
|
|
|
|
|
t0 = time.monotonic()
|
2026-02-21 13:54:38 +00:00
|
|
|
user_id = _get_user_id(user)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}")
|
2026-02-21 13:54:38 +00:00
|
|
|
repo = DecisionRepository(engine)
|
2026-02-21 15:48:02 +00:00
|
|
|
try:
|
2026-02-23 21:30:51 +00:00
|
|
|
t0 = time.monotonic()
|
2026-02-21 15:48:02 +00:00
|
|
|
deleted = decision_service.remove_decision(
|
|
|
|
|
repo, user_id, listing_id, listing_type
|
|
|
|
|
)
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"delete;dur={(time.monotonic() - t0) * 1000:.1f}")
|
2026-02-21 15:48:02 +00:00
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
2026-02-21 13:54:38 +00:00
|
|
|
if not deleted:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Decision not found")
|
2026-02-23 21:30:51 +00:00
|
|
|
timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}")
|
|
|
|
|
response.headers["Server-Timing"] = ", ".join(timings)
|
2026-02-21 13:54:38 +00:00
|
|
|
return {"success": True}
|