mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
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:
parent
b000353298
commit
c8c4cea5fa
1 changed files with 501 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue