chore: migrate project documentation into repo (18 files)
This commit is contained in:
@@ -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 (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`
|
||||
Reference in New Issue
Block a user