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)