2026-07-14 12:14:21 +02:00
|
|
|
"""Contacts router: CRUD, search, filter, and contact person management endpoints."""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.database import get_db
|
|
|
|
|
from app.dependencies import get_current_user, get_pagination, require_role
|
|
|
|
|
from app.models.user import User
|
|
|
|
|
from app.schemas.contact import (
|
|
|
|
|
ContactCreate,
|
|
|
|
|
ContactListResponse,
|
|
|
|
|
ContactPersonCreate,
|
|
|
|
|
ContactPersonResponse,
|
|
|
|
|
ContactResponse,
|
|
|
|
|
ContactUpdate,
|
|
|
|
|
)
|
|
|
|
|
from app.services import contact_service
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/contacts", tags=["contacts"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/", response_model=ContactListResponse, status_code=status.HTTP_200_OK)
|
|
|
|
|
async def list_contacts(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
pagination: dict = Depends(get_pagination),
|
2026-07-17 21:28:58 +02:00
|
|
|
search: str | None = Query(
|
|
|
|
|
None, description="Search in company_name, city, email, vat_id"
|
|
|
|
|
),
|
|
|
|
|
role: str | None = Query(
|
|
|
|
|
None, description="Filter by role (kaeufer, verkaeufer, beide)"
|
|
|
|
|
),
|
2026-07-14 12:14:21 +02:00
|
|
|
is_eu: bool | None = Query(None, description="Filter EU (true) or Inland (false)"),
|
|
|
|
|
is_private: bool | None = Query(None, description="Filter private contacts"),
|
|
|
|
|
sort: str | None = Query(None, description="Sort field (prefix - for descending)"),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""List contacts with pagination, filtering, and sorting.
|
|
|
|
|
|
|
|
|
|
All authenticated roles can read.
|
|
|
|
|
"""
|
|
|
|
|
contacts, total = await contact_service.list_contacts(
|
|
|
|
|
db,
|
|
|
|
|
page=pagination["page"],
|
|
|
|
|
page_size=pagination["page_size"],
|
|
|
|
|
search=search,
|
|
|
|
|
role=role,
|
|
|
|
|
is_eu=is_eu,
|
|
|
|
|
is_private=is_private,
|
|
|
|
|
sort=sort,
|
|
|
|
|
)
|
|
|
|
|
return ContactListResponse(
|
|
|
|
|
items=[ContactResponse.model_validate(c) for c in contacts],
|
|
|
|
|
total=total,
|
|
|
|
|
page=pagination["page"],
|
|
|
|
|
page_size=pagination["page_size"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/", response_model=ContactResponse, status_code=status.HTTP_201_CREATED)
|
|
|
|
|
async def create_contact(
|
|
|
|
|
body: ContactCreate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(require_role(["admin", "verkaeufer"])),
|
|
|
|
|
):
|
|
|
|
|
"""Create a new contact. Requires admin or verkaeufer role."""
|
|
|
|
|
data = body.model_dump(exclude_unset=False)
|
|
|
|
|
contact = await contact_service.create_contact(db, data)
|
|
|
|
|
return ContactResponse.model_validate(contact)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 21:28:58 +02:00
|
|
|
@router.get(
|
|
|
|
|
"/{contact_id}", response_model=ContactResponse, status_code=status.HTTP_200_OK
|
|
|
|
|
)
|
2026-07-14 12:14:21 +02:00
|
|
|
async def get_contact(
|
|
|
|
|
contact_id: uuid.UUID,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
):
|
|
|
|
|
"""Get a single contact by ID with contact persons."""
|
|
|
|
|
contact = await contact_service.get_contact_by_id(db, contact_id)
|
|
|
|
|
if contact is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
2026-07-17 21:28:58 +02:00
|
|
|
detail={
|
|
|
|
|
"error": {"code": "CONTACT_NOT_FOUND", "message": "Contact not found"}
|
|
|
|
|
},
|
2026-07-14 12:14:21 +02:00
|
|
|
)
|
|
|
|
|
return ContactResponse.model_validate(contact)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 21:28:58 +02:00
|
|
|
@router.put(
|
|
|
|
|
"/{contact_id}", response_model=ContactResponse, status_code=status.HTTP_200_OK
|
|
|
|
|
)
|
2026-07-14 12:14:21 +02:00
|
|
|
async def update_contact(
|
|
|
|
|
contact_id: uuid.UUID,
|
|
|
|
|
body: ContactUpdate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(require_role(["admin", "verkaeufer"])),
|
|
|
|
|
):
|
|
|
|
|
"""Update a contact's fields. Requires admin or verkaeufer role."""
|
|
|
|
|
updates = body.model_dump(exclude_unset=True)
|
|
|
|
|
if not updates:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
detail={"error": {"code": "NO_FIELDS", "message": "No fields to update"}},
|
|
|
|
|
)
|
|
|
|
|
contact = await contact_service.update_contact(db, contact_id, updates)
|
|
|
|
|
if contact is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
2026-07-17 21:28:58 +02:00
|
|
|
detail={
|
|
|
|
|
"error": {"code": "CONTACT_NOT_FOUND", "message": "Contact not found"}
|
|
|
|
|
},
|
2026-07-14 12:14:21 +02:00
|
|
|
)
|
|
|
|
|
return ContactResponse.model_validate(contact)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 21:28:58 +02:00
|
|
|
@router.delete(
|
|
|
|
|
"/{contact_id}", response_model=ContactResponse, status_code=status.HTTP_200_OK
|
|
|
|
|
)
|
2026-07-14 12:14:21 +02:00
|
|
|
async def delete_contact(
|
|
|
|
|
contact_id: uuid.UUID,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(require_role(["admin", "verkaeufer"])),
|
|
|
|
|
):
|
|
|
|
|
"""Soft-delete a contact (sets deleted_at). Requires admin or verkaeufer role."""
|
|
|
|
|
contact = await contact_service.soft_delete_contact(db, contact_id)
|
|
|
|
|
if contact is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
2026-07-17 21:28:58 +02:00
|
|
|
detail={
|
|
|
|
|
"error": {"code": "CONTACT_NOT_FOUND", "message": "Contact not found"}
|
|
|
|
|
},
|
2026-07-14 12:14:21 +02:00
|
|
|
)
|
|
|
|
|
return ContactResponse.model_validate(contact)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/{contact_id}/persons",
|
|
|
|
|
response_model=ContactPersonResponse,
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
)
|
|
|
|
|
async def add_contact_person(
|
|
|
|
|
contact_id: uuid.UUID,
|
|
|
|
|
body: ContactPersonCreate,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(require_role(["admin", "verkaeufer"])),
|
|
|
|
|
):
|
|
|
|
|
"""Add a contact person to an existing contact. Requires admin or verkaeufer role."""
|
|
|
|
|
person = await contact_service.add_contact_person(
|
|
|
|
|
db, contact_id, body.model_dump(exclude_unset=False)
|
|
|
|
|
)
|
|
|
|
|
if person is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
2026-07-17 21:28:58 +02:00
|
|
|
detail={
|
|
|
|
|
"error": {"code": "CONTACT_NOT_FOUND", "message": "Contact not found"}
|
|
|
|
|
},
|
2026-07-14 12:14:21 +02:00
|
|
|
)
|
|
|
|
|
return ContactPersonResponse.model_validate(person)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
|
|
|
"/{contact_id}/persons/{person_id}",
|
|
|
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
|
|
|
)
|
|
|
|
|
async def remove_contact_person(
|
|
|
|
|
contact_id: uuid.UUID,
|
|
|
|
|
person_id: uuid.UUID,
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
current_user: User = Depends(require_role(["admin", "verkaeufer"])),
|
|
|
|
|
):
|
|
|
|
|
"""Remove a contact person from a contact. Requires admin or verkaeufer role."""
|
|
|
|
|
deleted = await contact_service.remove_contact_person(db, contact_id, person_id)
|
|
|
|
|
if not deleted:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
2026-07-17 21:28:58 +02:00
|
|
|
detail={
|
|
|
|
|
"error": {
|
|
|
|
|
"code": "PERSON_NOT_FOUND",
|
|
|
|
|
"message": "Contact person not found",
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-07-14 12:14:21 +02:00
|
|
|
)
|
|
|
|
|
return None
|