"""Contact routes — CRUD, N:M, soft-delete, GDPR hard-delete.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import check_permission from app.core.db import get_db from app.deps import get_current_user from app.schemas.contact import ContactCreate, ContactUpdate from app.services import contact_service router = APIRouter(prefix="/api/v1/contacts", tags=["contacts"]) @router.get("") async def list_contacts( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), search: str | None = Query(None), sort_by: str = Query("last_name"), sort_order: str = Query("asc", pattern="^(asc|desc)$"), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List contacts with pagination and optional search.""" tenant_id = uuid.UUID(current_user["tenant_id"]) result = await contact_service.list_contacts( db, tenant_id, page=page, page_size=page_size, search=search, sort_by=sort_by, sort_order=sort_order, ) return result @router.post("", status_code=status.HTTP_201_CREATED) async def create_contact( body: ContactCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Create a contact. Optionally link to companies via company_ids array.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "contacts", "create"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail={"detail": "Insufficient permissions", "code": "forbidden"}, ) data = body.model_dump() return await contact_service.create_contact(db, tenant_id, user_id, data) @router.get("/{contact_id}") async def get_contact( contact_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get a single contact with companies array. Cross-tenant returns 404.""" tenant_id = uuid.UUID(current_user["tenant_id"]) try: cid = uuid.UUID(contact_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid contact_id", "code": "invalid_id"}) data = await contact_service.get_contact_detail(db, tenant_id, cid) if data is None: raise HTTPException(404, detail={"detail": "Contact not found", "code": "not_found"}) return data @router.put("/{contact_id}") async def update_contact( contact_id: str, body: ContactUpdate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Update a contact.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "contacts", "update"): raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(contact_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid contact_id", "code": "invalid_id"}) data = body.model_dump(exclude_unset=True) result = await contact_service.update_contact(db, tenant_id, user_id, cid, data) if result is None: raise HTTPException(404, detail={"detail": "Contact not found", "code": "not_found"}) return result @router.delete("/{contact_id}") async def delete_contact( contact_id: str, gdpr: bool = Query(False), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Delete a contact. Default: soft-delete. Use gdpr=true for hard-delete with deletion_log.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) role = current_user.get("role", "viewer") if not check_permission(role, "contacts", "delete"): raise HTTPException(403, detail={"detail": "Insufficient permissions", "code": "forbidden"}) try: cid = uuid.UUID(contact_id) except ValueError: raise HTTPException(400, detail={"detail": "Invalid contact_id", "code": "invalid_id"}) if gdpr: deleted = await contact_service.gdpr_hard_delete_contact(db, tenant_id, user_id, cid) else: deleted = await contact_service.soft_delete_contact(db, tenant_id, user_id, cid) if not deleted: raise HTTPException(404, detail={"detail": "Contact not found", "code": "not_found"}) return Response(status_code=status.HTTP_204_NO_CONTENT)