Files
erp-nutzfahrzeuge/docs/architecture.md
T

1157 lines
47 KiB
Markdown
Raw Normal View 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
/* 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