Files

44 KiB
Raw Permalink Blame History

ERP Nutzfahrzeuge Architekturdokument

Version: 1.0
Datum: 2026-07-12
Status: Approved for Implementation
Solution Architect: A0 Orchestrator


1. Übersicht

ERP-System für den Handel mit Nutzfahrzeugen (LKW, Baumaschinen, PKW, Stapler, Transporter). Backend in Python/FastAPI, Frontend in React/Next.js (mobile-first), PostgreSQL als Datenbank, Redis für Caching/Queues. Hosting via Coolify self-hosted mit Docker-Compose (4 Container).

1.1 Stakeholder & Rollen

Rolle Berechtigungen Nutzer (~10)
Admin Vollzugriff, User-Management, Systemeinstellungen 1-2
Verkäufer Fahrzeuge, Kontakte, Verkäufe, OCR, KI-Copilot 5-7
Buchhaltung Verkäufe (read), DATEV-Export, USt-IdNr.-Prüfung 1-2

1.2 Sprachen

  • Deutsch (DE) Primärsprache
  • Englisch (EN) Sekundärsprache

2. Tech-Stack

Schicht Technologie Version
Backend Python / FastAPI Python 3.12, FastAPI 0.111+
Frontend React / Next.js Next.js 15 (App Router), React 19
CSS Tailwind CSS 3.4+
Datenbank PostgreSQL 16+
Cache/Queue Redis 7+
ORM SQLAlchemy 2.0 + Alembic async
KI OpenRouter API Qwen2.5-VL (OCR), Flux.1-Pro (Bild), Claude/GPT-4 (Copilot)
mobile.de Seller API (Push only) REST
OCR OpenRouter Vision (Qwen2.5-VL)
DATEV CSV-Export (DATEV-Format)
USt-IdNr. BZSt API (geplant, aktuell kein Zugang) REST
Deployment Docker-Compose via Coolify 4 Container
Auth JWT (access + refresh)
Testing pytest + httpx (Backend), Vitest + Playwright (Frontend)

3. Deployment-Architektur

3.1 Docker-Compose (4 Container)

┌─────────────────────────────────────────────────┐
│                Coolify (46.225.91.159)            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ frontend │  │ backend  │  │  redis   │       │
│  │ Next.js  │  │ FastAPI  │  │  cache   │       │
│  │ :3000    │  │ :8000    │  │  :6379   │       │
│  └────┬─────┘  └────┬─────┘  └──────────┘       │
│       │              │                           │
│       │         ┌────┴─────┐                     │
│       │         │ postgres │                     │
│       │         │   :5432  │                     │
│       │         └──────────┘                     │
│       │                                          │
│  Traefik (SSL, Let's Encrypt)                    │
└─────────────────────────────────────────────────┘

3.2 Netzwerk

  • Traefik als Reverse Proxy mit automatischem SSL
  • Frontend erreichbar unter https://erp.media-on.de
  • Backend API unter https://erp.media-on.de/api
  • Interne Kommunikation über Docker-Netzwerk

3.3 Volumes

  • postgres_data: Datenbank-Persistenz
  • redis_data: Redis-Persistenz (optional)
  • uploads_data: Fahrzeug-Dateien (Bilder, Dokumente, OCR-Scans)
  • retouched_images: Retuschierte Bilder

4. Modulstruktur

4.1 Backend-Struktur (FastAPI)

backend/
├── app/
│   ├── main.py                 # FastAPI app entry, CORS, routers
│   ├── config.py               # Settings (Pydantic BaseSettings)
│   ├── database.py             # async engine, session factory
│   ├── dependencies.py         # Common deps (auth, pagination)
│   ├── models/                 # SQLAlchemy models
│   │   ├── user.py
│   │   ├── vehicle.py
│   │   ├── contact.py
│   │   ├── sale.py
│   │   ├── document.py
│   │   └── ocr_result.py
│   ├── schemas/                # Pydantic schemas (request/response)
│   │   ├── user.py
│   │   ├── vehicle.py
│   │   ├── contact.py
│   │   ├── sale.py
│   │   └── ocr.pyn│   ├── routers/                # API route handlers
│   │   ├── auth.py
│   │   ├── vehicles.py
│   │   ├── contacts.py
│   │   ├── sales.py
│   │   ├── ocr.py
│   │   ├── files.py
│   │   ├── copilot.py
│   │   └── image_retouch.py
│   ├── services/               # Business logic
│   │   ├── auth_service.py
│   │   ├── vehicle_service.py
│   │   ├── mobilede_service.py
│   │   ├── contact_service.py
│   │   ├── sale_service.py
│   │   ├── ocr_service.py
│   │   ├── file_service.py
│   │   ├── copilot_service.py
│   │   ├── retouch_service.py
│   │   └── datev_service.py
│   ├── tasks/                  # Background tasks (async)
│   │   ├── mobilede_push.py
│   │   └── ocr_processing.py
│   └── utils/
│       ├── openrouter.py       # OpenRouter API client
│       ├── i18n.py             # Backend i18n helpers
│       └── datev.py            # DATEV export formatter
├── alembic/                    # DB migrations
├── tests/                      # pytest test suite
├── requirements.txt
├── Dockerfile
└── pyproject.toml

4.2 Frontend-Struktur (Next.js)

frontend/
├── app/                        # App Router
│   ├── layout.tsx              # Root layout (i18n provider, auth)
│   ├── page.tsx                # Dashboard
│   ├── (auth)/
│   │   └── login/page.tsx
│   ├── vehicles/
│   │   ├── page.tsx            # List + filter
│   │   ├── [id]/page.tsx       # Detail
│   │   └── new/page.tsx        # Create form
│   ├── contacts/
│   │   ├── page.tsx            # List
│   │   ├── [id]/page.tsx       # Detail
│   │   └── new/page.tsx        # Create form
│   ├── sales/
│   │   ├── page.tsx            # List
│   │   ├── [id]/page.tsx       # Detail + contract
│   │   └── new/page.tsx        # Create sale
│   ├── ocr/
│   │   └── page.tsx            # OCR upload + results
│   ├── copilot/
│   │   └── page.tsx            # KI-Copilot chat
│   └── settings/
│       └── page.tsx            # Admin settings
├── components/                 # Reusable components
│   ├── ui/                     # Base UI (Button, Input, Card, Table)
│   ├── vehicles/               # Vehicle-specific components
│   ├── contacts/               # Contact-specific components
│   ├── sales/                  # Sale-specific components
│   ├── ocr/                    # OCR components
│   └── copilot/                # Copilot chat components
├── lib/                        # Client utilities
│   ├── api.ts                  # API client (fetch wrapper)
│   ├── auth.ts                 # Auth context/hooks
│   └── i18n.ts                 # i18n config
├── messages/                   # i18n translation files
│   ├── de.json
│   └── en.json
├── public/
├── Dockerfile
├── tailwind.config.ts
└── package.json

5. Datenmodell

5.1 Entity-Relationship-Übersicht

User ──< Vehicle ──< File (Dateiablage)
  │         │
  │         ├──< OCRResult
  │         ├──< RetouchedImage
  │         └──< Sale ──< ContractDocument
  │                    └──< DATEVExport
  │
  └──< Contact (Kunde/Lieferant)

5.2 Kern-Entitäten

User

Feld Typ Constraints
id UUID PK
email VARCHAR(255) unique, not null
full_name VARCHAR(200) not null
role ENUM('admin','verkaeufer','buchhaltung') not null
password_hash VARCHAR(255) not null
is_active BOOLEAN default true
created_at TIMESTAMPTZ default now()

Vehicle

Feld Typ Constraints
id UUID PK
vehicle_type ENUM('lkw','baumaschine','pkw','stapler','transporter') not null
brand VARCHAR(100) not null
model VARCHAR(150) not null
vin VARCHAR(17) unique
registration_plate VARCHAR(20)
first_registration DATE
mileage_km INTEGER
power_kw INTEGER
fuel_type ENUM('diesel','petrol','electric','hybrid','other')
purchase_price DECIMAL(12,2) not null
sale_price DECIMAL(12,2)
status ENUM('in_stock','reserved','sold','deleted') default 'in_stock'
mobilede_ad_id VARCHAR(50)
mobilede_synced_at TIMESTAMPTZ
notes TEXT
created_by UUID FK → User
created_at TIMESTAMPTZ default now()
updated_at TIMESTAMPTZ default now()

Contact

Feld Typ Constraints
id UUID PK
contact_type ENUM('kunde','lieferant','beide') not null
company_name VARCHAR(200)
first_name VARCHAR(100)
last_name VARCHAR(100)
street VARCHAR(255)
zip_code VARCHAR(10)
city VARCHAR(100)
country VARCHAR(2) default 'DE'
is_eu BOOLEAN default false
ust_id_nr VARCHAR(20)
phone VARCHAR(30)
email VARCHAR(255)
created_at TIMESTAMPTZ default now()

Sale

Feld Typ Constraints
id UUID PK
vehicle_id UUID FK → Vehicle, not null
buyer_contact_id UUID FK → Contact, not null
seller_user_id UUID FK → User, not null
sale_price DECIMAL(12,2) not null
sale_date DATE not null
payment_method ENUM('bank_transfer','cash','financing','other')
is_gwg BOOLEAN default false (Geringwertiges Wirtschaftsgut)
gwg_amount DECIMAL(12,2)
vat_rate DECIMAL(5,2) default 19.00
ust_id_nr_verified BOOLEAN default false
contract_pdf_path VARCHAR(500)
datev_export_id UUID FK → DATEVExport (nullable)
created_at TIMESTAMPTZ default now()

File (Dateiablage pro Fahrzeug)

Feld Typ Constraints
id UUID PK
vehicle_id UUID FK → Vehicle, not null
file_type ENUM('image','document','ocr_scan','retouched_image') not null
original_filename VARCHAR(255) not null
stored_path VARCHAR(500) not null
mime_type VARCHAR(100) not null
file_size_bytes BIGINT not null
uploaded_by UUID FK → User
created_at TIMESTAMPTZ default now()

OCRResult

Feld Typ Constraints
id UUID PK
vehicle_id UUID FK → Vehicle, not null
file_id UUID FK → File, not null
raw_text TEXT extracted text
structured_data JSONB parsed fields (brand, model, vin, etc.)
ocr_type ENUM('zb_i','zb_ii') not null
confidence_score FLOAT
status ENUM('pending','completed','failed') default 'pending'
created_at TIMESTAMPTZ default now()

DATEVExport

Feld Typ Constraints
id UUID PK
export_date DATE not null
period_from DATE not null
period_to DATE not null
file_path VARCHAR(500) generated CSV path
record_count INTEGER
created_by UUID FK → User
created_at TIMESTAMPTZ default now()

6. API Design

6.1 Auth

Method Endpoint Beschreibung
POST /api/auth/login Login → JWT (access + refresh)
POST /api/auth/refresh Token refresh
GET /api/auth/me Aktuellen User abrufen
PUT /api/auth/me Profil aktualisieren

6.2 Vehicles (M1)

Method Endpoint Beschreibung
GET /api/vehicles List with pagination, filter (type, status, brand), sort
GET /api/vehicles/:id Detail view
POST /api/vehicles Create new vehicle
PUT /api/vehicles/:id Update vehicle
DELETE /api/vehicles/:id Soft delete (status='deleted')
POST /api/vehicles/:id/mobilede-push Push to mobile.de Seller API
GET /api/vehicles/:id/mobilede-status Sync status abrufen

6.3 OCR (M2)

Method Endpoint Beschreibung
POST /api/ocr/upload Upload ZB I/II scan → async OCR processing
GET /api/ocr/results/:id OCR result abrufen
GET /api/ocr/results?vehicle_id=X OCR results für Fahrzeug
POST /api/ocr/results/:id/apply OCR-Daten auf Vehicle anwenden

6.4 Contacts (M3)

Method Endpoint Beschreibung
GET /api/contacts List with pagination, filter (type, eu/inland), search
GET /api/contacts/:id Detail view
POST /api/contacts Create contact
PUT /api/contacts/:id Update contact
DELETE /api/contacts/:id Delete contact

6.5 Files (M4)

Method Endpoint Beschreibung
GET /api/vehicles/:id/files List files for vehicle
POST /api/vehicles/:id/files Upload file (multipart)
GET /api/vehicles/:id/files/:fileId Download file
DELETE /api/vehicles/:id/files/:fileId Delete file

6.6 Sales (M5)

Method Endpoint Beschreibung
GET /api/sales List with pagination, filter (date, status)
GET /api/sales/:id Detail view
POST /api/sales Create sale (generates contract PDF)
PUT /api/sales/:id Update sale
DELETE /api/sales/:id Cancel/delete sale
POST /api/sales/:id/contract Re-generate contract PDF
GET /api/sales/:id/contract Download contract PDF
POST /api/sales/:id/verify-ust-id USt-IdNr. BZSt API Prüfung (geplant)
POST /api/datev/export DATEV CSV Export für Zeitraum
GET /api/datev/exports List DATEV exports
GET /api/datev/exports/:id/download Download DATEV CSV

6.7 KI-Copilot (M7)

Method Endpoint Beschreibung
POST /api/copilot/chat Text chat → OpenRouter (Claude/GPT-4)
POST /api/copilot/voice Voice input (STT) → chat response
GET /api/copilot/history Chat history (paginated)
POST /api/copilot/action System action ausführen (vehicle create, search, etc.)

6.8 Image Retouch (M8)

Method Endpoint Beschreibung
POST /api/retouch/process Bild → Flux.1-Pro Retusche
GET /api/retouch/results/:id Retuschiertes Bild abrufen
POST /api/retouch/price-compare Preisvergleich mit mobile.de Listings

6.9 Users (Admin)

Method Endpoint Beschreibung
GET /api/users List users (admin only)
POST /api/users Create user (admin only)
PUT /api/users/:id Update user (admin only)
DELETE /api/users/:id Deactivate user (admin only)

7. Test-Strategie

7.1 Backend (pytest)

  • Unit Tests: Services isoliert mit Mocks (OpenRouter, mobile.de API)
  • Integration Tests: API Endpoints mit Test-DB (SQLite/PostgreSQL testcontainer)
  • Coverage Target: ≥ 80% pro Modul
  • Fixtures: DB session, test client, mock OpenRouter responses

7.2 Frontend (Vitest + Playwright)

  • Unit Tests: Komponenten mit Vitest + React Testing Library
  • E2E Tests: Playwright für kritische User-Flows (Login, Vehicle CRUD, OCR, Sale)
  • Coverage Target: ≥ 70% pro Komponentengruppe

7.3 Test-Commands

# Backend
cd backend && pytest --cov=app --cov-report=term-missing

# Frontend
cd frontend && npm run test
cd frontend && npm run e2e

# Full integration
docker compose -f docker-compose.test.yml up --abort-on-container-exit

8. Integrationsarchitektur

8.1 OpenRouter KI-Pipeline

User Request → FastAPI → OpenRouter API → Response
                 │
                 ├── OCR: Qwen2.5-VL (Vision) → structured JSON
                 ├── Retouch: Flux.1-Pro → Image URL/base64
                 └── Copilot: Claude/GPT-4 → Text/Action JSON
  • API-Key via Environment Variable OPENROUTER_API_KEY
  • Rate-Limiting via Redis Token Bucket
  • Fehlerbehandlung: Retry mit exponential backoff (max 3)
  • Response-Caching für identische OCR-Scans

8.2 mobile.de Seller API

Vehicle Update → FastAPI → mobile.de Seller API (POST/PUT)
                            ↓
                          Ad ID gespeichert in Vehicle.mobilede_ad_id
  • Nur Push-Richtung (kein Import von mobile.de)
  • Auth: OAuth2 oder API-Key (in mobile.de Seller Portal konfigurierbar)
  • Mapping: ERP Vehicle → mobile.de Ad Format
  • Retry-Queue bei Fehlern (Redis)
  • Status-Tracking: mobilede_synced_at, mobilede_ad_id

8.3 DATEV-Export

Buchhaltung User → DATEV Export Request → FastAPI
  → Sammle Sales im Zeitraum → Format CSV (DATEV-Format) → Download
  • Format: DATEV CSV (Buchungsstapel)
  • Felder: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext
  • GwG-Behandlung: Sofortabzug bei ≤ 800€ (§6 Abs. 2 EStG)
  • USt-IdNr.-Prüfung: BZSt API (geplant, Feature-Flag bzst_api_enabled)

9. ADRs (Architecture Decision Records)

ADR-001: FastAPI statt Django

Status: Accepted
Entscheidung: FastAPI als Backend-Framework.
Begründung: Async-native, bessere Performance, automatische OpenAPI-Dokumentation, lichtgewichtiger als Django, native Pydantic-Integration.
Alternativen: Django REST Framework (zu schwerfällig), Flask (kein async).

ADR-002: Next.js App Router statt Pages Router

Status: Accepted
Entscheidung: Next.js 15 mit App Router.
Begründung: Server Components, Streaming, bessere Code-Organisation, Zukunftssicherheit.
Alternativen: Pages Router (deprecated), CRA (deprecated).

ADR-003: OpenRouter als KI-Gateway

Status: Accepted
Entscheidung: Alle KI-Aufrufe über OpenRouter API.
Begründung: Ein API-Key für multiple Modelle, einheitliche API, kein Vendor Lock-in, schnelle Modellwechsel.
Alternativen: Direkte OpenAI/Anthropic API (Vendor Lock-in), lokale Modelle (zu langsam für OCR).

ADR-004: OCR via Vision-Modell statt Tesseract

Status: Accepted
Entscheidung: OCR über OpenRouter Qwen2.5-VL Vision-Modell.
Begründung: Bessere Erkennung bei handschriftlichen und undeutlichen ZB-Scans, keine Infrastruktur für Tesseract nötig, strukturierbare JSON-Ausgabe.
Alternativen: Tesseract (schlechte Qualität bei Scans), Google Cloud Vision (Privacy-Bedenken, kostenpflichtig).

ADR-005: Soft-Delete für Vehicles

Status: Accepted
Entscheidung: Vehicles werden nicht physisch gelöscht, sondern status='deleted'.
Begründung: Referentielle Integrität (Sales, Files, OCR Results), Audit-Trail für Buchhaltung.
Alternativen: Hard Delete (Datenverlust, FK-Constraints).

ADR-006: JWT statt Session-based Auth

Status: Accepted
Entscheidung: JWT (access + refresh token) für Authentifizierung.
Begründung: Stateless, geeignet für API + SPA, keine Server-Side-Session nötig.
Alternativen: Session-Cookies (CSRF-Risiko, Server-State nötig).

ADR-007: Redis für Caching und Background Queues

Status: Accepted
Entscheidung: Redis als Cache und Task-Queue.
Begründung: Schnelles In-Memory-Caching für OCR-Results und mobile.de-Status, Background-Queues für async OCR und mobile.de Push.
Alternativen: Celery+RabbitMQ (zu komplex für ~10 Nutzer), keine Queue (blocking API).

ADR-008: Docker-Compose mit 4 Containern statt K8s

Status: Accepted
Entscheidung: Docker-Compose mit frontend, backend, postgres, redis.
Begründung: Einfach zu deployen via Coolify, ausreichend für ~10 Nutzer, keine K8s-Komplexität.
Alternativen: Kubernetes (overkill), einzelne Coolify Services (mehr Konfigurationsaufwand).


10. Nicht-funktionale Anforderungen

10.1 Performance

  • API-Response < 500ms für Standard-CRUD (ohne KI-Calls)
  • OCR-Verarbeitung < 30s pro Scan (async, User wird benachrichtigt)
  • Bildretusche < 60s pro Bild (async)
  • Frontend LCP < 2.5s (mobile.de Performance Budget)

10.2 Sicherheit

  • JWT mit kurzer Access-Token-Gültigkeit (15min) + Refresh (7d)
  • Role-based Access Control (RBAC) auf Endpoint-Ebene
  • Input-Validierung via Pydantic auf allen Endpoints
  • SQL-Injection-Schutz durch SQLAlchemy parameterized queries
  • File-Upload: MIME-Type-Validierung, max 20MB pro Datei
  • OpenRouter API-Key in Environment Variables, nie im Code
  • HTTPS-only via Traefik + Let's Encrypt

10.3 Skalierbarkeit

  • Horizontal skalierbar (stateless Backend, sticky sessions nicht nötig)
  • Redis als verteiltes Cache → mehrere Backend-Instanzen möglich
  • PostgreSQL Connection Pooling via SQLAlchemy async engine
  • Für ~10 Nutzer: Single-Instance ausreichend

10.4 Verfügbarkeit

  • Docker-Compose mit restart: unless-stopped
  • PostgreSQL mit WAL-Archiving für Point-in-Time-Recovery
  • Daily Backup via Coolify (PostgreSQL dump)
  • Health-Checks auf /api/health (Backend) und / (Frontend)

11. Risiken & Mitigation

Risiko Wahrscheinlichkeit Impact Mitigation
OpenRouter API-Ausfall Mittel Hoch Retry mit Backoff, Fallback-Modell, Cache
mobile.de API-Änderung Niedrig Mittel API-Version pinnen, Monitoring
OCR-Qualität unzureichend Mittel Mittel Confidence-Score → Manual Review bei < 0.7
BZSt API kein Zugang Hoch Niedrig Feature-Flag, manuelle Prüfung als Fallback
Datenverlust PostgreSQL Niedrig Kritisch Daily Backups, WAL-Archiving
Token-Kosten OpenRouter Mittel Mittel Caching, Rate-Limiting pro User

12. i18n Architektur

  • Backend: Fehlermeldungen auf Deutsch/Englisch via Accept-Language Header
  • Frontend: next-intl oder react-i18next
  • Translation Files: frontend/messages/de.json, frontend/messages/en.json
  • Sprachumschaltung: User-Settings → Locale in localStorage + Cookie
  • Backend-Fehler: Error-Codes + lokalisierte Messages

13. Konventionen

14. PDF-Generierung & Briefpapier (Letterhead)

14.1 Library-Wahl: WeasyPrint

Entscheidung: WeasyPrint als PDF-Generierungs-Library.

Begründung: WeasyPrint rendert HTML/CSS → PDF. Dadurch können PDF-Templates mit Standard-Web-Technologien (HTML, CSS, Jinja2) erstellt werden. Briefpapier (Letterhead) wird über CSS @page Rules mit @top/@bottom Margin-Boxes umgesetzt inkl. Logo, Firmenadresse, Footer, Seitenzahlen.

Vergleich:

Kriterium WeasyPrint ReportLab
Template-Erstellung HTML/CSS + Jinja2 Python-Code (Programmatic)
Briefpapier/Header/Footer CSS @page Rules, repeating on every page PageTemplate + Frame + onPage callback
Logo/Bild-Embedding <img> oder CSS background-image Image() flowable
Lernkurve Niedrig (Web-Entwickler kennen HTML/CSS) Mittel (ReportLab-spezifische API)
Flexibilität Hoch (volle CSS-Unterstützung) Mittel (eingeschränkte Layout-Optionen)
Wartbarkeit Hoch (Templates separiert von Code) Niedrig (Layout im Python-Code)

Alternativen: ReportLab (pixel-präzise Kontrolle, aber komplexer), fpdf2 (einfach aber weniger Features), wkhtmltopdf (veraltet).

14.2 Briefpapier-Implementierung

Template-Struktur:

backend/app/utils/pdf/
├── letterhead_template.html    # Jinja2 HTML-Template mit Briefpapier
├── letterhead_style.css        # CSS mit @page Rules (Header/Footer/Logo)
├── contract_template.html       # Vertrag-Template (erbt Briefpapier)
├── invoice_template.html        # Rechnung-Template (erbt Briefpapier)
└── pdf_generator.py             # WeasyPrint Wrapper-Service

CSS @page für Briefpapier:

@page {
  size: A4;
  margin: 120px 40px 80px 40px;  /* top right bottom left */
  
  @top-left {
    content: url('/data/letterhead/logo.png');
    width: 180px;
  }
  @top-right {
    content: 'Firmenname GmbH';
    font-size: 9pt;
    color: #666;
  }
  @bottom-left {
    content: 'Firmenname GmbH · Straßenname 1 · 12345 Stadt';
    font-size: 8pt;
    color: #999;
  }
  @bottom-right {
    content: 'Seite ' counter(page) ' von ' counter(pages);
    font-size: 8pt;
    color: #999;
  }
}

Briefpapier-Elemente (konfigurierbar über Admin-Settings):

  • Logo (PNG/SVG, Position: top-left oder top-center)
  • Firmenname & Adresse (Header oder Footer)
  • Kontaktdaten (Tel, Email, Website)
  • Steuernummer / USt-IdNr. (Footer)
  • Seitenzahlen (Footer rechts)
  • Hintergrund-Wasserzeichen (optional, z.B. "ENTWURF")
  • Bankverbindung (Footer)

Admin-Settings für Briefpapier:

  • Endpoint: PUT /api/settings/letterhead (admin only)
  • Felder: logo_path, company_name, street, zip_city, phone, email, website, tax_number, ust_id_nr, bank_name, bank_iban, bank_bic
  • Briefpapier-Vorschau: GET /api/settings/letterhead/preview → PDF Preview

14.3 Dokument-Typen

Dokument Template Briefpapier
Kaufvertrag (Sale Contract) contract_template.html Ja (Full Letterhead)
Rechnung (Invoice) invoice_template.html Ja (Full Letterhead)
DATEV-Export (CSV, kein PDF) Nein
Fahrzeug-Inventarliste inventory_template.html Ja (Full Letterhead)

ADR-009: WeasyPrint statt ReportLab

Status: Accepted
Entscheidung: WeasyPrint für alle PDF-Generierungen.
Begründung: HTML/CSS-Templates sind einfacher zu erstellen und zu warten als ReportLab-Code. Briefpapier über CSS @page ist flexibler und Web-Entwickler können Templates ohne Python-Kenntnisse anpassen.
Alternativen: ReportLab (pixel-präzise aber komplex), wkhtmltopdf (veraltet).


15. mobile.de Seller API Detaillierte Integration

15.1 API-Übersicht

mobile.de bietet zwei Schnittstellen:

Schnittstelle Format Use Case
Seller API (REST) XML (application/vnd.de.mobile.seller-ad-v1.1+xml) Granulare CRUD-Operationen, Real-Time Sync
CSV Upload Interface CSV (Semikolon-getrennt, ISO-8859-15) Bulk-Upload aller Fahrzeuge auf einmal

Entscheidung: Seller API (REST) als primäre Schnittstelle. CSV Upload als Fallback/Initial-Bulk-Import.

15.2 Authentifizierung

HTTP Basic Authentication (empfohlen):

  • Username + Password aus mobile.de Seller Portal
  • Authorization: Basic <base64(username:password)>
  • Alternativ (deprecated): X-MOBILE-SELLER-TOKEN Header

Credentials in Environment Variables:

MOBILEDE_SELLER_API_USERNAME=<username>
MOBILEDE_SELLER_API_PASSWORD=<password>
MOBILEDE_SELLER_KEY=<seller-key>
MOBILEDE_SELLER_API_URL=https://services.mobile.de

15.3 REST API Endpunkte

Base URL: https://services.mobile.de

Method Endpoint Beschreibung
GET /seller-api/sellers Alle Sellers abrufen
GET /seller-api/sellers/{seller-key} Einzelnen Seller abrufen
GET /seller-api/sellers/{seller-key}/ads Alle Anzeigen eines Sellers
POST /seller-api/sellers/{seller-key}/ads Anzeige erstellen (XML Body)
GET /seller-api/sellers/{seller-key}/ads/{adId} Einzelne Anzeige abrufen
PUT /seller-api/sellers/{seller-key}/ads/{adId} Anzeige aktualisieren (XML Body)
DELETE /seller-api/sellers/{seller-key}/ads/{adId} Anzeige löschen
POST /seller-api/sellers/{seller-key}/ads/{adId}/vehicle-attribute/renewal-date Anzeige erneuern
PUT /seller-api/sellers/{seller-key}/ads/{adId}/images Bilder hochladen/ändern
GET /seller-api/sellers/{seller-key}/ads/{adId}/images Bild-URLs abrufen
DELETE /seller-api/sellers/{seller-key}/ads/{adId}/images Alle Bilder löschen
GET /seller-api/sellers/{seller-key}/ads/{ad-key}/statistic Anzeigen-Statistiken

15.4 XML Ad-Format (Beispiel)

<ad:ad xmlns:ad="http://services.mobile.de/schema/ad">
  <ad:vehicle>
    <ad:category>Lkw</ad:category>
    <ad:make>Mercedes-Benz</ad:make>
    <ad:model>Actros</ad:model>
    <ad:kilometre>150000</ad:kilometre>
    <ad:firstRegistration>2020-03-15</ad:firstRegistration>
    <ad:fuel>Diesel</ad:fuel>
    <ad:gearbox>Manual</ad:gearbox>
    <ad:price consumerPrice="true">45000.00</ad:price>
    <ad:vat>1</ad:vat>
    <ad:damagedVehicle>0</ad:damagedVehicle>
    <ad:images>
      <ad:image url="https://erp.media-on.de/files/vehicle-123/img1.jpg"/>
    </ad:images>
  </ad:vehicle>
</ad:ad>

15.5 Mapping: ERP Vehicle → mobile.de Ad

ERP Feld mobile.de XML Feld Anmerkung
vehicle_type ad:category Lkw/Baumaschine/Pkw/Stapler/Transporter
brand ad:make
model ad:model
mileage_km ad:kilometre
first_registration ad:firstRegistration ISO Datum
power_kw ad:power kW-Wert
fuel_type ad:fuel Diesel/Petrol/Electric/Hybrid
sale_price ad:price consumerPrice="true"
registration_plate ad:licensePlate
vin ad:vin Fahrzeug-Identifikationsnummer
notes ad:description Freitext

15.6 CSV Upload Interface (Fallback)

Format: Semikolon-getrennt, ISO-8859-15, .csv in .zip verpackt. Pflichtfelder: internal number, category, make, model, kilometre, VAT, damaged_vehicle, one-year-old car, new car, our recommendation, metallic, warranty.

Use Case: Initial-Bulk-Import aller Fahrzeuge nach mobile.de, oder wenn Seller API nicht verfügbar.

15.7 Rate-Limits

Die mobile.de Dokumentation nennt keine expliziten Rate-Limits. Dennoch implementieren wir einen eigenen Rate-Limiter (Redis Token Bucket):

  • Max 10 Requests/Sekunde an mobile.de
  • Retry-Queue bei HTTP 429 oder 5xx Fehlern (max 3 Retries mit Backoff)

ADR-010: Seller API (REST) statt CSV Upload als primäre Schnittstelle

Status: Accepted
Entscheidung: REST Seller API als primäre mobile.de Schnittstelle.
Begründung: Granulare CRUD-Operationen, Real-Time Sync, Bild-Management, Statistiken. CSV nur für Initial-Import oder Bulk-Operationen.
Alternativen: CSV Upload nur (kein Real-Time, kein Bild-Management).


16. Buchhaltungsschnittstellen

16.1 DATEV-Export (primär)

Format: DATEV CSV (ASCII, semikolon-getrennt) Buchungsstapel.

DATEV CSV Felder (Buchungsstapel-Format):

"Buchungsstapel";"01.01.2026";"31.01.2026";"0001";"Mandantenname";"1000";"Beraternummer"
"Umsatz";"Soll";"Haben";"Konto";"Gegenkonto";"Datum";"Belegfeld";"Buchungstext"
45000.00;"S";4200;0840;15.01.2026;"RE-2026-001";"Fahrzeugverkauf Mercedes Actros"

DATEV XML-Schnittstelle (erweitert):

  • DATEV unterstützt zusätzlich XML-Format für Belegdaten
  • Belegbilder + Belegdaten als DATEV-konforme XML
  • Vorteil: Belegbilder (Rechnungen/Verträge) können direkt mitgeliefert werden
  • Implementiert als Feature-Flag datev_xml_export_enabled

16.2 Weitere Buchhaltungsschnittstellen

System Format Status Implementierung
DATEV CSV (ASCII) + XML Geplant (primär) T06 datev_service
Lexware DATEV-CSV kompatibel Über DATEV-Export nutzbar Keine separate Implementierung nötig
SAP CSV/IDoc Zukunft (v2) Feature-Flag, separates Export-Modul
DATEV Belegbilder XML + Bilder Zukunft (v2) Feature-Flag datev_xml_export_enabled

Lexware-Kompatibilität: Lexware unterstützt den DATEV-Import (CSV-Format). Daher ist der DATEV-CSV-Export automatisch Lexware-kompatibel. Lexware nutzt denselben DATEV-Datenexport "Belegbilder mit Belegdaten".

16.3 Export-Service Architektur

backend/app/utils/datev.py          # DATEV CSV Formatter
backend/app/utils/accounting_export.py  # Generic Export Base (für zukünftige Formate)
backend/app/services/datev_service.py   # DATEV Export Logic

Generic Export Pattern (für zukünftige SAP/Weitere):

class AccountingExporter(ABC):
    @abstractmethod
    def export(self, sales: list[Sale], period: DateRange) -> bytes: ...

class DatevCSVExporter(AccountingExporter): ...
class DatevXMLExporter(AccountingExporter): ...  # v2
class SAPExporter(AccountingExporter): ...        # v2

ADR-011: DATEV CSV als primäre Buchhaltungsschnittstelle

Status: Accepted
Entscheidung: DATEV CSV-Format als primärer Export. Generic Export Base für zukünftige Formate (XML, SAP).
Begründung: DATEV ist der deutsche Standard, Lexware-kompatibel, einfach zu generieren. Abstract Base Class ermöglicht Erweiterung ohne Code-Duplikation.
Alternativen: Direkte DATEV online API (zu komplex, benötigt Zertifizierung), SAP IDoc (overkill für ~10 Nutzer).


17. KI-Copilot: Vollständige Systemsteuerung

17.1 Anforderung

Der KI-Copilot soll ALLE System-Funktionen steuern können. Der User kann per Text oder Sprache mit dem Copilot interagieren und beliebige Aktionen ausführen lassen.

17.2 Technische Umsetzung: Function Calling

Mechanismus: OpenRouter Function Calling / Tool Use

Der LLM (Claude/GPT-4) erhält einen System-Prompt mit allen verfügbaren Functions (Tools). Bei einer User-Anfrage entscheidet der LLM, welche Function aufgerufen werden muss, und gibt strukturiertes JSON zurück.

User: "Erstelle ein neues Fahrzeug: Mercedes Actros, 150000 km, 45000 EUR"
  ↓
LLM Output: {
  "function": "create_vehicle",
  "arguments": {
    "vehicle_type": "lkw",
    "brand": "Mercedes-Benz",
    "model": "Actros",
    "mileage_km": 150000,
    "purchase_price": 45000.00
  }
}
  ↓
Copilot Service: POST /api/vehicles (intern, mit User-JWT)
  ↓
Response to User: "Fahrzeug erstellt ✓ (ID: abc-123). Soll ich es 
  nach mobile.de pushen?"

17.3 Verfügbare System-Functions (Tools)

Function Beschreibung Endpoint
search_vehicles Fahrzeuge suchen/filtern GET /api/vehicles
create_vehicle Neues Fahrzeug anlegen POST /api/vehicles
update_vehicle Fahrzeug aktualisieren PUT /api/vehicles/:id
delete_vehicle Fahrzeug löschen (soft) DELETE /api/vehicles/:id
push_to_mobilede Nach mobile.de pushen POST /api/vehicles/:id/mobilede-push
upload_ocr OCR-Scan hochladen POST /api/ocr/upload
apply_ocr OCR-Daten anwenden POST /api/ocr/results/:id/apply
search_contacts Kontakte suchen GET /api/contacts
create_contact Kontakt anlegen POST /api/contacts
update_contact Kontakt aktualisieren PUT /api/contacts/:id
upload_file Datei hochladen POST /api/vehicles/:id/files
list_files Dateien auflisten GET /api/vehicles/:id/files
create_sale Verkauf anlegen POST /api/sales
get_sale Verkaufsdetails GET /api/sales/:id
generate_contract Vertrag generieren POST /api/sales/:id/contract
export_datev DATEV-Export POST /api/datev/export
retouch_image Bild retuschieren POST /api/retouch/process
price_compare Preisvergleich POST /api/retouch/price-compare
get_statistics Dashboard-Statistiken GET /api/statistics
search_copilot Eigene Historie durchsuchen GET /api/copilot/history

Echt ALLES inkl. Admin-Functions (nur für Admin-Role):

Function Beschreibung Endpoint
list_users User auflisten GET /api/users
create_user User anlegen POST /api/users
deactivate_user User deaktivieren DELETE /api/users/:id
update_letterhead Briefpapier ändern PUT /api/settings/letterhead

17.4 Safety & Confirmation

Action Preview Pattern (wie in T07 definiert):

  1. LLM schlägt Action vor (Function + Arguments)
  2. Frontend zeigt Action Preview: "Ich möchte folgendes tun: [Action]"
  3. User bestätigt oder ablehnt
  4. Bei Bestätigung: Copilot führt Action aus (interner API-Call mit User-JWT)
  5. Ergebnis wird im Chat angezeigt

Auto-Execute bei Read-Only Actions:

  • search_vehicles, search_contacts, get_sale, list_files, get_statistics, price_compare → automatisch ausgeführt (kein Bestätigung nötig)
  • Alle schreibenden Actions → User-Bestätigung erforderlich

RBAC durchgesetzt: Copilot nutzt den JWT des aktuellen Users. Ein Verkäufer kann keine User verwalten, auch nicht über den Copilot.

17.5 System-Prompt Struktur

Du bist der KI-Assistent für ein Nutzfahrzeug-ERP-System.
Du kannst Fahrzeuge verwalten, Kontakte anlegen, Verkäufe erstellen,
OCR-Scans verarbeiten, Bilder retuschieren und DATEV-Exporte erstellen.

Verfügbare Aktionen:
[Liste aller Functions mit Beschreibung und Parameters]

Regeln:
- Bei schreibenden Aktionen: schlage die Aktion vor und warte auf Bestätigung
- Bei Lese-Aktionen: führe sie direkt aus und präsentiere die Ergebnisse
- Antworte auf Deutsch (oder Englisch je nach User-Setting)
- Wenn Informationen fehlen: frage nach

ADR-012: Function Calling für KI-Vollsteuerung

Status: Accepted
Entscheidung: OpenRouter Function Calling für alle System-Aktionen.
Begründung: Strukturierte JSON-Ausgabe, zuverlässig, von Claude/GPT-4 unterstützt. User-Bestätigung für schreibende Aktionen als Safety-Mechanismus.
Alternativen: Free-text parsing (fehleranfällig), MCP Protocol (zu neu, noch nicht etabliert).


18. KI Multi-Provider Support

18.1 OpenRouter als Unified Gateway

Entscheidung: OpenRouter als alleiniges KI-Gateway. Keine direkten Provider-Anbindungen.

Begründung: OpenRouter bietet:

  • 400+ Modelle von allen major providers (OpenAI, Anthropic, Google, Meta, Mistral, etc.)
  • Ein API-Key für alle Modelle (kein separates Key-Management)
  • OpenAI-kompatible API (drop-in replacement)
  • Provider-Routing: Automatic fallback auf Backup-Provider bei Ausfall
  • Side-by-side Pricing: Kostenvergleich aller Modelle
  • Tool Calling Support: Modelle mit Function Calling verfügbar

18.2 Modell-Konfiguration pro Use-Case

Der User kann in den Admin-Settings konfigurieren, welches Modell für welchen Use-Case verwendet wird:

Use Case Default Modell Alternative
OCR (ZB I/II) qwen/qwen-2.5-vl-72b-instruct openai/gpt-4o
Bild-Retusche black-forest-labs/flux-1.1-pro stabilityai/stable-diffusion-xl
Copilot (Chat) anthropic/claude-3.5-sonnet openai/gpt-4o, google/gemini-flash-1.5
Copilot (Voice STT) openai/whisper-large-v3

Settings Endpoint:

GET /api/settings/ai-models     → Aktuelle Modell-Konfiguration
PUT /api/settings/ai-models     → Modell-Konfiguration aktualisieren (admin only)

Config in DB (Settings-Tabelle):

{
  "ocr_model": "qwen/qwen-2.5-vl-72b-instruct",
  "retouch_model": "black-forest-labs/flux-1.1-pro",
  "copilot_chat_model": "anthropic/claude-3.5-sonnet",
  "copilot_voice_model": "openai/whisper-large-v3"
}

18.3 Provider-Fallback

OpenRouter bietet automatisches Provider-Routing:

  • Bei Ausfall des primären Providers → automatischer Fallback auf Backup-Provider
  • Beispiel: anthropic/claude-3.5-sonnet → bei Anthropic-Ausfall → OpenRouter versucht alternative Provider für dasselbe Modell

Eigenes Fallback-Handling (zusätzlich):

COPILOT_MODELS = [
  "anthropic/claude-3.5-sonnet",
  "openai/gpt-4o",
  "google/gemini-flash-1.5",
  "meta-llama/llama-3.1-70b-instruct"
]
# Bei Fehler: nächstes Modell in der Liste probieren

18.4 Kostenkontrolle

  • OpenRouter zeigt Token-Kosten pro Request
  • Logging: ai_usage_log Tabelle (model, tokens_in, tokens_out, cost, user_id, timestamp)
  • Rate-Limiting pro User (Redis Token Bucket)
  • Monatsbudget konfigurierbar (Admin-Setting ai_monthly_budget_eur)
  • Bei Überschreitung: Warnung + ggf. Drosselung

ADR-013: OpenRouter als alleiniges KI-Gateway

Status: Accepted
Entscheidung: Alle KI-Aufrufe über OpenRouter, keine direkten Provider-APIs.
Begründung: Ein API-Key, 400+ Modelle, Provider-Routing, einheitliche API, keine Multi-SDK-Wartung. Modell-Konfiguration pro Use-Case ermöglicht flexible Anpassung ohne Code-Änderung.
Alternativen: Direkte OpenAI + Anthropic APIs (Multi-Key-Management, höhere Wartung), lokale Modelle via Ollama (zu langsam für OCR/Retusche, aber möglich als Fallback für Copilot-Chat).

18.5 Optional: Lokale Modelle via Ollama (Copilot Fallback)

Für den Copilot-Chat (nicht OCR/Retusche) kann optional ein lokales Modell via Ollama als Offline-Fallback konfiguriert werden:

  • Feature-Flag local_model_fallback_enabled
  • Ollama läuft als 5. Container (optional, nur bei Bedarf)
  • Modell: llama3.1:8b-instruct oder qwen2.5:14b
  • Use Case: Offline-Betrieb oder Kostenersparnis für einfache Chat-Tasks
  • Function Calling Support: Llama 3.1 unterstützt native Tool-Calling

Architektur-Erweiterung (v2, Feature-Flag):

Copilot Request → Try OpenRouter → Fail → Try local Ollama → Fail → Error

13. Konventionen

13.1 Backend

  • async/await für alle DB-Operationen und externen API-Calls
  • Pydantic v2 für alle Request/Response-Schemas
  • SQLAlchemy 2.0 mit typed Mapped columns
  • Alembic für DB-Migrationen (kein auto-create in Produktion)
  • Router-Prefix: /api/<resource>
  • Error-Format: {"error": {"code": "VEHICLE_NOT_FOUND", "message": "...", "details": {}}}

13.2 Frontend

  • TypeScript strict mode
  • Tailwind CSS für Styling (kein CSS-in-JS)
  • Server Components für statische Daten, Client Components für Interaktion
  • API-Client: zentrale fetch-Wrapper mit Auth-Header-Injection
  • Date-Naming: kebab-case für Dateien, camelCase für Variablen

13.3 Git

  • Conventional Commits: feat:, fix:, docs:, test:, refactor:
  • Branch-Naming: feature/T01-vehicle-module, fix/..., hotfix/...
  • PRs erforderlich für main-Branch

19. Provider Architecture (Multi-Provider mit Ollama Cloud)

19.1 Provider Abstraktion (ABC Pattern)

from abc import ABC, abstractmethod

class BaseAIProvider(ABC):
    @abstractmethod
    async def chat(self, messages, tools=None, model=None) -> dict: ...
    @abstractmethod
    async def vision(self, image_base64, prompt, model=None) -> str: ...
    @abstractmethod
    async def image_edit(self, image_base64, instruction, model=None) -> str: ...
    @abstractmethod
    def list_models(self) -> list[str]: ...
    @abstractmethod
    def health_check(self) -> bool: ...

class OpenRouterProvider(BaseAIProvider): ...
class OllamaCloudProvider(BaseAIProvider): ...
class OllamaLocalProvider(BaseAIProvider): ...

Neue Provider durch Implementierung von BaseAIProvider + Eintrag in providers.yaml hinzufügbar.

19.2 providers.yaml

providers:
  openrouter:
    enabled: true
    api_base: https://openrouter.ai/api/v1
    api_key: ${OPENROUTER_API_KEY}
    models: [anthropic/claude-3.5-sonnet, openai/gpt-4o, qwen/qwen2.5-vl]
  ollama_cloud:
    enabled: true
    api_base: ${OLLAMA_CLOUD_URL}
    api_key: ${OLLAMA_CLOUD_API_KEY}
    models: [glm-5.2:cloud, qwen2.5:14b, llava:13b]
  ollama_local:
    enabled: false
    api_base: http://ollama:11434
    models: [llama3.1:8b-instruct, qwen2.5:14b]

19.3 Use-Case → Provider Mapping

Use Case Default Fallback
Copilot Chat ollama_cloud (glm-5.2) openrouter (claude-3.5)
OCR openrouter (qwen2.5-vl) ollama_cloud (llava)
Bildretusche openrouter (flux.1-pro) -
Voice openrouter browser-native

Admin konfigurierbar via PUT /api/settings/ai-providers

ADR-014: Multi-Provider mit Ollama Cloud

  • Ollama Cloud als ECHTER Provider (nicht nur Fallback)
  • Provider-Abstraktion ermöglicht beliebige zukünftige Provider
  • Admin kann pro Use-Case Provider wählen