# 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 (5–25 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, 0–1) - **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`