310 lines
9.1 KiB
Python
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
|