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)