Files
crm-system/docs/01-requirements.md
T

393 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`