Files

47 KiB
Raw Permalink Blame History

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 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 <JWT> (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 <access_token>
   → 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

{
  "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 <iframe> oder PDF.js Viewer.
  • Download: GET /api/v1/vehicles/:id/documents/:doc_id/download (signed URL or direct)

S3-Kompatibel (Future-Proof)

  • Abstraktion via StorageBackend Interface (local_filesystem | s3)
  • MVP: Local Filesystem mit Coolify Volume
  • Später: Wechsel zu MinIO oder S3 ohne Code-Änderung (nur Config)

8. OpenRouter-Integration

Architektur

Backend Service Layer
├── openrouter_client.py       ← Shared HTTP Client (httpx async)
│   ├── chat_completion()      ← LLM Text (Claude/GPT-4)
│   ├── vision_completion()    ← Vision (Qwen2.5-VL)
│   └── image_generation()     ← Bild (Flux.1-Pro)
│
├── ocr_service.py             ← Nutzt vision_completion()
├── copilot_service.py         ← Nutzt chat_completion()
└── image_retouch_service.py   ← Nutzt image_generation()

API-Calls

OCR (Qwen2.5-VL)

POST https://openrouter.ai/api/v1/chat/completions
{
  "model": "qwen/qwen-2.5-vl-72b-instruct",
  "messages": [{
    "role": "user",
    "content": [
      {"type": "text", "text": "Extract all fields from this vehicle registration document (ZB I)..."},
      {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,{base64_image}"}}
    ]
  }],
  "response_format": {"type": "json_schema", "json_schema": {"name": "zb1_fields", "schema": {...}}}
}

KI-Copilot (Claude/GPT-4)

POST https://openrouter.ai/api/v1/chat/completions
{
  "model": "anthropic/claude-3.5-sonnet",
  "messages": [
    {"role": "system", "content": "You are an ERP assistant for a vehicle dealer. Parse user commands and return structured actions..."},
    {"role": "user", "content": "Lege neues Fahrzeug an: Mercedes Actros, Baujahr 2019, 150000 km, 45000 Euro"}
  ],
  "response_format": {"type": "json_schema", "json_schema": {"name": "copilot_action", "schema": {"type": "object", "properties": {"action": {"type": "string"}, "data": {"type": "object"}}}}}
}

Bildretusche (Flux.1-Pro)

POST https://openrouter.ai/api/v1/images/generations
{
  "model": "black-forest-labs/flux-1.1-pro",
  "prompt": "Remove background from this vehicle photo, replace with neutral white background...",
  "image": "data:image/jpeg;base64,{base64_image}"
}

DSGVO-Konformität

  • NICHT an OpenRouter senden: Ausweisdaten, personenbezogene Kundendaten
  • Senden erlaubt: Fahrzeugdaten, Fahrzeugfotos (keine Ausweise auf Fotos), ZB I/ZB II Bilder
  • AI-Interactions werden in DB protokolliert (ai_interactions Tabelle)

9. mobile.de Seller API Integration

Push-Only Architektur

Backend Service Layer
├── mobile_de_service.py
│   ├── push_listing(vehicle)      → POST /api/seller/listings
│   ├── update_listing(vehicle)     → PUT /api/seller/listings/{id}
│   ├── delete_listing(listing_id)  → DELETE /api/seller/listings/{id}
│   ├── get_listing_status(id)      → GET /api/seller/listings/{id}/status
│   └── map_fields(vehicle)         → ERP → mobile.de Feldmapping

Feldmapping

ERP-Feld mobile.de Feld Transformation
make make direkt
model model direkt
fin vin direkt (17 chars)
first_registration firstRegistration YYYY-MM Format
mileage_km / operating_hours mileage + unit: 'km' or 'h'
price price EUR, netto/brutto flag
power_kw powerKw direkt
power_hp powerHp computed (kW * 1.35962)
fuel_type fuelType lowercase mapping
transmission transmission lowercase mapping
color color direkt
condition condition 'new' or 'used'
vehicle_type + lkw_type category mapping table
machine_type bodyType direkt
body_type equipment direkt
location sellerLocation PLZ + Ort
documents (Fotos) images Base64 oder URL
description description direkt, multi-language
availability availabilityStatus mapping

Sync-Status-Flow

Entwurf → (push successful) → Gelistet → (price change + push) → Aktualisiert
                                                                ↓
                                                          (delisting) → Entfernt
Entwurf → (push failed) → Fehler (error_log gespeichert)

10. i18n-Konzept

Frontend (next-intl)

  • Routing: /de/... und /en/... (Next.js App Router mit [locale])
  • Translation Files: src/messages/de.json, src/messages/en.json (250+ Keys)
  • Datumsformat: de → DD.MM.YYYY, en → MM/DD/YYYY (via next-intl Formatters)
  • Zahlenformat: de → 1.234,56 €, en → 1,234.56 €
  • Sprache pro User: In users.language gespeichert, Frontend liest bei Login

Backend

  • Fehlermeldungen: Accept-Language Header → deutsche oder englische Fehlermeldungen
  • Implementierung: Simple Dictionary in app/exceptions.py (kein gettext-Komplex für MVP)
  • Stammdaten: master_data.value_de und value_en Spalten
  • Vertragsvorlagen: contract_templates.content_de und content_en

11. Test-Strategie

Backend (pytest)

  • Framework: pytest + pytest-asyncio + httpx (AsyncClient)
  • Coverage-Target: ≥80%
  • Test-DB: Separate PostgreSQL test database (nicht SQLite PG-spezifische Features)
  • Fixtures: conftest.py mit async DB session, test client, seed data
# Test-Struktur
tests/
├── conftest.py          # Fixtures: async_db, client, auth_headers, seed_data
├── test_auth.py         # Login, refresh, logout, RBAC
├── test_vehicles.py     # CRUD, filter, validation, soft-delete
├── test_contacts.py     # CRUD, search, ust-id status
├── test_sales.py        # CRUD, contract, invoice, GwG, DATEV
├── test_documents.py    # Upload, preview, delete, size limit
├── test_mobile_de.py    # Push, batch, delisting, field mapping
├── test_ocr.py          # ZB I, ZB II, merge (mocked OpenRouter)
├── test_copilot.py      # Text, voice, actions (mocked OpenRouter)
├── test_image_retouch.py # Background, reflections, batch (mocked OpenRouter)
└── test_datev.py        # Export, format, date range

Frontend (Vitest + Playwright)

  • Unit/Component: Vitest + React Testing Library
  • E2E: Playwright (Kernprozesse: Fahrzeug anlegen, Verkauf starten, KI-Befehl)
  • Coverage-Target: ≥80%

OpenRouter Mocking

  • Tests mocken OpenRouter API (keine realen API-Calls in CI)
  • Mock-Responses: Vordefinierte JSON-Responses für OCR-Felder, Copilot-Actions, Bild-URLs
  • Integration Tests: Manuell mit echtem API-Key (nicht in CI)

12. Deployment-Architektur

Docker Compose Struktur

# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NEXT_PUBLIC_API_URL=http://backend:8000/api/v1
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
    depends_on:
      - backend
    networks:
      - erp-net

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://erp_user:${DB_PASSWORD}@postgres:5432/erp_db
      - REDIS_URL=redis://redis:6379/0
      - JWT_SECRET=${JWT_SECRET}
      - JWT_ALGORITHM=HS256
      - JWT_ACCESS_TTL_MINUTES=15
      - JWT_REFRESH_TTL_DAYS=7
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
      - MOBILE_DE_API_KEY=${MOBILE_DE_API_KEY}
      - MOBILE_DE_SELLER_ID=${MOBILE_DE_SELLER_ID}
      - UPLOAD_DIR=/data/uploads
      - MAX_FILE_SIZE_MB=50
      - CORS_ORIGINS=https://erp.domain.tld
    volumes:
      - erp-uploads:/data/uploads
    depends_on:
      - postgres
      - redis
    networks:
      - erp-net

  postgres:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=erp_db
      - POSTGRES_USER=erp_user
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - erp-pgdata:/var/lib/postgresql/data
    networks:
      - erp-net

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - erp-redis:/data
    networks:
      - erp-net

volumes:
  erp-pgdata:
  erp-uploads:
  erp-redis:

networks:
  erp-net:
    driver: bridge

Coolify-Konfiguration

  1. Project: ERP Nutzfahrzeuge
  2. Service Type: Docker Compose (Coolify parse docker-compose.yml)
  3. Domain: erp.domain.tld (via Coolify Traefik)
  4. SSL: Let's Encrypt (automatic via Coolify)
  5. Environment Variables (in Coolify Dashboard, NIE in Code):
    • DB_PASSWORD PostgreSQL password
    • JWT_SECRET JWT signing secret (min 32 chars)
    • ENCRYPTION_KEY AES-256 key for ID data encryption (32 bytes hex)
    • OPENROUTER_API_KEY OpenRouter API key
    • MOBILE_DE_API_KEY mobile.de Seller API key
    • MOBILE_DE_SELLER_ID mobile.de seller ID
    • NEXTAUTH_SECRET Next.js auth secret
    • CORS_ORIGINS Allowed CORS origins
  6. Health Checks:
    • Backend: GET /api/v1/health → 200
    • Frontend: GET / → 200
    • PostgreSQL: pg_isready
    • Redis: redis-cli ping

Backup-Strategie

  1. PostgreSQL: Täglicher pg_dump/data/backups/erp_db_YYYY-MM-DD.sql.gz (Coolify Cron Job)
  2. Uploads (Volume): Wöchentliches Volume-Backup (Coolify Backup Feature)
  3. Retention: 30 Tage für DB-Backups, 4 Wochen für Volume-Backups
  4. Restore-Test: Monatlicher Restore-Test in Dev-Environment

CI/CD Pipeline

Forgejo Actions:
1. Push to main → trigger pipeline
2. Backend: pip install → pytest → ruff → build Docker image → push to Forgejo registry
3. Frontend: npm install → vitest → eslint → build → build Docker image → push to Forgejo registry
4. Coolify: Webhook trigger → pull new images → docker-compose up -d

13. Architecture Decision Records (ADRs)

ADR-001: FastAPI + Next.js + PostgreSQL

Status: Accepted
Context: ERP-System für ~10 Nutzer, Python-Ökosystem für KI/OCR, React-Frontend, relationale DB.
Decision: FastAPI (async, OpenAPI auto-docs) + Next.js 14 (SSR, App Router) + PostgreSQL 16 (JSONB, robust).
Alternatives: Django + React (heavier), Flask + Vue (less async), Node.js full-stack (weniger KI-Ökosystem).
Rationale: Python für KI/OpenRouter nativ, FastAPI async für I/O-heavy KI-Calls, Next.js für SSR + SEO + i18n, PostgreSQL für relationale Integrität + JSONB für flexible Felder.

ADR-002: JWT + Refresh Token (Redis)

Status: Accepted
Context: ~10 Nutzer, keine externe Auth nötig, Statelessness gewünscht.
Decision: JWT (15min) + Refresh Token (7d, stored in Redis). Token Rotation bei Refresh.
Alternatives: Session-based (DB lookup per request), OAuth2 mit externem Provider.
Rationale: JWT = stateless + schnell für kleine Nutzerzahl. Redis für Refresh Token Invalidierung bei Logout. Kein Overhead für 10 Nutzer.

ADR-003: Local Filesystem statt S3

Status: Accepted
Context: Self-hosted auf Coolify, kein S3-Budget, ~50MB max pro Datei, ~hunderte Dateien.
Decision: Lokales Filesystem mit Docker Volume, abstrahiert via StorageBackend Interface.
Alternatives: MinIO (S3-kompatibel, extra Container), direktes S3 (Cloud-Abhängigkeit).
Rationale: MVP-Einfachheit, Coolify Volume ist persistent. Interface erlaubt später S3-Wechsel ohne Code-Änderung. Bei Wachstum: MinIO als Sidecar.

ADR-004: OpenRouter als einziger KI-Provider

Status: Accepted
Context: KI für OCR (Qwen2.5-VL), Copilot (Claude/GPT-4), Bildretusche (Flux.1-Pro). Kein Vendor-Lock-in gewünscht.
Decision: Alle KI-Calls über OpenRouter API. Modellwechsel durch Config, nicht Code.
Alternatives: Direkte API-Calls an Anthropic/OpenAI/Qwen (Multiple Clients), Ollama lokal (keine Vision-Modelle ausreichend). Rationale: Single integration point, flexible Modell-Auswahl, Auto-Routing Option. DSGVO: Cloud-Verarbeitung vom User akzeptiert.

ADR-005: Soft-Delete für Vehicles und Contacts

Status: Accepted Context: Fahrzeuge/Kontakte können verkauft/archiviert werden, aber historische Verträge referenzieren sie.
Decision: deleted_at Timestamp für Soft-Delete. Queries filtern standardmäßig WHERE deleted_at IS NULL. Alternatives: Hard-Delete mit referential integrity (bricht historische Verträge), separate Archiv-Tabelle. Rationale: DSGVO-Löschkonzept möglich (hard-delete nach Aufbewahrungsfrist), aber Referenz-Integrität für Verträge erhalten.

ADR-006: WeasyPrint für PDF-Generierung

Status: Accepted Context: Kaufverträge, Rechnungen, Lieferbescheinigungen als PDF generieren. HTML-Templates mit Variablen. Decision: WeasyPrint (HTML → PDF) mit Jinja2 Templates. Alternatives: ReportLab (programmatic, weniger flexibel), Puppeteer (extra Chrome-Container), pdfkit (wkhtmltopdf, veraltet). Rationale: WeasyPrint = pure Python, HTML/CSS Templates (wie Vertragsvorlagen), gut für strukturierte Dokumente. Keine externen Dependencies.

ADR-007: Push-Only mobile.de (keine Bidirektionalität)

Status: Accepted Context: User will Fahrzeuge auf mobile.de listen, aber keine Daten von dort zurück importieren. Decision: ERP → mobile.de Push via Seller API. Keine Pull/Import-Logik. Alternatives: Bidirektionale Sync (komplex, Konfliktlösung nötig). Rationale: Explizites Non-Goal in Requirements. Reduziert Komplexität massiv. ERP bleibt Single Source of Truth.

ADR-008: AES-256-GCM für Ausweisdaten

Status: Accepted Context: GwG erfordert Erfassung von Ausweisdaten. DSGVO-konforme Speicherung nötig. Decision: AES-256-GCM Verschlüsselung für identification_data.id_number_encrypted. Key aus ENCRYPTION_KEY env var. Entschlüsselung nur in Service-Layer mit RBAC-Check (Admin + Buchhaltung). Alternatives: PostgreSQL pgcrypto (weniger flexibel), Hashing (nicht brauchbar Daten müssen lesbar sein). Rationale: Application-Level Verschlüsselung = volle Kontrolle, Key nicht in DB, RBAC vor Entschlüsselung. GCM-Mode = Authenticated Encryption.


14. Non-Functional Requirements

Kategorie Anforderung Implementierung
Performance API-Response < 500ms (ohne KI-Calls) PostgreSQL Indexes, async I/O, pagination
Performance KI-Calls: < 30s Timeout httpx timeout=30s, async background für Batch
Security DSGVO-konform Audit-Log, Soft-Delete, AES-256, RBAC
Security GwG-konform Warnung >10.000€ Bar, Identifikation, Meldedokument
Availability ~10 Nutzer, keine HA nötig Single Instance, Coolify Restart bei Crash
Scalability Nicht relevant für MVP Horizontal scaling möglich via Docker, aber nicht nötig
Maintainability ≥80% Test-Coverage pytest + Vitest, CI/CD pipeline
i18n DE + EN von Anfang an next-intl + Backend dictionary
Accessibility WCAG 2.1 AA Basics Semantic HTML, ARIA-Labels, Keyboard-Nav, Kontrast

15. Risiken

Risiko Wahrscheinlichkeit Impact Mitigation
OpenRouter API-Ausfall Mittel Hoch (KI-Features nicht verfügbar) Graceful degradation, manuelle Eingabe als Fallback
mobile.de API-Änderung Niedrig Mittel Service-Layer isoliert Mapping, einfach anpassbar
OpenRouter Kosten explosion Mittel Mittel Token-Tracking in ai_interactions, Monats-Limit konfigurierbar
PDF-Generierung Performance Niedrig Niedrig WeasyPrint ist sync, aber nur bei Bedarf (nicht realtime)
DSGVO-Audit Niedrig Hoch Audit-Log vollständig, Verschlüsselung implementiert, keine Ausweisdaten an KI
Datenverlust (PostgreSQL) Niedrig Kritisch Tägliche pg_dump Backups, Volume-Backups

16. Open Questions

  1. Domain: Genauer Domain-Name für ERP (via Coolify konfigurierbar) später
  2. BZSt eVatR API: Zugang beschaffen für automatische USt-IdNr.-Prüfung post-MVP
  3. DATEV-Format: Exaktes CSV-Format mit Steuerberater abklären während M5 Implementierung
  4. Vertragsvorlagen: Konkrete Vorlagen-Texte vom User während M5 Implementierung
  5. mobile.de API-Dokumentation: Aktuelle Seller API Spec beschaffen während M1 Implementierung

17. Handoff

  • Architecture Status: Complete alle Module, Tabellen, Endpoints, ADRs dokumentiert
  • Ready for Task Breakdown: YES
  • Ready for Implementation: Pending task_graph.json approval