commit afe6d0a168abc8be2556d282c4978dab00d61e62 Author: Leopoldadmin Date: Mon Jul 13 03:34:35 2026 +0200 feat: initial architecture - requirements, architecture.md, task_graph, AGENTS.md, UI prototype v8d diff --git a/.a0/UI-Prototyp_state.md b/.a0/UI-Prototyp_state.md new file mode 100644 index 0000000..54a0fba --- /dev/null +++ b/.a0/UI-Prototyp_state.md @@ -0,0 +1,21 @@ +# UI-Prototyp State - ERP Nutzfahrzeuge + +## Version: 4 (v4) +## Datum: 2026-07-11 +## Status: Vierte Iteration - wartet auf User-Feedback + +## Prototyp-URL +https://webspace.media-on.de/erp-ui-v1/index.html?v=4 + +## v4 Aenderungen (User-Feedback) +1. **Fahrzeug-Formular in Tabs/Reiter** - 11 Tabs: Allgemein, Technische Daten, Abmessungen, PKW/LKW/Baumaschine/Stapler (conditional), Preise & Kosten, Bilder, Dokumente, mobile.de +2. **Felder erweitert** - HSN/TSN, EG-Typgenehmigung, Vorbesitzer, Unfallschaeden, TUEV/AU, Service-Intervall, Abmessungen (Laenge/Breite/Hoehe), Gewichte (Leergewicht/Gesamtgewicht), Anhaengelast, Kuppelast, Bordgeraet, Hubwerk, Ladebordwand, Klimaanlage, Schlafplatz, Hydraulik, ZB I/ZB II Feld-Referenzen +3. **Bild-Upload per Datei-Auswahl** - File input button neben Drag&Drop Zone +4. **Dashboard konfigurierbar** - Config-Button oeffnet Modal mit Toggle pro Widget (KPIs, Schnellzugriffe, Aktivitaeten, Tasks, Top-Fahrzeuge), gespeichert in localStorage + +## Verifiziert (Screenshots) +- Dashboard v4 mit Config-Button: ✅ +- Fahrzeug-Formular v4 mit Tabs: ✅ + +## Nächste Schritte +- User-Feedback zu v4 abwarten diff --git a/.a0/task_graph.json b/.a0/task_graph.json new file mode 100644 index 0000000..dcbcc9a --- /dev/null +++ b/.a0/task_graph.json @@ -0,0 +1,511 @@ +{ + "project": "erp-nutzfahrzeuge", + "version": "1.0", + "created": "2026-07-12", + "status": "approved_for_implementation", + "total_tasks": 8, + "tasks": [ + { + "id": "T01", + "title": "Auth + User Management + RBAC + Base Frontend Layout + i18n Setup", + "category": "foundation", + "description": "Komplettes Auth-System: JWT login/refresh, User CRUD (admin only), RBAC Middleware (admin/verkaeufer/buchhaltung). Backend: User Model, auth_service, auth router, users router, JWT utils, RBAC dependency. Frontend: Root layout, Login page, Auth context/hooks, i18n setup (next-intl oder react-i18next), de.json + en.json Basistranslationen, API client (fetch wrapper mit Auth-Header), Base UI components (Button, Input, Card, Table, Modal, Toast). Health endpoint /api/health. Config via Pydantic BaseSettings (DB URL, Redis URL, JWT Secret, OpenRouter Key, mobile.de credentials).", + "assigned_subagent": "implementation_engineer", + "dependencies": [], + "requirement_ids": ["M6-F1", "M6-F2", "AUTH-1", "AUTH-2", "AUTH-3", "AUTH-4", "I18N-1", "I18N-2"], + "estimated_effort": "L", + "estimated_lines": 600, + "acceptance_criteria": [ + "POST /api/auth/login mit valid credentials → 200 + JWT access_token + refresh_token", + "POST /api/auth/login mit invalid credentials → 401", + "POST /api/auth/refresh mit valid refresh_token → 200 + new access_token", + "GET /api/auth/me mit valid JWT → 200 + user object (email, full_name, role)", + "GET /api/users ohne admin role → 403", + "GET /api/users mit admin role → 200 + paginated user list", + "POST /api/users mit valid data → 201 + user object", + "DELETE /api/users/:id → 200 + user.is_active=false", + "GET /api/health → 200 + {status: 'ok'}", + "Frontend Login page rendert und sendet POST /api/auth/login", + "Frontend zeigt Toast bei login error", + "i18n: de.json und en.json existieren mit mindestens 20 keys", + "Sprachumschaltung DE/EN wechselt UI-Texte live", + "RBAC: /api/users blocked für verkaeufer role → 403", + "pytest coverage >= 80% für auth module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_auth.py tests/test_users.py --cov=app/services/auth_service --cov=app/routers/auth --cov=app/routers/users --cov-report=term-missing -v", + "cd backend && python -m pytest tests/test_health.py -v", + "cd frontend && npx vitest run tests/auth.test.tsx tests/i18n.test.tsx --coverage" + ], + "expected_results": "All auth tests pass (login, refresh, RBAC, user CRUD). Health endpoint returns 200. Frontend login form renders and submits. i18n loads DE/EN translations. Coverage >= 80% backend auth, >= 70% frontend auth.", + "test_files": [ + "backend/tests/test_auth.py", + "backend/tests/test_users.py", + "backend/tests/test_health.py", + "backend/tests/conftest.py", + "frontend/tests/auth.test.tsx", + "frontend/tests/i18n.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/main.py", + "backend/app/config.py", + "backend/app/database.py", + "backend/app/dependencies.py", + "backend/app/models/user.py", + "backend/app/schemas/user.py", + "backend/app/routers/auth.py", + "backend/app/routers/users.py", + "backend/app/services/auth_service.py", + "backend/app/utils/jwt.py", + "backend/app/utils/i18n.py", + "backend/tests/test_auth.py", + "backend/tests/test_users.py", + "backend/tests/test_health.py", + "backend/tests/conftest.py", + "frontend/app/layout.tsx", + "frontend/app/(auth)/login/page.tsx", + "frontend/lib/api.ts", + "frontend/lib/auth.ts", + "frontend/lib/i18n.ts", + "frontend/messages/de.json", + "frontend/messages/en.json", + "frontend/components/ui/Button.tsx", + "frontend/components/ui/Input.tsx", + "frontend/components/ui/Card.tsx", + "frontend/components/ui/Table.tsx", + "frontend/components/ui/Modal.tsx", + "frontend/components/ui/Toast.tsx", + "frontend/tests/auth.test.tsx", + "frontend/tests/i18n.test.tsx" + ] + }, + { + "id": "T02", + "title": "Vehicle Management + mobile.de Push + Vehicle UI", + "category": "vehicle", + "description": "Komplettes Fahrzeug-Modul: Vehicle Model (LKW, Baumaschine, PKW, Stapler, Transporter), vehicle_service mit CRUD + filter/sort/paginate, vehicles router mit allen Endpoints. mobile.de Seller API Integration (Push only): mobilede_service, mobilede_push background task, Ad-Format Mapping, Retry-Queue via Redis. Frontend: Vehicle List page mit Filter (type, status, brand) + Pagination + Sort, Vehicle Detail page, Vehicle Create/Edit form, mobile.de Push Button + Status indicator. Soft-Delete Implementierung (status='deleted').", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01"], + "requirement_ids": ["M1-F1", "M1-F2", "M1-F3", "M1-F4", "M1-F5", "M1-F6", "M1-F7"], + "estimated_effort": "L", + "estimated_lines": 700, + "acceptance_criteria": [ + "GET /api/vehicles → 200 + paginated list (page, per_page, total)", + "GET /api/vehicles?type=lkw&status=in_stock → 200 + filtered list", + "GET /api/vehicles?sort=-created_at → 200 + sorted list", + "GET /api/vehicles/:id → 200 + vehicle detail object", + "GET /api/vehicles/:nonexistent → 404", + "POST /api/vehicles mit valid data → 201 + vehicle object", + "POST /api/vehicles mit missing brand → 422 validation error", + "PUT /api/vehicles/:id → 200 + updated vehicle", + "DELETE /api/vehicles/:id → 200 + status='deleted' (soft delete)", + "POST /api/vehicles/:id/mobilede-push → 202 (async push queued)", + "GET /api/vehicles/:id/mobilede-status → 200 + {synced: bool, ad_id, synced_at}", + "mobile.de Push sendet korrektes Ad-Format an Seller API", + "mobile.de Push Fehler → Retry-Queue (Redis), max 3 retries", + "Frontend Vehicle List zeigt Tabelle mit Filter + Pagination", + "Frontend Vehicle Detail zeigt alle Felder + mobile.de Status", + "Frontend Vehicle Create Form validiert Pflichtfelder", + "Frontend mobile.de Push Button → zeigt Sync-Status", + "pytest coverage >= 80% für vehicle module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_vehicles.py tests/test_mobilede.py --cov=app/services/vehicle_service --cov=app/services/mobilede_service --cov=app/routers/vehicles --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/vehicles.test.tsx --coverage" + ], + "expected_results": "All vehicle CRUD tests pass. Filter, sort, pagination work. mobile.de push queues async task. Soft delete sets status=deleted. Frontend list renders with filters. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_vehicles.py", + "backend/tests/test_mobilede.py", + "frontend/tests/vehicles.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/vehicle.py", + "backend/app/schemas/vehicle.py", + "backend/app/routers/vehicles.py", + "backend/app/services/vehicle_service.py", + "backend/app/services/mobilede_service.py", + "backend/app/tasks/mobilede_push.py", + "backend/app/utils/mobilede_mapping.py", + "backend/tests/test_vehicles.py", + "backend/tests/test_mobilede.py", + "frontend/app/vehicles/page.tsx", + "frontend/app/vehicles/[id]/page.tsx", + "frontend/app/vehicles/new/page.tsx", + "frontend/components/vehicles/VehicleList.tsx", + "frontend/components/vehicles/VehicleForm.tsx", + "frontend/components/vehicles/VehicleDetail.tsx", + "frontend/components/vehicles/MobileDeStatus.tsx", + "frontend/tests/vehicles.test.tsx" + ] + }, + { + "id": "T03", + "title": "OCR-Erfassung (ZB I/II) via OpenRouter Vision + OCR UI", + "category": "ocr", + "description": "Komplettes OCR-Modul: OCRResult Model, ocr_service mit OpenRouter Qwen2.5-VL Integration, OCR router (upload, get results, apply to vehicle). Async OCR processing via Redis Queue. Prompt-Engineering für ZB I/II: structured JSON output (brand, model, vin, first_registration, mileage, power_kw, fuel_type). Confidence-Score Berechnung. Manual Review bei confidence < 0.7. Response-Caching für identische Scans. Frontend: OCR Upload page (drag-and-drop), OCR Results view mit side-by-side Original Scan + Extracted Data, Apply-to-Vehicle Button.", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01", "T02"], + "requirement_ids": ["M2-F1", "M2-F2", "M2-F3", "M2-F4", "M2-F5"], + "estimated_effort": "L", + "estimated_lines": 600, + "acceptance_criteria": [ + "POST /api/ocr/upload mit image file → 202 + ocr_result_id (async processing)", + "POST /api/ocr/upload ohne file → 422", + "POST /api/ocr/upload mit invalid MIME type → 422", + "GET /api/ocr/results/:id → 200 + {status, raw_text, structured_data, confidence_score}", + "GET /api/ocr/results?vehicle_id=X → 200 + list of OCR results for vehicle", + "POST /api/ocr/results/:id/apply → 200 + vehicle updated with OCR data", + "OCR processing mit mocked OpenRouter → structured_data enthält brand, model, vin", + "Confidence score < 0.7 → status='pending' (manual review needed)", + "Confidence score >= 0.7 → status='completed'", + "OCR Fehler (OpenRouter unavailable) → status='failed' + error message", + "Frontend OCR Upload akzeptiert Drag-and-Drop", + "Frontend OCR Results zeigt Original + Extracted Data side-by-side", + "Frontend Apply Button → sendet POST /api/ocr/results/:id/apply", + "pytest coverage >= 80% für ocr module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_ocr.py --cov=app/services/ocr_service --cov=app/routers/ocr --cov=app/utils/openrouter --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/ocr.test.tsx --coverage" + ], + "expected_results": "All OCR tests pass with mocked OpenRouter. Upload queues async task. Results retrievable. Apply updates vehicle. Confidence threshold works. Frontend upload + results render. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_ocr.py", + "frontend/tests/ocr.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/ocr_result.py", + "backend/app/schemas/ocr.py", + "backend/app/routers/ocr.py", + "backend/app/services/ocr_service.py", + "backend/app/tasks/ocr_processing.py", + "backend/app/utils/openrouter.py", + "backend/tests/test_ocr.py", + "frontend/app/ocr/page.tsx", + "frontend/components/ocr/OCRUpload.tsx", + "frontend/components/ocr/OCRResults.tsx", + "frontend/components/ocr/OCRDetail.tsx", + "frontend/tests/ocr.test.tsx" + ] + }, + { + "id": "T04", + "title": "Kontakt-/Kundenverwaltung + Contact UI", + "category": "contact", + "description": "Komplettes Kontakt-Modul: Contact Model (Kunde/Lieferant/Beide, EU/Inland, USt-IdNr.), contact_service mit CRUD + search + filter (type, eu/inland), contacts router. USt-IdNr.-Feld mit Format-Validierung (DE + EU Formate). Contact-Search (name, company, city, email). Frontend: Contact List mit Search + Filter, Contact Detail page, Contact Create/Edit form. EU/Inland Toggle. USt-IdNr. input mit Format-Validierung frontend-seitig.", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01"], + "requirement_ids": ["M3-F1", "M3-F2", "M3-F3", "M3-F4", "M3-F5", "M3-F6"], + "estimated_effort": "M", + "estimated_lines": 500, + "acceptance_criteria": [ + "GET /api/contacts → 200 + paginated list", + "GET /api/contacts?type=kunde → 200 + filtered list", + "GET /api/contacts?is_eu=true → 200 + EU contacts only", + "GET /api/contacts?search=mueller → 200 + matching contacts", + "GET /api/contacts/:id → 200 + contact detail", + "GET /api/contacts/:nonexistent → 404", + "POST /api/contacts mit valid data → 201 + contact object", + "POST /api/contacts mit invalid ust_id_nr format → 422", + "PUT /api/contacts/:id → 200 + updated contact", + "DELETE /api/contacts/:id → 200 (or 404 if not found)", + "Contact mit contact_type='beide' erscheint in kunde und lieferant filter", + "Frontend Contact List zeigt Tabelle mit Search + Filter", + "Frontend Contact Form validiert USt-IdNr. Format", + "Frontend EU/Inland Toggle ändert Pflichtfelder", + "pytest coverage >= 80% für contact module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_contacts.py --cov=app/services/contact_service --cov=app/routers/contacts --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/contacts.test.tsx --coverage" + ], + "expected_results": "All contact CRUD tests pass. Search, filter, pagination work. USt-IdNr. validation rejects invalid formats. Frontend list renders with search/filter. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_contacts.py", + "frontend/tests/contacts.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/contact.py", + "backend/app/schemas/contact.py", + "backend/app/routers/contacts.py", + "backend/app/services/contact_service.py", + "backend/app/utils/ust_validation.py", + "backend/tests/test_contacts.py", + "frontend/app/contacts/page.tsx", + "frontend/app/contacts/[id]/page.tsx", + "frontend/app/contacts/new/page.tsx", + "frontend/components/contacts/ContactList.tsx", + "frontend/components/contacts/ContactForm.tsx", + "frontend/components/contacts/ContactDetail.tsx", + "frontend/tests/contacts.test.tsx" + ] + }, + { + "id": "T05", + "title": "Dateiablage pro Fahrzeug + File UI", + "category": "file", + "description": "Komplettes Dateiablage-Modul: File Model, file_service mit Upload/Download/Delete, files router. MIME-Type-Validierung (images: jpg/png/webp, documents: pdf/doc/docx, scans: jpg/png). Max 20MB pro Datei. Dateien gespeichert im uploads_data Volume. Thumbnail-Generierung für Images. Frontend: File Upload Component (Drag-and-Drop, multi-file), File List mit Thumbnails, File Download, File Delete mit Confirm. Gallery view für Fahrzeug-Bilder.", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01", "T02"], + "requirement_ids": ["M4-F1", "M4-F2", "M4-F3", "M4-F4", "M4-F5"], + "estimated_effort": "M", + "estimated_lines": 500, + "acceptance_criteria": [ + "GET /api/vehicles/:id/files → 200 + list of files", + "POST /api/vehicles/:id/files mit multipart image → 201 + file object", + "POST /api/vehicles/:id/files mit >20MB file → 413", + "POST /api/vehicles/:id/files mit invalid MIME type → 422", + "GET /api/vehicles/:id/files/:fileId → 200 + file content (download)", + "DELETE /api/vehicles/:id/files/:fileId → 200 + file deleted", + "File gespeichert in uploads_data volume, path in DB gespeichert", + "Image files → Thumbnail generiert (200x200)", + "Frontend File Upload akzeptiert Drag-and-Drop multi-file", + "Frontend File List zeigt Thumbnails für images", + "Frontend File Delete zeigt Confirm Dialog", + "Frontend Gallery view für Fahrzeug-Bilder", + "pytest coverage >= 80% für file module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_files.py --cov=app/services/file_service --cov=app/routers/files --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/files.test.tsx --coverage" + ], + "expected_results": "All file CRUD tests pass. MIME validation rejects invalid types. Size limit enforced. Download returns file content. Thumbnails generated for images. Frontend upload + list + gallery render. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_files.py", + "frontend/tests/files.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/file.py", + "backend/app/schemas/file.py", + "backend/app/routers/files.py", + "backend/app/services/file_service.py", + "backend/app/utils/thumbnails.py", + "backend/tests/test_files.py", + "frontend/components/files/FileUpload.tsx", + "frontend/components/files/FileList.tsx", + "frontend/components/files/FileGallery.tsx", + "frontend/components/files/FilePreview.tsx", + "frontend/tests/files.test.tsx" + ] + }, + { + "id": "T06", + "title": "Verkaufsmodul + Rechtsdokumente + DATEV-Export + Sales UI", + "category": "sales", + "description": "Komplettes Verkaufsmodul: Sale Model, sale_service mit CRUD, sales router. Contract PDF Generierung (Verkaufvertrag mit Vehicle + Contact Daten, GwG-Klausel, USt-IdNr. Feld). DATEV-Export: datev_service, DATEV CSV Format (Buchungsstapel: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext). GwG-Logik: Sofortabzug bei <= 800EUR (§6 Abs. 2 EStG). USt-IdNr.-Prüfung: BZSt API endpoint (Feature-Flag bzst_api_enabled, aktuell disabled → manual review). DATEVExport Model + router. Frontend: Sales List, Sale Detail mit Contract Preview, Sale Create form (Vehicle select, Contact select, Price, GwG toggle), DATEV Export page (Zeitraum wählen, Download CSV).", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T02", "T04"], + "requirement_ids": ["M5-F1", "M5-F2", "M5-F3", "M5-F4", "M5-F5", "M5-F6", "M5-F7", "M5-F8", "M5-F9"], + "estimated_effort": "XL", + "estimated_lines": 800, + "acceptance_criteria": [ + "GET /api/sales → 200 + paginated list with filter (date range, status)", + "GET /api/sales/:id → 200 + sale detail with vehicle + contact nested", + "POST /api/sales mit valid data → 201 + sale object + contract_pdf_path", + "POST /api/sales ohne vehicle_id → 422", + "POST /api/sales ohne buyer_contact_id → 422", + "PUT /api/sales/:id → 200 + updated sale", + "DELETE /api/sales/:id → 200 (sale cancelled, vehicle status back to in_stock)", + "POST /api/sales/:id/contract → 200 + regenerated contract PDF path", + "GET /api/sales/:id/contract → 200 + PDF content (application/pdf)", + "Sale mit is_gwg=true und sale_price <= 800 → GwG-Klausel in contract", + "POST /api/sales/:id/verify-ust-id → 200 + {verified: false, message: 'BZSt API not available'} (feature flag disabled)", + "POST /api/datev/export mit date range → 201 + DATEVExport object", + "POST /api/datev/export mit invalid date range → 422", + "GET /api/datev/exports → 200 + list of exports", + "GET /api/datev/exports/:id/download → 200 + CSV content (text/csv)", + "DATEV CSV enthält korrekte Felder: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext", + "Sale creation → Vehicle status='sold'", + "Sale deletion → Vehicle status='in_stock'", + "Frontend Sales List zeigt Tabelle mit Date Filter", + "Frontend Sale Detail zeigt Contract Preview (PDF embed)", + "Frontend Sale Create Form mit Vehicle + Contact Selectors", + "Frontend DATEV Export page mit Date Range Picker + Download", + "pytest coverage >= 80% für sales + datev module" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_sales.py tests/test_datev.py --cov=app/services/sale_service --cov=app/services/datev_service --cov=app/routers/sales --cov=app/routers/datev --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/sales.test.tsx tests/datev.test.tsx --coverage" + ], + "expected_results": "All sales CRUD tests pass. Contract PDF generated. DATEV CSV export with correct format. GwG logic applied. Vehicle status changes on sale create/delete. USt-IdNr. endpoint returns feature-flag message. Frontend list + detail + form + DATEV export render. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_sales.py", + "backend/tests/test_datev.py", + "frontend/tests/sales.test.tsx", + "frontend/tests/datev.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/sale.py", + "backend/app/models/datev_export.py", + "backend/app/schemas/sale.py", + "backend/app/schemas/datev.py", + "backend/app/routers/sales.py", + "backend/app/routers/datev.py", + "backend/app/services/sale_service.py", + "backend/app/services/datev_service.py", + "backend/app/utils/contract_pdf.py", + "backend/app/utils/datev.py", + "backend/tests/test_sales.py", + "backend/tests/test_datev.py", + "frontend/app/sales/page.tsx", + "frontend/app/sales/[id]/page.tsx", + "frontend/app/sales/new/page.tsx", + "frontend/components/sales/SaleList.tsx", + "frontend/components/sales/SaleForm.tsx", + "frontend/components/sales/ContractPreview.tsx", + "frontend/components/sales/DatevExport.tsx", + "frontend/tests/sales.test.tsx", + "frontend/tests/datev.test.tsx" + ] + }, + { + "id": "T07", + "title": "KI-Copilot (Text + Sprache) + Systemsteuerung + Copilot UI", + "category": "copilot", + "description": "Komplettes KI-Copilot-Modul: Copilot chat_service mit OpenRouter (Claude/GPT-4), copilot router (chat, voice, history, action). System-Prompt mit ERP-Kontext (verfügbare Aktionen: vehicle search/create, contact search, sale overview). Action-System: Copilot kann strukturierte Actions ausführen (GET /api/vehicles, POST /api/contacts, etc.) via function-calling. Voice Input: STT via Browser Web Speech API → text → chat. Chat History (paginated, pro User). Frontend: Copilot Chat Interface (message list, input, send), Voice Input Button (microphone), Action Preview (zeigt was Copilot tun will vor Ausführung), Chat History sidebar.", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01", "T02"], + "requirement_ids": ["M7-F1", "M7-F2", "M7-F3", "M7-F4", "M7-F5", "M7-F6", "M7-F7"], + "estimated_effort": "L", + "estimated_lines": 700, + "acceptance_criteria": [ + "POST /api/copilot/chat mit {message: 'Zeige alle LKWs'} → 200 + {response: '...', actions: [{type: 'search_vehicles', params: {type: 'lkw'}}]}", + "POST /api/copilot/chat mit empty message → 422", + "POST /api/copilot/action mit {action: 'search_vehicles', params: {type: 'lkw'}} → 200 + {result: [...vehicles...]}", + "POST /api/copilot/action mit invalid action → 400", + "GET /api/copilot/history → 200 + paginated chat history for current user", + "POST /api/copilot/voice → 200 (accepts audio, transcribes, returns chat response)", + "Copilot System-Prompt enthält ERP-Kontext und verfügbare Actions", + "Copilot kann vehicle search action auslösen (mocked OpenRouter)", + "Copilot kann contact search action auslösen (mocked OpenRouter)", + "Copilot response enthält action proposal (nicht direkt ausgeführt, User bestätigt)", + "Frontend Copilot Chat rendert message list + input", + "Frontend Voice Button startet Web Speech API", + "Frontend Action Preview zeigt geplante Aktion vor Bestätigung", + "Frontend Chat History sidebar zeigt vergangene Konversationen", + "pytest coverage >= 80% für copilot module (mit mocked OpenRouter)" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_copilot.py --cov=app/services/copilot_service --cov=app/routers/copilot --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/copilot.test.tsx --coverage" + ], + "expected_results": "All copilot tests pass with mocked OpenRouter. Chat returns response + action proposals. Action endpoint executes approved actions. History paginated. Voice endpoint accepts audio. Frontend chat + voice + action preview render. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_copilot.py", + "frontend/tests/copilot.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/copilot.py", + "backend/app/schemas/copilot.py", + "backend/app/routers/copilot.py", + "backend/app/services/copilot_service.py", + "backend/app/utils/copilot_actions.py", + "backend/app/utils/copilot_prompt.py", + "backend/tests/test_copilot.py", + "frontend/app/copilot/page.tsx", + "frontend/components/copilot/ChatInterface.tsx", + "frontend/components/copilot/MessageList.tsx", + "frontend/components/copilot/ChatInput.tsx", + "frontend/components/copilot/VoiceInput.tsx", + "frontend/components/copilot/ActionPreview.tsx", + "frontend/components/copilot/ChatHistory.tsx", + "frontend/tests/copilot.test.tsx" + ] + }, + { + "id": "T08", + "title": "Bildretusche + Preisvergleich (Flux.1-Pro + mobile.de) + Retouch UI", + "category": "retouch", + "description": "Komplettes Bildretusche-Modul: Retouch service mit OpenRouter Flux.1-Pro, retouch router (process, get result, price-compare). Bild-Verarbeitung: Upload → Flux.1-Pro Retusche (Hintergrund entfernen/verbessern, Spiegelungen, Farbkorrektur) → retouched image gespeichert. Preisvergleich: mobile.de Listings Scraper/API → comparable vehicles (brand, model, year, price) → Durchschnittspreis berechnet. Frontend: Retouch Upload page (Bild wählen, Retusche starten), Before/After Vergleich (slider), Preisvergleich view (Tabelle mit comparable listings + Durchschnittspreis).", + "assigned_subagent": "implementation_engineer", + "dependencies": ["T01", "T02", "T05"], + "requirement_ids": ["M8-F1", "M8-F2", "M8-F3", "M8-F4", "M8-F5"], + "estimated_effort": "L", + "estimated_lines": 600, + "acceptance_criteria": [ + "POST /api/retouch/process mit image file → 202 + {retouch_id} (async processing)", + "POST /api/retouch/process ohne image → 422", + "POST /api/retouch/process mit invalid MIME → 422", + "GET /api/retouch/results/:id → 200 + {status, original_url, retouched_url}", + "GET /api/retouch/results/:id vor completion → 200 + {status: 'processing'}", + "Retouch Fehler (OpenRouter unavailable) → 200 + {status: 'failed', error: '...'}", + "POST /api/retouch/price-compare mit {vehicle_id} → 200 + {comparable_listings: [...], average_price: X}", + "POST /api/retouch/price-compare ohne vehicle_id → 422", + "Price-compare mit no comparable listings → 200 + {comparable_listings: [], average_price: null}", + "Frontend Retouch Upload → Bild auswählen + Retusche starten", + "Frontend Before/After Slider → vergleicht Original vs Retuschiert", + "Frontend Preisvergleich Tabelle → zeigt comparable listings + Durchschnittspreis", + "pytest coverage >= 80% für retouch module (mit mocked OpenRouter)" + ], + "test_spec": { + "commands": [ + "cd backend && python -m pytest tests/test_retouch.py --cov=app/services/retouch_service --cov=app/routers/image_retouch --cov-report=term-missing -v", + "cd frontend && npx vitest run tests/retouch.test.tsx --coverage" + ], + "expected_results": "All retouch tests pass with mocked OpenRouter. Process queues async task. Results retrievable with status. Price-compare returns listings + average. Frontend upload + before/after + price comparison render. Coverage >= 80% backend, >= 70% frontend.", + "test_files": [ + "backend/tests/test_retouch.py", + "frontend/tests/retouch.test.tsx" + ], + "coverage_target": 80 + }, + "files_to_create": [ + "backend/app/models/retouch.py", + "backend/app/schemas/retouch.py", + "backend/app/routers/image_retouch.py", + "backend/app/services/retouch_service.py", + "backend/app/services/price_compare_service.py", + "backend/app/tasks/retouch_processing.py", + "backend/tests/test_retouch.py", + "frontend/app/retouch/page.tsx", + "frontend/components/retouch/RetouchUpload.tsx", + "frontend/components/retouch/BeforeAfterSlider.tsx", + "frontend/components/retouch/PriceComparison.tsx", + "frontend/tests/retouch.test.tsx" + ] + } + ], + "dependency_graph": { + "T01": [], + "T02": ["T01"], + "T03": ["T01", "T02"], + "T04": ["T01"], + "T05": ["T01", "T02"], + "T06": ["T02", "T04"], + "T07": ["T01", "T02"], + "T08": ["T01", "T02", "T05"] + }, + "execution_order": ["T01", "T02", "T04", "T03", "T05", "T06", "T07", "T08"], + "summary": { + "total_tasks": 8, + "tasks_with_test_spec": 8, + "tasks_with_acceptance_criteria": 8, + "tasks_with_dependencies": 8, + "total_requirement_ids": 45, + "total_acceptance_criteria": 115, + "total_files_to_create": 120, + "estimated_total_lines": 5000 + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8aaab1a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,490 @@ +# AGENTS.md – ERP Nutzfahrzeuge + +> Build & Test Commands, Test Rules, Conventions für Implementierung + +--- + +## 1. Build & Test Commands + +### 1.1 Backend (Python/FastAPI) + +```bash +# Dependencies installieren +cd backend +pip install -r requirements.txt + +# DB Migration (Alembic) +alembic upgrade head + +# Dev Server starten +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Tests ausführen (alle) +pytest --cov=app --cov-report=term-missing -v + +# Tests für spezifisches Modul +pytest tests/test_vehicles.py --cov=app/services/vehicle_service --cov=app/routers/vehicles --cov-report=term-missing -v + +# Type checking +mypy app/ --ignore-missing-imports + +# Linting +ruff check app/ + +# Format check +black --check app/ +``` + +### 1.2 Frontend (Next.js) + +```bash +# Dependencies installieren +cd frontend +npm install + +# Dev Server starten +npm run dev + +# Build (production) +npm run build + +# Tests ausführen (Unit) +npx vitest run --coverage + +# E2E Tests +npx playwright test + +# Type checking +npx tsc --noEmit + +# Linting +npm run lint +``` + +### 1.3 Docker (Full Stack) + +```bash +# Alle Container starten +docker compose up -d + +# Logs anzeigen +docker compose logs -f backend +docker compose logs -f frontend + +# Rebuild +docker compose up -d --build + +# Stop +docker compose down + +# Test environment +docker compose -f docker-compose.test.yml up --abort-on-container-exit +``` + +### 1.4 Database + +```bash +# PostgreSQL Shell +docker compose exec postgres psql -U erp_user -d erp_db + +# Backup erstellen +docker compose exec postgres pg_dump -U erp_user erp_db > backup.sql + +# Restore +docker compose exec -T postgres psql -U erp_user -d erp_db < backup.sql + +# Migration erstellen +cd backend +alembic revision --autogenerate -m "description" +alembic upgrade head +``` + +--- + +## 2. Test Rules (MANDATORY) + +### 2.1 TDD – Test Driven Development +- **Tests werden NICHT modifiziert** – Tests sind die Spec, Code muss sich anpassen +- **Kein 'done' ohne Test-Evidence** – Build, Test, Smoke-Test müssen durchlaufen +- **Coverage Target**: ≥ 80% Backend, ≥ 70% Frontend +- **Test-First**: Tests schreiben → Code implementieren → Tests grün +- **Kein Skip**: `pytest.skip` oder `it.skip` nur mit Begründung als Kommentar + +### 2.2 Test-Struktur Backend + +``` +tests/ +├── conftest.py # Fixtures: test client, DB session, mock OpenRouter +├── test_auth.py # Auth + JWT + RBAC tests +├── test_users.py # User CRUD tests +├── test_health.py # Health endpoint test +├── test_vehicles.py # Vehicle CRUD + filter + pagination tests +├── test_mobilede.py # mobile.de push + retry queue tests +├── test_ocr.py # OCR upload + processing + apply tests +├── test_contacts.py # Contact CRUD + search + USt-IdNr. validation tests +├── test_files.py # File upload + download + MIME validation tests +├── test_sales.py # Sale CRUD + contract PDF + GwG tests +├── test_datev.py # DATEV export + CSV format tests +├── test_copilot.py # Copilot chat + action + history tests +└── test_retouch.py # Retouch + price comparison tests +``` + +### 2.3 Test-Struktur Frontend + +``` +tests/ +├── auth.test.tsx # Login form, auth context, token refresh +├── i18n.test.tsx # Translation loading, language switch +├── vehicles.test.tsx # Vehicle list, form, detail, mobile.de status +├── ocr.test.tsx # OCR upload, results, apply +├── contacts.test.tsx # Contact list, form, USt-IdNr. validation +├── files.test.tsx # File upload, list, gallery +├── sales.test.tsx # Sale list, form, contract preview +├── datev.test.tsx # DATEV export, download +├── copilot.test.tsx # Chat interface, voice input, action preview +└── retouch.test.tsx # Retouch upload, before/after, price comparison +``` + +### 2.4 Test-Fixtures (conftest.py) + +```python +# Pflicht-Fixtures in conftest.py: +# - test_client: httpx.AsyncClient mit Test-App +# - db_session: async SQLAlchemy Session mit rollback +# - mock_openrouter: Mock für OpenRouter API (OCR, Copilot, Retouch) +# - mock_mobilede: Mock für mobile.de Seller API +# - auth_headers: JWT headers für admin/verkaeufer/buchhaltung roles +# - test_vehicle: Pre-created vehicle for tests +# - test_contact: Pre-created contact for tests +``` + +### 2.5 Mocking-Regeln +- **OpenRouter API**: IMMER mocken in Tests (kein realer API-Call) +- **mobile.de API**: IMMER mocken in Tests +- **BZSt API**: IMMER mocken (Feature-Flag disabled in tests) +- **Redis**: Test mit fakeredis oder in-memory mock +- **PostgreSQL**: Test-DB mit SQLite (async) oder testcontainers +- **File Storage**: Temp-Verzeichnis (tmp_path fixture) + +--- + +## 3. Code-Konventionen + +### 3.1 Backend + +#### Python Style +- **Formatter**: black (line-length=100) +- **Linter**: ruff +- **Type Checker**: mypy (strict für services) +- **Import Order**: stdlib → third-party → local (isort) + +#### FastAPI Patterns +```python +# Router-Struktur (Pflicht): +router = APIRouter(prefix="/api/vehicles", tags=["vehicles"]) + +# Async für alle DB-Operationen +async def get_vehicle(vehicle_id: UUID, db: AsyncSession) -> Vehicle: + ... + +# Pydantic Schema für Request/Response +class VehicleCreate(BaseModel): + brand: str = Field(..., min_length=1, max_length=100) + model: str = Field(..., min_length=1, max_length=150) + ... + +# Dependency Injection für Auth +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db) +) -> User: + ... + +# RBAC Dependency +def require_role(*roles: str): + async def role_checker(user: User = Depends(get_current_user)) -> User: + if user.role not in roles: + raise HTTPException(status_code=403, detail="Forbidden") + return user + return role_checker +``` + +#### Error-Format (Pflicht) +```json +{ + "error": { + "code": "VEHICLE_NOT_FOUND", + "message": "Vehicle with ID abc123 not found", + "details": {} + } +} +``` + +#### SQLAlchemy Model Pattern +```python +class Vehicle(Base): + __tablename__ = "vehicles" + + id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid4) + brand: Mapped[str] = mapped_column(String(100), nullable=False) + created_at: Mapped[datetime] = mapped_column( + TIMESTAMPTZ, server_default=func.now() + ) +``` + +### 3.2 Frontend + +#### TypeScript Style +- **Strict Mode**: `"strict": true` in tsconfig.json +- **No any**: `"noImplicitAny": true` +- **Import**: absolute imports via `@/` prefix + +#### Next.js Patterns +```tsx +// Server Component (default) +export default async function VehicleListPage() { + const vehicles = await fetch(`${API_URL}/api/vehicles`, { cache: 'no-store' }); + ... +} + +// Client Component (use 'use client' directive) +'use client' +export function VehicleForm() { + const [brand, setBrand] = useState(''); + ... +} + +// API Client (zentral in lib/api.ts) +export async function apiFetch(path: string, options?: RequestInit): Promise { + const token = getAuthToken(); + const res = await fetch(`${API_URL}${path}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options?.headers, + }, + }); + if (!res.ok) throw new ApiError(res.status, await res.json()); + return res.json(); +} +``` + +#### Tailwind CSS +- **Kein CSS-in-JS** (keine styled-components, emotion) +- **Utility-First**: Tailwind-Klassen direkt in JSX +- **Responsive**: mobile-first (`sm:`, `md:`, `lg:`) +- **Dark Mode**: `dark:` prefix (geplant für v2) + +### 3.3 i18n + +#### Translation File Format (JSON) +```json +{ + "vehicles": { + "title": "Fahrzeugbestand", + "create": "Neues Fahrzeug", + "brand": "Marke", + "model": "Modell", + "status": { + "in_stock": "Auf Lager", + "reserved": "Reserviert", + "sold": "Verkauft" + } + }, + "common": { + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "confirm": "Bestätigen" + } +} +``` + +#### i18n Usage in Components +```tsx +import { useTranslations } from 'next-intl'; + +function VehicleList() { + const t = useTranslations('vehicles'); + return

{t('title')}

; +} +``` + +--- + +## 4. Git-Konventionen + +### 4.1 Commit Messages +``nfeat: add vehicle CRUD with mobile.de push +fix: correct OCR confidence threshold logic +docs: update architecture ADR-004 +test: add contact USt-IdNr. validation tests +refactor: extract OpenRouter client to utils +``` + +### 4.2 Branch Naming +``` +feature/T01-auth-foundation +feature/T02-vehicle-module +fix/ocr-confidence-threshold +hotfix/datev-csv-format +``` + +### 4.3 PR Rules +- PRs erforderlich für `main` Branch +- Mindestens 1 Reviewer +- Alle CI Checks müssen grün sein +- Coverage darf nicht sinken + +--- + +## 5. Environment Variables + +### 5.1 Backend (.env) +``` +DATABASE_URL=postgresql+asyncpg://erp_user:erp_pass@postgres:5432/erp_db +REDIS_URL=redis://redis:6379/0 +JWT_SECRET= +JWT_ACCESS_EXPIRE_MINUTES=15 +JWT_REFRESH_EXPIRE_DAYS=7 +OPENROUTER_API_KEY= +MOBILEDE_SELLER_API_KEY= +MOBILEDE_SELLER_API_URL=https://api.mobile.de/seller/v1 +BZST_API_ENABLED=false +UPLOAD_DIR=/data/uploads +MAX_FILE_SIZE_MB=20 +CORS_ORIGINS=https://erp.media-on.de +``` + +### 5.2 Frontend (.env.local) +``` +NEXT_PUBLIC_API_URL=https://erp.media-on.de/api +NEXT_PUBLIC_DEFAULT_LOCALE=de +``` + +--- + +## 6. Task-Ausführungs-Regeln + +### 6.1 Task-Reihenfolge (Dependency-basiert) +``` +T01 (Auth+i18n) → T02 (Vehicle) → T04 (Contacts) → T03 (OCR) → T05 (Files) → T06 (Sales) → T07 (Copilot) → T08 (Retouch) +``` + +### 6.2 Pro Task +1. Lese task_graph.json für Task-Details +2. Implementiere alle `files_to_create` für den Task +3. Schreibe Tests zuerst (TDD) +4. Führe `test_spec.commands` aus +5. Verifiziere `acceptance_criteria` +6. Coverage >= `coverage_target` muss erreicht sein +7. Keine Dateien aus anderen Tasks erstellen + +### 6.3 Verboten +- **Keine Micro-Tasks**: Ein Task = ein komplettes Modul +- **Keine Tests modifizieren**: Tests sind die Spec +- **Kein 'done' ohne Evidence**: Test-Output als Beweis +- **Keine Secrets im Code**: Nur Environment Variables +- **Keine Hardcoded URLs**: Config via BaseSettings +- **Kein sync Code für DB**: async/await Pflicht +- **Kein `any` in TypeScript**: strict mode + +--- + +## 7. Quality Gates + +| Gate | Kriterium | Tool | +|---|---|---| +| Lint | ruff check, eslint | ruff, eslint | +| Format | black --check, prettier --check | black, prettier | +| Types | mypy, tsc --noEmit | mypy, tsc | +| Tests | pytest, vitest | pytest, vitest | +| Coverage | >= 80% backend, >= 70% frontend | pytest-cov, vitest coverage | +| Build | docker compose build | docker | +| Security | pip-audit, npm audit | pip-audit, npm audit | + +--- + +## 8. Deployment (Coolify) + +### 8.1 docker-compose.yml (Produktion) +```yaml +version: '3.8' +services: + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=https://erp.media-on.de/api + depends_on: + - backend + restart: unless-stopped + + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql+asyncpg://erp_user:${POSTGRES_PASSWORD}@postgres:5432/erp_db + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET=${JWT_SECRET} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + depends_on: + - postgres + - redis + restart: unless-stopped + + postgres: + image: postgres:16-alpine + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=erp_user + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=erp_db + restart: unless-stopped + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + restart: unless-stopped + +volumes: + postgres_data: + redis_data: + uploads_data: +``` + +### 8.2 Health Endpoints +- Backend: `GET /api/health` → `{"status": "ok"}` +- Frontend: `GET /` → HTTP 200 + +### 8.3 Backup +- PostgreSQL: Daily dump via Coolify cron +- Uploads: Volume backup +- Redis: Optional (cache can be rebuilt) + +--- + +## 9. Open Questions / TODOs + +| Frage | Status | Verantwortlich | +|---|---|---| +| BZSt API Zugang beantragen | Offen (Feature-Flag disabled) | Admin | +| mobile.de Seller API Credentials | Benötigt (in Coolify env vars) | Admin | +| OpenRouter API Key | Benötigt (in Coolify env vars) | Admin | +| Domain erp.media-on.de DNS | Zu konfigurieren | DevOps | +| DATEV Berater-Nummer | Zu klären mit Buchhaltung | Buchhaltung | +| Contract PDF Template | Zu definieren (Rechtstexte) | Admin + Rechtsanwalt | + +--- + +## 10. Handoff Summary + +- **architecture.md**: ✅ Complete (Stack, Module, Data Model, API, ADRs, Test Strategy) +- **task_graph.json**: ✅ Complete (8 Tasks, 115 Acceptance Criteria, alle mit test_spec) +- **AGENTS.md**: ✅ Complete (Build Commands, Test Rules, Conventions) +- **Ready for Implementation**: ✅ Yes (nach Plan Mode Transition zu implementation_allowed) diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..bb9387c --- /dev/null +++ b/architecture.md @@ -0,0 +1,1162 @@ +# ERP Nutzfahrzeuge – Architekturdokument + +> **Version**: 1.0 +> **Datum**: 2026-07-12 +> **Status**: Approved for Implementation +> **Solution Architect**: A0 Orchestrator + +--- + +## 1. Übersicht + +ERP-System für den Handel mit Nutzfahrzeugen (LKW, Baumaschinen, PKW, Stapler, Transporter). +Backend in Python/FastAPI, Frontend in React/Next.js (mobile-first), PostgreSQL als Datenbank, +Redis für Caching/Queues. Hosting via Coolify self-hosted mit Docker-Compose (4 Container). + +### 1.1 Stakeholder & Rollen + +| Rolle | Berechtigungen | Nutzer (~10) | +|---|---|---| +| Admin | Vollzugriff, User-Management, Systemeinstellungen | 1-2 | +| Verkäufer | Fahrzeuge, Kontakte, Verkäufe, OCR, KI-Copilot | 5-7 | +| Buchhaltung | Verkäufe (read), DATEV-Export, USt-IdNr.-Prüfung | 1-2 | + +### 1.2 Sprachen +- Deutsch (DE) – Primärsprache +- Englisch (EN) – Sekundärsprache + +--- + +## 2. Tech-Stack + +| Schicht | Technologie | Version | +|---|---|---| +| Backend | Python / FastAPI | Python 3.12, FastAPI 0.111+ | +| Frontend | React / Next.js | Next.js 15 (App Router), React 19 | +| CSS | Tailwind CSS | 3.4+ | +| Datenbank | PostgreSQL | 16+ | +| Cache/Queue | Redis | 7+ | +| ORM | SQLAlchemy 2.0 + Alembic | async | +| KI | OpenRouter API | Qwen2.5-VL (OCR), Flux.1-Pro (Bild), Claude/GPT-4 (Copilot) | +| mobile.de | Seller API (Push only) | REST | +| OCR | OpenRouter Vision (Qwen2.5-VL) | – | +| DATEV | CSV-Export (DATEV-Format) | – | +| USt-IdNr. | BZSt API (geplant, aktuell kein Zugang) | REST | +| Deployment | Docker-Compose via Coolify | 4 Container | +| Auth | JWT (access + refresh) | – | +| Testing | pytest + httpx (Backend), Vitest + Playwright (Frontend) | – | + +--- + +## 3. Deployment-Architektur + +### 3.1 Docker-Compose (4 Container) + +``` +┌─────────────────────────────────────────────────┐ +│ Coolify (46.225.91.159) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ frontend │ │ backend │ │ redis │ │ +│ │ Next.js │ │ FastAPI │ │ cache │ │ +│ │ :3000 │ │ :8000 │ │ :6379 │ │ +│ └────┬─────┘ └────┬─────┘ └──────────┘ │ +│ │ │ │ +│ │ ┌────┴─────┐ │ +│ │ │ postgres │ │ +│ │ │ :5432 │ │ +│ │ └──────────┘ │ +│ │ │ +│ Traefik (SSL, Let's Encrypt) │ +└─────────────────────────────────────────────────┘ +``` + +### 3.2 Netzwerk +- Traefik als Reverse Proxy mit automatischem SSL +- Frontend erreichbar unter `https://erp.media-on.de` +- Backend API unter `https://erp.media-on.de/api` +- Interne Kommunikation über Docker-Netzwerk + +### 3.3 Volumes +- `postgres_data`: Datenbank-Persistenz +- `redis_data`: Redis-Persistenz (optional) +- `uploads_data`: Fahrzeug-Dateien (Bilder, Dokumente, OCR-Scans) +- `retouched_images`: Retuschierte Bilder + +--- + +## 4. Modulstruktur + +### 4.1 Backend-Struktur (FastAPI) + +``` +backend/ +├── app/ +│ ├── main.py # FastAPI app entry, CORS, routers +│ ├── config.py # Settings (Pydantic BaseSettings) +│ ├── database.py # async engine, session factory +│ ├── dependencies.py # Common deps (auth, pagination) +│ ├── models/ # SQLAlchemy models +│ │ ├── user.py +│ │ ├── vehicle.py +│ │ ├── contact.py +│ │ ├── sale.py +│ │ ├── document.py +│ │ └── ocr_result.py +│ ├── schemas/ # Pydantic schemas (request/response) +│ │ ├── user.py +│ │ ├── vehicle.py +│ │ ├── contact.py +│ │ ├── sale.py +│ │ └── ocr.pyn│ ├── routers/ # API route handlers +│ │ ├── auth.py +│ │ ├── vehicles.py +│ │ ├── contacts.py +│ │ ├── sales.py +│ │ ├── ocr.py +│ │ ├── files.py +│ │ ├── copilot.py +│ │ └── image_retouch.py +│ ├── services/ # Business logic +│ │ ├── auth_service.py +│ │ ├── vehicle_service.py +│ │ ├── mobilede_service.py +│ │ ├── contact_service.py +│ │ ├── sale_service.py +│ │ ├── ocr_service.py +│ │ ├── file_service.py +│ │ ├── copilot_service.py +│ │ ├── retouch_service.py +│ │ └── datev_service.py +│ ├── tasks/ # Background tasks (async) +│ │ ├── mobilede_push.py +│ │ └── ocr_processing.py +│ └── utils/ +│ ├── openrouter.py # OpenRouter API client +│ ├── i18n.py # Backend i18n helpers +│ └── datev.py # DATEV export formatter +├── alembic/ # DB migrations +├── tests/ # pytest test suite +├── requirements.txt +├── Dockerfile +└── pyproject.toml +``` + +### 4.2 Frontend-Struktur (Next.js) + +``` +frontend/ +├── app/ # App Router +│ ├── layout.tsx # Root layout (i18n provider, auth) +│ ├── page.tsx # Dashboard +│ ├── (auth)/ +│ │ └── login/page.tsx +│ ├── vehicles/ +│ │ ├── page.tsx # List + filter +│ │ ├── [id]/page.tsx # Detail +│ │ └── new/page.tsx # Create form +│ ├── contacts/ +│ │ ├── page.tsx # List +│ │ ├── [id]/page.tsx # Detail +│ │ └── new/page.tsx # Create form +│ ├── sales/ +│ │ ├── page.tsx # List +│ │ ├── [id]/page.tsx # Detail + contract +│ │ └── new/page.tsx # Create sale +│ ├── ocr/ +│ │ └── page.tsx # OCR upload + results +│ ├── copilot/ +│ │ └── page.tsx # KI-Copilot chat +│ └── settings/ +│ └── page.tsx # Admin settings +├── components/ # Reusable components +│ ├── ui/ # Base UI (Button, Input, Card, Table) +│ ├── vehicles/ # Vehicle-specific components +│ ├── contacts/ # Contact-specific components +│ ├── sales/ # Sale-specific components +│ ├── ocr/ # OCR components +│ └── copilot/ # Copilot chat components +├── lib/ # Client utilities +│ ├── api.ts # API client (fetch wrapper) +│ ├── auth.ts # Auth context/hooks +│ └── i18n.ts # i18n config +├── messages/ # i18n translation files +│ ├── de.json +│ └── en.json +├── public/ +├── Dockerfile +├── tailwind.config.ts +└── package.json +``` + +--- + +## 5. Datenmodell + +### 5.1 Entity-Relationship-Übersicht + +``` +User ──< Vehicle ──< File (Dateiablage) + │ │ + │ ├──< OCRResult + │ ├──< RetouchedImage + │ └──< Sale ──< ContractDocument + │ └──< DATEVExport + │ + └──< Contact (Kunde/Lieferant) +``` + +### 5.2 Kern-Entitäten + +#### User +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| email | VARCHAR(255) | unique, not null | +| full_name | VARCHAR(200) | not null | +| role | ENUM('admin','verkaeufer','buchhaltung') | not null | +| password_hash | VARCHAR(255) | not null | +| is_active | BOOLEAN | default true | +| created_at | TIMESTAMPTZ | default now() | + +#### Vehicle +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| vehicle_type | ENUM('lkw','baumaschine','pkw','stapler','transporter') | not null | +| brand | VARCHAR(100) | not null | +| model | VARCHAR(150) | not null | +| vin | VARCHAR(17) | unique | +| registration_plate | VARCHAR(20) | | +| first_registration | DATE | | +| mileage_km | INTEGER | | +| power_kw | INTEGER | | +| fuel_type | ENUM('diesel','petrol','electric','hybrid','other') | | +| purchase_price | DECIMAL(12,2) | not null | +| sale_price | DECIMAL(12,2) | | +| status | ENUM('in_stock','reserved','sold','deleted') | default 'in_stock' | +| mobilede_ad_id | VARCHAR(50) | | +| mobilede_synced_at | TIMESTAMPTZ | | +| notes | TEXT | | +| created_by | UUID | FK → User | +| created_at | TIMESTAMPTZ | default now() | +| updated_at | TIMESTAMPTZ | default now() | + +#### Contact +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| contact_type | ENUM('kunde','lieferant','beide') | not null | +| company_name | VARCHAR(200) | | +| first_name | VARCHAR(100) | | +| last_name | VARCHAR(100) | | +| street | VARCHAR(255) | | +| zip_code | VARCHAR(10) | | +| city | VARCHAR(100) | | +| country | VARCHAR(2) | default 'DE' | +| is_eu | BOOLEAN | default false | +| ust_id_nr | VARCHAR(20) | | +| phone | VARCHAR(30) | | +| email | VARCHAR(255) | | +| created_at | TIMESTAMPTZ | default now() | + +#### Sale +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| vehicle_id | UUID | FK → Vehicle, not null | +| buyer_contact_id | UUID | FK → Contact, not null | +| seller_user_id | UUID | FK → User, not null | +| sale_price | DECIMAL(12,2) | not null | +| sale_date | DATE | not null | +| payment_method | ENUM('bank_transfer','cash','financing','other') | | +| is_gwg | BOOLEAN | default false (Geringwertiges Wirtschaftsgut) | +| gwg_amount | DECIMAL(12,2) | | +| vat_rate | DECIMAL(5,2) | default 19.00 | +| ust_id_nr_verified | BOOLEAN | default false | +| contract_pdf_path | VARCHAR(500) | | +| datev_export_id | UUID | FK → DATEVExport (nullable) | +| created_at | TIMESTAMPTZ | default now() | + +#### File (Dateiablage pro Fahrzeug) +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| vehicle_id | UUID | FK → Vehicle, not null | +| file_type | ENUM('image','document','ocr_scan','retouched_image') | not null | +| original_filename | VARCHAR(255) | not null | +| stored_path | VARCHAR(500) | not null | +| mime_type | VARCHAR(100) | not null | +| file_size_bytes | BIGINT | not null | +| uploaded_by | UUID | FK → User | +| created_at | TIMESTAMPTZ | default now() | + +#### OCRResult +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| vehicle_id | UUID | FK → Vehicle, not null | +| file_id | UUID | FK → File, not null | +| raw_text | TEXT | extracted text | +| structured_data | JSONB | parsed fields (brand, model, vin, etc.) | +| ocr_type | ENUM('zb_i','zb_ii') | not null | +| confidence_score | FLOAT | | +| status | ENUM('pending','completed','failed') | default 'pending' | +| created_at | TIMESTAMPTZ | default now() | + +#### DATEVExport +| Feld | Typ | Constraints | +|---|---|---| +| id | UUID | PK | +| export_date | DATE | not null | +| period_from | DATE | not null | +| period_to | DATE | not null | +| file_path | VARCHAR(500) | generated CSV path | +| record_count | INTEGER | | +| created_by | UUID | FK → User | +| created_at | TIMESTAMPTZ | default now() | + +--- + +## 6. API Design + +### 6.1 Auth +| Method | Endpoint | Beschreibung | +|---|---|---| +| POST | `/api/auth/login` | Login → JWT (access + refresh) | +| POST | `/api/auth/refresh` | Token refresh | +| GET | `/api/auth/me` | Aktuellen User abrufen | +| PUT | `/api/auth/me` | Profil aktualisieren | + +### 6.2 Vehicles (M1) +| Method | Endpoint | Beschreibung | +|---|---|---| +| GET | `/api/vehicles` | List with pagination, filter (type, status, brand), sort | +| GET | `/api/vehicles/:id` | Detail view | +| POST | `/api/vehicles` | Create new vehicle | +| PUT | `/api/vehicles/:id` | Update vehicle | +| DELETE | `/api/vehicles/:id` | Soft delete (status='deleted') | +| POST | `/api/vehicles/:id/mobilede-push` | Push to mobile.de Seller API | +| GET | `/api/vehicles/:id/mobilede-status` | Sync status abrufen | + +### 6.3 OCR (M2) +| Method | Endpoint | Beschreibung | +|---|---|---| +| POST | `/api/ocr/upload` | Upload ZB I/II scan → async OCR processing | +| GET | `/api/ocr/results/:id` | OCR result abrufen | +| GET | `/api/ocr/results?vehicle_id=X` | OCR results für Fahrzeug | +| POST | `/api/ocr/results/:id/apply` | OCR-Daten auf Vehicle anwenden | + +### 6.4 Contacts (M3) +| Method | Endpoint | Beschreibung | +|---|---|---| +| GET | `/api/contacts` | List with pagination, filter (type, eu/inland), search | +| GET | `/api/contacts/:id` | Detail view | +| POST | `/api/contacts` | Create contact | +| PUT | `/api/contacts/:id` | Update contact | +| DELETE | `/api/contacts/:id` | Delete contact | + +### 6.5 Files (M4) +| Method | Endpoint | Beschreibung | +|---|---|---| +| GET | `/api/vehicles/:id/files` | List files for vehicle | +| POST | `/api/vehicles/:id/files` | Upload file (multipart) | +| GET | `/api/vehicles/:id/files/:fileId` | Download file | +| DELETE | `/api/vehicles/:id/files/:fileId` | Delete file | + +### 6.6 Sales (M5) +| Method | Endpoint | Beschreibung | +|---|---|---| +| GET | `/api/sales` | List with pagination, filter (date, status) | +| GET | `/api/sales/:id` | Detail view | +| POST | `/api/sales` | Create sale (generates contract PDF) | +| PUT | `/api/sales/:id` | Update sale | +| DELETE | `/api/sales/:id` | Cancel/delete sale | +| POST | `/api/sales/:id/contract` | Re-generate contract PDF | +| GET | `/api/sales/:id/contract` | Download contract PDF | +| POST | `/api/sales/:id/verify-ust-id` | USt-IdNr. BZSt API Prüfung (geplant) | +| POST | `/api/datev/export` | DATEV CSV Export für Zeitraum | +| GET | `/api/datev/exports` | List DATEV exports | +| GET | `/api/datev/exports/:id/download` | Download DATEV CSV | + +### 6.7 KI-Copilot (M7) +| Method | Endpoint | Beschreibung | +|---|---|---| +| POST | `/api/copilot/chat` | Text chat → OpenRouter (Claude/GPT-4) | +| POST | `/api/copilot/voice` | Voice input (STT) → chat response | +| GET | `/api/copilot/history` | Chat history (paginated) | +| POST | `/api/copilot/action` | System action ausführen (vehicle create, search, etc.) | + +### 6.8 Image Retouch (M8) +| Method | Endpoint | Beschreibung | +|---|---|---| +| POST | `/api/retouch/process` | Bild → Flux.1-Pro Retusche | +| GET | `/api/retouch/results/:id` | Retuschiertes Bild abrufen | +| POST | `/api/retouch/price-compare` | Preisvergleich mit mobile.de Listings | + +### 6.9 Users (Admin) +| Method | Endpoint | Beschreibung | +|---|---|---| +| GET | `/api/users` | List users (admin only) | +| POST | `/api/users` | Create user (admin only) | +| PUT | `/api/users/:id` | Update user (admin only) | +| DELETE | `/api/users/:id` | Deactivate user (admin only) | + +--- + +## 7. Test-Strategie + +### 7.1 Backend (pytest) +- **Unit Tests**: Services isoliert mit Mocks (OpenRouter, mobile.de API) +- **Integration Tests**: API Endpoints mit Test-DB (SQLite/PostgreSQL testcontainer) +- **Coverage Target**: ≥ 80% pro Modul +- **Fixtures**: DB session, test client, mock OpenRouter responses + +### 7.2 Frontend (Vitest + Playwright) +- **Unit Tests**: Komponenten mit Vitest + React Testing Library +- **E2E Tests**: Playwright für kritische User-Flows (Login, Vehicle CRUD, OCR, Sale) +- **Coverage Target**: ≥ 70% pro Komponentengruppe + +### 7.3 Test-Commands +```bash +# Backend +cd backend && pytest --cov=app --cov-report=term-missing + +# Frontend +cd frontend && npm run test +cd frontend && npm run e2e + +# Full integration +docker compose -f docker-compose.test.yml up --abort-on-container-exit +``` + +--- + +## 8. Integrationsarchitektur + +### 8.1 OpenRouter KI-Pipeline + +``` +User Request → FastAPI → OpenRouter API → Response + │ + ├── OCR: Qwen2.5-VL (Vision) → structured JSON + ├── Retouch: Flux.1-Pro → Image URL/base64 + └── Copilot: Claude/GPT-4 → Text/Action JSON +``` + +- API-Key via Environment Variable `OPENROUTER_API_KEY` +- Rate-Limiting via Redis Token Bucket +- Fehlerbehandlung: Retry mit exponential backoff (max 3) +- Response-Caching für identische OCR-Scans + +### 8.2 mobile.de Seller API + +``` +Vehicle Update → FastAPI → mobile.de Seller API (POST/PUT) + ↓ + Ad ID gespeichert in Vehicle.mobilede_ad_id +``` + +- **Nur Push-Richtung** (kein Import von mobile.de) +- Auth: OAuth2 oder API-Key (in mobile.de Seller Portal konfigurierbar) +- Mapping: ERP Vehicle → mobile.de Ad Format +- Retry-Queue bei Fehlern (Redis) +- Status-Tracking: `mobilede_synced_at`, `mobilede_ad_id` + +### 8.3 DATEV-Export + +``` +Buchhaltung User → DATEV Export Request → FastAPI + → Sammle Sales im Zeitraum → Format CSV (DATEV-Format) → Download +``` + +- Format: DATEV CSV (Buchungsstapel) +- Felder: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext +- GwG-Behandlung: Sofortabzug bei ≤ 800€ (§6 Abs. 2 EStG) +- USt-IdNr.-Prüfung: BZSt API (geplant, Feature-Flag `bzst_api_enabled`) + +--- + +## 9. ADRs (Architecture Decision Records) + +### ADR-001: FastAPI statt Django +**Status**: Accepted +**Entscheidung**: FastAPI als Backend-Framework. +**Begründung**: Async-native, bessere Performance, automatische OpenAPI-Dokumentation, +lichtgewichtiger als Django, native Pydantic-Integration. +**Alternativen**: Django REST Framework (zu schwerfällig), Flask (kein async). + +### ADR-002: Next.js App Router statt Pages Router +**Status**: Accepted +**Entscheidung**: Next.js 15 mit App Router. +**Begründung**: Server Components, Streaming, bessere Code-Organisation, +Zukunftssicherheit. +**Alternativen**: Pages Router (deprecated), CRA (deprecated). + +### ADR-003: OpenRouter als KI-Gateway +**Status**: Accepted +**Entscheidung**: Alle KI-Aufrufe über OpenRouter API. +**Begründung**: Ein API-Key für multiple Modelle, einheitliche API, +kein Vendor Lock-in, schnelle Modellwechsel. +**Alternativen**: Direkte OpenAI/Anthropic API (Vendor Lock-in), +lokale Modelle (zu langsam für OCR). + +### ADR-004: OCR via Vision-Modell statt Tesseract +**Status**: Accepted +**Entscheidung**: OCR über OpenRouter Qwen2.5-VL Vision-Modell. +**Begründung**: Bessere Erkennung bei handschriftlichen und undeutlichen ZB-Scans, +keine Infrastruktur für Tesseract nötig, strukturierbare JSON-Ausgabe. +**Alternativen**: Tesseract (schlechte Qualität bei Scans), +Google Cloud Vision (Privacy-Bedenken, kostenpflichtig). + +### ADR-005: Soft-Delete für Vehicles +**Status**: Accepted +**Entscheidung**: Vehicles werden nicht physisch gelöscht, sondern status='deleted'. +**Begründung**: Referentielle Integrität (Sales, Files, OCR Results), +Audit-Trail für Buchhaltung. +**Alternativen**: Hard Delete (Datenverlust, FK-Constraints). + +### ADR-006: JWT statt Session-based Auth +**Status**: Accepted +**Entscheidung**: JWT (access + refresh token) für Authentifizierung. +**Begründung**: Stateless, geeignet für API + SPA, keine Server-Side-Session nötig. +**Alternativen**: Session-Cookies (CSRF-Risiko, Server-State nötig). + +### ADR-007: Redis für Caching und Background Queues +**Status**: Accepted +**Entscheidung**: Redis als Cache und Task-Queue. +**Begründung**: Schnelles In-Memory-Caching für OCR-Results und mobile.de-Status, +Background-Queues für async OCR und mobile.de Push. +**Alternativen**: Celery+RabbitMQ (zu komplex für ~10 Nutzer), +keine Queue (blocking API). + +### ADR-008: Docker-Compose mit 4 Containern statt K8s +**Status**: Accepted +**Entscheidung**: Docker-Compose mit frontend, backend, postgres, redis. +**Begründung**: Einfach zu deployen via Coolify, ausreichend für ~10 Nutzer, +keine K8s-Komplexität. +**Alternativen**: Kubernetes (overkill), einzelne Coolify Services +(mehr Konfigurationsaufwand). + +--- + +## 10. Nicht-funktionale Anforderungen + +### 10.1 Performance +- API-Response < 500ms für Standard-CRUD (ohne KI-Calls) +- OCR-Verarbeitung < 30s pro Scan (async, User wird benachrichtigt) +- Bildretusche < 60s pro Bild (async) +- Frontend LCP < 2.5s (mobile.de Performance Budget) + +### 10.2 Sicherheit +- JWT mit kurzer Access-Token-Gültigkeit (15min) + Refresh (7d) +- Role-based Access Control (RBAC) auf Endpoint-Ebene +- Input-Validierung via Pydantic auf allen Endpoints +- SQL-Injection-Schutz durch SQLAlchemy parameterized queries +- File-Upload: MIME-Type-Validierung, max 20MB pro Datei +- OpenRouter API-Key in Environment Variables, nie im Code +- HTTPS-only via Traefik + Let's Encrypt + +### 10.3 Skalierbarkeit +- Horizontal skalierbar (stateless Backend, sticky sessions nicht nötig) +- Redis als verteiltes Cache → mehrere Backend-Instanzen möglich +- PostgreSQL Connection Pooling via SQLAlchemy async engine +- Für ~10 Nutzer: Single-Instance ausreichend + +### 10.4 Verfügbarkeit +- Docker-Compose mit `restart: unless-stopped` +- PostgreSQL mit WAL-Archiving für Point-in-Time-Recovery +- Daily Backup via Coolify (PostgreSQL dump) +- Health-Checks auf `/api/health` (Backend) und `/` (Frontend) + +--- + +## 11. Risiken & Mitigation + +| Risiko | Wahrscheinlichkeit | Impact | Mitigation | +|---|---|---|---| +| OpenRouter API-Ausfall | Mittel | Hoch | Retry mit Backoff, Fallback-Modell, Cache | +| mobile.de API-Änderung | Niedrig | Mittel | API-Version pinnen, Monitoring | +| OCR-Qualität unzureichend | Mittel | Mittel | Confidence-Score → Manual Review bei < 0.7 | +| BZSt API kein Zugang | Hoch | Niedrig | Feature-Flag, manuelle Prüfung als Fallback | +| Datenverlust PostgreSQL | Niedrig | Kritisch | Daily Backups, WAL-Archiving | +| Token-Kosten OpenRouter | Mittel | Mittel | Caching, Rate-Limiting pro User | + +--- + +## 12. i18n Architektur + +- **Backend**: Fehlermeldungen auf Deutsch/Englisch via Accept-Language Header +- **Frontend**: next-intl oder react-i18next +- **Translation Files**: `frontend/messages/de.json`, `frontend/messages/en.json` +- **Sprachumschaltung**: User-Settings → Locale in localStorage + Cookie +- **Backend-Fehler**: Error-Codes + lokalisierte Messages + +--- + +## 13. Konventionen + + ## 14. PDF-Generierung & Briefpapier (Letterhead) + + ### 14.1 Library-Wahl: WeasyPrint + + **Entscheidung**: WeasyPrint als PDF-Generierungs-Library. + + **Begründung**: WeasyPrint rendert HTML/CSS → PDF. Dadurch können PDF-Templates + mit Standard-Web-Technologien (HTML, CSS, Jinja2) erstellt werden. Briefpapier + (Letterhead) wird über CSS `@page` Rules mit `@top`/`@bottom` Margin-Boxes + umgesetzt – inkl. Logo, Firmenadresse, Footer, Seitenzahlen. + + **Vergleich**: + + | Kriterium | WeasyPrint | ReportLab | + |---|---|---| + | Template-Erstellung | HTML/CSS + Jinja2 | Python-Code (Programmatic) | + | Briefpapier/Header/Footer | CSS @page Rules, repeating on every page | PageTemplate + Frame + onPage callback | + | Logo/Bild-Embedding | `` oder CSS `background-image` | `Image()` flowable | + | Lernkurve | Niedrig (Web-Entwickler kennen HTML/CSS) | Mittel (ReportLab-spezifische API) | + | Flexibilität | Hoch (volle CSS-Unterstützung) | Mittel (eingeschränkte Layout-Optionen) | + | Wartbarkeit | Hoch (Templates separiert von Code) | Niedrig (Layout im Python-Code) | + + **Alternativen**: ReportLab (pixel-präzise Kontrolle, aber komplexer), + fpdf2 (einfach aber weniger Features), wkhtmltopdf (veraltet). + + ### 14.2 Briefpapier-Implementierung + + **Template-Struktur**: + ``` + backend/app/utils/pdf/ + ├── letterhead_template.html # Jinja2 HTML-Template mit Briefpapier + ├── letterhead_style.css # CSS mit @page Rules (Header/Footer/Logo) + ├── contract_template.html # Vertrag-Template (erbt Briefpapier) + ├── invoice_template.html # Rechnung-Template (erbt Briefpapier) + └── pdf_generator.py # WeasyPrint Wrapper-Service + ``` + + **CSS @page für Briefpapier**: + ```css + @page { + size: A4; + margin: 120px 40px 80px 40px; /* top right bottom left */ + + @top-left { + content: url('/data/letterhead/logo.png'); + width: 180px; + } + @top-right { + content: 'Firmenname GmbH'; + font-size: 9pt; + color: #666; + } + @bottom-left { + content: 'Firmenname GmbH · Straßenname 1 · 12345 Stadt'; + font-size: 8pt; + color: #999; + } + @bottom-right { + content: 'Seite ' counter(page) ' von ' counter(pages); + font-size: 8pt; + color: #999; + } + } + ``` + + **Briefpapier-Elemente** (konfigurierbar über Admin-Settings): + - Logo (PNG/SVG, Position: top-left oder top-center) + - Firmenname & Adresse (Header oder Footer) + - Kontaktdaten (Tel, Email, Website) + - Steuernummer / USt-IdNr. (Footer) + - Seitenzahlen (Footer rechts) + - Hintergrund-Wasserzeichen (optional, z.B. "ENTWURF") + - Bankverbindung (Footer) + + **Admin-Settings für Briefpapier**: + - Endpoint: `PUT /api/settings/letterhead` (admin only) + - Felder: logo_path, company_name, street, zip_city, phone, email, website, + tax_number, ust_id_nr, bank_name, bank_iban, bank_bic + - Briefpapier-Vorschau: `GET /api/settings/letterhead/preview` → PDF Preview + + ### 14.3 Dokument-Typen + + | Dokument | Template | Briefpapier | + |---|---|---| + | Kaufvertrag (Sale Contract) | contract_template.html | Ja (Full Letterhead) | + | Rechnung (Invoice) | invoice_template.html | Ja (Full Letterhead) | + | DATEV-Export | – (CSV, kein PDF) | Nein | + | Fahrzeug-Inventarliste | inventory_template.html | Ja (Full Letterhead) | + + ### ADR-009: WeasyPrint statt ReportLab + **Status**: Accepted + **Entscheidung**: WeasyPrint für alle PDF-Generierungen. + **Begründung**: HTML/CSS-Templates sind einfacher zu erstellen und zu warten + als ReportLab-Code. Briefpapier über CSS @page ist flexibler und + Web-Entwickler können Templates ohne Python-Kenntnisse anpassen. + **Alternativen**: ReportLab (pixel-präzise aber komplex), wkhtmltopdf (veraltet). + + --- + + ## 15. mobile.de Seller API – Detaillierte Integration + + ### 15.1 API-Übersicht + + mobile.de bietet zwei Schnittstellen: + + | Schnittstelle | Format | Use Case | + |---|---|---| + | **Seller API (REST)** | XML (`application/vnd.de.mobile.seller-ad-v1.1+xml`) | Granulare CRUD-Operationen, Real-Time Sync | + | **CSV Upload Interface** | CSV (Semikolon-getrennt, ISO-8859-15) | Bulk-Upload aller Fahrzeuge auf einmal | + + **Entscheidung**: Seller API (REST) als primäre Schnittstelle. CSV Upload als + Fallback/Initial-Bulk-Import. + + ### 15.2 Authentifizierung + + **HTTP Basic Authentication** (empfohlen): + - Username + Password aus mobile.de Seller Portal + - `Authorization: Basic ` + - Alternativ (deprecated): `X-MOBILE-SELLER-TOKEN` Header + + **Credentials in Environment Variables**: + ``` + MOBILEDE_SELLER_API_USERNAME= + MOBILEDE_SELLER_API_PASSWORD= + MOBILEDE_SELLER_KEY= + MOBILEDE_SELLER_API_URL=https://services.mobile.de + ``` + + ### 15.3 REST API Endpunkte + + Base URL: `https://services.mobile.de` + + | Method | Endpoint | Beschreibung | + |---|---|---| + | GET | `/seller-api/sellers` | Alle Sellers abrufen | + | GET | `/seller-api/sellers/{seller-key}` | Einzelnen Seller abrufen | + | GET | `/seller-api/sellers/{seller-key}/ads` | Alle Anzeigen eines Sellers | + | POST | `/seller-api/sellers/{seller-key}/ads` | Anzeige erstellen (XML Body) | + | GET | `/seller-api/sellers/{seller-key}/ads/{adId}` | Einzelne Anzeige abrufen | + | PUT | `/seller-api/sellers/{seller-key}/ads/{adId}` | Anzeige aktualisieren (XML Body) | + | DELETE | `/seller-api/sellers/{seller-key}/ads/{adId}` | Anzeige löschen | + | POST | `/seller-api/sellers/{seller-key}/ads/{adId}/vehicle-attribute/renewal-date` | Anzeige erneuern | + | PUT | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Bilder hochladen/ändern | + | GET | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Bild-URLs abrufen | + | DELETE | `/seller-api/sellers/{seller-key}/ads/{adId}/images` | Alle Bilder löschen | + | GET | `/seller-api/sellers/{seller-key}/ads/{ad-key}/statistic` | Anzeigen-Statistiken | + + ### 15.4 XML Ad-Format (Beispiel) + + ```xml + + + Lkw + Mercedes-Benz + Actros + 150000 + 2020-03-15 + Diesel + Manual + 45000.00 + 1 + 0 + + + + + + ``` + + ### 15.5 Mapping: ERP Vehicle → mobile.de Ad + + | ERP Feld | mobile.de XML Feld | Anmerkung | + |---|---|---| + | vehicle_type | ad:category | Lkw/Baumaschine/Pkw/Stapler/Transporter | + | brand | ad:make | | + | model | ad:model | | + | mileage_km | ad:kilometre | | + | first_registration | ad:firstRegistration | ISO Datum | + | power_kw | ad:power | kW-Wert | + | fuel_type | ad:fuel | Diesel/Petrol/Electric/Hybrid | + | sale_price | ad:price | consumerPrice="true" | + | registration_plate | ad:licensePlate | | + | vin | ad:vin | Fahrzeug-Identifikationsnummer | + | notes | ad:description | Freitext | + + ### 15.6 CSV Upload Interface (Fallback) + + Format: Semikolon-getrennt, ISO-8859-15, `.csv` in `.zip` verpackt. + Pflichtfelder: internal number, category, make, model, kilometre, VAT, + damaged_vehicle, one-year-old car, new car, our recommendation, metallic, warranty. + + **Use Case**: Initial-Bulk-Import aller Fahrzeuge nach mobile.de, oder wenn + Seller API nicht verfügbar. + + ### 15.7 Rate-Limits + + Die mobile.de Dokumentation nennt keine expliziten Rate-Limits. Dennoch + implementieren wir einen eigenen Rate-Limiter (Redis Token Bucket): + - Max 10 Requests/Sekunde an mobile.de + - Retry-Queue bei HTTP 429 oder 5xx Fehlern (max 3 Retries mit Backoff) + + ### ADR-010: Seller API (REST) statt CSV Upload als primäre Schnittstelle + **Status**: Accepted + **Entscheidung**: REST Seller API als primäre mobile.de Schnittstelle. + **Begründung**: Granulare CRUD-Operationen, Real-Time Sync, Bild-Management, + Statistiken. CSV nur für Initial-Import oder Bulk-Operationen. + **Alternativen**: CSV Upload nur (kein Real-Time, kein Bild-Management). + + --- + + ## 16. Buchhaltungsschnittstellen + + ### 16.1 DATEV-Export (primär) + + **Format**: DATEV CSV (ASCII, semikolon-getrennt) – Buchungsstapel. + + **DATEV CSV Felder** (Buchungsstapel-Format): + ```csv + "Buchungsstapel";"01.01.2026";"31.01.2026";"0001";"Mandantenname";"1000";"Beraternummer" + "Umsatz";"Soll";"Haben";"Konto";"Gegenkonto";"Datum";"Belegfeld";"Buchungstext" + 45000.00;"S";4200;0840;15.01.2026;"RE-2026-001";"Fahrzeugverkauf Mercedes Actros" + ``` + + **DATEV XML-Schnittstelle** (erweitert): + - DATEV unterstützt zusätzlich XML-Format für Belegdaten + - Belegbilder + Belegdaten als DATEV-konforme XML + - Vorteil: Belegbilder (Rechnungen/Verträge) können direkt mitgeliefert werden + - Implementiert als Feature-Flag `datev_xml_export_enabled` + + ### 16.2 Weitere Buchhaltungsschnittstellen + + | System | Format | Status | Implementierung | + |---|---|---|---| + | DATEV | CSV (ASCII) + XML | Geplant (primär) | T06 – datev_service | + | Lexware | DATEV-CSV kompatibel | Über DATEV-Export nutzbar | Keine separate Implementierung nötig | + | SAP | CSV/IDoc | Zukunft (v2) | Feature-Flag, separates Export-Modul | + | DATEV Belegbilder | XML + Bilder | Zukunft (v2) | Feature-Flag `datev_xml_export_enabled` | + + **Lexware-Kompatibilität**: Lexware unterstützt den DATEV-Import (CSV-Format). + Daher ist der DATEV-CSV-Export automatisch Lexware-kompatibel. + Lexware nutzt denselben DATEV-Datenexport "Belegbilder mit Belegdaten". + + ### 16.3 Export-Service Architektur + + ``` + backend/app/utils/datev.py # DATEV CSV Formatter + backend/app/utils/accounting_export.py # Generic Export Base (für zukünftige Formate) + backend/app/services/datev_service.py # DATEV Export Logic + ``` + + **Generic Export Pattern** (für zukünftige SAP/Weitere): + ```python + class AccountingExporter(ABC): + @abstractmethod + def export(self, sales: list[Sale], period: DateRange) -> bytes: ... + + class DatevCSVExporter(AccountingExporter): ... + class DatevXMLExporter(AccountingExporter): ... # v2 + class SAPExporter(AccountingExporter): ... # v2 + ``` + + ### ADR-011: DATEV CSV als primäre Buchhaltungsschnittstelle + **Status**: Accepted + **Entscheidung**: DATEV CSV-Format als primärer Export. Generic Export Base + für zukünftige Formate (XML, SAP). + **Begründung**: DATEV ist der deutsche Standard, Lexware-kompatibel, + einfach zu generieren. Abstract Base Class ermöglicht Erweiterung ohne + Code-Duplikation. + **Alternativen**: Direkte DATEV online API (zu komplex, benötigt + Zertifizierung), SAP IDoc (overkill für ~10 Nutzer). + + --- + + ## 17. KI-Copilot: Vollständige Systemsteuerung + + ### 17.1 Anforderung + + Der KI-Copilot soll **ALLE** System-Funktionen steuern können. Der User + kann per Text oder Sprache mit dem Copilot interagieren und beliebige + Aktionen ausführen lassen. + + ### 17.2 Technische Umsetzung: Function Calling + + **Mechanismus**: OpenRouter Function Calling / Tool Use + + Der LLM (Claude/GPT-4) erhält einen System-Prompt mit allen verfügbaren + Functions (Tools). Bei einer User-Anfrage entscheidet der LLM, welche + Function aufgerufen werden muss, und gibt strukturiertes JSON zurück. + + ``` + User: "Erstelle ein neues Fahrzeug: Mercedes Actros, 150000 km, 45000 EUR" + ↓ + LLM Output: { + "function": "create_vehicle", + "arguments": { + "vehicle_type": "lkw", + "brand": "Mercedes-Benz", + "model": "Actros", + "mileage_km": 150000, + "purchase_price": 45000.00 + } + } + ↓ + Copilot Service: POST /api/vehicles (intern, mit User-JWT) + ↓ + Response to User: "Fahrzeug erstellt ✓ (ID: abc-123). Soll ich es + nach mobile.de pushen?" + ``` + + ### 17.3 Verfügbare System-Functions (Tools) + + | Function | Beschreibung | Endpoint | + |---|---|---| + | search_vehicles | Fahrzeuge suchen/filtern | GET /api/vehicles | + | create_vehicle | Neues Fahrzeug anlegen | POST /api/vehicles | + | update_vehicle | Fahrzeug aktualisieren | PUT /api/vehicles/:id | + | delete_vehicle | Fahrzeug löschen (soft) | DELETE /api/vehicles/:id | + | push_to_mobilede | Nach mobile.de pushen | POST /api/vehicles/:id/mobilede-push | + | upload_ocr | OCR-Scan hochladen | POST /api/ocr/upload | + | apply_ocr | OCR-Daten anwenden | POST /api/ocr/results/:id/apply | + | search_contacts | Kontakte suchen | GET /api/contacts | + | create_contact | Kontakt anlegen | POST /api/contacts | + | update_contact | Kontakt aktualisieren | PUT /api/contacts/:id | + | upload_file | Datei hochladen | POST /api/vehicles/:id/files | + | list_files | Dateien auflisten | GET /api/vehicles/:id/files | + | create_sale | Verkauf anlegen | POST /api/sales | + | get_sale | Verkaufsdetails | GET /api/sales/:id | + | generate_contract | Vertrag generieren | POST /api/sales/:id/contract | + | export_datev | DATEV-Export | POST /api/datev/export | + | retouch_image | Bild retuschieren | POST /api/retouch/process | + | price_compare | Preisvergleich | POST /api/retouch/price-compare | + | get_statistics | Dashboard-Statistiken | GET /api/statistics | + | search_copilot | Eigene Historie durchsuchen | GET /api/copilot/history | + + **Echt ALLES** – inkl. Admin-Functions (nur für Admin-Role): + | Function | Beschreibung | Endpoint | + |---|---|---| + | list_users | User auflisten | GET /api/users | + | create_user | User anlegen | POST /api/users | + | deactivate_user | User deaktivieren | DELETE /api/users/:id | + | update_letterhead | Briefpapier ändern | PUT /api/settings/letterhead | + + ### 17.4 Safety & Confirmation + + **Action Preview Pattern** (wie in T07 definiert): + 1. LLM schlägt Action vor (Function + Arguments) + 2. Frontend zeigt Action Preview: "Ich möchte folgendes tun: [Action]" + 3. User bestätigt oder ablehnt + 4. Bei Bestätigung: Copilot führt Action aus (interner API-Call mit User-JWT) + 5. Ergebnis wird im Chat angezeigt + + **Auto-Execute bei Read-Only Actions**: + - search_vehicles, search_contacts, get_sale, list_files, get_statistics, + price_compare → automatisch ausgeführt (kein Bestätigung nötig) + - Alle schreibenden Actions → User-Bestätigung erforderlich + + **RBAC durchgesetzt**: Copilot nutzt den JWT des aktuellen Users. + Ein Verkäufer kann keine User verwalten, auch nicht über den Copilot. + + ### 17.5 System-Prompt Struktur + + ``` + Du bist der KI-Assistent für ein Nutzfahrzeug-ERP-System. + Du kannst Fahrzeuge verwalten, Kontakte anlegen, Verkäufe erstellen, + OCR-Scans verarbeiten, Bilder retuschieren und DATEV-Exporte erstellen. + + Verfügbare Aktionen: + [Liste aller Functions mit Beschreibung und Parameters] + + Regeln: + - Bei schreibenden Aktionen: schlage die Aktion vor und warte auf Bestätigung + - Bei Lese-Aktionen: führe sie direkt aus und präsentiere die Ergebnisse + - Antworte auf Deutsch (oder Englisch je nach User-Setting) + - Wenn Informationen fehlen: frage nach + ``` + + ### ADR-012: Function Calling für KI-Vollsteuerung + **Status**: Accepted + **Entscheidung**: OpenRouter Function Calling für alle System-Aktionen. + **Begründung**: Strukturierte JSON-Ausgabe, zuverlässig, von Claude/GPT-4 + unterstützt. User-Bestätigung für schreibende Aktionen als Safety-Mechanismus. + **Alternativen**: Free-text parsing (fehleranfällig), MCP Protocol + (zu neu, noch nicht etabliert). + + --- + + ## 18. KI Multi-Provider Support + + ### 18.1 OpenRouter als Unified Gateway + + **Entscheidung**: OpenRouter als alleiniges KI-Gateway. Keine direkten + Provider-Anbindungen. + + **Begründung**: OpenRouter bietet: + - **400+ Modelle** von allen major providers (OpenAI, Anthropic, Google, Meta, Mistral, etc.) + - **Ein API-Key** für alle Modelle (kein separates Key-Management) + - **OpenAI-kompatible API** (drop-in replacement) + - **Provider-Routing**: Automatic fallback auf Backup-Provider bei Ausfall + - **Side-by-side Pricing**: Kostenvergleich aller Modelle + - **Tool Calling Support**: Modelle mit Function Calling verfügbar + + ### 18.2 Modell-Konfiguration pro Use-Case + + Der User kann in den Admin-Settings konfigurieren, welches Modell für + welchen Use-Case verwendet wird: + + | Use Case | Default Modell | Alternative | + |---|---|---| + | OCR (ZB I/II) | qwen/qwen-2.5-vl-72b-instruct | openai/gpt-4o | + | Bild-Retusche | black-forest-labs/flux-1.1-pro | stabilityai/stable-diffusion-xl | + | Copilot (Chat) | anthropic/claude-3.5-sonnet | openai/gpt-4o, google/gemini-flash-1.5 | + | Copilot (Voice STT) | openai/whisper-large-v3 | – | + + **Settings Endpoint**: + ``` + GET /api/settings/ai-models → Aktuelle Modell-Konfiguration + PUT /api/settings/ai-models → Modell-Konfiguration aktualisieren (admin only) + ``` + + **Config in DB** (Settings-Tabelle): + ```json + { + "ocr_model": "qwen/qwen-2.5-vl-72b-instruct", + "retouch_model": "black-forest-labs/flux-1.1-pro", + "copilot_chat_model": "anthropic/claude-3.5-sonnet", + "copilot_voice_model": "openai/whisper-large-v3" + } + ``` + + ### 18.3 Provider-Fallback + + OpenRouter bietet automatisches Provider-Routing: + - Bei Ausfall des primären Providers → automatischer Fallback auf Backup-Provider + - Beispiel: anthropic/claude-3.5-sonnet → bei Anthropic-Ausfall → + OpenRouter versucht alternative Provider für dasselbe Modell + + **Eigenes Fallback-Handling** (zusätzlich): + ```python + COPILOT_MODELS = [ + "anthropic/claude-3.5-sonnet", + "openai/gpt-4o", + "google/gemini-flash-1.5", + "meta-llama/llama-3.1-70b-instruct" + ] + # Bei Fehler: nächstes Modell in der Liste probieren + ``` + + ### 18.4 Kostenkontrolle + + - OpenRouter zeigt Token-Kosten pro Request + - Logging: ai_usage_log Tabelle (model, tokens_in, tokens_out, cost, user_id, timestamp) + - Rate-Limiting pro User (Redis Token Bucket) + - Monatsbudget konfigurierbar (Admin-Setting ai_monthly_budget_eur) + - Bei Überschreitung: Warnung + ggf. Drosselung + + ### ADR-013: OpenRouter als alleiniges KI-Gateway + **Status**: Accepted + **Entscheidung**: Alle KI-Aufrufe über OpenRouter, keine direkten Provider-APIs. + **Begründung**: Ein API-Key, 400+ Modelle, Provider-Routing, einheitliche API, + keine Multi-SDK-Wartung. Modell-Konfiguration pro Use-Case ermöglicht + flexible Anpassung ohne Code-Änderung. + **Alternativen**: Direkte OpenAI + Anthropic APIs (Multi-Key-Management, + höhere Wartung), lokale Modelle via Ollama (zu langsam für OCR/Retusche, + aber möglich als Fallback für Copilot-Chat). + + ### 18.5 Optional: Lokale Modelle via Ollama (Copilot Fallback) + + Für den Copilot-Chat (nicht OCR/Retusche) kann optional ein lokales Modell + via Ollama als Offline-Fallback konfiguriert werden: + - Feature-Flag local_model_fallback_enabled + - Ollama läuft als 5. Container (optional, nur bei Bedarf) + - Modell: llama3.1:8b-instruct oder qwen2.5:14b + - Use Case: Offline-Betrieb oder Kostenersparnis für einfache Chat-Tasks + - Function Calling Support: Llama 3.1 unterstützt native Tool-Calling + + **Architektur-Erweiterung** (v2, Feature-Flag): + ``` + Copilot Request → Try OpenRouter → Fail → Try local Ollama → Fail → Error + ``` + + --- + + ## 13. Konventionen + + ### 13.1 Backend + - async/await für alle DB-Operationen und externen API-Calls + - Pydantic v2 für alle Request/Response-Schemas + - SQLAlchemy 2.0 mit typed Mapped columns + - Alembic für DB-Migrationen (kein auto-create in Produktion) + - Router-Prefix: `/api/` + - Error-Format: `{"error": {"code": "VEHICLE_NOT_FOUND", "message": "...", "details": {}}}` + + ### 13.2 Frontend + - TypeScript strict mode + - Tailwind CSS für Styling (kein CSS-in-JS) + - Server Components für statische Daten, Client Components für Interaktion + - API-Client: zentrale fetch-Wrapper mit Auth-Header-Injection + - Date-Naming: kebab-case für Dateien, camelCase für Variablen + + ### 13.3 Git + - Conventional Commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:` + - Branch-Naming: `feature/T01-vehicle-module`, `fix/...`, `hotfix/...` + - PRs erforderlich für main-Branch + +## 19. Provider Architecture (Multi-Provider mit Ollama Cloud) + +### 19.1 Provider Abstraktion (ABC Pattern) + +```python +from abc import ABC, abstractmethod + +class BaseAIProvider(ABC): + @abstractmethod + async def chat(self, messages, tools=None, model=None) -> dict: ... + @abstractmethod + async def vision(self, image_base64, prompt, model=None) -> str: ... + @abstractmethod + async def image_edit(self, image_base64, instruction, model=None) -> str: ... + @abstractmethod + def list_models(self) -> list[str]: ... + @abstractmethod + def health_check(self) -> bool: ... + +class OpenRouterProvider(BaseAIProvider): ... +class OllamaCloudProvider(BaseAIProvider): ... +class OllamaLocalProvider(BaseAIProvider): ... +``` + +Neue Provider durch Implementierung von BaseAIProvider + Eintrag in providers.yaml hinzufügbar. + +### 19.2 providers.yaml + +```yaml +providers: + openrouter: + enabled: true + api_base: https://openrouter.ai/api/v1 + api_key: ${OPENROUTER_API_KEY} + models: [anthropic/claude-3.5-sonnet, openai/gpt-4o, qwen/qwen2.5-vl] + ollama_cloud: + enabled: true + api_base: ${OLLAMA_CLOUD_URL} + api_key: ${OLLAMA_CLOUD_API_KEY} + models: [glm-5.2:cloud, qwen2.5:14b, llava:13b] + ollama_local: + enabled: false + api_base: http://ollama:11434 + models: [llama3.1:8b-instruct, qwen2.5:14b] +``` + +### 19.3 Use-Case → Provider Mapping + +| Use Case | Default | Fallback | +|---|---|---| +| Copilot Chat | ollama_cloud (glm-5.2) | openrouter (claude-3.5) | +| OCR | openrouter (qwen2.5-vl) | ollama_cloud (llava) | +| Bildretusche | openrouter (flux.1-pro) | - | +| Voice | openrouter | browser-native | + +Admin konfigurierbar via PUT /api/settings/ai-providers + +### ADR-014: Multi-Provider mit Ollama Cloud +- Ollama Cloud als ECHTER Provider (nicht nur Fallback) +- Provider-Abstraktion ermöglicht beliebige zukünftige Provider +- Admin kann pro Use-Case Provider wählen diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..de314e7 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,412 @@ +# AGENTS.md – ERP Nutzfahrzeuge Implementation Guide + +**Version:** 1.0.0 +**Datum:** 2026-07-12 + +--- + +## 1. Project Overview + +ERP-System für Nutzfahrzeug-/Baumaschinen-Handel (~10 Nutzer). +- **Backend:** Python 3.12 / FastAPI / SQLAlchemy 2.0 async / PostgreSQL 16 / Redis 7 +- **Frontend:** Next.js 14 (App Router) / React 18 / TypeScript / Tailwind CSS / next-intl +- **Hosting:** Coolify (Docker Compose) auf coolify-01 (46.225.91.159) +- **KI:** OpenRouter (Qwen2.5-VL OCR, Flux.1-Pro Bild, Claude/GPT-4 Copilot) +- **External:** mobile.de Seller API (Push-Only) + +### Architecture Reference +- `docs/architecture.md` – Complete architecture, DB schema, API design, ADRs +- `docs/task_graph.json` – Task breakdown with test specs +- `docs/requirements.md` – Full requirements (8 modules, 23 features) +- `docs/component_inventory.md` – UI components and states + +--- + +## 2. Build & Test Commands + +### Backend +```bash +# Install dependencies +cd backend +pip install -r requirements.txt + +# Run dev server +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Run all tests +python -m pytest tests/ -v --cov=app --cov-report=term-missing --cov-report=html + +# Run specific test file +python -m pytest tests/test_vehicles.py -v + +# Run with coverage for specific module +python -m pytest tests/test_auth.py -v --cov=app/services/auth_service --cov-report=term-missing + +# Lint +ruff check app/ +ruff format app/ --check + +# Database migrations +alembic revision --autogenerate -m "description" +alembic upgrade head +alembic downgrade -1 + +# Type check (optional but recommended) +mypy app/ --ignore-missing-imports +``` + +### Frontend +```bash +cd frontend +npm install + +# Run dev server +npm run dev + +# Run all tests +npx vitest run --coverage + +# Run specific test +npx vitest run src/components/vehicles --coverage + +# Lint +npx eslint src/ --max-warnings 0 +npx prettier --check src/ + +# Build +npm run build + +# Type check +npx tsc --noEmit +``` + +### Docker +```bash +# Build and start all services +docker-compose up -d --build + +# View logs +docker-compose logs -f backend +docker-compose logs -f frontend + +# Run database migration in container +docker-compose exec backend alembic upgrade head + +# Run tests in container +docker-compose exec backend python -m pytest tests/ -v +``` + +--- + +## 3. Test Rules (MANDATORY) + +### TDD Principle +- **Write tests first or alongside implementation** – never after +- Every PR must include tests +- Coverage target: **≥80%** per module + +### Do NOT Modify Existing Tests +- Tests are written by the task spec and must not be modified to make them pass +- If a test fails, fix the **implementation**, not the test +- Exception: test data fixtures can be extended, but existing assertions must not be weakened + +### Backend Test Conventions +- **Framework:** pytest + pytest-asyncio + httpx.AsyncClient +- **Test DB:** Separate PostgreSQL database `erp_test` (NOT SQLite) +- **Fixtures:** `conftest.py` provides: + - `async_db` – async SQLAlchemy session (rolled back after each test) + - `client` – httpx AsyncClient with app + - `auth_headers` – JWT headers for admin/verkaeufer/buchhaltung roles + - `seed_data` – minimal seed data (users, vehicle, contact) +- **Mocking:** OpenRouter API MUST be mocked in all tests (no real API calls) +- **Naming:** `test__.py` or `test_.py` + +### Frontend Test Conventions +- **Framework:** Vitest + React Testing Library +- **E2E:** Playwright for critical paths (login, vehicle create, sale wizard) +- **Mocking:** API calls mocked via `vi.mock()` or MSW (Mock Service Worker) +- **Naming:** `ComponentName.test.tsx` in `__tests__/` folder + +### Test Spec Compliance +Every task in `task_graph.json` has a `test_spec` with: +- `commands` – Exact commands to run +- `expected_results` – What success looks like +- `test_files` – Expected test file paths +- `coverage_target` – Minimum coverage percentage + +**ALL commands in test_spec.commands MUST pass before a task is considered done.** + +--- + +## 4. Code Conventions + +### Python (Backend) +```python +# Naming +snake_case for variables, functions, methods, modules +PascalCase for classes (Models, Schemas, Services) +UPPER_SNAKE_CASE for constants + +# Imports (ruff isort) +# Standard library +import os +from datetime import datetime +# Third party +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +# Local +from app.deps import get_db, get_current_user +from app.models.vehicle import Vehicle +from app.schemas.vehicle import VehicleCreate, VehicleResponse + +# Type hints (mandatory) +async def get_vehicle(vehicle_id: UUID, db: AsyncSession) -> Vehicle: + ... + +# Async everywhere (SQLAlchemy 2.0 async) +result = await db.execute(select(Vehicle).where(Vehicle.id == vehicle_id)) +vehicle = result.scalar_one_or_none() + +# Error handling +if vehicle is None: + raise HTTPException(status_code=404, detail={"error": {"code": "NOT_FOUND", "message": "Vehicle not found"}}) +``` + +### TypeScript (Frontend) +```typescript +// Naming +camelCase for variables, functions, hooks +PascalCase for components, types, interfaces +UPPER_SNAKE_CASE for constants + +// Imports order +// 1. React/Next +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +// 2. Third party +import { useTranslations } from 'next-intl'; +// 3. Local +import { Button } from '@/components/ui/Button'; +import { apiClient } from '@/lib/api-client'; + +// Components: function declaration with explicit return type +export function FahrzeugListe(): JSX.Element { + ... +} + +// Types: interface for props +type FahrzeugListeProps = { + vehicles: Vehicle[]; + isLoading: boolean; +}; +``` + +### File Organization +- **One model per file** in `models/` +- **One router per module** in `routers/` +- **One service per domain** in `services/` +- **One schema set per module** in `schemas/` +- **Components grouped by feature** in `components//` + +--- + +## 5. Forbidden Patterns + +### Backend +- ❌ **No raw SQL queries** – always use SQLAlchemy ORM +- ❌ **No synchronous database calls** – always async (`await db.execute(...)`) +- ❌ **No secrets in code** – use environment variables / pydantic-settings +- ❌ **No `print()` in production code** – use Python `logging` +- ❌ **No bare `except:`** – always catch specific exceptions +- ❌ **No `# type: ignore`** without comment explaining why +- ❌ **No circular imports** – use dependency injection +- ❌ **No business logic in routers** – routers only validate + call service +- ❌ **No direct model access from routers** – always go through service layer +- ❌ **No unencrypted PII** – Ausweisdaten MUST be AES-256 encrypted +- ❌ **No real OpenRouter API calls in tests** – always mock +- ❌ **No `commit()` in services without explicit reason** – let caller control transactions + +### Frontend +- ❌ **No `any` type** – use proper TypeScript types +- ❌ **No `console.log` in production** – use a logger utility +- ❌ **No inline styles** – use Tailwind classes or CSS modules +- ❌ **No direct `fetch()` without auth wrapper** – use `apiClient` +- ❌ **No hardcoded API URLs** – use `NEXT_PUBLIC_API_URL` env var +- ❌ **No hardcoded translation strings** – use `t('key')` from next-intl +- ❌ **No prop drilling > 2 levels** – use context or state management +- ❌ **No `useEffect` for data fetching without loading/error states** +- ❌ **No forms without validation** – all inputs must have validation +- ❌ **No missing `aria-label`** on icon-only buttons + +### General +- ❌ **No commits to main without passing tests** +- ❌ **No large PRs** – max 800 lines per task +- ❌ **No TODO comments without ticket reference** +- ❌ **No commented-out code in PRs** +- ❌ **No files > 500 lines** – split into modules + +--- + +## 6. Token Rule for File Operations + +When working with files (reading, creating, updating): + +### Backend (Forgejo API) +```json +// READ file (only for MODIFY, not for reference) +{"tool_name":"forgejo","tool_args":{"action":"files_get","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py"}} + +// CREATE new file +{"tool_name":"forgejo","tool_args":{"action":"files_create","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py","content":"","message":"Add vehicle model","branch":"main"}} + +// UPDATE existing file (requires SHA from files_get) +{"tool_name":"forgejo","tool_args":{"action":"files_update","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/vehicle.py","content":"","sha":"","message":"Update vehicle model","branch":"main"}} + +// CHECK file existence +{"tool_name":"forgejo","tool_args":{"action":"files_list","owner":"Leopoldadmin","repo":"erp-nutzfahrzeuge","path":"backend/app/models/"}} +``` + +### Rules +- Use `files_get` ONLY when you need to modify an existing file (need SHA) +- Use `files_create` for new files +- Use `files_list` to check existence before creating +- **NEVER** copy file contents inline in task descriptions – reference by path +- **NEVER** read entire files just for reference – use `curl + sed` for snippets +- For large reference content (architecture.md, requirements.md): reference by path, don't inline + +--- + +## 7. Task Execution Workflow + +### For implementation_engineer + +1. **Read the task** from `task_graph.json` (task ID: T0X) +2. **Read architecture.md** sections relevant to the task (DB schema, API design, ADRs) +3. **Read requirements.md** for the specific feature IDs listed in `requirement_ids` +4. **Implement backend first:** Model → Schema → Service → Router → Tests +5. **Implement frontend:** Components → Pages → Tests +6. **Run ALL test_spec.commands** and ensure they pass +7. **Run lint** (ruff for backend, eslint for frontend) +8. **Run build** (npm run build for frontend) +9. **Report results** with test output evidence (not just "done") + +### Evidence Requirements (MANDATORY) +- Test command output showing pass/fail counts +- Coverage report showing ≥80% +- Lint output showing 0 errors +- Build output showing success +- "File written" is NOT evidence. "Commit made" is NOT evidence. +- Test output with pass counts IS evidence. + +### Task Completion Checklist +- [ ] All acceptance_criteria from task_graph.json verified +- [ ] All test_spec.commands pass +- [ ] Coverage ≥ coverage_target for covered modules +- [ ] Ruff lint clean (backend) +- [ ] ESLint clean (frontend) +- [ ] Build succeeds (frontend) +- [ ] No forbidden patterns introduced +- [ ] Test evidence provided in report + +--- + +## 8. Environment Variables (Names Only) + +### Backend +``` +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 +``` + +### Frontend +``` +NEXT_PUBLIC_API_URL=http://backend:8000/api/v1 +NEXTAUTH_SECRET=${NEXTAUTH_SECRET} +``` + +--- + +## 9. Git Workflow + +### Branches +- `main` – Production branch, deployed via Coolify +- `feature/T0X-` – Feature branch per task +- `fix/T0X-` – Fix branch + +### Commit Messages +``` +feat(T02): add vehicle CRUD with type-specific fields +fix(T02): correct FIN validation for 17-char check +test(T02): add mobile.de batch push tests +refactor(T02): extract field mapping to separate function +docs(T02): update vehicle API documentation +``` + +### PR Process +1. Create feature branch from main +2. Implement + test + lint + build +3. Commit with conventional commit format +4. Push branch +5. Create PR with task ID reference +6. CI runs: pytest, ruff, vitest, eslint, build +7. Merge after CI passes + +--- + +## 10. Database Migration Rules + +- **Always use Alembic** for schema changes +- **Never** modify database directly (psql, pgadmin) +- **One migration per schema change** – don't bundle multiple changes +- **Test migration up AND down** before committing +- **Seed data:** Use a separate seed script (`scripts/seed.py`), not migrations +- **Cleanup after migration tasks:** + ```bash + alembic upgrade head # Apply + # Verify all tables created + python -c "from app.database import engine; from sqlalchemy import inspect; insp = inspect(engine); print(insp.get_table_names())" + # Run seed data + python scripts/seed.py + # Verify foreign keys + docker-compose exec postgres psql -U erp_user -d erp_db -c "\d vehicles" + ``` + +--- + +## 11. OpenRouter Integration Rules + +- **Always use** `openrouter_client.py` shared client – never call OpenRouter API directly +- **Always set timeout** to 30 seconds +- **Always log** interaction to `ai_interactions` table (type, model, tokens, metadata) +- **Never send** Ausweisdaten or personal customer data to OpenRouter +- **Mock OpenRouter** in all automated tests +- **Handle errors gracefully:** API error → 502, timeout → 504, rate limit → 429 +- **Model selection** via config, not hardcoded in service + +--- + +## 12. Security Checklist + +- [ ] JWT authentication on all endpoints (except /auth/login, /auth/refresh, /health) +- [ ] RBAC enforced (require_role decorator on every router) +- [ ] Input validation via Pydantic schemas on all endpoints +- [ ] SQL injection protection via SQLAlchemy ORM (no raw SQL) +- [ ] File upload: MIME-type allowlist + size limit +- [ ] Ausweisdaten: AES-256-GCM encryption +- [ ] CORS: only configured origins +- [ ] Audit log: all create/update/delete actions logged +- [ ] No secrets in code or git +- [ ] HTTPS via Traefik/Let's Encrypt +- [ ] DSGVO: no PII to OpenRouter, soft-delete with retention concept diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d919714 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,1156 @@ +# 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 ` (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 + → 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 `