Add tests for Kommunikation Sync implementation and verification scripts
- Implemented comprehensive tests for the Kommunikation Sync functionality, covering base64 encoding, marker parsing, creation, type detection, and integration scenarios. - Added a verification script to check for unique IDs in Advoware communications, ensuring stability and integrity of the IDs. - Created utility scripts for code validation, notification testing, and PUT response detail analysis to enhance development and testing processes. - Updated README with details on new tools and their usage.
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user