| 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) |
| |
| 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 |
|
|
| |
| 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." |
| ) |
|
|
| |
| 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: |
| found_dog = db.get(UnknownDog, case.unknown_dog_id) |
| found_case = case |
|
|
| |
| 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) |
|
|