PawTrace / backend /app /api /matches.py
Elliott Duke
HomingPet: lost-dog reunification (FastAPI + React) with Render deploy
de1e3fc
Raw
History Blame Contribute Delete
7.11 kB
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import Case, KnownDog, Match, UnknownDog, User
from ..models.base import (
CaseStatus,
CaseType,
KnownDogStatus,
MatchStatus,
SubjectType,
UnknownDogStatus,
)
from ..schemas.case import MatchOut, ReclaimContact, ReclaimInfo
from ..security import get_current_user
from .helpers import pictures_for
from .hydrate import build_match_out
from .shelters import nearby_shelters
router = APIRouter(prefix="/matches", tags=["matches"])
def _load_owned_match(db: Session, match_id: int, user: User) -> tuple[Match, Case]:
match = db.get(Match, match_id)
if not match:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Match not found")
case = db.get(Case, match.case_id)
if not case:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Case not found")
if case.person_id != user.id and user.role.value != "admin":
raise HTTPException(status.HTTP_403_FORBIDDEN, "Not your match")
return match, case
@router.post("/{match_id}/confirm", response_model=MatchOut)
def confirm_match(
match_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> MatchOut:
match, case = _load_owned_match(db, match_id, user)
# Once resolved/closed a match is already confirmed — don't let a different dog be matched.
if case.status in (CaseStatus.resolved, CaseStatus.closed) and match.status != MatchStatus.confirmed:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "This case is already resolved."
)
match.status = MatchStatus.confirmed
match.reviewed = True
match.reviewed_by = user.id
# A confirmed match drives both involved cases/dogs toward resolution (spec §1, §9.3).
case.status = CaseStatus.resolved
if match.candidate_type == SubjectType.unknown:
dog = db.get(UnknownDog, match.candidate_id)
if dog:
dog.status = UnknownDogStatus.reunited
if case.known_dog_id:
known = db.get(KnownDog, case.known_dog_id)
if known:
known.status = KnownDogStatus.reunited
else:
known = db.get(KnownDog, match.candidate_id)
if known:
known.status = KnownDogStatus.reunited
if case.unknown_dog_id:
unknown = db.get(UnknownDog, case.unknown_dog_id)
if unknown:
unknown.status = UnknownDogStatus.reunited
if match.candidate_case_id:
other = db.get(Case, match.candidate_case_id)
if other:
other.status = CaseStatus.resolved
db.commit()
db.refresh(match)
return build_match_out(db, match)
@router.get("/{match_id}/reclaim", response_model=ReclaimInfo)
def match_reclaim(
match_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> ReclaimInfo:
"""After confirming a match, tell the owner where their dog is now and how to go get it.
Finder contact is only revealed once the match is confirmed (spec §11 mediated contact); a
shelter/vet location is public either way.
"""
match, case = _load_owned_match(db, match_id, user)
if match.status != MatchStatus.confirmed:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, "Confirm the match before viewing reclaim details."
)
# Resolve the found (unknown) dog + the found case that reported it.
found_dog = found_case = None
if match.candidate_type == SubjectType.unknown:
found_dog = db.get(UnknownDog, match.candidate_id)
if match.candidate_case_id:
found_case = db.get(Case, match.candidate_case_id)
elif case.unknown_dog_id: # owner is the finder confirming their own found case
found_dog = db.get(UnknownDog, case.unknown_dog_id)
found_case = case
# The owner's own (known) dog — name + photos for side-by-side verification.
known_id = case.known_dog_id if case.type == CaseType.lost else (
match.candidate_id if match.candidate_type == SubjectType.known else None
)
your_dog_name = None
your_photos: list[dict] = []
if known_id:
kd = db.get(KnownDog, known_id)
your_dog_name = kd.name if kd else None
your_photos = [p.model_dump() for p in pictures_for(db, SubjectType.known, known_id)]
if not found_dog:
return ReclaimInfo(
match_id=match.id, match_status=match.status,
your_dog_name=your_dog_name, your_photos=your_photos, found=False, held_at="unknown",
)
at_shelter = (
found_dog.status == UnknownDogStatus.at_shelter or bool(found_dog.current_location_detail)
)
zip_ = found_dog.current_zip or (found_case.event_zip if found_case else None)
contact = None
if not at_shelter and found_case and (
found_case.finder_name or found_case.finder_email or found_case.finder_phone
):
contact = ReclaimContact(
name=found_case.finder_name,
email=found_case.finder_email,
phone=found_case.finder_phone,
)
return ReclaimInfo(
match_id=match.id,
match_status=match.status,
your_dog_name=your_dog_name,
your_photos=your_photos,
found=True,
held_at="shelter_or_vet" if at_shelter else "finder_custody",
location_name=found_dog.current_location_detail,
current_zip=zip_,
current_location=found_case.current_location if found_case else None,
found_date=found_case.event_date if found_case else None,
contact=contact,
photos=[p.model_dump() for p in pictures_for(db, SubjectType.unknown, found_dog.id)],
nearby_shelters=nearby_shelters(zip_),
)
@router.post("/{match_id}/reject", response_model=MatchOut)
def reject_match(
match_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> MatchOut:
match, _ = _load_owned_match(db, match_id, user)
match.status = MatchStatus.rejected
match.reviewed = True
match.reviewed_by = user.id
db.commit()
db.refresh(match)
return build_match_out(db, match)
@router.post("/{match_id}/reconsider", response_model=MatchOut)
def reconsider_match(
match_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> MatchOut:
"""Move a "not a match" back to a pending candidate so the owner can review it again."""
match, case = _load_owned_match(db, match_id, user)
if case.status in (CaseStatus.resolved, CaseStatus.closed):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "This case is already resolved.")
if match.status != MatchStatus.rejected:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Only rejected matches can be reconsidered.")
match.status = MatchStatus.pending
match.reviewed = False
match.reviewed_by = None
db.commit()
db.refresh(match)
return build_match_out(db, match)