Files

1157 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architecture ERP-System Nutzfahrzeug-/Baumaschinen-Handel
**Version:** 1.0.0
**Datum:** 2026-07-12
**Status:** Draft Ready for Review
**Architect:** Solution Architect (A0)
---
## 1. System-Architektur-Übersicht
### Container-Diagramm (textuell)
```
┌─────────────────────────────────────────────────────────┐
│ Coolify (Docker Host) │
│ coolify-01 / 46.225.91.159 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Traefik │──│ Frontend │ │ Backend │ │
│ │ (SSL/TLS) │ │ Next.js │ │ FastAPI │ │
│ │ Let's Enc│ │ :3000 │ │ :8000 │ │
│ └────┬─────┘ └─────┬────┘ └─────┬────┘ │
│ │ │ │ │
│ │ │ │ │
│ ┌────┴──────────────┴───────────────┴────┐ │
│ │ Docker Network (erp-net) │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
│ ┌──────────┐ ┌───┴─────┐ ┌──────────┐ │
│ │ PostgreSQL│ │ Redis │ │ Volume │ │
│ │ :5432 │ │ :6379 │ │ /data/erp │ │
│ │ (DB) │ │ (cache) │ │ (uploads) │ │
│ └──────────┘ └─────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
OpenRouter API mobile.de Seller API
(LLM/Vision/Bild) (Push-Only REST)
```
### Container-Übersicht
| Container | Image | Port | Volume | Beschreibung |
|---|---|---|---|---|
| **frontend** | node:20-alpine → Next.js standalone build | 3000 | | SSR/SSG React Frontend, Tailwind CSS, next-intl |
| **backend** | python:3.12-slim → FastAPI + Uvicorn | 8000 | | REST API, SQLAlchemy async, Alembic, OpenRouter Client |
| **postgres** | postgres:16-alpine | 5432 | pgdata | Primäre Datenbank,持久化存储 |
| **redis** | redis:7-alpine | 6379 | | Refresh-Token Store, Background Task Queue (RQ/Celery lite) |
### Netzwerk-Architektur
- **erp-net**: Internes Docker Bridge Network, isoliert von Host
- **Traefik**: Reverse Proxy mit SSL/TLS (Let's Encrypt), Routing:
- `erp.domain.tld` → Frontend (Next.js)
- `erp.domain.tld/api/*` → Backend (FastAPI)
- **Externe APIs**: Backend ruft OpenRouter + mobile.de Seller API auf (outbound HTTPS)
---
## 2. Backend-Architektur
### Projekt-Struktur
```
backend/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app entry point
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # SQLAlchemy async engine + session
│ ├── deps.py # Dependency injection (DB session, current user)
│ ├── models/ # SQLAlchemy ORM models
│ │ ├── __init__.py
│ │ ├── base.py # Declarative base, mixins (TimestampMixin)
│ │ ├── user.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ ├── document.py
│ │ ├── mobile_de.py
│ │ ├── ai_interaction.py
│ │ ├── audit_log.py
│ │ ├── master_data.py
│ │ └── settings.py
│ ├── schemas/ # Pydantic v2 request/response schemas
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── vehicle.py
│ │ ├── contact.py
│ │ ├── sale.py
│ │ ├── document.py
│ │ ├── mobile_de.py
│ │ ├── ai.py
│ │ ├── settings.py
│ │ └── common.py # Pagination, filters, error responses
│ ├── routers/ # API route handlers
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── vehicles.py
│ │ ├── contacts.py
│ │ ├── sales.py
│ │ ├── documents.py
│ │ ├── mobile_de.py
│ │ ├── ai.py
│ │ ├── settings.py
│ │ ├── users.py
│ │ └── dashboard.py
│ ├── services/ # Business logic layer
│ │ ├── __init__.py
│ │ ├── auth_service.py
│ │ ├── vehicle_service.py
│ │ ├── contact_service.py
│ │ ├── sale_service.py
│ │ ├── document_service.py
│ │ ├── mobile_de_service.py
│ │ ├── ocr_service.py
│ │ ├── copilot_service.py
│ │ ├── image_retouch_service.py
│ │ ├── contract_service.py
│ │ ├── datev_service.py
│ │ ├── gwg_service.py
│ │ ├── ust_id_service.py
│ │ ├── price_comparison_service.py
│ │ ├── audit_service.py
│ │ └── openrouter_client.py # Shared OpenRouter HTTP client
│ ├── middleware/
│ │ ├── __init__.py
│ │ ├── auth.py # JWT verification middleware
│ │ ├── rbac.py # Role-based access control
│ │ ├── audit.py # Audit logging middleware
│ │ └── i18n.py # Accept-Language → response locale
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── crypto.py # AES-256 encrypt/decrypt for ID data
│ │ ├── pdf.py # WeasyPrint PDF generation helper
│ │ └── pagination.py # Pagination helper
│ └── exceptions.py # Custom exceptions + handlers
├── alembic/ # Database migrations
│ ├── versions/
│ ├── env.py
│ └── alembic.ini
├── tests/
│ ├── conftest.py # pytest fixtures (async DB, test client)
│ ├── test_auth.py
│ ├── test_vehicles.py
│ ├── test_contacts.py
│ ├── test_sales.py
│ ├── test_documents.py
│ ├── test_mobile_de.py
│ ├── test_ocr.py
│ ├── test_copilot.py
│ ├── test_image_retouch.py
│ └── test_datev.py
├── requirements.txt
├── Dockerfile
└── pyproject.toml
```
### Layer-Architektur
```
Router (API Layer) → Service (Business Logic) → Model (ORM) → PostgreSQL
↑ ↑ ↑
Pydantic Schema Dependencies SQLAlchemy 2.0
Request/Response (DB Session, User) Async Engine
```
- **Router**: HTTP request handling, input validation (Pydantic), response serialization
- **Service**: Business logic, orchestration, external API calls, audit logging
- **Model**: SQLAlchemy ORM, database constraints, relationships
- **Schema**: Pydantic v2 models for request/response validation
---
## 3. Frontend-Architektur
### Projekt-Struktur
```
frontend/
├── src/
│ ├── app/ # Next.js 14 App Router
│ │ ├── [locale]/ # i18n routing (de, en)
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── fahrzeuge/
│ │ │ │ ├── page.tsx # Fahrzeug-Liste
│ │ │ │ ├── neu/page.tsx # Fahrzeug-Formular
│ │ │ │ └── [id]/page.tsx # Fahrzeug-Detail
│ │ │ ├── kontakte/page.tsx
│ │ │ ├── verkauf/page.tsx
│ │ │ ├── ki-copilot/page.tsx
│ │ │ └── einstellungen/page.tsx
│ │ └── api/ # Next.js API routes (BFF proxy)
│ ├── components/ # React components
│ │ ├── ui/ # Shared (Button, Card, Input, etc.)
│ │ ├── layout/ # Sidebar, Topbar, AppShell
│ │ ├── vehicles/ # Vehicle-specific components
│ │ ├── contacts/
│ │ ├── sales/
│ │ ├── ai/ # Copilot, Image retouch
│ │ └── settings/
│ ├── lib/
│ │ ├── api-client.ts # Fetch wrapper with auth
│ │ ├── auth.ts # JWT token management
│ │ └── i18n.ts # next-intl config
│ ├── messages/ # i18n translation files
│ │ ├── de.json
│ │ └── en.json
│ └── styles/
│ └── globals.css # Tailwind + design tokens
├── public/
├── next.config.js
├── tailwind.config.ts
├── package.json
├── Dockerfile
└── tsconfig.json
```
### Design-Token-System
```css
/* CSS Custom Properties → Tailwind Config */
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-success: #16a34a;
--color-warning: #d97706;
--color-error: #dc2626;
--color-neutral-50: #f8fafc;
--color-neutral-900: #0f172a;
--radius: 8px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--breakpoint-mobile: 768px;
--breakpoint-tablet: 1280px;
```
---
## 4. Datenbank-Schema
### Übersicht: 15 Tabellen
```
users ──┬── audit_log
├── ai_interactions
├── documents (uploaded_by)
└── ust_id_checks (checked_by)
vehicles ──┬── documents
├── mobile_de_listings
└── sales (vehicle_id)
contacts ──┬── contact_persons
├── sales (buyer_id, seller_id)
└── ust_id_checks
sales ──┬── identification_data
└── ust_id_checks (sale_id)
master_data (independent)
contract_templates (independent)
settings (independent)
```
### Tabellen-Definitionen
#### 4.1 users
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK, default gen_random_uuid() | yes |
| email | VARCHAR(255) | UNIQUE, NOT NULL | yes |
| password_hash | VARCHAR(255) | NOT NULL | |
| full_name | VARCHAR(255) | NOT NULL | |
| role | VARCHAR(20) | NOT NULL, CHECK IN ('admin','verkaeufer','buchhaltung') | |
| language | VARCHAR(5) | NOT NULL, DEFAULT 'de' | |
| is_active | BOOLEAN | DEFAULT true | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.2 vehicles
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| make | VARCHAR(100) | NOT NULL | yes |
| model | VARCHAR(100) | NOT NULL | yes |
| fin | VARCHAR(17) | UNIQUE, NOT NULL, CHECK(length=17) | yes |
| year | INTEGER | | |
| first_registration | DATE | | |
| power_kw | INTEGER | | |
| power_hp | INTEGER | | (computed from kW) |
| fuel_type | VARCHAR(50) | | |
| transmission | VARCHAR(20) | | |
| color | VARCHAR(50) | | |
| condition | VARCHAR(20) | CHECK IN ('new','used') | |
| location | VARCHAR(255) | | |
| availability | VARCHAR(20) | CHECK IN ('available','reserved','sold') | yes |
| price | DECIMAL(12,2) | NOT NULL | yes |
| vehicle_type | VARCHAR(20) | NOT NULL, CHECK IN ('lkw','pkw','baumaschine','stapler','transporter') | yes |
| lkw_type | VARCHAR(50) | nullable (nur LKW) | |
| machine_type | VARCHAR(50) | nullable (nur Baumaschine) | |
| body_type | VARCHAR(100) | nullable | |
| operating_hours | DECIMAL(12,1) | nullable (Baumaschine/Stapler) | |
| operating_hours_unit | VARCHAR(5) | nullable, CHECK IN ('h','min') | |
| mileage_km | INTEGER | nullable (LKW/PKW/Transporter) | |
| description | TEXT | nullable | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
| deleted_at | TIMESTAMPTZ | nullable (Soft-Delete) | yes |
**Indexes:** idx_vehicles_fin (UNIQUE), idx_vehicles_type, idx_vehicles_availability, idx_vehicles_price, idx_vehicles_deleted_at
#### 4.3 contacts
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| company_name | VARCHAR(255) | NOT NULL | yes |
| legal_form | VARCHAR(50) | nullable | |
| address_street | VARCHAR(255) | nullable | |
| address_zip | VARCHAR(10) | nullable | |
| address_city | VARCHAR(100) | nullable | |
| address_country | VARCHAR(2) | NOT NULL, DEFAULT 'DE' (ISO 3166-1 alpha-2) | yes |
| vat_id | VARCHAR(20) | nullable | |
| phone | VARCHAR(50) | nullable | |
| email | VARCHAR(255) | nullable | |
| website | VARCHAR(255) | nullable | |
| role | VARCHAR(20) | NOT NULL, CHECK IN ('kaeufer','verkaeufer','beide') | yes |
| vat_id_status | VARCHAR(20) | DEFAULT 'ungeprueft', CHECK IN ('ungeprueft','geprueft','ungueltig','manuell_bestätigt') | |
| is_private | BOOLEAN | DEFAULT false | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
| deleted_at | TIMESTAMPTZ | nullable (Soft-Delete) | yes |
#### 4.4 contact_persons
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| contact_id | UUID | FK → contacts.id, ON DELETE CASCADE | yes |
| name | VARCHAR(255) | NOT NULL | |
| function | VARCHAR(100) | nullable | |
| phone | VARCHAR(50) | nullable | |
| email | VARCHAR(255) | nullable | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.5 sales
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| vehicle_id | UUID | FK → vehicles.id, NOT NULL | yes |
| buyer_id | UUID | FK → contacts.id, NOT NULL | yes |
| seller_id | UUID | FK → contacts.id, nullable (Verkäufer = eigene Firma) | |
| sale_type | VARCHAR(20) | NOT NULL, CHECK IN ('inland','eu_ausland','drittland') | yes |
| price_net | DECIMAL(12,2) | NOT NULL | |
| price_gross | DECIMAL(12,2) | NOT NULL | |
| vat_rate | DECIMAL(5,2) | NOT NULL (0, 7, 19) | |
| payment_method | VARCHAR(20) | CHECK IN ('bar','ueberweisung','finanzierung','other') | |
| payment_date | DATE | nullable | |
| handover_date | DATE | nullable | |
| status | VARCHAR(20) | DEFAULT 'entwurf', CHECK IN ('entwurf','pruefung','abgeschlossen','storniert') | yes |
| contract_number | VARCHAR(50) | nullable, UNIQUE | yes |
| invoice_number | VARCHAR(50) | nullable, UNIQUE | yes |
| notes | TEXT | nullable | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.6 documents
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| vehicle_id | UUID | FK → vehicles.id, ON DELETE CASCADE | yes |
| sale_id | UUID | FK → sales.id, nullable, ON DELETE SET NULL | |
| filename | VARCHAR(255) | NOT NULL | |
| file_path | VARCHAR(500) | NOT NULL (relative to upload root) | |
| file_type | VARCHAR(50) | NOT NULL (MIME type) | |
| file_size | BIGINT | NOT NULL (bytes) | |
| category | VARCHAR(20) | NOT NULL, CHECK IN ('vertrag','rechnung','zb1','zb2','foto','sonstiges') | yes |
| uploaded_by | UUID | FK → users.id | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.7 mobile_de_listings
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| vehicle_id | UUID | FK → vehicles.id, UNIQUE (1 vehicle = 1 listing) | yes |
| mobile_de_id | VARCHAR(100) | nullable (mobile.de listing ID, null until listed) | |
| sync_status | VARCHAR(20) | DEFAULT 'entwurf', CHECK IN ('entwurf','gelistet','aktualisiert','fehler','entfernt') | yes |
| last_sync_at | TIMESTAMPTZ | nullable | |
| error_log | TEXT | nullable (last error message) | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.8 ai_interactions
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| user_id | UUID | FK → users.id | yes |
| interaction_type | VARCHAR(20) | CHECK IN ('copilot_text','copilot_voice','ocr_zb1','ocr_zb2','image_retouch','price_comparison') | yes |
| input_text | TEXT | nullable | |
| output_text | TEXT | nullable | |
| model_used | VARCHAR(100) | nullable | |
| tokens_used | INTEGER | nullable | |
| metadata | JSONB | nullable (structured data: extracted fields, etc.) | |
| created_at | TIMESTAMPTZ | DEFAULT now() | yes |
#### 4.9 audit_log
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| user_id | UUID | FK → users.id, nullable (system actions) | |
| action | VARCHAR(50) | NOT NULL (e.g. 'create','update','delete','login','export') | yes |
| entity_type | VARCHAR(50) | NOT NULL (e.g. 'vehicle','contact','sale') | yes |
| entity_id | UUID | nullable | |
| old_values | JSONB | nullable | |
| new_values | JSONB | nullable | |
| created_at | TIMESTAMPTZ | DEFAULT now() | yes |
#### 4.10 master_data
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| category | VARCHAR(50) | NOT NULL (e.g. 'steuerklasse','waehrung','incoterm','kraftstoffart','getriebe','farbe','land','rechtsform','fahrzeugtyp','lkw_typ','maschinenart','aufbau','zahlungsmethode') | yes (composite with key) |
| key | VARCHAR(100) | NOT NULL | |
| value_de | VARCHAR(255) | NOT NULL | |
| value_en | VARCHAR(255) | nullable | |
| sort_order | INTEGER | DEFAULT 0 | |
| is_active | BOOLEAN | DEFAULT true | |
**Unique Index:** idx_master_data_category_key (category, key) UNIQUE
#### 4.11 contract_templates
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| template_type | VARCHAR(30) | NOT NULL, CHECK IN ('kaufvertrag_inland','kaufvertrag_eu','kaufvertrag_drittland','rechnung','lieferbescheinigung') | yes |
| name | VARCHAR(255) | NOT NULL | |
| content_de | TEXT | NOT NULL (HTML/Markdown template with {{variables}}) | |
| content_en | TEXT | nullable | |
| variables | JSONB | nullable (array of variable names: ["fahrzeug","kaeufer","preis",...]) | |
| is_active | BOOLEAN | DEFAULT true | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.12 identification_data
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| sale_id | UUID | FK → sales.id, UNIQUE (1:1), ON DELETE CASCADE | yes |
| id_type | VARCHAR(30) | NOT NULL, CHECK IN ('personalausweis','reisepass','fuehrerschein','anderes') | |
| id_number_encrypted | BYTEA | NOT NULL (AES-256-GCM encrypted) | |
| issuing_country | VARCHAR(2) | NOT NULL (ISO 3166-1 alpha-2) | |
| checked_by | UUID | FK → users.id | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
**Security:** `id_number_encrypted` wird mit AES-256-GCM verschlüsselt. Key aus Environment-Variable `ENCRYPTION_KEY`. Nur Admin + Buchhaltung können entschlüsseln.
#### 4.13 ust_id_checks
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| contact_id | UUID | FK → contacts.id | yes |
| sale_id | UUID | FK → sales.id, nullable | |
| vat_id | VARCHAR(20) | NOT NULL | |
| check_result | VARCHAR(20) | NOT NULL, CHECK IN ('gueltig','ungueltig','unbekannt') | |
| check_method | VARCHAR(20) | NOT NULL, DEFAULT 'manuell', CHECK IN ('manuell','bzst_api') | |
| check_date | DATE | NOT NULL | |
| checked_by | UUID | FK → users.id | |
| created_at | TIMESTAMPTZ | DEFAULT now() | |
#### 4.14 settings
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| key | VARCHAR(100) | UNIQUE, NOT NULL | yes |
| value | JSONB | NOT NULL (flexible structure) | |
| category | VARCHAR(50) | NOT NULL (e.g. 'firmenprofil','system','mobile_de','openrouter') | yes |
| updated_by | UUID | FK → users.id | |
| updated_at | TIMESTAMPTZ | DEFAULT now() | |
**Beispiel-Keys:** `firmenprofil.name`, `firmenprofil.adresse`, `firmenprofil.ust_id`, `system.datev_berater`, `system.datev_mandant`, `mobile_de.api_key_ref`, `openrouter.api_key_ref`
#### 4.15 ust_id_status_history
| Spalte | Typ | Constraints | Index |
|---|---|---|---|
| id | UUID | PK | yes |
| contact_id | UUID | FK → contacts.id, ON DELETE CASCADE | yes |
| old_status | VARCHAR(20) | nullable | |
| new_status | VARCHAR(20) | NOT NULL | |
| changed_by | UUID | FK → users.id | |
| changed_at | TIMESTAMPTZ | DEFAULT now() | yes |
---
## 5. API-Design
### Konventionen
- **Base URL:** `/api/v1`
- **Format:** JSON (request + response)
- **Auth:** `Authorization: Bearer <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
```json
{
"sub": "user-uuid",
"role": "admin|verkaeufer|buchhaltung",
"email": "user@example.com",
"lang": "de",
"exp": 1234567890,
"iat": 1234567890
}
```
### Rollen-Berechtigungs-Matrix (Endpoint-Level)
Implementiert via `Depends(require_role(["admin", "verkaeufer"]))` Decorator.
| Endpoint-Gruppe | Admin | Verkäufer | Buchhaltung |
|---|---|---|---|
| /auth/* | ✅ | ✅ | ✅ |
| /users (CRUD) | ✅ | ❌ | ❌ |
| /vehicles (CRUD) | ✅ RW | ✅ RW | 📖 R |
| /vehicles/ocr/* | ✅ | ✅ | ❌ |
| /vehicles/:id/documents | ✅ RW | ✅ RW | 📖 R |
| /mobile-de/* | ✅ | ✅ | ❌ |
| /contacts (CRUD) | ✅ RW | ✅ RW | 📖 R |
| /sales (CRUD) | ✅ RW | ✅ RW | 📖 R |
| /sales/:id/contract | ✅ | ✅ | ❌ |
| /sales/:id/invoice | ✅ | ❌ | ✅ |
| /sales/:id/ust-id-check | ✅ | ❌ | ✅ |
| /sales/:id/identification | ✅ | ❌ | ✅ |
| /sales/datev-export | ✅ | ❌ | ✅ |
| /ai/copilot/* | ✅ | ✅ | 📖 |
| /ai/image/* | ✅ | ✅ | ❌ |
| /vehicles/:id/price-comparison | ✅ | ✅ | 📖 |
| /settings/* | ✅ | ❌ | ❌ |
| /audit-log | ✅ | ❌ | ❌ |
---
## 7. Dateiablage-Konzept
### Architektur
```
Backend Container
├── /data/uploads/ ← Coolify Volume Mount (persistent)
│ ├── vehicles/
│ │ ├── {vehicle_id}/
│ │ │ ├── {doc_id}_{filename} ← Original-Datei
│ │ │ └── {doc_id}_thumb.jpg ← Thumbnail (Bilder)
│ └── contracts/
│ ├── {sale_id}_vertrag.pdf
│ └── {sale_id}_rechnung.pdf
```
### Upload-Flow
1. Client: multipart/form-data → POST /vehicles/:id/documents
2. Backend: MIME-Type-Check (allowlist), Size-Check (≤50MB)
3. Backend: Datei speichern unter `/data/uploads/vehicles/{vehicle_id}/{doc_id}_{filename}`
4. Backend: Thumbnail generieren (für Bilder, Pillow)
5. Backend: documents-Row in DB anlegen
6. Response: Document-Metadata
### File-Preview
- **Bilder:** Backend generiert Thumbnail beim Upload. Frontend zeigt Thumbnail + Lightbox.
- **PDFs:** Frontend nutzt `<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)
```python
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)
```python
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)
```python
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
```python
# 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
```yaml
# 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