from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from sqlalchemy import or_, func from typing import List, Optional from app.db.base import get_db from app.core.deps import get_current_user from app.models.user import User from app.models.contact import Contact, ContactTag, ContactTagMap, DNDList from app.schemas.contact import ( ContactCreate, ContactUpdate, ContactResponse, ContactTagCreate, ContactTagResponse, DNDCreate, DNDResponse ) from app.utils.phone import normalize_phone router = APIRouter() @router.post("", response_model=ContactResponse, status_code=status.HTTP_201_CREATED) def create_contact( contact: ContactCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # Normalize phone normalized_phone = normalize_phone(contact.phone_e164) if not normalized_phone: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid phone number" ) # Check DND list dnd_entry = db.query(DNDList).filter( DNDList.user_id == current_user.id, DNDList.phone_e164 == normalized_phone ).first() if dnd_entry: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Phone number is in DND list" ) # Check if contact exists existing = db.query(Contact).filter( Contact.user_id == current_user.id, Contact.phone_e164 == normalized_phone ).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Contact already exists" ) db_contact = Contact( user_id=current_user.id, phone_e164=normalized_phone, first_name=contact.first_name, last_name=contact.last_name, email=contact.email, opted_in=contact.opted_in, conversation_window_open=contact.conversation_window_open, source="manual" ) db.add(db_contact) db.commit() db.refresh(db_contact) return db_contact @router.get("", response_model=List[ContactResponse]) def list_contacts( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), search: Optional[str] = None, tag_id: Optional[int] = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): query = db.query(Contact).filter(Contact.user_id == current_user.id) if search: query = query.filter( or_( Contact.phone_e164.ilike(f"%{search}%"), Contact.first_name.ilike(f"%{search}%"), Contact.last_name.ilike(f"%{search}%"), Contact.email.ilike(f"%{search}%") ) ) if tag_id: query = query.join(ContactTagMap).filter(ContactTagMap.tag_id == tag_id) contacts = query.offset(skip).limit(limit).all() # Add tags to response for contact in contacts: tag_mappings = db.query(ContactTagMap).filter( ContactTagMap.contact_id == contact.id ).all() tag_ids = [tm.tag_id for tm in tag_mappings] tags = db.query(ContactTag).filter(ContactTag.id.in_(tag_ids)).all() if tag_ids else [] contact.tags = [tag.name for tag in tags] return contacts @router.get("/{contact_id}", response_model=ContactResponse) def get_contact( contact_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): contact = db.query(Contact).filter( Contact.id == contact_id, Contact.user_id == current_user.id ).first() if not contact: raise HTTPException(status_code=404, detail="Contact not found") # Add tags tag_mappings = db.query(ContactTagMap).filter(ContactTagMap.contact_id == contact.id).all() tag_ids = [tm.tag_id for tm in tag_mappings] tags = db.query(ContactTag).filter(ContactTag.id.in_(tag_ids)).all() if tag_ids else [] contact.tags = [tag.name for tag in tags] return contact @router.put("/{contact_id}", response_model=ContactResponse) def update_contact( contact_id: int, contact_update: ContactUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): contact = db.query(Contact).filter( Contact.id == contact_id, Contact.user_id == current_user.id ).first() if not contact: raise HTTPException(status_code=404, detail="Contact not found") update_data = contact_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(contact, field, value) db.commit() db.refresh(contact) return contact @router.delete("/{contact_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_contact( contact_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): contact = db.query(Contact).filter( Contact.id == contact_id, Contact.user_id == current_user.id ).first() if not contact: raise HTTPException(status_code=404, detail="Contact not found") db.delete(contact) db.commit() return None # Tags @router.post("/tags", response_model=ContactTagResponse, status_code=status.HTTP_201_CREATED) def create_tag( tag: ContactTagCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): db_tag = ContactTag(user_id=current_user.id, name=tag.name) db.add(db_tag) db.commit() db.refresh(db_tag) return db_tag @router.get("/tags", response_model=List[ContactTagResponse]) def list_tags( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): return db.query(ContactTag).filter(ContactTag.user_id == current_user.id).all() @router.post("/{contact_id}/tags/{tag_id}", status_code=status.HTTP_204_NO_CONTENT) def add_tag_to_contact( contact_id: int, tag_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # Verify ownership contact = db.query(Contact).filter( Contact.id == contact_id, Contact.user_id == current_user.id ).first() if not contact: raise HTTPException(status_code=404, detail="Contact not found") tag = db.query(ContactTag).filter( ContactTag.id == tag_id, ContactTag.user_id == current_user.id ).first() if not tag: raise HTTPException(status_code=404, detail="Tag not found") # Check if already exists existing = db.query(ContactTagMap).filter( ContactTagMap.contact_id == contact_id, ContactTagMap.tag_id == tag_id ).first() if existing: return None mapping = ContactTagMap(contact_id=contact_id, tag_id=tag_id) db.add(mapping) db.commit() return None @router.delete("/{contact_id}/tags/{tag_id}", status_code=status.HTTP_204_NO_CONTENT) def remove_tag_from_contact( contact_id: int, tag_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # Verify ownership contact = db.query(Contact).filter( Contact.id == contact_id, Contact.user_id == current_user.id ).first() if not contact: raise HTTPException(status_code=404, detail="Contact not found") mapping = db.query(ContactTagMap).filter( ContactTagMap.contact_id == contact_id, ContactTagMap.tag_id == tag_id ).first() if mapping: db.delete(mapping) db.commit() return None # DND List @router.post("/dnd", response_model=DNDResponse, status_code=status.HTTP_201_CREATED) def add_to_dnd( dnd: DNDCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): normalized_phone = normalize_phone(dnd.phone_e164) if not normalized_phone: raise HTTPException(status_code=400, detail="Invalid phone number") existing = db.query(DNDList).filter( DNDList.user_id == current_user.id, DNDList.phone_e164 == normalized_phone ).first() if existing: raise HTTPException(status_code=400, detail="Phone already in DND list") db_dnd = DNDList( user_id=current_user.id, phone_e164=normalized_phone, reason=dnd.reason ) db.add(db_dnd) db.commit() db.refresh(db_dnd) return db_dnd @router.get("/dnd", response_model=List[DNDResponse]) def list_dnd( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): return db.query(DNDList).filter(DNDList.user_id == current_user.id).all() @router.delete("/dnd/{dnd_id}", status_code=status.HTTP_204_NO_CONTENT) def remove_from_dnd( dnd_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): dnd = db.query(DNDList).filter( DNDList.id == dnd_id, DNDList.user_id == current_user.id ).first() if not dnd: raise HTTPException(status_code=404, detail="DND entry not found") db.delete(dnd) db.commit() return None