chore: migrate project documentation into repo (18 files)

This commit is contained in:
CRM Bot
2026-06-08 23:56:27 +00:00
parent 822ffb6ccb
commit 3f5b7df178
18 changed files with 2982 additions and 0 deletions
+392
View File
@@ -0,0 +1,392 @@
# CRM System Requirements (v1.0 MVP)
> Phase: INTAKE → REQUIREMENTS | Single-Tenant CRM | Status: Draft for User Approval
> Stack: FastAPI + SQLAlchemy (async) + Alembic + Pydantic + SQLite/PostgreSQL + Alpine.js + Tailwind + Docker + Coolify
> Auth: JWT im localStorage | Deployment: Coolify `localhost` standalone (uuid `lw80w8scs4044gwcw084s00s4`)
## 1. Vision & Scope
**Was ist v1?**
v1 ist ein leichtgewichtiges, self-hosted CRM für kleine bis mittelgroße Sales-Teams (525 Vertriebler), die ihre Pipeline, Accounts, Contacts und Verkaufsaktivitäten an einem Ort bündeln wollen ohne Salesforce-Lock-in, ohne Cloud-Zwang, ohne monatliche Lizenzkosten. Kern ist die tägliche Arbeit eines Sales-Reps: Account anlegen, Contact verknüpfen, Deal durch die Pipeline ziehen, Aktivitäten (Calls/Meetings/Notes) tracken, im Dashboard den Überblick behalten. Single-Tenant bedeutet: Eine Org, ein Server, ein Datenbestand dafür aber voller Datenschutz, DSGVO-Konformität out-of-the-box und Coolify-Deployment per Knopfdruck.
**Was ist v1 NICHT?**
v1 ist **kein** Enterprise-CRM mit Multi-Tenancy, komplexem Permission-Granular-RBAC oder AI-Features. v1 hat **kein** integriertes Email-Marketing, keine Marketing-Automation, keine Lead-Scoring-Maschine. v1 ist **kein** Mobile-First-System (responsive Web ja, native App nein), **kein** Realtime-Collaboration-Tool (kein Websocket-Editing wie in Notion), und **kein** vollumfängliches Reporting-Suite (KPIs ja, Custom-Reports/Exports nein). v1 verzichtet bewusst auf komplexe Workflow-Engines, Sales-Territory-Management und Product-Catalog-Features.
**Annahmen:**
- Eine Org, ein Deployment, ein Datenbank-Container kein Mandanten-Trennungs-Layer
- Erster Admin wird per Bootstrap-Registrierung angelegt (kein Invite-Only-Flow)
- Maximale User-Anzahl: 50 (Soft-Cap, Hard-Limit erst in v2 via DB-Constraint)
- Sales-Reps arbeiten im 1:1 mit Accounts/Contacts, kein Shared-Inbox-Pattern
- Browser: aktuelle Evergreen-Chrome/Firefox/Safari/Edge, kein IE11
- Internet-Anbindung an Coolify-Server ist stabil, kein Offline-Modus
**Non-Goals (explizit out):**
- Multi-Tenancy / Mandantenfähigkeit (auch wenn `org_id` bereits in DB vorbereitet wird)
- Email-Marketing-Automation (Newsletter, Drip-Campaigns)
- Mobile-App (iOS/Android)
- Lead-Scoring / AI-gestützte Next-Best-Action
- Custom-Report-Builder / BI-Integration
- Rechnungsstellung / Quote-to-Cash (kein CPQ)
- Marketing-Attribution / UTM-Tracking
- Telefonie-Integration (CTI, Twilio, etc.)
- Slack/Teams-Integration
- Custom-Fields-Builder (DB-Schema ist fix für v1)
## 2. Personas & Haupt-Use-Cases
**Persona 1: Sales-Rep (Primärnutzer)**
- Use-Case: „Ich logge mich morgens ein, sehe im Dashboard meine offenen Deals und überfälligen Activities, arbeite die Calls ab, verschiebe gewonnene Deals in die Won-Stage, lege neue Accounts für Inbound-Leads an."
- Use-Case: „Ich durchsuche meine Accounts nach Branche/Größe, öffne einen Account, sehe alle Contacts und letzten Activities, plane für nächste Woche ein Follow-up-Meeting und vergebe Tags zur Segmentierung."
- Use-Case: „Ich öffne die Pipeline-View, sehe alle meine Deals als Kanban, ziehe einen Deal von Qualified' nach Proposal' und ergänze einen Note mit dem Verhandlungsstand."
**Persona 2: Sales-Manager**
- Use-Case: „Ich logge mich ein, prüfe die Team-Pipeline im Dashboard, sehe Won-this-Month, Conversion-Rate, und filtere die Pipeline auf den Rep, der Backlog hat."
- Use-Case: „Ich lege einen neuen Sales-Rep-User an, vergebe die Rolle, und der Rep kann sofort loslegen."
- Use-Case: „Ich passe Org-Info an (Name, Logo-URL, Default-Currency) und sehe im Audit-Log (falls aktiviert), wer wann welche Einstellung geändert hat."
**Persona 3: Admin (technisch/organisatorisch)**
- Use-Case: „Ich führe das initiale Bootstrap-Setup durch, lege den ersten Admin an, konfiguriere Org-Settings, und überwache Health/Backups in Coolify."
## 3. Funktionale Anforderungen (FR)
### FR-1: Authentication & User Management
- **FR-1.1** Registrierung des ersten Admin-Users (Bootstrap, nur einmalig möglich wenn `users`-Tabelle leer)
- **FR-1.2** Login (email + password) → JWT (24h expiry, HS256, im Response-Body)
- **FR-1.3** Logout (JWT clientseitig verworfen, optional Server-seitige Token-Blacklist in v2)
- **FR-1.4** Passwort-Reset (token-basiert, per Email, Token 1h gültig, v1: Token-Endpoint-Output für Tests, SMTP-Integration out-of-scope)
- **FR-1.5** User-CRUD (nur Admin): Anlegen, Bearbeiten, Soft-Delete, Rolle zuweisen (Admin / Sales-Manager / Sales-Rep)
- **FR-1.6** `/api/users/me` Endpunkt für aktuellen User (für Auth-Validierung & UI-Profil)
**Akzeptanzkriterien (testbar):**
- [ ] POST /api/auth/register mit gültigem Payload (email, password ≥ 8 Zeichen, name) → 201 + User-Objekt + JWT
- [ ] POST /api/auth/register mit existierender Email → 409 + generische Fehlermeldung
- [ ] POST /api/auth/login mit korrekten Credentials → 200 + JWT + User-Objekt
- [ ] POST /api/auth/login mit falschen Credentials → 401 + generische Fehlermeldung (kein Email-Leak)
- [ ] GET /api/users/me mit gültigem JWT → 200 + User-Daten (ohne password_hash)
- [ ] GET /api/users/me ohne JWT → 401
- [ ] GET /api/users/me mit expired JWT → 401 + Hinweis "token_expired"
- [ ] DB-User wird in `users` Tabelle angelegt mit gehashtem `password_hash` (bcrypt oder argon2)
- [ ] Zweiter POST /api/auth/register nach erfolgreichem ersten → 403 (Bootstrap bereits erfolgt)
### FR-2: Accounts (Firmen/Kunden)
- **FR-2.1** Account anlegen (Pflicht: name; Optional: website, address (street/city/zip/country), industry, size [1-10/11-50/51-200/201-1000/1000+], owner_id default = current_user)
- **FR-2.2** Account anzeigen, bearbeiten (PATCH), soft-löschen (DELETE → `deleted_at` timestamp)
- **FR-2.3** Account-Liste mit Paginierung (page, page_size ≤ 100), Volltext-Suche (name), Filter (industry, size, owner_id)
- **FR-2.4** Account-Detail-Page mit allen zugehörigen Contacts, Deals, Activities, Notes (via Eager-Loading)
- **FR-2.5** Ownership-Transfer (PATCH owner_id, mit Audit-Logging in v2)
**Akzeptanzkriterien:**
- [ ] POST /api/accounts mit gültigem Payload → 201 + Account-Objekt
- [ ] POST /api/accounts mit fehlendem Pflichtfeld `name` → 422 (Pydantic validation error)
- [ ] GET /api/accounts?page=1&page_size=20 → 200 + Paginierte Liste (items, total, page, page_size)
- [ ] GET /api/accounts?industry=SaaS&size=51-200 → 200 + gefilterte Liste
- [ ] GET /api/accounts/{id} → 200 + Account-Details mit relationen (contacts[], deals[], activities[])
- [ ] PATCH /api/accounts/{id} mit Teil-Update → 200 + aktualisiertes Objekt
- [ ] DELETE /api/accounts/{id} → 204 (soft-delete: `deleted_at` gesetzt, Datensatz bleibt in DB)
- [ ] DB-Account in `accounts` Tabelle angelegt, FK zu `users` (owner_id) ON DELETE RESTRICT
### FR-3: Contacts (Personen)
- **FR-3.1** Contact anlegen (Pflicht: first_name, last_name; Optional: email, phone, title, account_id)
- **FR-3.2** Contact bearbeiten (PATCH), soft-löschen, duplizieren (POST /api/contacts/{id}/duplicate)
- **FR-3.3** Contact-Liste mit Suche (first_name, last_name, email ILIKE), Filter (account_id, owner_id)
- **FR-3.4** Email-Uniqueness nur innerhalb einer Org (nicht global), damit mehrere Reps denselben Contact anlegen können
**Akzeptanzkriterien:**
- [ ] POST /api/contacts mit gültiger account_id → 201, FK-Constraint geprüft (Account existiert)
- [ ] POST /api/contacts mit ungültiger account_id → 404 (Account not found)
- [ ] GET /api/contacts?q=muster&account_id=5 → 200 + gefilterte Liste
- [ ] POST /api/contacts/{id}/duplicate → 201 + neuer Contact (Suffix "(Kopie)" im first_name, ohne Notes/Activities)
### FR-4: Deals (Verkaufschancen/Pipeline)
- **FR-4.1** Deal anlegen (Pflicht: title, account_id; Optional: value, currency [default EUR], stage [default "Lead"], close_date, owner_id)
- **FR-4.2** Pipeline-View (Kanban-Endpoint: GET /api/deals/pipeline → Deals gruppiert nach stage, sortiert nach close_date ASC)
- **FR-4.3** Stage-Transition (PATCH /api/deals/{id}/stage) mit History-Tracking in `deal_stage_history`
- **FR-4.4** Won/Lost-Tracking: Stage "Won" oder "Lost" erfordert `won_reason` bzw. `lost_reason` (text, pflicht im Request-Body)
- **FR-4.5** Drag-and-Drop im Frontend: Optimistic Update + PATCH, bei Fehler Rollback + Toast
**Akzeptanzkriterien:**
- [ ] POST /api/deals mit gültigem Payload → 201
- [ ] PATCH /api/deals/{id}/stage mit neuer stage → 200, History-Eintrag in `deal_stage_history` (from_stage, to_stage, changed_by, changed_at)
- [ ] PATCH /api/deals/{id}/stage mit stage="Won" ohne `won_reason` → 422
- [ ] GET /api/deals/pipeline → 200 + JSON mit Stages als Keys, Deal-Listen als Values
- [ ] DELETE /api/deals/{id} → 204 (soft-delete, keine Cascade-Löschung der History)
### FR-5: Activities (Calls, Meetings, Emails, Notes-Lite)
- **FR-5.1** Activity anlegen (Pflicht: type [call/meeting/email/task], subject; Optional: body, due_date, account_id?, contact_id?, deal_id?)
- **FR-5.2** Activity-Liste mit Filter (type, status [open/completed/overdue], owner_id, due_date_from/to)
- **FR-5.3** Activity-Complete (PATCH /api/activities/{id}/complete) mit Outcome-Text und `completed_at = now()`
- **FR-5.4** Overdue-View: GET /api/activities?overdue=true → nur Activities mit `due_date < now AND completed_at IS NULL`
**Akzeptanzkriterien:**
- [ ] POST /api/activities mit Polymorphic-FK (z.B. nur account_id) → 201
- [ ] POST /api/activities mit unbekanntem type → 422
- [ ] GET /api/activities?overdue=true → 200 + nur überfällige Activities
- [ ] PATCH /api/activities/{id}/complete mit outcome → 200 + completed_at gesetzt
### FR-6: Tags & Notes
- **FR-6.1** Tag-System (color [hex], name) auf Accounts/Contacts/Deals verknüpfbar
- **FR-6.2** Notes (Polymorphic, freier Text + author_id + created_at, parent_type ∈ {account, contact, deal})
- **FR-6.3** Tag-CRUD (POST /api/tags, GET /api/tags, DELETE /api/tags/{id}) und Tag-Linking (POST /api/tags/link)
**Akzeptanzkriterien:**
- [ ] POST /api/tags mit gültigem Payload → 201
- [ ] POST /api/tags/link mit (tag_id, parent_type, parent_id) → 201, Link in `tag_links`
- [ ] GET /api/accounts/{id} liefert Tags inklusive der via `tag_links` verknüpften
- [ ] POST /api/notes mit parent_type="deal" und parent_id=42 → 201, Note polymorph verknüpft
### FR-7: Dashboard
- **FR-7.1** KPIs (GET /api/dashboard/kpis):
- `open_deals_count` (Deals nicht in Won/Lost)
- `open_deals_value` (Summe value aller offenen Deals)
- `won_this_month_count` (Deals mit stage=Won und close_date im aktuellen Monat)
- `won_this_month_value` (Summe value dieser Deals)
- `conversion_rate` (Won / (Won + Lost) in den letzten 90 Tagen, 01)
- **FR-7.2** Activity-Feed (GET /api/dashboard/feed?limit=20): Letzte 20 Activities gruppiert nach `date(created_at) = today` und `older`
**Akzeptanzkriterien:**
- [ ] GET /api/dashboard/kpis → 200 + JSON mit allen 5 KPIs, korrekt berechnet
- [ ] GET /api/dashboard/feed?limit=20 → 200 + max 20 Activities, neueste zuerst
- [ ] KPIs sind org-isoliert (kein Cross-Tenant-Leak)
### FR-8: Settings (User-Profile, Org-Info)
- **FR-8.1** User-Profile bearbeiten (PATCH /api/users/me): name, avatar_url, email_notification_prefs (JSON: {deal_won, activity_due})
- **FR-8.2** Org-Info bearbeiten (PATCH /api/org) nur Admin: name, logo_url, default_currency, timezone
- **FR-8.3** Email-Change erfordert Re-Authentifizierung (Password-Bestätigung) in v1.1; v1 erlaubt direkten PATCH
**Akzeptanzkriterien:**
- [ ] PATCH /api/users/me mit gültigem Payload → 200 + aktualisierter User
- [ ] PATCH /api/org als Non-Admin → 403
- [ ] PATCH /api/org als Admin mit gültigem Payload → 200 + aktualisierte Org
## 4. Nicht-funktionale Anforderungen (NFR)
- **NFR-1 Performance**: p95 Response-Time < 300 ms für Standard-CRUD-Endpoints (unter Last 50 concurrent Users, SQLite/PostgreSQL lokal); Pipeline-Endpoint < 500 ms p95
- **NFR-2 Security**:
- Passwort-Hashing: argon2id (Primary) oder bcrypt (Fallback), kein MD5/SHA1
- JWT: HS256 mit `AUTH_SECRET` ≥ 32 Zeichen aus Coolify Env-Vars
- Input-Validation: Pydantic v2 für alle Request-Bodies und Query-Params
- CORS-Whitelist: explizite Origins, kein `*`
- Rate-Limiting: 60 req/min/IP für Auth-Endpoints (SlowAPI oder ähnliches)
- SQL-Injection-Schutz: ausschließlich SQLAlchemy ORM/parameterisierte Queries
- **NFR-3 Skalierbarkeit**: Horizontale Skalierung über mehrere FastAPI-Container via Coolify (stateless App), PostgreSQL als shared DB; SQLite-Limit (single-writer) in Dev-Setup dokumentiert
- **NFR-4 Maintainability**:
- Type-Hints überall (mypy strict)
- Ruff/Black-Formatierung, pre-commit-Hook
- pytest-Coverage ≥ 70 % (Lines + Branches)
- OpenAPI-Spec wird automatisch generiert unter `/docs` und `/redoc`
- **NFR-5 Observability**:
- Strukturiertes Logging: JSON (python-json-logger), Felder `timestamp`, `level`, `request_id`, `user_id`, `path`, `method`, `status`, `latency_ms`
- `/health` Endpoint: 200 wenn DB-Connection OK, sonst 503
- `/metrics` Prometheus-compatible (optional v1.1, vorbereitet via `prometheus-fastapi-instrumentator`)
- Request-ID-Middleware für Tracing
- **NFR-6 Backup & Recovery**:
- Tägliches DB-Backup via Coolify-Side (PostgreSQL-Dump, 7 Tage Retention)
- Restore-Runbook in `docs/runbook-backup-restore.md`
- Recovery-Time-Objective (RTO) ≤ 4h, Recovery-Point-Objective (RPO) ≤ 24h
- **NFR-7 Internationalization (i18n)**:
- v1: Deutsch + Englisch, Strings in separater `locales/` JSON-Datei
- Frontend: einfaches i18n-Objekt, Sprache umschaltbar via LocalStorage
- vorbereitet für v2 (3+ Sprachen, Crowdin-Integration)
- **NFR-8 Accessibility (a11y)**:
- WCAG 2.1 AA: Kontraste, Keyboard-Navigation, ARIA-Labels
- Alpine.js + Tailwind ermöglichen semantisches HTML
## 5. Datenmodell-Skizze
```
users (1) ──< (N) accounts (1) ──< (N) contacts
└──< (N) deals ──< (N) activities
│ │
│ └──< (N) notes (polymorphic)
└──< (N) deal_stage_history
accounts, contacts, deals ──< (N) tags (polymorphic via tag_links)
```
**Wichtige Entities** (Felder-Übersicht, vollständige DDL in Phase 2):
| Entity | Schlüsselfelder | Indizes |
|---|---|---|
| `users` | id (UUID), email (unique, citext), password_hash, name, role (enum), org_id, created_at, deleted_at | UNIQUE (org_id, email) WHERE deleted_at IS NULL |
| `orgs` | id (UUID), name, logo_url, default_currency, timezone, created_at | |
| `accounts` | id (UUID), name, website, industry, size (enum), street, city, zip, country, owner_id, org_id, created_at, deleted_at | INDEX (org_id, deleted_at), INDEX (owner_id) |
| `contacts` | id (UUID), first_name, last_name, email, phone, title, account_id, owner_id, org_id, created_at, deleted_at | INDEX (account_id), INDEX (org_id, last_name) |
| `deals` | id (UUID), title, value (Decimal), currency, stage (enum), close_date, won_reason, lost_reason, account_id, owner_id, org_id, created_at, deleted_at | INDEX (org_id, stage), INDEX (owner_id, close_date) |
| `activities` | id (UUID), type (enum), subject, body, due_date, completed_at, outcome, account_id?, contact_id?, deal_id?, owner_id, org_id, created_at | INDEX (owner_id, completed_at), INDEX (due_date) WHERE completed_at IS NULL |
| `notes` | id (UUID), body (text), author_id, parent_type, parent_id, org_id, created_at | INDEX (parent_type, parent_id) |
| `tags` | id (UUID), name, color, org_id, created_at | UNIQUE (org_id, name) |
| `tag_links` | tag_id, parent_type, parent_id, org_id, created_at | UNIQUE (tag_id, parent_type, parent_id), INDEX (parent_type, parent_id) |
| `deal_stage_history` | id, deal_id, from_stage, to_stage, changed_by, changed_at | INDEX (deal_id, changed_at DESC) |
**Multi-Tenancy-Hinweis**: Single-Tenant v1, aber `org_id` Spalte ist in **allen** Tabellen mit dabei und alle Queries filtern automatisch nach `org_id = current_user.org_id`. Dies ermöglicht eine saubere Migration zu Multi-Tenant in v2 ohne Schema-Bruch (nur Middleware-Erweiterung).
## 6. API-Surface-Skizze (OpenAPI-Lite)
| Method | Path | Auth | Beschreibung |
|---|---|---|---|
| POST | /api/auth/register | | Erster Admin registrieren (Bootstrap) |
| POST | /api/auth/login | | Login → JWT |
| POST | /api/auth/refresh | JWT | JWT refreshen |
| POST | /api/auth/logout | JWT | Logout (clientseitig) |
| POST | /api/auth/password-reset/request | | Reset-Token anfordern |
| POST | /api/auth/password-reset/confirm | | Reset-Token + neues Passwort |
| GET | /api/users/me | JWT | Aktueller User |
| PATCH | /api/users/me | JWT | Eigene Profil-Daten bearbeiten |
| GET | /api/users | JWT+Admin | Alle User (paginiert) |
| POST | /api/users | JWT+Admin | User anlegen |
| PATCH | /api/users/{id} | JWT+Admin | User bearbeiten |
| DELETE | /api/users/{id} | JWT+Admin | User löschen (soft) |
| GET | /api/accounts | JWT | Accounts-Liste (paginiert, suchbar) |
| POST | /api/accounts | JWT | Account anlegen |
| GET | /api/accounts/{id} | JWT | Account-Detail inkl. Relationen |
| PATCH | /api/accounts/{id} | JWT | Account bearbeiten |
| DELETE | /api/accounts/{id} | JWT | Account löschen (soft) |
| GET | /api/contacts | JWT | Contacts-Liste |
| POST | /api/contacts | JWT | Contact anlegen |
| GET | /api/contacts/{id} | JWT | Contact-Detail |
| PATCH | /api/contacts/{id} | JWT | Contact bearbeiten |
| DELETE | /api/contacts/{id} | JWT | Contact löschen (soft) |
| POST | /api/contacts/{id}/duplicate | JWT | Contact duplizieren |
| GET | /api/deals | JWT | Deals-Liste (filterbar) |
| POST | /api/deals | JWT | Deal anlegen |
| GET | /api/deals/pipeline | JWT | Kanban-View (gruppiert) |
| GET | /api/deals/{id} | JWT | Deal-Detail |
| PATCH | /api/deals/{id} | JWT | Deal bearbeiten |
| PATCH | /api/deals/{id}/stage | JWT | Stage-Transition + History |
| DELETE | /api/deals/{id} | JWT | Deal löschen (soft) |
| GET | /api/activities | JWT | Activities-Liste (filterbar) |
| POST | /api/activities | JWT | Activity anlegen |
| GET | /api/activities/{id} | JWT | Activity-Detail |
| PATCH | /api/activities/{id} | JWT | Activity bearbeiten |
| PATCH | /api/activities/{id}/complete | JWT | Activity abschließen |
| DELETE | /api/activities/{id} | JWT | Activity löschen (soft) |
| GET | /api/notes | JWT | Notes (parent_type+parent_id) |
| POST | /api/notes | JWT | Note anlegen (polymorph) |
| DELETE | /api/notes/{id} | JWT | Note löschen |
| GET | /api/tags | JWT | Tags-Liste |
| POST | /api/tags | JWT | Tag anlegen |
| PATCH | /api/tags/{id} | JWT | Tag bearbeiten (name/color) |
| DELETE | /api/tags/{id} | JWT | Tag löschen (cascade zu tag_links) |
| POST | /api/tags/link | JWT | Tag mit Entity verknüpfen |
| DELETE | /api/tags/link | JWT | Tag-Verknüpfung löschen |
| GET | /api/dashboard/kpis | JWT | KPIs (5 Werte) |
| GET | /api/dashboard/feed | JWT | Activity-Feed (latest 20) |
| GET | /api/org | JWT | Org-Info lesen |
| PATCH | /api/org | JWT+Admin | Org-Info bearbeiten |
| GET | /health | | Healthcheck (200/503) |
| GET | /docs | | Swagger-UI |
| GET | /redoc | | ReDoc-UI |
| GET | /openapi.json | | OpenAPI-Spec (JSON) |
## 7. UI-Skizze (Pages)
- **/login** Login-Form (Email + Passwort), Redirect auf /
- **/register** Erster-Admin-Registrierung (Bootstrap), nur erreichbar solange `users`-Tabelle leer
- **/** Dashboard (KPI-Karten oben: Open Deals, Pipeline Value, Won This Month, Conversion Rate; Activity-Feed unten)
- **/accounts** Account-Liste (Tabelle mit Suche, Filter-Sidebar für Industry/Size, Pagination)
- **/accounts/{id}** Account-Detail (Tabs: Info | Contacts | Deals | Activities | Notes)
- **/contacts** Contact-Liste (Tabelle mit Suche nach Name/Email, Filter nach Account)
- **/contacts/{id}** Contact-Detail (Tabs: Info | Activities | Notes)
- **/pipeline** Kanban-Board (Deals gruppiert nach Stage als Spalten, Drag-and-Drop)
- **/activities** Activity-Liste (Tabelle mit Overdue-Highlight, Filter nach Type/Status)
- **/settings/profile** User-Profile (Name, Avatar-URL, Notification-Prefs)
- **/settings/users** User-Verwaltung (nur Admin, Tabelle + Invite-Form)
- **/settings/org** Org-Info (nur Admin, Name, Logo-URL, Default-Currency, Timezone)
- **/404** Not-Found-Page
- **/403** Forbidden-Page (z.B. Non-Admin auf /settings/users)
## 8. Test-Strategie (Kurzfassung, Details in Phase 5)
- **Unit-Tests**: pytest, isolierte Tests für `models/`, `schemas/`, `services/`
- **Integration-Tests**: pytest + `httpx.AsyncClient` gegen eine live FastAPI-Test-Instance mit **echter SQLite-DB in tmp-Dir + Alembic `upgrade head`** pro Test-Session
- **DB-Write-Tests**: jedes SQLAlchemy-Model bekommt einen CRUD-Test gegen die echte Test-DB
- **Auth-Tests**: positiver Login, negativer Login, expired JWT, fehlender JWT, fehlende Rollen (403)
- **API-Contract-Tests**: Smoke-Tests für alle 51 Endpoints (200/201/204/400/401/403/404/422)
- **E2E-Tests (optional v1)**: Playwright gegen den Docker-Stack (login → account anlegen → deal erstellen → pipeline-view → drag-and-drop)
- **Smoke-Tests (Runtime)**: nach Docker-up werden via `curl` getestet: `/health`, Auth-Flow, 3+ zufällige Endpoints
- **Coverage-Target**: ≥ 70 % Lines + Branches, gemessen mit `pytest-cov`, Threshold enforced in CI
- **Test-DB-Isolation**: jede Test-Function bekommt eigene Transaktion (rollback nach Test), parallele Test-Runs via `pytest-xdist`
## 9. Deployment-Plan (Kurzfassung, Details in Phase 5)
- **Dev** (lokal):
- SQLite, `uvicorn main:app --reload --port 8000`
- `alembic upgrade head` vor Start
- `.env`-Datei mit Dev-Secrets (nicht in Git)
- Frontend: `python -m http.server 8080` (statische Files) oder direkt in FastAPI als StaticFiles gemounted
- **Prod** (Coolify):
- PostgreSQL via Coolify DB-Service (Image: `postgres:16-alpine`)
- FastAPI-Container (Image: `python:3.12-slim`, Build via Dockerfile mit multi-stage)
- Caddy/Nginx-Proxy in Coolify für TLS + Domain-Routing
- Frontend: gleicher Container, statische Files via FastAPI StaticFiles
- **Coolify-Server**:
- Server: `localhost` (uuid `lw80w8scs4044gwcw084s00s4`)
- Limits: 2 concurrent_builds, 25 queue_limit, 3600s build_timeout
- Coolify-Build-Command: `docker build -t crm-backend .`
- Coolify-Start-Command: `alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2`
- **Healthcheck**: `/health` Endpoint, Coolify-side `curl -f http://localhost:8000/health || exit 1`
- **Secrets** (über Coolify Env-Vars, nie im Repo):
- `AUTH_SECRET` (≥ 32 Zeichen, JWT-Signing-Key)
- `DATABASE_URL` (PostgreSQL-Connection-String, von Coolify automatisch injected)
- `CORS_ORIGINS` (komma-separierte Liste erlaubter Origins)
- `LOG_LEVEL` (default INFO, in Prod WARNING)
- **Migration-Run**: `alembic upgrade head` als Pre-Start-Schritt in Coolify (idempotent)
- **Rollback-Strategie**: Coolify-Deployment-History, manuelle Rollback via Coolify-UI auf vorherigen Container-Tag
## 10. Risiken & offene Punkte
**Risiken:**
- **R-1: Single-Tenant jetzt, aber Migrations-Pfad zu Multi-Tenant**: Trotz `org_id`-Spalte in allen Tabellen muss die Middleware/Query-Logik strikt nach `org_id` filtern. Risiko: ein vergessenes `.where(org_id=...)` in einem Query leakt Daten. Mitigation: zentraler `OrgScopedQuery`-Helper, der in allen Repositories genutzt wird.
- **R-2: Polymorphic-FKs (Notes, Tags) komplexer als relational-FKs**: Kein DB-Constraint, der sicherstellt, dass `parent_id` in der richtigen Tabelle existiert. Risiko: orphaned Notes/Tags. Mitigation: Validierung in Service-Layer + Cleanup-Job in Phase 5.
- **R-3: Coolify `localhost` = lokaler Container**: Coolify läuft auf `localhost`, der CRM-Container ist nur intern erreichbar. Eine echte Public-Domain mit TLS ist für v1.1 geplant. Risiko: Dev-Stack ≠ Prod-Stack. Mitigation: Docker-Compose-File, das identisch lokal wie in Coolify läuft.
- **R-4: Single-Writer SQLite in Dev-Parallelbetrieb**: Wenn mehrere Developer/Tests parallel laufen, blockt SQLite. Mitigation: Tests nutzen separate in-memory-SQLites pro Worker; Prod-Plan ist explizit PostgreSQL.
- **R-5: JWT im localStorage = XSS-Risiko**: Wenn ein Angreifer XSS schafft, kann er das JWT stehlen. Mitigation: strikte CSP-Header, kein `innerHTML` mit User-Input, Alpine.js mit `x-text` statt `x-html` als Default.
**Offene Punkte (Entscheidungen ausstehend):**
- **OP-1: Avatar-Upload ja/nein?** Empfehlung: **Nein in v1**, nur `avatar_url` (User gibt externe URL an). Begründung: File-Upload-Endpoint + Storage-Lösung (S3-kompatibel oder Coolify-Volume) wäre eigener Scope.
- **OP-2: Email-Integration (SMTP) ja/nein?** Empfehlung: **Nein in v1**. Begründung: SMTP-Provider-Konfiguration, Email-Templates, Bounce-Handling, SPF/DKIM-Setup = signifikante Komplexität. Passwort-Reset-Token werden in v1 als Response-Output zurückgegeben (für Tests/Dev), SMTP-Integration in v1.1.
- **OP-3: Audit-Log ja/nein?** Empfehlung: **Minimal in v1** (`deal_stage_history` deckt das Wichtigste ab), **vollständig in v2** (alle PATCH/DELETE-Aktionen mit Before/After-Snapshot). Begründung: Volles Audit-Log braucht Trigger oder Middleware, die Performance kostet.
- **OP-4: i18n: Deutsch + Englisch oder nur Deutsch v1?** Empfehlung: **Beide** (Aufwand gering bei JSON-Locale-File, großer UX-Wert).
- **OP-5: Soft-Delete vs. Hard-Delete für DSGVO „Recht auf Vergessenwerden"?** Empfehlung: **Soft-Delete** + Admin-Tool für Hard-Delete in v1.1. Begründung: Audit-Trail bleibt erhalten, Revoke möglich.
## 11. User-Story-Mapping (Traceability)
| # | Story | FR-Refs | API-Endpoint | UI-Page |
|---|---|---|---|---|
| 1 | Als Sales-Rep logge ich mich ein | FR-1.2 | POST /api/auth/login | /login |
| 2 | Als Sales-Rep sehe ich beim ersten Besuch die Registrierung | FR-1.1 | POST /api/auth/register | /register |
| 3 | Als Sales-Rep lege ich einen neuen Account an | FR-2.1 | POST /api/accounts | /accounts (Modal) |
| 4 | Als Sales-Rep durchsuche Accounts nach Branche | FR-2.3 | GET /api/accounts?industry=... | /accounts |
| 5 | Als Sales-Rep öffne einen Account und sehe alle Contacts | FR-2.4 | GET /api/accounts/{id} | /accounts/{id} (Tab Contacts) |
| 6 | Als Sales-Rep füge ich einen Contact zu einem Account hinzu | FR-3.1 | POST /api/contacts | /accounts/{id} (Modal) |
| 7 | Als Sales-Rep lege ich einen Deal auf einem Account an | FR-4.1 | POST /api/deals | /accounts/{id} (Modal) |
| 8 | Als Sales-Rep öffne die Pipeline und sehe alle meine Deals als Kanban | FR-4.2 | GET /api/deals/pipeline | /pipeline |
| 9 | Als Sales-Rep ziehe einen Deal per Drag-and-Drop in die nächste Stage | FR-4.3, FR-4.5 | PATCH /api/deals/{id}/stage | /pipeline (Kanban) |
| 10 | Als Sales-Rep schließe einen Deal als Won ab mit Grund | FR-4.4 | PATCH /api/deals/{id}/stage | /pipeline / /deals/{id} (Modal) |
| 11 | Als Sales-Rep plane eine Follow-up-Aktivität | FR-5.1 | POST /api/activities | /accounts/{id} / /contacts/{id} (Modal) |
| 12 | Als Sales-Rep sehe ich alle meine überfälligen Aktivitäten | FR-5.4 | GET /api/activities?overdue=true | /activities |
| 13 | Als Sales-Rep schließe eine Aktivität mit Outcome ab | FR-5.3 | PATCH /api/activities/{id}/complete | /activities (Row) |
| 14 | Als Sales-Rep füge einem Account einen Tag hinzu | FR-6.1, FR-6.3 | POST /api/tags/link | /accounts/{id} (Tag-Picker) |
| 15 | Als Sales-Rep füge eine Note zu einem Deal hinzu | FR-6.2 | POST /api/notes | /deals (Tab Notes) |
| 16 | Als Sales-Rep öffne das Dashboard und sehe meine KPIs | FR-7.1 | GET /api/dashboard/kpis | / |
| 17 | Als Sales-Rep sehe im Dashboard den Activity-Feed | FR-7.2 | GET /api/dashboard/feed | / |
| 18 | Als Sales-Rep bearbeite mein Profil (Avatar, Notification-Prefs) | FR-8.1 | PATCH /api/users/me | /settings/profile |
| 19 | Als Sales-Manager lege einen neuen Sales-Rep an | FR-1.5 | POST /api/users | /settings/users |
| 20 | Als Sales-Manager sehe alle Deals meines Teams in der Pipeline | FR-4.2 | GET /api/deals/pipeline?owner_id=... | /pipeline (Filter) |
| 21 | Als Admin bearbeite Org-Info (Name, Logo, Default-Currency) | FR-8.2 | PATCH /api/org | /settings/org |
| 22 | Als User fordere ich einen Passwort-Reset an | FR-1.4 | POST /api/auth/password-reset/request | /login ("Passwort vergessen") |
| 23 | Als User setze ich mein Passwort mit Token zurück | FR-1.4 | POST /api/auth/password-reset/confirm | /reset-password?token=... |
| 24 | Als User logge mich aus | FR-1.3 | POST /api/auth/logout | (Header-Logout-Button) |
---
**Phase-Status**: REQUIREMENTS — Draft for User Approval
**Nächste Phase nach User-Approval:** solution_architect → `02-architecture.md` + `03-task-graph.json`