diff --git a/CHANGELOG.md b/CHANGELOG.md
index f352fec8..3d85a49f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,16 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
### Behoben
+- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
+ Eintraege wurden bei jedem Sync neu angelegt statt verknuepft, weil der
+ Nummern-Abgleich feldgenau (geschaeftlich↔geschaeftlich) und formatabhaengig
+ war. Jetzt werden Rufnummern feldUEBERGREIFEND und formatunabhaengig
+ verglichen (`+49`/`0049` == `0`), sodass diese Eintraege verknuepft werden.
+ Der "Nummer + Firma"-Abgleich greift bewusst nur bei Eintraegen OHNE
+ Personennamen, damit keine zwei Kollegen mit gleicher Zentrale-Nummer
+ verschmolzen werden. (Bereits entstandene Dubletten muessen einmalig manuell
+ bereinigt werden.)
+
- **Dubletten auf beiden Seiten beim Synchronisieren.** Mehrere zusammenhaengende
Ursachen wurden beseitigt:
- Eine unvollstaendig geladene Starface-Kontaktliste (z.B. durch einen
diff --git a/src/StarfaceOutlookSync/Services/ContactMerger.cs b/src/StarfaceOutlookSync/Services/ContactMerger.cs
index 4f7ce1c5..c55e8fe8 100644
--- a/src/StarfaceOutlookSync/Services/ContactMerger.cs
+++ b/src/StarfaceOutlookSync/Services/ContactMerger.cs
@@ -140,7 +140,11 @@ namespace StarfaceOutlookSync.Services
private static string NormalizePhone(string phone)
{
if (string.IsNullOrEmpty(phone)) return "";
- return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
+ var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
+ if (s.StartsWith("+49")) s = "0" + s.Substring(3);
+ else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
+ else if (s.StartsWith("+")) s = s.Substring(1);
+ return s;
}
}
}
diff --git a/src/StarfaceOutlookSync/Services/SyncEngine.cs b/src/StarfaceOutlookSync/Services/SyncEngine.cs
index 5c806215..48b9297a 100644
--- a/src/StarfaceOutlookSync/Services/SyncEngine.cs
+++ b/src/StarfaceOutlookSync/Services/SyncEngine.cs
@@ -67,12 +67,9 @@ namespace StarfaceOutlookSync.Services
bool nameMatch = hasName
&& (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase)
&& (a.LastName ?? "").Equals(b.LastName ?? "", StringComparison.OrdinalIgnoreCase);
- bool phoneMatch = (!string.IsNullOrEmpty(a.PhoneWork) && !string.IsNullOrEmpty(b.PhoneWork)
- && NormalizePhone(a.PhoneWork) == NormalizePhone(b.PhoneWork))
- || (!string.IsNullOrEmpty(a.PhoneMobile) && !string.IsNullOrEmpty(b.PhoneMobile)
- && NormalizePhone(a.PhoneMobile) == NormalizePhone(b.PhoneMobile))
- || (!string.IsNullOrEmpty(a.Fax) && !string.IsNullOrEmpty(b.Fax)
- && NormalizePhone(a.Fax) == NormalizePhone(b.Fax));
+ // Telefon feldUEBERGREIFEND vergleichen (die gleiche Nummer steht mal
+ // als geschaeftlich, mal als privat) und formatunabhaengig (+49 == 0).
+ bool phoneMatch = SharedPhone(a, b);
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
@@ -86,17 +83,54 @@ namespace StarfaceOutlookSync.Services
// (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.)
if (nameMatch && !emailContradiction) return true;
- // Schwacher Pfad: Telefon/Fax nur zusammen mit gleicher Firma und ohne E-Mail-Widerspruch.
- if (phoneMatch && companyMatch && !emailContradiction) return true;
+ // Firma-/Service-Eintraege OHNE Personennamen: gleiche Firma + gemeinsame
+ // Nummer. Bewusst nur ohne Namen, damit nicht zwei Kollegen mit gleicher
+ // Zentrale-Nummer + Firma faelschlich verschmolzen werden.
+ bool aHasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
+ bool bHasName = !string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName);
+ if (phoneMatch && companyMatch && !aHasName && !bHasName && !emailContradiction) return true;
+
+ // Reine Nummern-Eintraege (kein Name, keine Firma, keine E-Mail auf beiden
+ // Seiten): eine gemeinsame Rufnummer identifiziert den Eintrag.
+ if (phoneMatch && IsBareNumberEntry(a) && IsBareNumberEntry(b)) return true;
return false;
}
+ private static bool IsBareNumberEntry(UnifiedContact c) =>
+ string.IsNullOrEmpty(c.FirstName) && string.IsNullOrEmpty(c.LastName)
+ && string.IsNullOrEmpty(c.Company) && string.IsNullOrEmpty(c.Email);
+
+ /// True, wenn a und b mindestens eine (normalisierte) Rufnummer gemeinsam haben.
+ private static bool SharedPhone(UnifiedContact a, UnifiedContact b)
+ {
+ var pa = PhoneSet(a);
+ if (pa.Count == 0) return false;
+ var pb = PhoneSet(b);
+ return pb.Count > 0 && pa.Overlaps(pb);
+ }
+
+ private static HashSet PhoneSet(UnifiedContact c)
+ {
+ var set = new HashSet();
+ foreach (var p in new[] { c.PhoneWork, c.PhoneMobile, c.PhoneHome, c.Fax })
+ {
+ var n = NormalizePhone(p);
+ if (n.Length >= 5) set.Add(n); // zu kurze (z.B. reine Durchwahlen) ignorieren
+ }
+ return set;
+ }
+
private static string NormalizePhone(string phone)
{
if (string.IsNullOrEmpty(phone)) return "";
// Nur Ziffern und + behalten
- return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
+ var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
+ // Landesvorwahl vereinheitlichen, damit +49/0049 und 0 gleich sind.
+ if (s.StartsWith("+49")) s = "0" + s.Substring(3);
+ else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
+ else if (s.StartsWith("+")) s = s.Substring(1);
+ return s;
}
public async Task SyncProfileAsync(SyncProfile profile)