Duplikat-Finder & Zusammenführung
Zweck
Doppelt angelegte Stammdaten (durch Import, manuelle Eingabe oder Fusionen) verwässern Auswertungen und Zuordnungen. Der Duplikat-Finder (/duplicates) findet Dubletten und führt sie zusammen — für Kunden, Standorte, Kontakte, Lieferanten und Hersteller. Ergänzend verhindert eine Duplikat-Prüfung beim Anlegen neue Dubletten, und Mehrfach-Nummern halten die Nummern eines zusammengeführten Datensatzes erhalten.
Voraussetzungen
Berechtigungen (CASL)
| Action | Subject | Wirkung |
|---|---|---|
do | MergeEntities | Duplikat-Finder aufrufen, Gruppen suchen, Zusammenführen |
view | EntityMerge | Zusammenführungs-Audit-Log lesen |
view | Customer / Supplier / Manufacturer / Location / Contact | Duplikat-Prüfung beim Anlegen der jeweiligen Entität |
update | NumberCircleAssignment | Primär-Nummer festlegen (make-primary) |
Merge-Wizard — Duplikate zusammenführen
Der Wizard auf /duplicates führt in vier Schritten durch die Zusammenführung:
- Entität + Duplikate wählen —
GET /entity-merges/duplicate-groups?parentType=…liefert Kandidaten-Gruppen, gruppiert nach normalisiertem Namen (Standorte zusätzlich je Kunde, Kontakte nach Vor- + Nachname). Gruppen nach Größe sortiert, Kandidaten älteste zuerst. - Master (Gewinner) wählen — der Datensatz, der erhalten bleibt.
- Verlierer wählen — ein oder mehrere Duplikate, die in den Master übergehen.
- Vorschau (Dry-Run) → Zusammenführen —
POST /entity-mergesmitdryRun: truezeigt je Verlierer, welche Referenzen umgehängt würden (moved) und mögliche Warnungen; mitdryRun: falsewird die Zusammenführung ausgeführt.
Der Request-Body ist { parentType, winnerId, loserIds, dryRun }.
Was beim Zusammenführen technisch passiert
- Konflikte deduplizieren:
AttributeParent(Adressen, Telefon, E-Mail),ContactParentund Kategorie-Zuordnungen — fehlt ein Wert am Gewinner, wird der Verlierer-Wert übernommen; doppelte Einträge werden gelöscht. - Nummern übernehmen: Alle Nummern des Verlierers werden Sekundär-Nummern des Gewinners (
isPrimary = false) — siehe Mehrfach-Nummern. - Referenzen umhängen: Alle direkten Fremdschlüssel (z. B.
SalesDocument.customerId,PurchaseDocument.supplierId) und polymorphe Parent-Felder (parentType/parentId,sourceType/sourceId) werden auf den Gewinner umgezogen. - Verlierer stilllegen: Der Verlierer wird soft-gelöscht und erhält einen Tombstone-Zeiger
mergedIntoIdauf den Gewinner. - Audit: Je Verlierer entsteht ein
EntityMerge-Datensatz mit Snapshot und Referenz-Zählungen.
Datenmodell — EntityMerge (Audit)
| Feld | Datentyp | Beschreibung |
|---|---|---|
parentType | String | zusammengeführte Entität (Customer/Location/Contact/Supplier/Manufacturer) |
winnerId | UUID | erhaltener Datensatz (Master) |
loserId | UUID | zusammengeführter (stillgelegter) Datensatz |
loserName | String, nullable | Anzeigename des Verlierers zum Merge-Zeitpunkt |
snapshot | JSON, nullable | vollständiger Snapshot des Verlierers |
movedReferences | JSON, nullable | Anzahl umgehängter Referenzen je Modell |
mergedByEmployeeId | UUID, nullable | wer die Zusammenführung ausgeführt hat |
Am zusammengeführten Datensatz selbst zeigt mergedIntoId auf den Gewinner (Tombstone).
Duplikat-Prüfung beim Anlegen
Beim Anlegen eines Kunden, Lieferanten, Herstellers, Standorts oder Kontakts prüft ein Modal live (debounced) gegen bestehende Datensätze — über POST /{entität}/duplicate-check:
| Entität | Prüfkriterien |
|---|---|
| Kunde / Lieferant / Hersteller | Name (unscharf über Elasticsearch, Fallback DB-Suche), USt-IdNr. (exakt), Nummernkreis |
| Standort | Name (unscharf), begrenzt auf den zugehörigen Kunden |
| Kontakt | Vorname + Nachname |
Das Modal zeigt die gefundenen ähnlichen Datensätze mit Nummer, Name und einem Öffnen-Button (Score + matchedBy). Der Anwender kann dann einen bestehenden Datensatz öffnen statt einen neuen anzulegen — oder bewusst trotzdem anlegen.
Mehrfach-Nummern je Datensatz
Ein Datensatz kann mehrere Nummern tragen (z. B. nach einer Zusammenführung die Nummern beider ursprünglichen Kunden). Genau eine davon ist die Primär-Nummer (isPrimary = true) — sie erscheint standardmäßig auf Belegen; die übrigen bleiben such- und referenzierbar.
- Das Feld
isPrimaryliegt aufNumberCircleAssignment. - Über
POST /number-circle-assignments/:id/make-primarywird eine Nummer zur Primär-Nummer; alle anderen Nummern desselben Datensatzes werden automatisch aufisPrimary = falsegesetzt. - Im Frontend erscheint neben dem Nummernfeld (Kunde/Lieferant/Hersteller) ein Auswahl-Icon, sobald mehr als eine Nummer existiert; im Modal lässt sich die Primär-Nummer per Auswahl umstellen.
Verknüpfungen zu anderen Modulen
- Kunden, Standorte, Kontakte, Lieferanten, Hersteller — die zusammenführbaren bzw. duplikatgeprüften Entitäten.
- Polymorpher Parent-Pattern — erklärt, warum beim Merge auch
parentType/parentId-Referenzen umgehängt werden.
Wiederverwendbare Konzepte
API/Schnittstellen
| Methode | Endpoint | Zweck | CASL |
|---|---|---|---|
GET | /api/entity-merges/duplicate-groups?parentType=… | Duplikat-Gruppen finden | do MergeEntities |
POST | /api/entity-merges | Zusammenführen (Dry-Run über dryRun) | do MergeEntities |
GET | /api/entity-merges | Zusammenführungs-Audit lesen | view EntityMerge |
POST | /api/customers/duplicate-check (analog suppliers/manufacturers/locations/contacts) | Duplikat-Prüfung beim Anlegen | view der Entität |
POST | /api/number-circle-assignments/:id/make-primary | Primär-Nummer setzen | update NumberCircleAssignment |
Versionshinweise
- 2026-07-02: Initiale Veröffentlichung — Duplikat-Finder mit Merge-Wizard (
EntityMerge) für Kunden/Standorte/Kontakte/Lieferanten/Hersteller, Duplikat-Prüfung beim Anlegen (duplicate-check) und Mehrfach-Nummern je Datensatz (isPrimary/make-primary). Verifiziert anentityMerge.model.ts,entityMerge.router.ts,entityMerge.controller.ts,duplicateCheck.service.ts,numberCircleAssignment.model.ts/.router.ts,DuplicateFinderPage.tsx,routes.tsx,casl.ts.