feat(customers): add initial admin bootstrap and csv import

This commit is contained in:
DS | Schubert 2026-07-03 09:14:56 +02:00
parent c816e9869d
commit ecab0fe6b6
27 changed files with 1197 additions and 34 deletions

View file

@ -1,10 +1,10 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, Response, status
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 require_permission
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
@ -17,7 +17,13 @@ from app.schemas.customer import (
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__)
@ -26,6 +32,23 @@ router = APIRouter(
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)
@ -116,6 +139,44 @@ def create_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,