Files
motia/bitbylaw/scripts/test_kommunikation_matching_strategy.py
bitbylaw ebbbf419ee feat: Implement bidirectional synchronization utilities for Advoware and EspoCRM communications
- Added KommunikationSyncManager class to handle synchronization logic.
- Implemented methods for loading data, computing diffs, and applying changes between Advoware and EspoCRM.
- Introduced 3-way diffing mechanism to intelligently resolve conflicts.
- Added helper methods for creating empty slots and detecting changes in communications.
- Enhanced logging for better traceability during synchronization processes.
2026-02-08 19:53:40 +00:00

396 lines
14 KiB
Python

"""
Matching-Strategie für Kommunikation ohne ID
Problem:
- emailAddressData und phoneNumberData haben KEINE id-Felder
- Können keine rowId in EspoCRM speichern (keine Custom-Felder)
- Wie matchen wir Advoware ↔ EspoCRM?
Lösungsansätze:
1. Wert-basiertes Matching (emailAddress/phoneNumber als Schlüssel)
2. Advoware als Master (One-Way-Sync mit Neuanlage bei Änderung)
3. Hash-basiertes Matching in bemerkung-Feld
4. Position-basiertes Matching (primary-Flag)
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
from services.advoware import AdvowareAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
TEST_ADVOWARE_BETNR = 104860
class SimpleContext:
class Logger:
def info(self, msg): print(f"[INFO] {msg}")
def error(self, msg): print(f"[ERROR] {msg}")
def warning(self, msg): print(f"[WARN] {msg}")
def debug(self, msg): pass # Suppress debug
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_value_based_matching():
"""
Strategie 1: Wert-basiertes Matching
Idee: Verwende emailAddress/phoneNumber selbst als Schlüssel
Vorteile:
- Einfach zu implementieren
- Funktioniert für Duplikats-Erkennung
Nachteile:
- Wenn Wert ändert, verlieren wir Verbindung
- Keine Change-Detection möglich (kein Timestamp/rowId)
"""
print_section("STRATEGIE 1: Wert-basiertes Matching")
context = SimpleContext()
espo = EspoCRMAPI(context)
advo = AdvowareAPI(context)
# Hole Daten
espo_entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
print("\n📧 EspoCRM Emails:")
espo_emails = {e['emailAddress']: e for e in espo_entity.get('emailAddressData', [])}
for email, data in espo_emails.items():
print(f"{email:40s} primary={data.get('primary', False)}")
print("\n📧 Advoware Kommunikation (Typ MailGesch=4, MailPrivat=8):")
advo_komm = advo_entity.get('kommunikation', [])
advo_emails = [k for k in advo_komm if k.get('kommKz') in [4, 8]] # Email-Typen
for k in advo_emails:
print(f"{k.get('tlf', ''):40s} Typ={k.get('kommKz')} ID={k.get('id')} "
f"rowId={k.get('rowId')}")
print("\n🔍 Matching-Ergebnis:")
matched = []
unmatched_espo = []
unmatched_advo = []
for advo_k in advo_emails:
email_value = advo_k.get('tlf', '').strip()
if email_value in espo_emails:
matched.append((advo_k, espo_emails[email_value]))
print(f" ✅ MATCH: {email_value}")
else:
unmatched_advo.append(advo_k)
print(f" ❌ Nur in Advoware: {email_value}")
for email_value in espo_emails:
if not any(k.get('tlf', '').strip() == email_value for k in advo_emails):
unmatched_espo.append(espo_emails[email_value])
print(f" ⚠️ Nur in EspoCRM: {email_value}")
print(f"\n📊 Statistik:")
print(f" • Matched: {len(matched)}")
print(f" • Nur Advoware: {len(unmatched_advo)}")
print(f" • Nur EspoCRM: {len(unmatched_espo)}")
# Problem-Szenario: Was wenn Email-Adresse ändert?
print("\n⚠️ PROBLEM-SZENARIO: Email-Adresse ändert")
print(" 1. Advoware: max@old.de → max@new.de (UPDATE mit gleicher ID)")
print(" 2. Wert-Matching findet max@old.de nicht mehr in EspoCRM")
print(" 3. Sync würde max@new.de NEU anlegen statt UPDATE")
print(" 4. Ergebnis: Duplikat (max@old.de + max@new.de)")
return matched, unmatched_advo, unmatched_espo
async def test_advoware_master_sync():
"""
Strategie 2: Advoware als Master (One-Way-Sync)
Idee:
- Ignoriere EspoCRM-Änderungen
- Bei jedem Sync: Überschreibe komplette Arrays in EspoCRM
Vorteile:
- Sehr einfach
- Keine Change-Detection nötig
- Keine Matching-Probleme
Nachteile:
- Verliert EspoCRM-Änderungen
- Nicht bidirektional
"""
print_section("STRATEGIE 2: Advoware als Master (One-Way)")
context = SimpleContext()
advo = AdvowareAPI(context)
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
advo_komm = advo_entity.get('kommunikation', [])
print("\n📋 Sync-Ablauf:")
print(" 1. Hole alle Advoware Kommunikationen")
print(" 2. Konvertiere zu EspoCRM Format:")
# Konvertierung
email_data = []
phone_data = []
for k in advo_komm:
komm_kz = k.get('kommKz', 0)
wert = k.get('tlf', '').strip()
online = k.get('online', False)
# Email-Typen: 4=MailGesch, 8=MailPrivat, 11=EPost
if komm_kz in [4, 8, 11] and wert:
email_data.append({
'emailAddress': wert,
'lower': wert.lower(),
'primary': online, # online=true → primary
'optOut': False,
'invalid': False
})
# Phone-Typen: 1=TelGesch, 2=FaxGesch, 3=Mobil, 6=TelPrivat, 7=FaxPrivat
elif komm_kz in [1, 2, 3, 6, 7] and wert:
# Mapping kommKz → EspoCRM type
type_map = {
1: 'Office', # TelGesch
3: 'Mobile', # Mobil
6: 'Home', # TelPrivat
2: 'Fax', # FaxGesch
7: 'Fax', # FaxPrivat
}
phone_data.append({
'phoneNumber': wert,
'type': type_map.get(komm_kz, 'Other'),
'primary': online,
'optOut': False,
'invalid': False
})
print(f"\n 📧 {len(email_data)} Emails:")
for e in email_data:
print(f"{e['emailAddress']:40s} primary={e['primary']}")
print(f"\n 📞 {len(phone_data)} Phones:")
for p in phone_data:
print(f"{p['phoneNumber']:40s} type={p['type']:10s} primary={p['primary']}")
print("\n 3. UPDATE CBeteiligte (überschreibt komplette Arrays)")
print(" → emailAddressData: [...]")
print(" → phoneNumberData: [...]")
print("\n✅ Vorteile:")
print(" • Sehr einfach zu implementieren")
print(" • Keine Matching-Logik erforderlich")
print(" • Advoware ist immer Source of Truth")
print("\n❌ Nachteile:")
print(" • EspoCRM-Änderungen gehen verloren")
print(" • Nicht bidirektional")
print(" • User könnten verärgert sein")
return email_data, phone_data
async def test_hybrid_strategy():
"""
Strategie 3: Hybrid - Advoware Master + EspoCRM Ergänzungen
Idee:
- Advoware-Kommunikationen sind primary=true (wichtig, geschützt)
- EspoCRM kann zusätzliche Einträge mit primary=false hinzufügen
- Nur Advoware-Einträge werden synchronisiert
Vorteile:
- Flexibilität für EspoCRM-User
- Advoware behält Kontrolle über wichtige Daten
Nachteile:
- Komplexere Logik
- Braucht Markierung (primary-Flag)
"""
print_section("STRATEGIE 3: Hybrid (Advoware Primary + EspoCRM Secondary)")
context = SimpleContext()
espo = EspoCRMAPI(context)
advo = AdvowareAPI(context)
# Hole Daten
espo_entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
advo_komm = advo_entity.get('kommunikation', [])
espo_emails = espo_entity.get('emailAddressData', [])
print("\n📋 Regel:")
print(" • primary=true → Kommt von Advoware (synchronisiert)")
print(" • primary=false → Nur in EspoCRM (wird NICHT zu Advoware)")
print("\n📧 Aktuelle EspoCRM Emails:")
for e in espo_emails:
source = "Advoware" if e.get('primary') else "EspoCRM"
print(f"{e['emailAddress']:40s} primary={e.get('primary')}{source}")
print("\n🔄 Sync-Logik:")
print(" 1. Hole Advoware Kommunikationen")
print(" 2. Konvertiere zu EspoCRM (mit primary=true)")
print(" 3. Hole aktuelle EspoCRM Einträge mit primary=false")
print(" 4. Merge: Advoware (primary) + EspoCRM (secondary)")
print(" 5. UPDATE CBeteiligte mit gemergtem Array")
# Simulation
advo_emails = [k for k in advo_komm if k.get('kommKz') in [4, 8, 11]]
merged_emails = []
# Von Advoware (primary=true)
for k in advo_emails:
merged_emails.append({
'emailAddress': k.get('tlf', ''),
'lower': k.get('tlf', '').lower(),
'primary': True, # Immer primary für Advoware
'optOut': False,
'invalid': False
})
# Von EspoCRM (nur non-primary behalten)
for e in espo_emails:
if not e.get('primary', False):
merged_emails.append(e)
print(f"\n📊 Merge-Ergebnis: {len(merged_emails)} Emails")
for e in merged_emails:
source = "Advoware" if e.get('primary') else "EspoCRM"
print(f"{e['emailAddress']:40s} [{source}]")
print("\n✅ Vorteile:")
print(" • Advoware behält Kontrolle")
print(" • EspoCRM-User können ergänzen")
print(" • Kein Datenverlust")
print("\n⚠️ Einschränkungen:")
print(" • EspoCRM kann Advoware-Daten NICHT ändern")
print(" • primary-Flag muss geschützt werden")
return merged_emails
async def test_bemerkung_tracking():
"""
Strategie 4: Tracking via bemerkung-Feld
Idee: Speichere Advoware-ID in bemerkung
Format: "Advoware-ID: 149331 | Tatsächliche Bemerkung"
Vorteile:
- Stabiles Matching möglich
- Kann Änderungen tracken
Nachteile:
- bemerkung-Feld wird "verschmutzt"
- User sichtbar
- Fragil (User könnte löschen)
"""
print_section("STRATEGIE 4: Tracking via bemerkung-Feld")
context = SimpleContext()
advo = AdvowareAPI(context)
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
advo_komm = advo_entity.get('kommunikation', [])
print("\n⚠️ PROBLEM: EspoCRM emailAddressData/phoneNumberData haben KEIN bemerkung-Feld!")
print("\nStruktur emailAddressData:")
print(" {")
print(" 'emailAddress': 'max@example.com',")
print(" 'lower': 'max@example.com',")
print(" 'primary': true,")
print(" 'optOut': false,")
print(" 'invalid': false")
print(" }")
print("\n ❌ Kein 'bemerkung' oder 'notes' Feld verfügbar")
print(" ❌ Kein Custom-Feld möglich in Standard-Arrays")
print("\n📋 Alternative: Advoware bemerkung nutzen")
print(" → Speichere EspoCRM-Wert in Advoware bemerkung")
for k in advo_komm[:3]: # Erste 3 als Beispiel
advo_id = k.get('id')
wert = k.get('tlf', '')
bemerkung = k.get('bemerkung', '')
print(f"\n Advoware ID {advo_id}:")
print(f" Wert: {wert}")
print(f" Bemerkung: {bemerkung or '(leer)'}")
print(f" → Neue Bemerkung: 'EspoCRM: {wert} | {bemerkung}'")
print("\n✅ Matching-Strategie:")
print(" 1. Parse bemerkung: Extrahiere 'EspoCRM: <wert>'")
print(" 2. Matche Advoware ↔ EspoCRM via Wert in bemerkung")
print(" 3. Wenn Wert ändert: Update bemerkung")
print("\n❌ Nachteile:")
print(" • bemerkung für User sichtbar und änderbar")
print(" • Fragil wenn User bemerkung bearbeitet")
print(" • Komplexe Parse-Logik")
async def main():
print("\n" + "="*70)
print("KOMMUNIKATION MATCHING-STRATEGIEN OHNE ID")
print("="*70)
try:
# Test alle Strategien
await test_value_based_matching()
await test_advoware_master_sync()
await test_hybrid_strategy()
await test_bemerkung_tracking()
print_section("EMPFEHLUNG")
print("\n🎯 BESTE LÖSUNG: Strategie 3 (Hybrid)")
print("\n✅ Begründung:")
print(" 1. Advoware behält Kontrolle (primary=true)")
print(" 2. EspoCRM kann ergänzen (primary=false)")
print(" 3. Einfach zu implementieren")
print(" 4. Kein Datenverlust")
print(" 5. primary-Flag ist Standard in EspoCRM")
print("\n📋 Implementation:")
print(" • Advoware → EspoCRM: Setze primary=true")
print(" • EspoCRM → Advoware: Ignoriere primary=false Einträge")
print(" • Matching: Via Wert (emailAddress/phoneNumber)")
print(" • Change Detection: rowId in Advoware (wie bei Adressen)")
print("\n🔄 Sync-Ablauf:")
print(" 1. Webhook von Advoware")
print(" 2. Lade Advoware Kommunikationen")
print(" 3. Filter: Nur Typen die EspoCRM unterstützt")
print(" 4. Konvertiere zu emailAddressData/phoneNumberData")
print(" 5. Setze primary=true für alle")
print(" 6. Merge mit bestehenden primary=false Einträgen")
print(" 7. UPDATE CBeteiligte")
print("\n⚠️ Einschränkungen akzeptiert:")
print(" • EspoCRM → Advoware: Nur primary=false Einträge")
print(" • Keine bidirektionale Sync für Wert-Änderungen")
print(" • Bei Wert-Änderung: Neuanlage statt Update")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())