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)
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
Layer-Architektur
- 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
Design-Token-System
4. Datenbank-Schema
Übersicht: 15 Tabellen
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
JWT Payload
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
Upload-Flow
- Client: multipart/form-data → POST /vehicles/:id/documents
- Backend: MIME-Type-Check (allowlist), Size-Check (≤50MB)
- Backend: Datei speichern unter
/data/uploads/vehicles/{vehicle_id}/{doc_id}_{filename}
- Backend: Thumbnail generieren (für Bilder, Pillow)
- Backend: documents-Row in DB anlegen
- 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
API-Calls
OCR (Qwen2.5-VL)
KI-Copilot (Claude/GPT-4)
Bildretusche (Flux.1-Pro)
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
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
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
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
Coolify-Konfiguration
- Project: ERP Nutzfahrzeuge
- Service Type: Docker Compose (Coolify parse docker-compose.yml)
- Domain:
erp.domain.tld (via Coolify Traefik)
- SSL: Let's Encrypt (automatic via Coolify)
- 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
- Health Checks:
- Backend:
GET /api/v1/health → 200
- Frontend:
GET / → 200
- PostgreSQL:
pg_isready
- Redis:
redis-cli ping
Backup-Strategie
- PostgreSQL: Täglicher
pg_dump → /data/backups/erp_db_YYYY-MM-DD.sql.gz (Coolify Cron Job)
- Uploads (Volume): Wöchentliches Volume-Backup (Coolify Backup Feature)
- Retention: 30 Tage für DB-Backups, 4 Wochen für Volume-Backups
- Restore-Test: Monatlicher Restore-Test in Dev-Environment
CI/CD Pipeline
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
- Domain: Genauer Domain-Name für ERP (via Coolify konfigurierbar) – später
- BZSt eVatR API: Zugang beschaffen für automatische USt-IdNr.-Prüfung – post-MVP
- DATEV-Format: Exaktes CSV-Format mit Steuerberater abklären – während M5 Implementierung
- Vertragsvorlagen: Konkrete Vorlagen-Texte vom User – während M5 Implementierung
- 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