1157 lines
47 KiB
Markdown
1157 lines
47 KiB
Markdown
|
|
# 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
|