Olympus/backend/hermes/app/api/customers.py

352 lines
12 KiB
Python

import logging
from fastapi import APIRouter, Depends, File, Form, HTTPException, Response, UploadFile, status
from sqlalchemy.orm import Session
from starlette.requests import Request
from app.core.rbac import get_current_active_user, get_user_permission_names, require_permission
from app.db.database import get_db
from app.models.customer import Customer, CustomerContact
from app.models.user import User
from app.repositories.customer_repository import CustomerRepository
from app.schemas.customer import (
CustomerContactCreate,
CustomerContactResponse,
CustomerContactUpdate,
CustomerCreate,
CustomerResponse,
CustomerUpdate,
)
from app.schemas.customer_import import (
CustomerImportCommitResponse,
CustomerImportPreviewResponse,
ImportMode,
)
from app.services.audit_service import sanitize, write_audit_log
from app.services.customer_import_service import CustomerImportService
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/customers",
tags=["Customers"],
)
MAX_IMPORT_SIZE_BYTES = 5 * 1024 * 1024
async def read_import_file(file: UploadFile) -> bytes:
content = await file.read(MAX_IMPORT_SIZE_BYTES + 1)
if len(content) > MAX_IMPORT_SIZE_BYTES:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="CSV-Datei darf maximal 5 MB groß sein",
)
if not content:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="CSV-Datei ist leer",
)
return content
def get_customer_or_404(db: Session, customer_id: int) -> Customer:
customer = CustomerRepository.get_by_id(db, customer_id)
if customer is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Kunde nicht gefunden",
)
return customer
def get_contact_or_404(db: Session, customer_id: int, contact_id: int) -> CustomerContact:
contact = CustomerRepository.get_contact_by_id(db, customer_id, contact_id)
if contact is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Ansprechpartner nicht gefunden",
)
return contact
def ensure_unique_customer_number(
db: Session,
customer_number: str,
customer_id: int | None = None,
) -> None:
conflict = CustomerRepository.find_number_conflict(
db,
customer_number=customer_number,
exclude_customer_id=customer_id,
)
if conflict is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Kundennummer ist bereits vergeben",
)
@router.get("", response_model=list[CustomerResponse])
@router.get("/", response_model=list[CustomerResponse], include_in_schema=False)
def get_customers(
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.read")),
):
logger.info("customers.list", extra={"actor_user_id": current_user.id})
return CustomerRepository.get_all(db)
@router.get("/{customer_id}", response_model=CustomerResponse)
def get_customer(
customer_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.read")),
):
logger.info(
"customers.detail",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
return get_customer_or_404(db, customer_id)
@router.post("", response_model=CustomerResponse, status_code=status.HTTP_201_CREATED)
@router.post(
"/",
response_model=CustomerResponse,
status_code=status.HTTP_201_CREATED,
include_in_schema=False,
)
def create_customer(
customer: CustomerCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.create")),
):
ensure_unique_customer_number(db, customer.customer_number)
logger.info("customers.create", extra={"actor_user_id": current_user.id})
created_customer = CustomerRepository.create(db, customer)
write_audit_log(
db,
action="customers.create",
entity_type="customers",
entity_id=created_customer.id,
entity_label=created_customer.company_name,
actor=current_user,
request=request,
after_data=created_customer,
)
return created_customer
@router.post("/import/preview", response_model=CustomerImportPreviewResponse)
async def preview_customer_import(
mode: ImportMode = Form(...),
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.read")),
):
logger.info("customers.import.preview", extra={"actor_user_id": current_user.id, "mode": mode})
content = await read_import_file(file)
try:
return CustomerImportService.preview(db, content, mode)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
@router.post("/import/commit", response_model=CustomerImportCommitResponse)
async def commit_customer_import(
request: Request,
mode: ImportMode = Form(...),
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
):
permissions = get_user_permission_names(current_user)
required_permissions = {"customers.create"} if mode == "create_only" else {"customers.update"}
if mode == "upsert":
required_permissions = {"customers.create", "customers.update"}
if not required_permissions.issubset(permissions):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Keine Berechtigung")
logger.info("customers.import.commit", extra={"actor_user_id": current_user.id, "mode": mode})
content = await read_import_file(file)
try:
return CustomerImportService.commit(db, content, mode, actor=current_user, request=request)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
@router.put("/{customer_id}", response_model=CustomerResponse)
def update_customer(
customer_id: int,
customer: CustomerUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
db_customer = get_customer_or_404(db, customer_id)
before_data = sanitize(db_customer)
ensure_unique_customer_number(db, customer.customer_number, customer_id)
logger.info(
"customers.update",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
updated_customer = CustomerRepository.update(db, db_customer, customer)
write_audit_log(
db,
action="customers.update",
entity_type="customers",
entity_id=updated_customer.id,
entity_label=updated_customer.company_name,
actor=current_user,
request=request,
before_data=before_data,
after_data=updated_customer,
)
return updated_customer
@router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_customer(
customer_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.delete")),
):
db_customer = get_customer_or_404(db, customer_id)
before_data = sanitize(db_customer)
entity_label = db_customer.company_name
logger.info(
"customers.delete",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
CustomerRepository.delete(db, db_customer)
write_audit_log(
db,
action="customers.delete",
entity_type="customers",
entity_id=customer_id,
entity_label=entity_label,
actor=current_user,
request=request,
before_data=before_data,
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/{customer_id}/contacts", response_model=list[CustomerContactResponse])
def get_customer_contacts(
customer_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.read")),
):
get_customer_or_404(db, customer_id)
logger.info(
"customers.contacts.list",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
return CustomerRepository.get_contacts(db, customer_id)
@router.post(
"/{customer_id}/contacts",
response_model=CustomerContactResponse,
status_code=status.HTTP_201_CREATED,
)
def create_customer_contact(
customer_id: int,
contact: CustomerContactCreate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
customer = get_customer_or_404(db, customer_id)
logger.info(
"customers.contacts.create",
extra={"actor_user_id": current_user.id, "target_customer_id": customer_id},
)
created_contact = CustomerRepository.create_contact(db, customer_id, contact)
write_audit_log(
db,
action="customer_contacts.create",
entity_type="customer_contacts",
entity_id=created_contact.id,
entity_label=f"{created_contact.first_name} {created_contact.last_name}".strip(),
actor=current_user,
request=request,
after_data=created_contact,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return created_contact
@router.put("/{customer_id}/contacts/{contact_id}", response_model=CustomerContactResponse)
def update_customer_contact(
customer_id: int,
contact_id: int,
contact: CustomerContactUpdate,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
customer = get_customer_or_404(db, customer_id)
db_contact = get_contact_or_404(db, customer_id, contact_id)
before_data = sanitize(db_contact)
logger.info(
"customers.contacts.update",
extra={
"actor_user_id": current_user.id,
"target_customer_id": customer_id,
"target_contact_id": contact_id,
},
)
updated_contact = CustomerRepository.update_contact(db, db_contact, contact)
write_audit_log(
db,
action="customer_contacts.update",
entity_type="customer_contacts",
entity_id=updated_contact.id,
entity_label=f"{updated_contact.first_name} {updated_contact.last_name}".strip(),
actor=current_user,
request=request,
before_data=before_data,
after_data=updated_contact,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return updated_contact
@router.delete("/{customer_id}/contacts/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_customer_contact(
customer_id: int,
contact_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("customers.update")),
):
customer = get_customer_or_404(db, customer_id)
db_contact = get_contact_or_404(db, customer_id, contact_id)
before_data = sanitize(db_contact)
entity_label = f"{db_contact.first_name} {db_contact.last_name}".strip()
logger.info(
"customers.contacts.delete",
extra={
"actor_user_id": current_user.id,
"target_customer_id": customer_id,
"target_contact_id": contact_id,
},
)
CustomerRepository.delete_contact(db, db_contact)
write_audit_log(
db,
action="customer_contacts.delete",
entity_type="customer_contacts",
entity_id=contact_id,
entity_label=entity_label,
actor=current_user,
request=request,
before_data=before_data,
metadata={"customer_id": customer_id, "customer_label": customer.company_name},
)
return Response(status_code=status.HTTP_204_NO_CONTENT)