sendio/backend/app/api/contacts.py
2026-01-13 05:17:57 +02:00

310 lines
9.1 KiB
Python

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