docs(crm): add backend briefing for Phase 2.2, 2.3, 2.4

Comprehensive API contracts for CSV/Excel Import, Forecasting, and
Data Enrichment. Includes parallelization plan for backend + frontend
developers to work simultaneously against defined contracts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-12 19:19:59 +01:00
parent b000353298
commit c8c4cea5fa

View file

@ -2357,3 +2357,504 @@ docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force
- [ ] DROPDOWN/MULTI_SELECT im Formular: Options aus Feld-Definitionen laden statt Freitext-Fallback
- [ ] `isRequired` client-seitig validieren (Flag wird geliefert, Validierung noch nicht implementiert)
- [ ] Drag & Drop fuer Position-Sortierung (aktuell: Hoch/Runter-Buttons)
---
## 2026-03-12 | Architekt: Backend-Briefing Phase 2.2, 2.3, 2.4
### Parallelisierungsplan
Backend und Frontend arbeiten **parallel** an allen drei Phasen. Die API-Contracts sind unten festgelegt — Frontend baut UI gegen diese Contracts, Backend implementiert die Endpoints. Integration-Test wenn beide fertig sind.
```
Phase 2.3 (Forecast): Backend ━━━━━ Frontend ━━━━━ (parallel)
Phase 2.2 (Import): Backend ━━━━━━━━━ Frontend ━━━━━━━━━ (parallel)
Phase 2.4 (Enrichment): Backend ━━━━━━━ Frontend ━━━━━ (parallel)
```
**Reihenfolge:** 2.3 → 2.2 → 2.4 (nach aufsteigendem Aufwand).
Alle drei Phasen sind unabhaengig voneinander — keine Abhaengigkeiten zwischen den Phasen.
---
### Phase 2.3: Forecasting (Prioritaet 1 — kleinstes Delta)
#### Schema-Aenderung
Neues Feld in `PipelineStage`:
```prisma
model PipelineStage {
// ... bestehende Felder ...
probability Decimal @default(0) @db.Decimal(3,2) // NEU: 0.00 bis 1.00
}
```
**Migration:** `20260312_phase23_forecast`
```sql
ALTER TABLE app_crm.pipeline_stages
ADD COLUMN probability DECIMAL(3,2) NOT NULL DEFAULT 0;
```
**DTO-Aenderungen:**
- `CreatePipelineStageDto`: Neues optionales Feld `probability?: number` (0-1, @Min(0) @Max(1))
- `UpdatePipelineStageDto`: Neues optionales Feld `probability?: number`
- Bestehende Stage-Responses muessen `probability` enthalten
#### Neuer Endpoint: Forecast
```
GET /crm/deals/forecast?pipelineId=uuid&period=quarter|month|year
```
**Platzierung:** Im `DealsController` als eigene Route **VOR** der `:id`-Route, damit kein Routing-Konflikt entsteht. Alternativ: eigener `ForecastController` (bevorzugt fuer saubere Trennung).
**Query-Parameter:**
| Param | Typ | Default | Beschreibung |
|-------|-----|---------|-------------|
| `pipelineId` | UUID (optional) | alle | Nur Deals dieser Pipeline |
| `period` | `quarter` / `month` / `year` | `quarter` | Zeitraum-Gruppierung |
**Logik:**
1. Alle Deals mit `status = 'OPEN'` laden (im Tenant)
2. Optional nach `pipelineId` filtern
3. Period-Filter auf `expectedCloseDate`:
- `quarter`: Aktuelles Quartal (z.B. Q1 2026 = Jan-Mrz)
- `month`: Aktueller Monat
- `year`: Aktuelles Jahr
4. Nach Stage gruppieren
5. Pro Stage: `dealCount`, `totalValue` (Summe `value`), `weightedValue` (totalValue * stage.probability)
6. Totals ueber alle Stages berechnen
**Response-Contract:**
```json
{
"success": true,
"data": {
"pipeline": "Vertrieb",
"pipelineId": "uuid-oder-null",
"period": "2026-Q1",
"periodStart": "2026-01-01T00:00:00Z",
"periodEnd": "2026-03-31T23:59:59Z",
"stages": [
{
"stageId": "uuid",
"stageName": "Qualifiziert",
"probability": 0.3,
"dealCount": 12,
"totalValue": 150000.00,
"weightedValue": 45000.00
},
{
"stageId": "uuid",
"stageName": "Angebot",
"probability": 0.6,
"dealCount": 8,
"totalValue": 200000.00,
"weightedValue": 120000.00
}
],
"totals": {
"dealCount": 45,
"totalValue": 520000.00,
"weightedValue": 198000.00
}
},
"meta": { "timestamp": "2026-03-12T18:00:00Z" }
}
```
Wenn `pipelineId` nicht angegeben: `pipeline` = `"Alle Pipelines"`, `pipelineId` = `null`, Deals aus allen Pipelines zusammengefasst.
#### TODO Backend (Phase 2.3)
- [ ] Prisma-Schema: `probability` Feld in `PipelineStage`
- [ ] Migration erstellen und anwenden
- [ ] `CreatePipelineStageDto` + `UpdatePipelineStageDto` erweitern
- [ ] Pipeline-Stage-Responses: `probability` mitliefern (findAll, findOne)
- [ ] Forecast-Endpoint implementieren (eigener Controller oder in DealsController)
- [ ] Response-Contract exakt wie oben
---
### Phase 2.2: CSV/Excel Import (Prioritaet 2 — hoechster User-Value)
#### Neue Dependencies
```bash
npm install csv-parser xlsx
npm install -D @types/multer
```
(`multer` ist bereits in `@nestjs/platform-express` enthalten)
#### Neues Modul
```
src/import/
import.module.ts
import.controller.ts
import.service.ts
dto/
import-preview.dto.ts
import-execute.dto.ts
```
Registrierung in `app.module.ts`: `imports: [..., ImportModule]`
#### Endpoint 1: Preview
```
POST /crm/import/preview
Content-Type: multipart/form-data
```
**Form Fields:**
| Field | Typ | Beschreibung |
|-------|-----|-------------|
| `file` | File | CSV, XLSX oder vCard Datei |
| `entityType` | String | `PERSON`, `COMPANY` oder `DEAL` |
**NestJS-Pattern:**
```typescript
@Post('preview')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
fileFilter: (req, file, cb) => {
const allowed = ['.csv', '.xlsx', '.xls', '.vcf'];
const ext = extname(file.originalname).toLowerCase();
cb(null, allowed.includes(ext));
},
}))
async preview(
@CurrentUser() user: JwtPayload,
@UploadedFile() file: Express.Multer.File,
@Body('entityType') entityType: string,
)
```
**Verarbeitungs-Logik:**
1. Format erkennen (Extension oder MIME-Type)
2. **CSV:** Auto-detect Delimiter (`,` `;` `\t`). UTF-8 mit BOM-Erkennung. Erste Zeile = Header.
3. **XLSX:** Erstes Sheet lesen. Erste Zeile = Header.
4. **vCard (.vcf):** Felder extrahieren (FN, N, EMAIL, TEL, ORG, ADR, URL, NOTE)
5. Datei mit UUID-Prefix in `/tmp/` speichern fuer spaetere Verarbeitung
6. Erste 10 Datenzeilen + Spaltennamen + Gesamtzeilenzahl zurueckgeben
**Response-Contract:**
```json
{
"success": true,
"data": {
"importId": "uuid",
"format": "csv",
"columns": ["Name", "E-Mail", "Telefon", "Firma", "PLZ", "Stadt"],
"rows": [
["Max Mustermann", "max@firma.de", "+49 123 456", "Firma GmbH", "80331", "Muenchen"],
["Anna Schmidt", "anna@example.com", "+49 987 654", "Schmidt AG", "10115", "Berlin"]
],
"totalRows": 250,
"availableTargetFields": {
"PERSON": [
{ "field": "firstName", "label": "Vorname" },
{ "field": "lastName", "label": "Nachname" },
{ "field": "email", "label": "E-Mail" },
{ "field": "phone", "label": "Telefon" },
{ "field": "mobile", "label": "Mobil" },
{ "field": "companyName", "label": "Firmenname" },
{ "field": "position", "label": "Position" },
{ "field": "department", "label": "Abteilung" },
{ "field": "street", "label": "Strasse" },
{ "field": "zip", "label": "PLZ" },
{ "field": "city", "label": "Stadt" },
{ "field": "country", "label": "Land" },
{ "field": "website", "label": "Website" },
{ "field": "linkedinUrl", "label": "LinkedIn URL" },
{ "field": "notes", "label": "Notizen" },
{ "field": "tags", "label": "Tags (kommasepariert)" }
],
"COMPANY": [
{ "field": "name", "label": "Firmenname" },
{ "field": "email", "label": "E-Mail" },
{ "field": "phone", "label": "Telefon" },
{ "field": "website", "label": "Website" },
{ "field": "street", "label": "Strasse" },
{ "field": "zip", "label": "PLZ" },
{ "field": "city", "label": "Stadt" },
{ "field": "country", "label": "Land" },
{ "field": "vatId", "label": "USt-IdNr." },
{ "field": "tradeRegisterNumber", "label": "Handelsregisternr." },
{ "field": "registerCourt", "label": "Registergericht" },
{ "field": "notes", "label": "Notizen" },
{ "field": "tags", "label": "Tags (kommasepariert)" }
]
}
}
}
```
**Hinweis:** `availableTargetFields` liefert die moeglichen Zielfelder fuer den Entity-Typ, damit das Frontend das Mapping-Dropdown befuellen kann.
#### Endpoint 2: Execute
```
POST /crm/import/execute
Content-Type: application/json
```
**Request-Body:**
```json
{
"importId": "uuid-aus-preview",
"entityType": "PERSON",
"mapping": {
"Name": "lastName",
"E-Mail": "email",
"Telefon": "phone",
"Firma": "companyName",
"PLZ": "zip",
"Stadt": "city"
},
"duplicateStrategy": "SKIP",
"options": {
"skipFirstRow": true
}
}
```
**DTO-Validierung:**
- `importId`: @IsUUID()
- `entityType`: @IsEnum(['PERSON', 'COMPANY', 'DEAL'])
- `mapping`: Record<string, string> — Key = Datei-Spalte, Value = CRM-Feld
- `duplicateStrategy`: @IsEnum(['SKIP', 'UPDATE', 'MARK'])
- `options.skipFirstRow`: @IsBoolean() @IsOptional()
**Verarbeitungs-Logik:**
1. Temp-Datei ueber `importId` laden
2. Alle Zeilen parsen
3. Pro Zeile: Felder gemaess Mapping zuordnen
4. **Duplikat-Erkennung** ueber `email` Feld (case-insensitive):
- `SKIP`: Zeile ueberspringen, Zaehler erhoehen
- `UPDATE`: Bestehenden Datensatz aktualisieren (nur nicht-leere Felder ueberschreiben)
- `MARK`: Importieren und Tag `IMPORT_DUPLICATE` setzen
5. Validierung pro Zeile (E-Mail-Format, Pflichtfelder je nach entityType)
6. Batch-Insert via Prisma (nicht einzeln!)
7. **DSGVO:** Temp-Datei nach Verarbeitung LOESCHEN
8. Import-Statistik zurueckgeben
**Response-Contract:**
```json
{
"success": true,
"data": {
"created": 180,
"updated": 45,
"skipped": 20,
"errors": 5,
"totalProcessed": 250,
"errorDetails": [
{ "row": 42, "field": "email", "value": "ungueltig", "message": "Ungueltige E-Mail-Adresse" },
{ "row": 87, "field": "lastName", "value": "", "message": "Nachname ist ein Pflichtfeld" }
]
}
}
```
**Constraints:**
- Max **5.000 Zeilen** pro Import — bei mehr: `400 Bad Request` mit Fehlermeldung
- Max **10 MB** Dateigroesse (im FileInterceptor konfiguriert)
- Synchron bei <500 Zeilen
- Bei >500 Zeilen: Synchron verarbeiten ist OK fuer Alpha (async spaeter)
#### Endpoint 3: Import History (Nice-to-Have)
```
GET /crm/import/history
```
Optional — nur wenn Zeit uebrig. Zeigt letzte 20 Imports fuer den Tenant.
#### TODO Backend (Phase 2.2)
- [ ] Dependencies installieren: `csv-parser`, `xlsx`
- [ ] ImportModule + Controller + Service + DTOs erstellen
- [ ] In `app.module.ts` registrieren
- [ ] Preview-Endpoint: CSV/XLSX/vCard Parsing, Temp-Datei-Management
- [ ] Execute-Endpoint: Mapping, Duplikat-Erkennung, Batch-Insert
- [ ] Temp-Dateien nach Verarbeitung loeschen (DSGVO)
- [ ] Response-Contracts exakt wie oben
---
### Phase 2.4: Datenanreicherung (Prioritaet 3)
#### Neues Modul
```
src/enrichment/
enrichment.module.ts
enrichment.controller.ts
enrichment.service.ts
```
Registrierung in `app.module.ts`: `imports: [..., EnrichmentModule]`
#### Endpoint 1: Company Enrichment
```
POST /crm/companies/:id/enrich
```
**Logik:**
1. Company aus DB laden (Name, aktuelle Felder)
2. **Unternehmensregister.de** abfragen (kostenlos, immer verfuegbar):
- Suche nach Firmenname
- Felder: Handelsregisternr., Registergericht, Rechtsform, Sitz
- Timeout: 10 Sekunden
3. **North Data API** abfragen (nur wenn API-Key konfiguriert):
- `GET https://www.northdata.de/api/v1/company?name={name}&country=DE`
- Header: `X-Api-Key: {apiKey}`
- Felder: Adresse, Branche, Umsatz, Mitarbeiterzahl, Website
- Timeout: 10 Sekunden
4. Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
5. **NUR Vorschlaege zurueckgeben** — NICHT automatisch in DB schreiben!
6. Bei Fehler einer Quelle: verfuegbare Daten der anderen Quelle zurueckgeben
**Response-Contract:**
```json
{
"success": true,
"data": {
"companyId": "uuid",
"sources": ["unternehmensregister", "north_data"],
"suggestions": {
"street": {
"current": "",
"suggested": "Industriestr. 5",
"source": "north_data"
},
"city": {
"current": "Muenchen",
"suggested": "Muenchen",
"source": "north_data",
"match": true
},
"zip": {
"current": "",
"suggested": "80339",
"source": "north_data"
},
"vatId": {
"current": "",
"suggested": "DE123456789",
"source": "unternehmensregister"
},
"tradeRegisterNumber": {
"current": "",
"suggested": "HRB 12345",
"source": "unternehmensregister"
},
"registerCourt": {
"current": "",
"suggested": "AG Muenchen",
"source": "unternehmensregister"
},
"industry": {
"current": null,
"suggested": "Software & IT",
"source": "north_data"
},
"companySize": {
"current": null,
"suggested": "SIZE_51_200",
"source": "north_data"
},
"website": {
"current": "",
"suggested": "https://firma.de",
"source": "north_data"
}
},
"enrichedAt": "2026-03-12T18:30:00Z",
"warnings": []
}
}
```
**Hinweis:** `match: true` zeigt an, dass der aktuelle Wert mit dem Vorschlag uebereinstimmt (kein Update noetig). Nur Felder mit `suggested !== null` werden in der Response mitgegeben.
#### Endpoint 2: North Data API-Key Config
```
GET /crm/settings/integrations/north-data
```
Response:
```json
{
"success": true,
"data": {
"configured": true,
"apiKey": "****e4f8"
}
}
```
```
PUT /crm/settings/integrations/north-data
```
Request:
```json
{ "apiKey": "nd_abc123..." }
```
**Speicherung:** In Redis unter Key `crm:{tenantId}:integrations:north_data` oder in einer neuen `crm_integrations`-Tabelle.
#### Wichtig: dataEnrichedAt / dataEnrichedSource
Diese Felder existieren bereits im Company-Schema (Phase 1):
```
dataEnrichedAt DateTime? @map("data_enriched_at")
dataEnrichedSource String? @map("data_enriched_source") @db.VarChar(200)
```
Sie werden **NICHT** vom Enrich-Endpoint aktualisiert. Stattdessen setzt das Frontend sie beim naechsten `PATCH /crm/companies/:id` wenn der User Vorschlaege uebernimmt:
```json
{
"street": "Industriestr. 5",
"dataEnrichedAt": "2026-03-12T18:30:00Z",
"dataEnrichedSource": "north_data,unternehmensregister"
}
```
#### TODO Backend (Phase 2.4)
- [ ] EnrichmentModule + Controller + Service erstellen
- [ ] In `app.module.ts` registrieren
- [ ] Unternehmensregister.de Abfrage implementieren (Scraping oder API)
- [ ] North Data API Integration (mit API-Key aus Settings)
- [ ] Settings-Endpoints fuer API-Key (GET/PUT)
- [ ] Response-Contract exakt wie oben
- [ ] Timeouts + Fehlerbehandlung pro Quelle
---
### Hinweise fuer den Backend-Entwickler
1. **Reihenfolge:** Phase 2.3 zuerst (kleinstes Delta), dann 2.2, dann 2.4
2. **Patterns:** Alle bestehenden Patterns beibehalten (TenantGuard, singleResponse/paginatedResponse, class-validator DTOs, Swagger-Dekoratoren)
3. **Docker-Volume:** Bei Schema-Aenderungen Container mit `-V` neu erstellen:
```bash
docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V
```
4. **Tests:** Import-Edge-Cases besonders gruendlich testen (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails)
5. **DSGVO:** Import-Temp-Dateien MUESSEN nach Verarbeitung geloescht werden
6. **Completion-Report:** Nach jeder Phase einen Eintrag in dieser Datei ergaenzen (wie bei Phase 2.1)