From c8c4cea5fa06d4f8d5336b4874c95ee3a8f404c6 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Thu, 12 Mar 2026 19:19:59 +0100 Subject: [PATCH] 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 --- docs/INSIGHT-CRM.md | 501 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) diff --git a/docs/INSIGHT-CRM.md b/docs/INSIGHT-CRM.md index e915bf4..bcfbcad 100644 --- a/docs/INSIGHT-CRM.md +++ b/docs/INSIGHT-CRM.md @@ -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 — 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)