# Architecture – ERP-System Nutzfahrzeug-/Baumaschinen-Handel **Version:** 1.0.0 **Datum:** 2026-07-12 **Status:** Draft – Ready for Review **Architect:** Solution Architect (A0) --- ## 1. System-Architektur-Übersicht ### Container-Diagramm (textuell) ``` ┌─────────────────────────────────────────────────────────┐ │ Coolify (Docker Host) │ │ coolify-01 / 46.225.91.159 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Traefik │──│ Frontend │ │ Backend │ │ │ │ (SSL/TLS) │ │ Next.js │ │ FastAPI │ │ │ │ Let's Enc│ │ :3000 │ │ :8000 │ │ │ └────┬─────┘ └─────┬────┘ └─────┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌────┴──────────────┴───────────────┴────┐ │ │ │ Docker Network (erp-net) │ │ │ └──────────────────┬─────────────────────┘ │ │ │ │ │ ┌──────────┐ ┌───┴─────┐ ┌──────────┐ │ │ │ PostgreSQL│ │ Redis │ │ Volume │ │ │ │ :5432 │ │ :6379 │ │ /data/erp │ │ │ │ (DB) │ │ (cache) │ │ (uploads) │ │ │ └──────────┘ └─────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ▼ ▼ OpenRouter API mobile.de Seller API (LLM/Vision/Bild) (Push-Only REST) ``` ### Container-Übersicht | Container | Image | Port | Volume | Beschreibung | |---|---|---|---|---| | **frontend** | node:20-alpine → Next.js standalone build | 3000 | – | SSR/SSG React Frontend, Tailwind CSS, next-intl | | **backend** | python:3.12-slim → FastAPI + Uvicorn | 8000 | – | REST API, SQLAlchemy async, Alembic, OpenRouter Client | | **postgres** | postgres:16-alpine | 5432 | pgdata | Primäre Datenbank,持久化存储 | | **redis** | redis:7-alpine | 6379 | – | Refresh-Token Store, Background Task Queue (RQ/Celery lite) | ### Netzwerk-Architektur - **erp-net**: Internes Docker Bridge Network, isoliert von Host - **Traefik**: Reverse Proxy mit SSL/TLS (Let's Encrypt), Routing: - `erp.domain.tld` → Frontend (Next.js) - `erp.domain.tld/api/*` → Backend (FastAPI) - **Externe APIs**: Backend ruft OpenRouter + mobile.de Seller API auf (outbound HTTPS) --- ## 2. Backend-Architektur ### Projekt-Struktur ``` backend/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI app entry point │ ├── config.py # Settings (pydantic-settings) │ ├── database.py # SQLAlchemy async engine + session │ ├── deps.py # Dependency injection (DB session, current user) │ ├── models/ # SQLAlchemy ORM models │ │ ├── __init__.py │ │ ├── base.py # Declarative base, mixins (TimestampMixin) │ │ ├── user.py │ │ ├── vehicle.py │ │ ├── contact.py │ │ ├── sale.py │ │ ├── document.py │ │ ├── mobile_de.py │ │ ├── ai_interaction.py │ │ ├── audit_log.py │ │ ├── master_data.py │ │ └── settings.py │ ├── schemas/ # Pydantic v2 request/response schemas │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── vehicle.py │ │ ├── contact.py │ │ ├── sale.py │ │ ├── document.py │ │ ├── mobile_de.py │ │ ├── ai.py │ │ ├── settings.py │ │ └── common.py # Pagination, filters, error responses │ ├── routers/ # API route handlers │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── vehicles.py │ │ ├── contacts.py │ │ ├── sales.py │ │ ├── documents.py │ │ ├── mobile_de.py │ │ ├── ai.py │ │ ├── settings.py │ │ ├── users.py │ │ └── dashboard.py │ ├── services/ # Business logic layer │ │ ├── __init__.py │ │ ├── auth_service.py │ │ ├── vehicle_service.py │ │ ├── contact_service.py │ │ ├── sale_service.py │ │ ├── document_service.py │ │ ├── mobile_de_service.py │ │ ├── ocr_service.py │ │ ├── copilot_service.py │ │ ├── image_retouch_service.py │ │ ├── contract_service.py │ │ ├── datev_service.py │ │ ├── gwg_service.py │ │ ├── ust_id_service.py │ │ ├── price_comparison_service.py │ │ ├── audit_service.py │ │ └── openrouter_client.py # Shared OpenRouter HTTP client │ ├── middleware/ │ │ ├── __init__.py │ │ ├── auth.py # JWT verification middleware │ │ ├── rbac.py # Role-based access control │ │ ├── audit.py # Audit logging middleware │ │ └── i18n.py # Accept-Language → response locale │ ├── utils/ │ │ ├── __init__.py │ │ ├── crypto.py # AES-256 encrypt/decrypt for ID data │ │ ├── pdf.py # WeasyPrint PDF generation helper │ │ └── pagination.py # Pagination helper │ └── exceptions.py # Custom exceptions + handlers ├── alembic/ # Database migrations │ ├── versions/ │ ├── env.py │ └── alembic.ini ├── tests/ │ ├── conftest.py # pytest fixtures (async DB, test client) │ ├── test_auth.py │ ├── test_vehicles.py │ ├── test_contacts.py │ ├── test_sales.py │ ├── test_documents.py │ ├── test_mobile_de.py │ ├── test_ocr.py │ ├── test_copilot.py │ ├── test_image_retouch.py │ └── test_datev.py ├── requirements.txt ├── Dockerfile └── pyproject.toml ``` ### Layer-Architektur ``` Router (API Layer) → Service (Business Logic) → Model (ORM) → PostgreSQL ↑ ↑ ↑ Pydantic Schema Dependencies SQLAlchemy 2.0 Request/Response (DB Session, User) Async Engine ``` - **Router**: HTTP request handling, input validation (Pydantic), response serialization - **Service**: Business logic, orchestration, external API calls, audit logging - **Model**: SQLAlchemy ORM, database constraints, relationships - **Schema**: Pydantic v2 models for request/response validation --- ## 3. Frontend-Architektur ### Projekt-Struktur ``` frontend/ ├── src/ │ ├── app/ # Next.js 14 App Router │ │ ├── [locale]/ # i18n routing (de, en) │ │ │ ├── layout.tsx │ │ │ ├── page.tsx # Dashboard │ │ │ ├── fahrzeuge/ │ │ │ │ ├── page.tsx # Fahrzeug-Liste │ │ │ │ ├── neu/page.tsx # Fahrzeug-Formular │ │ │ │ └── [id]/page.tsx # Fahrzeug-Detail │ │ │ ├── kontakte/page.tsx │ │ │ ├── verkauf/page.tsx │ │ │ ├── ki-copilot/page.tsx │ │ │ └── einstellungen/page.tsx │ │ └── api/ # Next.js API routes (BFF proxy) │ ├── components/ # React components │ │ ├── ui/ # Shared (Button, Card, Input, etc.) │ │ ├── layout/ # Sidebar, Topbar, AppShell │ │ ├── vehicles/ # Vehicle-specific components │ │ ├── contacts/ │ │ ├── sales/ │ │ ├── ai/ # Copilot, Image retouch │ │ └── settings/ │ ├── lib/ │ │ ├── api-client.ts # Fetch wrapper with auth │ │ ├── auth.ts # JWT token management │ │ └── i18n.ts # next-intl config │ ├── messages/ # i18n translation files │ │ ├── de.json │ │ └── en.json │ └── styles/ │ └── globals.css # Tailwind + design tokens ├── public/ ├── next.config.js ├── tailwind.config.ts ├── package.json ├── Dockerfile └── tsconfig.json ``` ### Design-Token-System ```css /* CSS Custom Properties → Tailwind Config */ --color-primary: #2563eb; --color-primary-hover: #1d4ed8; --color-success: #16a34a; --color-warning: #d97706; --color-error: #dc2626; --color-neutral-50: #f8fafc; --color-neutral-900: #0f172a; --radius: 8px; --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); --breakpoint-mobile: 768px; --breakpoint-tablet: 1280px; ``` --- ## 4. Datenbank-Schema ### Übersicht: 15 Tabellen ``` users ──┬── audit_log ├── ai_interactions ├── documents (uploaded_by) └── ust_id_checks (checked_by) vehicles ──┬── documents ├── mobile_de_listings └── sales (vehicle_id) contacts ──┬── contact_persons ├── sales (buyer_id, seller_id) └── ust_id_checks sales ──┬── identification_data └── ust_id_checks (sale_id) master_data (independent) contract_templates (independent) settings (independent) ``` ### Tabellen-Definitionen #### 4.1 users | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK, default gen_random_uuid() | yes | | email | VARCHAR(255) | UNIQUE, NOT NULL | yes | | password_hash | VARCHAR(255) | NOT NULL | – | | full_name | VARCHAR(255) | NOT NULL | – | | role | VARCHAR(20) | NOT NULL, CHECK IN ('admin','verkaeufer','buchhaltung') | – | | language | VARCHAR(5) | NOT NULL, DEFAULT 'de' | – | | is_active | BOOLEAN | DEFAULT true | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.2 vehicles | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | make | VARCHAR(100) | NOT NULL | yes | | model | VARCHAR(100) | NOT NULL | yes | | fin | VARCHAR(17) | UNIQUE, NOT NULL, CHECK(length=17) | yes | | year | INTEGER | – | – | | first_registration | DATE | – | – | | power_kw | INTEGER | – | – | | power_hp | INTEGER | – | – (computed from kW) | | fuel_type | VARCHAR(50) | – | – | | transmission | VARCHAR(20) | – | – | | color | VARCHAR(50) | – | – | | condition | VARCHAR(20) | CHECK IN ('new','used') | – | | location | VARCHAR(255) | – | – | | availability | VARCHAR(20) | CHECK IN ('available','reserved','sold') | yes | | price | DECIMAL(12,2) | NOT NULL | yes | | vehicle_type | VARCHAR(20) | NOT NULL, CHECK IN ('lkw','pkw','baumaschine','stapler','transporter') | yes | | lkw_type | VARCHAR(50) | nullable (nur LKW) | – | | machine_type | VARCHAR(50) | nullable (nur Baumaschine) | – | | body_type | VARCHAR(100) | nullable | – | | operating_hours | DECIMAL(12,1) | nullable (Baumaschine/Stapler) | – | | operating_hours_unit | VARCHAR(5) | nullable, CHECK IN ('h','min') | – | | mileage_km | INTEGER | nullable (LKW/PKW/Transporter) | – | | description | TEXT | nullable | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | | deleted_at | TIMESTAMPTZ | nullable (Soft-Delete) | yes | **Indexes:** idx_vehicles_fin (UNIQUE), idx_vehicles_type, idx_vehicles_availability, idx_vehicles_price, idx_vehicles_deleted_at #### 4.3 contacts | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | company_name | VARCHAR(255) | NOT NULL | yes | | legal_form | VARCHAR(50) | nullable | – | | address_street | VARCHAR(255) | nullable | – | | address_zip | VARCHAR(10) | nullable | – | | address_city | VARCHAR(100) | nullable | – | | address_country | VARCHAR(2) | NOT NULL, DEFAULT 'DE' (ISO 3166-1 alpha-2) | yes | | vat_id | VARCHAR(20) | nullable | – | | phone | VARCHAR(50) | nullable | – | | email | VARCHAR(255) | nullable | – | | website | VARCHAR(255) | nullable | – | | role | VARCHAR(20) | NOT NULL, CHECK IN ('kaeufer','verkaeufer','beide') | yes | | vat_id_status | VARCHAR(20) | DEFAULT 'ungeprueft', CHECK IN ('ungeprueft','geprueft','ungueltig','manuell_bestätigt') | – | | is_private | BOOLEAN | DEFAULT false | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | | deleted_at | TIMESTAMPTZ | nullable (Soft-Delete) | yes | #### 4.4 contact_persons | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | contact_id | UUID | FK → contacts.id, ON DELETE CASCADE | yes | | name | VARCHAR(255) | NOT NULL | – | | function | VARCHAR(100) | nullable | – | | phone | VARCHAR(50) | nullable | – | | email | VARCHAR(255) | nullable | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.5 sales | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | vehicle_id | UUID | FK → vehicles.id, NOT NULL | yes | | buyer_id | UUID | FK → contacts.id, NOT NULL | yes | | seller_id | UUID | FK → contacts.id, nullable (Verkäufer = eigene Firma) | – | | sale_type | VARCHAR(20) | NOT NULL, CHECK IN ('inland','eu_ausland','drittland') | yes | | price_net | DECIMAL(12,2) | NOT NULL | – | | price_gross | DECIMAL(12,2) | NOT NULL | – | | vat_rate | DECIMAL(5,2) | NOT NULL (0, 7, 19) | – | | payment_method | VARCHAR(20) | CHECK IN ('bar','ueberweisung','finanzierung','other') | – | | payment_date | DATE | nullable | – | | handover_date | DATE | nullable | – | | status | VARCHAR(20) | DEFAULT 'entwurf', CHECK IN ('entwurf','pruefung','abgeschlossen','storniert') | yes | | contract_number | VARCHAR(50) | nullable, UNIQUE | yes | | invoice_number | VARCHAR(50) | nullable, UNIQUE | yes | | notes | TEXT | nullable | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.6 documents | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | vehicle_id | UUID | FK → vehicles.id, ON DELETE CASCADE | yes | | sale_id | UUID | FK → sales.id, nullable, ON DELETE SET NULL | – | | filename | VARCHAR(255) | NOT NULL | – | | file_path | VARCHAR(500) | NOT NULL (relative to upload root) | – | | file_type | VARCHAR(50) | NOT NULL (MIME type) | – | | file_size | BIGINT | NOT NULL (bytes) | – | | category | VARCHAR(20) | NOT NULL, CHECK IN ('vertrag','rechnung','zb1','zb2','foto','sonstiges') | yes | | uploaded_by | UUID | FK → users.id | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.7 mobile_de_listings | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | vehicle_id | UUID | FK → vehicles.id, UNIQUE (1 vehicle = 1 listing) | yes | | mobile_de_id | VARCHAR(100) | nullable (mobile.de listing ID, null until listed) | – | | sync_status | VARCHAR(20) | DEFAULT 'entwurf', CHECK IN ('entwurf','gelistet','aktualisiert','fehler','entfernt') | yes | | last_sync_at | TIMESTAMPTZ | nullable | – | | error_log | TEXT | nullable (last error message) | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.8 ai_interactions | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | user_id | UUID | FK → users.id | yes | | interaction_type | VARCHAR(20) | CHECK IN ('copilot_text','copilot_voice','ocr_zb1','ocr_zb2','image_retouch','price_comparison') | yes | | input_text | TEXT | nullable | – | | output_text | TEXT | nullable | – | | model_used | VARCHAR(100) | nullable | – | | tokens_used | INTEGER | nullable | – | | metadata | JSONB | nullable (structured data: extracted fields, etc.) | – | | created_at | TIMESTAMPTZ | DEFAULT now() | yes | #### 4.9 audit_log | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | user_id | UUID | FK → users.id, nullable (system actions) | – | | action | VARCHAR(50) | NOT NULL (e.g. 'create','update','delete','login','export') | yes | | entity_type | VARCHAR(50) | NOT NULL (e.g. 'vehicle','contact','sale') | yes | | entity_id | UUID | nullable | – | | old_values | JSONB | nullable | – | | new_values | JSONB | nullable | – | | created_at | TIMESTAMPTZ | DEFAULT now() | yes | #### 4.10 master_data | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | category | VARCHAR(50) | NOT NULL (e.g. 'steuerklasse','waehrung','incoterm','kraftstoffart','getriebe','farbe','land','rechtsform','fahrzeugtyp','lkw_typ','maschinenart','aufbau','zahlungsmethode') | yes (composite with key) | | key | VARCHAR(100) | NOT NULL | – | | value_de | VARCHAR(255) | NOT NULL | – | | value_en | VARCHAR(255) | nullable | – | | sort_order | INTEGER | DEFAULT 0 | – | | is_active | BOOLEAN | DEFAULT true | – | **Unique Index:** idx_master_data_category_key (category, key) UNIQUE #### 4.11 contract_templates | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | template_type | VARCHAR(30) | NOT NULL, CHECK IN ('kaufvertrag_inland','kaufvertrag_eu','kaufvertrag_drittland','rechnung','lieferbescheinigung') | yes | | name | VARCHAR(255) | NOT NULL | – | | content_de | TEXT | NOT NULL (HTML/Markdown template with {{variables}}) | – | | content_en | TEXT | nullable | – | | variables | JSONB | nullable (array of variable names: ["fahrzeug","kaeufer","preis",...]) | – | | is_active | BOOLEAN | DEFAULT true | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.12 identification_data | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | sale_id | UUID | FK → sales.id, UNIQUE (1:1), ON DELETE CASCADE | yes | | id_type | VARCHAR(30) | NOT NULL, CHECK IN ('personalausweis','reisepass','fuehrerschein','anderes') | – | | id_number_encrypted | BYTEA | NOT NULL (AES-256-GCM encrypted) | – | | issuing_country | VARCHAR(2) | NOT NULL (ISO 3166-1 alpha-2) | – | | checked_by | UUID | FK → users.id | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | **Security:** `id_number_encrypted` wird mit AES-256-GCM verschlüsselt. Key aus Environment-Variable `ENCRYPTION_KEY`. Nur Admin + Buchhaltung können entschlüsseln. #### 4.13 ust_id_checks | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | contact_id | UUID | FK → contacts.id | yes | | sale_id | UUID | FK → sales.id, nullable | – | | vat_id | VARCHAR(20) | NOT NULL | – | | check_result | VARCHAR(20) | NOT NULL, CHECK IN ('gueltig','ungueltig','unbekannt') | – | | check_method | VARCHAR(20) | NOT NULL, DEFAULT 'manuell', CHECK IN ('manuell','bzst_api') | – | | check_date | DATE | NOT NULL | – | | checked_by | UUID | FK → users.id | – | | created_at | TIMESTAMPTZ | DEFAULT now() | – | #### 4.14 settings | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | key | VARCHAR(100) | UNIQUE, NOT NULL | yes | | value | JSONB | NOT NULL (flexible structure) | – | | category | VARCHAR(50) | NOT NULL (e.g. 'firmenprofil','system','mobile_de','openrouter') | yes | | updated_by | UUID | FK → users.id | – | | updated_at | TIMESTAMPTZ | DEFAULT now() | – | **Beispiel-Keys:** `firmenprofil.name`, `firmenprofil.adresse`, `firmenprofil.ust_id`, `system.datev_berater`, `system.datev_mandant`, `mobile_de.api_key_ref`, `openrouter.api_key_ref` #### 4.15 ust_id_status_history | Spalte | Typ | Constraints | Index | |---|---|---|---| | id | UUID | PK | yes | | contact_id | UUID | FK → contacts.id, ON DELETE CASCADE | yes | | old_status | VARCHAR(20) | nullable | – | | new_status | VARCHAR(20) | NOT NULL | – | | changed_by | UUID | FK → users.id | – | | changed_at | TIMESTAMPTZ | DEFAULT now() | yes | --- ## 5. API-Design ### Konventionen - **Base URL:** `/api/v1` - **Format:** JSON (request + response) - **Auth:** `Authorization: Bearer ` (alle Endpoints außer /auth/login) - **Pagination:** `?page=1&page_size=20` → Response: `{ items: [...], total: N, page: 1, page_size: 20 }` - **Sort:** `?sort=field` oder `?sort=-field` (descending) - **Filter:** Module-spezifische Query-Parameter - **Error Format:** `{ error: { code: "NOT_FOUND", message: "Vehicle not found", details: {} } }` - **i18n:** `Accept-Language: de|en` → Fehlermeldungen in entsprechender Sprache ### Endpoint-Übersicht: 42 Endpoints #### 5.1 Auth (4) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/auth/login | Login (email, password) → JWT + refresh | public | | POST | /api/v1/auth/refresh | Refresh-Token → neuer JWT | public (refresh token) | | POST | /api/v1/auth/logout | Logout (invalidate refresh token) | all | | GET | /api/v1/auth/me | Aktueller User + Berechtigungen | all | #### 5.2 Users (4) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/users | User-Liste (pagination) | admin | | POST | /api/v1/users | User anlegen | admin | | PUT | /api/v1/users/:id | User bearbeiten | admin | | DELETE | /api/v1/users/:id | User deaktivieren (soft) | admin | #### 5.3 Dashboard (1) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/dashboard/stats | KPIs, letzte Aktivitäten, Top-Fahrzeuge | all | #### 5.4 Vehicles (5) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/vehicles | Fahrzeug-Liste (filter, sort, pagination, search) | all (📖 für buchhaltung) | | POST | /api/v1/vehicles | Fahrzeug anlegen | admin, verkaeufer | | GET | /api/v1/vehicles/:id | Fahrzeug-Detail | all (📖) | | PUT | /api/v1/vehicles/:id | Fahrzeug bearbeiten | admin, verkaeufer | | DELETE | /api/v1/vehicles/:id | Fahrzeug soft-delete | admin, verkaeufer | **Query-Parameter GET /vehicles:** `?type=baumaschine&availability=available&min_price=10000&max_price=50000&search=mercedes&sort=-created_at&page=1&page_size=20` #### 5.5 Vehicle OCR (2) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/vehicles/ocr/zb1 | Upload ZB I foto → OpenRouter Qwen2.5-VL → extracted fields | admin, verkaeufer | | POST | /api/v1/vehicles/ocr/zb2 | Upload ZB II foto → OpenRouter Qwen2.5-VL → extracted fields | admin, verkaeufer | **Request:** multipart/form-data with image file (max 10MB) **Response:** `{ fields: { make: "Mercedes", fin: "WDF...", ... }, confidence: 0.92, warnings: [] }` #### 5.6 Vehicle Documents (3) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/vehicles/:id/documents | Dokument-Liste pro Fahrzeug (filter by category) | all (📖) | | POST | /api/v1/vehicles/:id/documents | Dokument hochladen (multipart, max 50MB) | admin, verkaeufer | | DELETE | /api/v1/vehicles/:id/documents/:doc_id | Dokument löschen | admin, verkaeufer | #### 5.7 mobile.de (5) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/vehicles/:id/mobile-de/push | Einzeln push (create/update) | admin, verkaeufer | | DELETE | /api/v1/vehicles/:id/mobile-de/listing | Delisting auf mobile.de | admin, verkaeufer | | GET | /api/v1/mobile-de/listings | Listing-Übersicht (filter by sync_status) | admin, verkaeufer | | POST | /api/v1/mobile-de/batch-push | Batch-Push für mehrere Fahrzeuge | admin, verkaeufer | | GET | /api/v1/mobile-de/listings/:id/status | Listing-Status abrufen | admin, verkaeufer | #### 5.8 Contacts (5) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/contacts | Kontakt-Liste (search, filter by role/country/vat_status) | all (📖) | | POST | /api/v1/contacts | Kontakt anlegen | admin, verkaeufer | | GET | /api/v1/contacts/:id | Kontakt-Detail mit Ansprechpartnern | all (📖) | | PUT | /api/v1/contacts/:id | Kontakt bearbeiten | admin, verkaeufer | | DELETE | /api/v1/contacts/:id | Kontakt soft-delete | admin, verkaeufer | #### 5.9 Sales (6) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/sales | Verkaufs-Liste (filter by status, type, date range) | all (📖) | | POST | /api/v1/sales | Verkauf anlegen (startet Verkaufsprozess) | admin, verkaeufer | | GET | /api/v1/sales/:id | Verkaufs-Detail | all (📖) | | PUT | /api/v1/sales/:id | Verkauf bearbeiten (Status-Wechsel, Daten) | admin, verkaeufer (buchhaltung für status) | | POST | /api/v1/sales/:id/contract | Kaufvertrag-PDF generieren | admin, verkaeufer | | POST | /api/v1/sales/:id/invoice | Rechnung-PDF generieren | admin, buchhaltung | #### 5.10 Sales – Compliance (3) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/sales/:id/ust-id-check | USt-IdNr.-Prüfung (manuell) eintragen | admin, buchhaltung | | POST | /api/v1/sales/:id/identification | Identifikation (GwG) erfassen | admin, buchhaltung | | GET | /api/v1/sales/:id/delivery-certificate | Lieferbescheinigung-PDF generieren | admin, buchhaltung | #### 5.11 DATEV-Export (1) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/sales/datev-export | DATEV-CSV Export (?from=2026-01-01&to=2026-01-31) | admin, buchhaltung | #### 5.12 KI Copilot (3) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/ai/copilot | Text-Befehl → OpenRouter LLM → strukturierte Aktion | admin, verkaeufer (📖 buchhaltung) | | POST | /api/v1/ai/copilot/voice | Audio-Datei → Transkription → Befehl verarbeiten | admin, verkaeufer | | GET | /api/v1/ai/interactions | KI-Interaktions-Historie (pagination) | admin, verkaeufer (📖) | #### 5.13 KI Bildretusche (2) | Method | Path | Description | Roles | |---|---|---|---| | POST | /api/v1/ai/image/retouch | Bild-Retusche (Hintergrund/Spiegelungen) via Flux.1-Pro | admin, verkaeufer | | POST | /api/v1/ai/image/batch-retouch | Batch-Retusche für mehrere Fotos | admin, verkaeufer | #### 5.14 Preisvergleich (1) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/vehicles/:id/price-comparison | mobile.de Vergleichspreise für ähnliche Fahrzeuge | admin, verkaeufer (📖) | #### 5.15 Settings & Stammdaten (6) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/settings | Alle Settings (firmenprofil, system) | admin (alle), others: limited | | PUT | /api/v1/settings | Settings aktualisieren | admin | | GET | /api/v1/settings/master-data | Stammdaten-Liste (filter by category) | admin | | POST | /api/v1/settings/master-data | Stammdatum anlegen | admin | | PUT | /api/v1/settings/master-data/:id | Stammdatum bearbeiten | admin | | GET | /api/v1/settings/contract-templates | Vertragsvorlagen-Liste | admin | | POST | /api/v1/settings/contract-templates | Vorlage anlegen | admin | | PUT | /api/v1/settings/contract-templates/:id | Vorlage bearbeiten | admin | #### 5.16 Audit Log (1) | Method | Path | Description | Roles | |---|---|---|---| | GET | /api/v1/audit-log | Audit-Log (filter by entity_type, user_id, date range) | admin | --- ## 6. Auth-Konzept ### JWT + Refresh Token Flow ``` 1. POST /auth/login { email, password } → Validate credentials (bcrypt) → Generate JWT (15min TTL) + Refresh Token (7d TTL) → Store Refresh Token in Redis with user_id → Return { access_token, refresh_token, user: {...} } 2. Request: Authorization: Bearer → Middleware: Verify JWT, extract user_id + role → Inject current_user into route 3. JWT expired → 401 → Client: POST /auth/refresh { refresh_token } → Verify refresh token in Redis → Generate new JWT + new refresh token (rotation) → Return { access_token, refresh_token } 4. POST /auth/logout { refresh_token } → Delete refresh token from Redis ``` ### JWT Payload ```json { "sub": "user-uuid", "role": "admin|verkaeufer|buchhaltung", "email": "user@example.com", "lang": "de", "exp": 1234567890, "iat": 1234567890 } ``` ### Rollen-Berechtigungs-Matrix (Endpoint-Level) Implementiert via `Depends(require_role(["admin", "verkaeufer"]))` Decorator. | Endpoint-Gruppe | Admin | Verkäufer | Buchhaltung | |---|---|---|---| | /auth/* | ✅ | ✅ | ✅ | | /users (CRUD) | ✅ | ❌ | ❌ | | /vehicles (CRUD) | ✅ RW | ✅ RW | 📖 R | | /vehicles/ocr/* | ✅ | ✅ | ❌ | | /vehicles/:id/documents | ✅ RW | ✅ RW | 📖 R | | /mobile-de/* | ✅ | ✅ | ❌ | | /contacts (CRUD) | ✅ RW | ✅ RW | 📖 R | | /sales (CRUD) | ✅ RW | ✅ RW | 📖 R | | /sales/:id/contract | ✅ | ✅ | ❌ | | /sales/:id/invoice | ✅ | ❌ | ✅ | | /sales/:id/ust-id-check | ✅ | ❌ | ✅ | | /sales/:id/identification | ✅ | ❌ | ✅ | | /sales/datev-export | ✅ | ❌ | ✅ | | /ai/copilot/* | ✅ | ✅ | 📖 | | /ai/image/* | ✅ | ✅ | ❌ | | /vehicles/:id/price-comparison | ✅ | ✅ | 📖 | | /settings/* | ✅ | ❌ | ❌ | | /audit-log | ✅ | ❌ | ❌ | --- ## 7. Dateiablage-Konzept ### Architektur ``` Backend Container ├── /data/uploads/ ← Coolify Volume Mount (persistent) │ ├── vehicles/ │ │ ├── {vehicle_id}/ │ │ │ ├── {doc_id}_{filename} ← Original-Datei │ │ │ └── {doc_id}_thumb.jpg ← Thumbnail (Bilder) │ └── contracts/ │ ├── {sale_id}_vertrag.pdf │ └── {sale_id}_rechnung.pdf ``` ### Upload-Flow 1. Client: multipart/form-data → POST /vehicles/:id/documents 2. Backend: MIME-Type-Check (allowlist), Size-Check (≤50MB) 3. Backend: Datei speichern unter `/data/uploads/vehicles/{vehicle_id}/{doc_id}_{filename}` 4. Backend: Thumbnail generieren (für Bilder, Pillow) 5. Backend: documents-Row in DB anlegen 6. Response: Document-Metadata ### File-Preview - **Bilder:** Backend generiert Thumbnail beim Upload. Frontend zeigt Thumbnail + Lightbox. - **PDFs:** Frontend nutzt `