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:
2026-02-08 23:05:56 +00:00
parent a157d3fa1d
commit 7856dd1d68
37 changed files with 438 additions and 271 deletions

View File

@@ -0,0 +1,45 @@
# EspoCRM API - Test Scripts
Test-Scripts für EspoCRM Custom Entity Tests.
## Scripts
### test_espocrm_kommunikation.py
Test für CBeteiligte Kommunikation-Felder in EspoCRM.
**Testet:**
- emailAddressData[] Struktur
- phoneNumberData[] Struktur
- Primary Flags
- CRUD Operations
### test_espocrm_kommunikation_detail.py
Detaillierter Test der Kommunikations-Entities.
### test_espocrm_phone_email_entities.py
Test für Phone/Email Sub-Entities.
**Testet:**
- Nested Entity Structure
- Relationship Management
- Data Consistency
### test_espocrm_hidden_ids.py
Test für versteckte ID-Felder in EspoCRM.
### test_espocrm_id_collections.py
Test für ID-Collection Handling.
### test_espocrm_id_injection.py
Test für ID-Injection Vulnerabilities.
## Verwendung
```bash
cd /opt/motia-app/bitbylaw
python scripts/espocrm_tests/test_espocrm_kommunikation.py
```
## Verwandte Dokumentation
- [../../services/ESPOCRM_SERVICE.md](../../services/ESPOCRM_SERVICE.md) - EspoCRM API Service

View File

@@ -0,0 +1,261 @@
"""
Deep-Dive: Suche nach versteckten ID-Feldern
Die Relationships emailAddresses/phoneNumbers existieren (kein 404),
aber wir bekommen 403 Forbidden.
Möglichkeiten:
1. IDs sind in emailAddressData versteckt (vielleicht als 'id' Feld?)
2. Es gibt ein separates ID-Array
3. IDs sind in einem anderen Format gespeichert
4. Admin-API-Key hat nicht genug Rechte
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def inspect_email_data_structure():
"""Schaue sehr genau in emailAddressData/phoneNumberData"""
print_section("DEEP INSPECTION: emailAddressData Structure")
context = SimpleContext()
espo = EspoCRMAPI(context)
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
email_data = entity.get('emailAddressData', [])
print(f"\n📧 emailAddressData hat {len(email_data)} Einträge\n")
for i, email in enumerate(email_data):
print(f"[{i+1}] RAW Type: {type(email)}")
print(f" Keys: {list(email.keys())}")
print(f" JSON:\n")
print(json.dumps(email, indent=4, ensure_ascii=False))
# Prüfe ob 'id' Feld vorhanden ist
if 'id' in email:
print(f"\n ✅ ID GEFUNDEN: {email['id']}")
else:
print(f"\n ❌ Kein 'id' Feld")
# Prüfe alle Felder auf ID-ähnliche Werte
print(f"\n Alle Werte:")
for key, value in email.items():
print(f" {key:20s} = {value}")
print()
async def test_raw_api_call():
"""Mache rohe API-Calls um zu sehen was wirklich zurückkommt"""
print_section("RAW API CALL: Direkt ohne Wrapper")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Test 1: Normale Entity-Abfrage
print(f"\n1⃣ GET /CBeteiligte/{TEST_BETEILIGTE_ID}")
result1 = await espo.api_call(f'CBeteiligte/{TEST_BETEILIGTE_ID}')
# Zeige nur Email-relevante Felder
email_fields = {k: v for k, v in result1.items() if 'email' in k.lower()}
print(json.dumps(email_fields, indent=2, ensure_ascii=False))
# Test 2: Mit maxDepth Parameter (falls EspoCRM das unterstützt)
print(f"\n2⃣ GET mit maxDepth=2")
try:
result2 = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'maxDepth': '2'}
)
email_fields2 = {k: v for k, v in result2.items() if 'email' in k.lower()}
print(json.dumps(email_fields2, indent=2, ensure_ascii=False))
except Exception as e:
print(f" ❌ Error: {e}")
# Test 3: Select nur emailAddressData
print(f"\n3⃣ GET mit select=emailAddressData")
result3 = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': 'emailAddressData'}
)
print(json.dumps(result3, indent=2, ensure_ascii=False))
async def search_for_link_table():
"""Suche nach EntityEmailAddress oder EntityPhoneNumber Link-Tables"""
print_section("SUCHE: Link-Tables")
context = SimpleContext()
espo = EspoCRMAPI(context)
# In EspoCRM gibt es manchmal Link-Tables wie "EntityEmailAddress"
link_table_names = [
'EntityEmailAddress',
'EntityPhoneNumber',
'ContactEmailAddress',
'ContactPhoneNumber',
'CBeteiligteEmailAddress',
'CBeteiligtePhoneNumber'
]
for table_name in link_table_names:
print(f"\n🔍 Teste: {table_name}")
try:
result = await espo.api_call(table_name, params={'maxSize': 3})
print(f" ✅ Existiert! Total: {result.get('total', 'unknown')}")
if result.get('list'):
print(f" Beispiel:")
print(json.dumps(result['list'][0], indent=6, ensure_ascii=False))
except Exception as e:
error_msg = str(e)
if '404' in error_msg:
print(f" ❌ 404 - Existiert nicht")
elif '403' in error_msg:
print(f" ⚠️ 403 - Existiert aber kein Zugriff")
else:
print(f"{error_msg}")
async def test_update_with_ids():
"""Test: Kann ich beim UPDATE IDs setzen?"""
print_section("TEST: Update mit IDs")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n💡 Idee: Vielleicht kann man beim UPDATE IDs mitgeben")
print(f" und EspoCRM erstellt dann die Verknüpfung?\n")
# Hole aktuelle Daten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
current_emails = entity.get('emailAddressData', [])
print(f"Aktuelle Emails:")
for email in current_emails:
print(f"{email.get('emailAddress')}")
# Versuche ein Update mit expliziter ID
print(f"\n🧪 Teste: Füge 'id' Feld zu emailAddressData hinzu")
test_emails = []
for email in current_emails:
email_copy = email.copy()
# Generiere eine Test-ID (oder verwende eine echte wenn wir eine finden)
email_copy['id'] = f"test-id-{hash(email['emailAddress']) % 100000}"
test_emails.append(email_copy)
print(f"{email['emailAddress']:40s} → id={email_copy['id']}")
print(f"\n⚠️ ACHTUNG: Würde jetzt UPDATE machen mit:")
print(json.dumps({'emailAddressData': test_emails}, indent=2, ensure_ascii=False))
print(f"\n→ NICHT ausgeführt (zu riskant ohne Backup)")
async def check_database_or_config():
"""Prüfe ob es Config/Settings gibt die IDs aktivieren"""
print_section("ESPOCRM CONFIG: ID-Unterstützung")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n📋 Hole App-Informationen:")
try:
# EspoCRM hat oft einen /App endpoint
app_info = await espo.api_call('App/user')
# Zeige nur relevante Felder
if app_info:
relevant = ['acl', 'preferences', 'settings']
for key in relevant:
if key in app_info:
print(f"\n{key}:")
# Suche nach Email/Phone-relevanten Einstellungen
data = app_info[key]
if isinstance(data, dict):
email_phone_settings = {k: v for k, v in data.items()
if 'email' in k.lower() or 'phone' in k.lower()}
if email_phone_settings:
print(json.dumps(email_phone_settings, indent=2, ensure_ascii=False))
else:
print(" (keine Email/Phone-spezifischen Einstellungen)")
except Exception as e:
print(f" ❌ Error: {e}")
# Prüfe Settings
print(f"\n📋 System Settings:")
try:
settings = await espo.api_call('Settings')
if settings:
email_phone_settings = {k: v for k, v in settings.items()
if 'email' in k.lower() or 'phone' in k.lower()}
if email_phone_settings:
print(json.dumps(email_phone_settings, indent=2, ensure_ascii=False))
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("DEEP DIVE: SUCHE NACH PHONENUMBER/EMAILADDRESS IDs")
print("="*70)
try:
# Sehr detaillierte Inspektion
await inspect_email_data_structure()
# Rohe API-Calls
await test_raw_api_call()
# Link-Tables
await search_for_link_table()
# Update-Test (ohne tatsächlich zu updaten)
await test_update_with_ids()
# Config
await check_database_or_config()
print_section("FAZIT")
print("\n🎯 Mögliche Szenarien:")
print("\n1⃣ IDs existieren NICHT in emailAddressData")
print(" → Wert-basiertes Matching notwendig")
print(" → Hybrid-Strategie (primary-Flag)")
print("\n2⃣ IDs existieren aber sind versteckt/nicht zugänglich")
print(" → API-Rechte müssen erweitert werden")
print(" → Admin muss emailAddresses/phoneNumbers Relationship freigeben")
print("\n3⃣ IDs können beim UPDATE gesetzt werden")
print(" → Wir könnten eigene IDs generieren")
print(" → Advoware-ID direkt als EspoCRM-ID nutzen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,250 @@
"""
Test: Gibt es ID-Collections für EmailAddress/PhoneNumber?
In EspoCRM gibt es bei Many-to-Many Beziehungen oft:
- entityNameIds (Array von IDs)
- entityNameNames (Dict ID → Name)
Zum Beispiel: teamsIds, teamsNames
Hypothese: Es könnte emailAddressesIds oder ähnlich geben
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def search_for_id_fields():
"""Suche nach allen ID-ähnlichen Feldern"""
print_section("SUCHE: ID-Collections")
context = SimpleContext()
espo = EspoCRMAPI(context)
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print("\n🔍 Alle Felder die 'Ids' enthalten:")
ids_fields = {k: v for k, v in entity.items() if 'Ids' in k}
for key, value in sorted(ids_fields.items()):
print(f"{key:40s}: {value}")
print("\n🔍 Alle Felder die 'Names' enthalten:")
names_fields = {k: v for k, v in entity.items() if 'Names' in k}
for key, value in sorted(names_fields.items()):
print(f"{key:40s}: {value}")
print("\n🔍 Alle Felder mit 'email' oder 'phone' (case-insensitive):")
comm_fields = {k: v for k, v in entity.items()
if 'email' in k.lower() or 'phone' in k.lower()}
for key, value in sorted(comm_fields.items()):
value_str = str(value)[:80] if not isinstance(value, list) else f"[{len(value)} items]"
print(f"{key:40s}: {value_str}")
async def test_specific_fields():
"""Teste spezifische Feld-Namen die existieren könnten"""
print_section("TEST: Spezifische Feld-Namen")
context = SimpleContext()
espo = EspoCRMAPI(context)
potential_fields = [
'emailAddressesIds',
'emailAddressIds',
'phoneNumbersIds',
'phoneNumberIds',
'emailIds',
'phoneIds',
'emailAddressesNames',
'phoneNumbersNames',
]
print("\n📋 Teste mit select Parameter:\n")
for field in potential_fields:
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': f'id,{field}'}
)
if field in result and result[field] is not None:
print(f"{field:30s}: {result[field]}")
else:
print(f" ⚠️ {field:30s}: Im Response aber None/leer")
except Exception as e:
print(f"{field:30s}: {str(e)[:60]}")
async def test_with_loadAdditionalFields():
"""EspoCRM unterstützt manchmal loadAdditionalFields Parameter"""
print_section("TEST: loadAdditionalFields Parameter")
context = SimpleContext()
espo = EspoCRMAPI(context)
params_to_test = [
{'loadAdditionalFields': 'true'},
{'loadAdditionalFields': '1'},
{'withLinks': 'true'},
{'withRelated': 'emailAddresses,phoneNumbers'},
]
for params in params_to_test:
print(f"\n📋 Teste mit params: {params}")
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params=params
)
# Suche nach neuen Feldern
new_fields = {k: v for k, v in result.items()
if ('email' in k.lower() or 'phone' in k.lower())
and 'Data' not in k}
if new_fields:
print(" ✅ Neue Felder gefunden:")
for k, v in new_fields.items():
print(f"{k}: {v}")
else:
print(" ⚠️ Keine neuen Felder")
except Exception as e:
print(f" ❌ Error: {e}")
async def test_create_with_explicit_ids():
"""
Was wenn wir bei CREATE/UPDATE explizite IDs für Email/Phone mitgeben?
Vielleicht gibt EspoCRM dann IDs zurück?
"""
print_section("IDEE: Explizite IDs bei UPDATE mitgeben")
print("\n💡 EspoCRM Standard-Verhalten:")
print(" Bei Many-to-Many Beziehungen (z.B. Teams):")
print(" - INPUT: teamsIds: ['id1', 'id2']")
print(" - OUTPUT: teamsIds: ['id1', 'id2']")
print(" ")
print(" Könnte bei emailAddresses ähnlich funktionieren?")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole aktuelle Daten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
current_emails = entity.get('emailAddressData', [])
print("\n📋 Aktuelle emailAddressData:")
for e in current_emails:
print(f"{e.get('emailAddress')}")
# Versuche ein Update mit hypothetischen emailAddressesIds
print("\n🧪 Test: UPDATE mit emailAddressesIds Feld")
print(" (DRY RUN - nicht wirklich ausgeführt)")
# Generiere Test-IDs (EspoCRM IDs sind meist 17 Zeichen)
test_ids = [f"test{str(i).zfill(13)}" for i in range(len(current_emails))]
print(f"\n Würde senden:")
print(f" emailAddressesIds: {test_ids}")
print(f" emailAddressData: {[e['emailAddress'] for e in current_emails]}")
print("\n ⚠️ Zu riskant ohne zu wissen was passiert")
async def check_standard_contact_entity():
"""
Prüfe wie es bei Standard Contact Entity funktioniert
(als Referenz für Custom Entity)
"""
print_section("REFERENZ: Standard Contact Entity")
context = SimpleContext()
espo = EspoCRMAPI(context)
print("\n📋 Hole ersten Contact als Referenz:")
try:
contacts = await espo.api_call('Contact', params={'maxSize': 1})
if contacts and contacts.get('list'):
contact = contacts['list'][0]
print(f"\n Contact: {contact.get('name')}")
print(f"\n 🔍 Email/Phone-relevante Felder:")
for key, value in sorted(contact.items()):
if 'email' in key.lower() or 'phone' in key.lower():
value_str = str(value)[:80] if not isinstance(value, (list, dict)) else type(value).__name__
print(f"{key:35s}: {value_str}")
else:
print(" ⚠️ Keine Contacts vorhanden")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("SUCHE: EMAIL/PHONE ID-COLLECTIONS")
print("="*70)
print("\nZiel: Finde ID-Arrays für EmailAddress/PhoneNumber Entities\n")
try:
await search_for_id_fields()
await test_specific_fields()
await test_with_loadAdditionalFields()
await test_create_with_explicit_ids()
await check_standard_contact_entity()
print_section("FAZIT")
print("\n🎯 Wenn KEINE ID-Collections existieren:")
print("\n Option 1: Separate CKommunikation Entity ✅ BESTE LÖSUNG")
print(" Struktur:")
print(" {")
print(" 'id': 'espocrm-generated-id',")
print(" 'beteiligteId': '68e4af00...',")
print(" 'typ': 'Email/Phone',")
print(" 'wert': 'max@example.com',")
print(" 'advowareId': 149331,")
print(" 'advowareRowId': 'ABC...'")
print(" }")
print("\n Vorteile:")
print(" • Eigene Entity-ID für jede Kommunikation")
print(" • advowareId/advowareRowId als eigene Felder")
print(" • Sauberes Datenmodell")
print(" • Stabiles bidirektionales Matching")
print("\n Option 2: One-Way Sync (Advoware → EspoCRM)")
print(" • Matching via Wert (emailAddress/phoneNumber)")
print(" • Nur Advoware-Änderungen werden synchronisiert")
print(" • EspoCRM als Read-Only Viewer")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,225 @@
"""
TEST: Können wir eigene IDs in emailAddressData setzen?
Wenn EspoCRM IDs beim UPDATE akzeptiert und speichert,
dann können wir:
- Advoware-ID als 'id' in emailAddressData speichern
- Stabiles Matching haben
- Bidirektionalen Sync machen
Vorsichtiger Test mit Backup!
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_id_persistence():
"""
Teste ob EspoCRM IDs in emailAddressData speichert
Ablauf:
1. Hole aktuelle Daten (Backup)
2. Füge 'id' Feld zu EINEM Email hinzu
3. UPDATE
4. GET wieder
5. Prüfe ob 'id' noch da ist
6. Restore original falls nötig
"""
print_section("TEST: ID Persistence in emailAddressData")
context = SimpleContext()
espo = EspoCRMAPI(context)
# 1. Backup
print("\n1⃣ Backup: Hole aktuelle Daten")
entity_backup = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
emails_backup = entity_backup.get('emailAddressData', [])
print(f" Backup: {len(emails_backup)} Emails gesichert")
for email in emails_backup:
print(f"{email['emailAddress']}")
# 2. Modifiziere NUR das erste Email (primary)
print("\n2⃣ Modifikation: Füge 'id' zu primary Email hinzu")
emails_modified = []
for i, email in enumerate(emails_backup):
email_copy = email.copy()
if email_copy.get('primary'): # Nur primary modifizieren
# Nutze einen recognizable Test-Wert
test_id = f"advoware-{i+1}-test-123"
email_copy['id'] = test_id
print(f" ✏️ {email['emailAddress']:40s} → id={test_id}")
else:
print(f" ⏭️ {email['emailAddress']:40s} (unverändert)")
emails_modified.append(email_copy)
# 3. UPDATE
print("\n3⃣ UPDATE: Sende modifizierte Daten")
try:
await espo.update_entity('CBeteiligte', TEST_BETEILIGTE_ID, {
'emailAddressData': emails_modified
})
print(" ✅ UPDATE erfolgreich")
except Exception as e:
print(f" ❌ UPDATE fehlgeschlagen: {e}")
return
# 4. GET wieder
print("\n4⃣ GET: Hole Daten wieder ab")
await asyncio.sleep(0.5) # Kurze Pause
entity_after = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
emails_after = entity_after.get('emailAddressData', [])
print(f" Nach UPDATE: {len(emails_after)} Emails")
# 5. Vergleiche
print("\n5⃣ VERGLEICH: Ist 'id' noch da?")
id_found = False
for email in emails_after:
email_addr = email['emailAddress']
has_id = 'id' in email
if has_id:
print(f"{email_addr:40s} → id={email['id']}")
id_found = True
else:
print(f"{email_addr:40s} → KEIN id Feld")
# 6. Ergebnis
print(f"\n6⃣ ERGEBNIS:")
if id_found:
print(" 🎉 SUCCESS! EspoCRM speichert und liefert 'id' Feld zurück!")
print(" → Wir können Advoware-IDs in emailAddressData speichern")
print(" → Stabiles bidirektionales Matching möglich")
else:
print(" ❌ FAILED: EspoCRM ignoriert/entfernt 'id' Feld")
print(" → Wert-basiertes Matching notwendig")
print(" → Hybrid-Strategie (primary-Flag) ist beste Option")
# 7. Restore (optional - nur wenn User will)
print(f"\n7⃣ CLEANUP:")
print(" Original-Daten (ohne id):")
for email in emails_backup:
print(f"{email['emailAddress']}")
if id_found:
restore = input("\n 🔄 Restore zu Original (ohne id)? [y/N]: ").strip().lower()
if restore == 'y':
await espo.update_entity('CBeteiligte', TEST_BETEILIGTE_ID, {
'emailAddressData': emails_backup
})
print(" ✅ Restored")
else:
print(" ⏭️ Nicht restored (id bleibt)")
return id_found
async def test_custom_field_approach():
"""
Alternative: Nutze ein custom field in CBeteiligte für ID-Mapping
Idee: Speichere JSON-Mapping in einem Textfeld
"""
print_section("ALTERNATIVE: Custom Field für ID-Mapping")
print("\n💡 Idee: Nutze custom field 'kommunikationMapping'")
print(" Struktur:")
print(" {")
print(' "emails": [')
print(' {"emailAddress": "max@example.com", "advowareId": 123, "advowareRowId": "ABC"}')
print(' ],')
print(' "phones": [')
print(' {"phoneNumber": "+49...", "advowareId": 456, "advowareRowId": "DEF"}')
print(' ]')
print(" }")
print("\n✅ Vorteile:")
print(" • Stabiles Matching via advowareId")
print(" • Change Detection via advowareRowId")
print(" • Bidirektionaler Sync möglich")
print("\n❌ Nachteile:")
print(" • Erfordert custom field in EspoCRM")
print(" • Daten-Duplikation (in Data + Mapping)")
print(" • Fragil wenn emailAddress/phoneNumber ändert")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Prüfe ob custom field existiert
print("\n🔍 Prüfe ob 'kommunikationMapping' Feld existiert:")
try:
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
if 'kommunikationMapping' in entity:
print(f" ✅ Feld existiert: {entity['kommunikationMapping']}")
else:
print(f" ❌ Feld existiert nicht")
print(f" → Müsste in EspoCRM angelegt werden")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("TEST: KÖNNEN WIR EIGENE IDs IN emailAddressData SETZEN?")
print("="*70)
print("\nZiel: Herausfinden ob EspoCRM 'id' Felder akzeptiert und speichert\n")
try:
# Haupttest
id_works = await test_id_persistence()
# Alternative
await test_custom_field_approach()
print_section("FINAL RECOMMENDATION")
if id_works:
print("\n🎯 EMPFEHLUNG: Nutze 'id' Feld in emailAddressData")
print("\n📋 Implementation:")
print(" 1. Bei Advoware → EspoCRM: Füge 'id' mit Advoware-ID hinzu")
print(" 2. Matching via 'id' Feld")
print(" 3. Change Detection via Advoware rowId")
print(" 4. Bidirektionaler Sync möglich")
else:
print("\n🎯 EMPFEHLUNG A: Hybrid-Strategie (primary-Flag)")
print(" • Einfach zu implementieren")
print(" • Nutzt Standard-EspoCRM")
print(" • Eingeschränkt bidirektional")
print("\n🎯 EMPFEHLUNG B: Custom Field 'kommunikationMapping'")
print(" • Vollständig bidirektional")
print(" • Erfordert EspoCRM-Anpassung")
print(" • Komplexere Implementation")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,277 @@
"""
Test: EspoCRM Kommunikation - Wie werden Kontaktdaten gespeichert?
Prüfe:
1. Gibt es ein separates CKommunikation Entity?
2. Wie sind Telefon/Email/Fax in CBeteiligte gespeichert?
3. Sind es Arrays oder einzelne Felder?
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
# Test-Beteiligter mit Kommunikationsdaten
TEST_BETEILIGTE_ID = '68e4af00172be7924' # Angela Mustermanns
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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_cbeteiligte_structure():
"""Analysiere CBeteiligte Kommunikationsfelder"""
print_section("TEST 1: CBeteiligte Entity Struktur")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Beteiligten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter geladen: {entity.get('name')}")
print(f" ID: {entity.get('id')}")
print(f" betNr: {entity.get('betnr')}")
# Suche nach Kommunikationsfeldern
print("\n📊 Kommunikations-relevante Felder:")
comm_fields = [
'phoneNumber', 'phoneNumberData',
'emailAddress', 'emailAddressData',
'fax', 'faxData',
'mobile', 'mobileData',
'website',
# Plural Varianten
'phoneNumbers', 'emailAddresses', 'faxNumbers',
# Link-Felder
'kommunikationIds', 'kommunikationNames',
'kommunikationenIds', 'kommunikationenNames',
'ckommunikationIds', 'ckommunikationNames'
]
found_fields = {}
for field in comm_fields:
if field in entity:
value = entity[field]
found_fields[field] = value
print(f"\n{field}:")
print(f" Typ: {type(value).__name__}")
if isinstance(value, list):
print(f" Anzahl: {len(value)}")
if len(value) > 0:
print(f" Beispiel: {json.dumps(value[0], indent=6, ensure_ascii=False)}")
elif isinstance(value, dict):
print(f" Keys: {list(value.keys())}")
print(f" Content: {json.dumps(value, indent=6, ensure_ascii=False)}")
else:
print(f" Wert: {value}")
if not found_fields:
print("\n ⚠️ Keine Standard-Kommunikationsfelder gefunden")
# Zeige alle Felder die "comm", "phone", "email", "fax", "tel" enthalten
print("\n📋 Alle Felder mit Kommunikations-Keywords:")
keywords = ['comm', 'phone', 'email', 'fax', 'tel', 'mobil', 'kontakt']
matching_fields = {}
for key, value in entity.items():
key_lower = key.lower()
if any(kw in key_lower for kw in keywords):
matching_fields[key] = value
print(f"{key}: {type(value).__name__}")
if isinstance(value, (str, int, bool)) and value:
print(f" = {value}")
return entity, found_fields
async def test_ckommunikation_entity():
"""Prüfe ob CKommunikation Entity existiert"""
print_section("TEST 2: CKommunikation Entity")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche CKommunikation zu listen
try:
result = await espo.list_entities('CKommunikation', max_size=5)
print(f"✅ CKommunikation Entity existiert!")
print(f" Anzahl gefunden: {len(result)}")
if result:
print(f"\n📋 Beispiel-Kommunikation:")
print(json.dumps(result[0], indent=2, ensure_ascii=False))
return True, result
except Exception as e:
if '404' in str(e) or 'not found' in str(e).lower():
print(f"❌ CKommunikation Entity existiert NICHT")
print(f" Fehler: {e}")
return False, None
else:
print(f"⚠️ Fehler beim Abrufen: {e}")
return None, None
async def test_entity_metadata():
"""Hole Entity-Metadaten von CBeteiligte"""
print_section("TEST 3: CBeteiligte Metadaten")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Metadaten (falls API das unterstützt)
try:
# Versuche Entity-Defs zu holen
metadata = await espo.api_call('/Metadata', method='GET')
if 'entityDefs' in metadata and 'CBeteiligte' in metadata['entityDefs']:
beteiligte_def = metadata['entityDefs']['CBeteiligte']
print("✅ Metadaten verfügbar")
if 'fields' in beteiligte_def:
fields = beteiligte_def['fields']
print(f"\n📊 Kommunikations-Felder in Definition:")
for field_name, field_def in fields.items():
field_lower = field_name.lower()
if any(kw in field_lower for kw in ['comm', 'phone', 'email', 'fax', 'tel']):
print(f"\n{field_name}:")
print(f" type: {field_def.get('type')}")
if 'entity' in field_def:
print(f" entity: {field_def.get('entity')}")
if 'link' in field_def:
print(f" link: {field_def.get('link')}")
return metadata
except Exception as e:
print(f"⚠️ Metadaten nicht verfügbar: {e}")
return None
async def test_list_all_entities():
"""Liste alle verfügbaren Entities"""
print_section("TEST 4: Alle verfügbaren Entities")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Häufige Entity-Namen die mit Kommunikation zu tun haben könnten
test_entities = [
'CKommunikation',
'Kommunikation',
'Communication',
'PhoneNumber',
'EmailAddress',
'CPhoneNumber',
'CEmailAddress',
'CPhone',
'CEmail',
'CContact',
'ContactData'
]
print("\n🔍 Teste verschiedene Entity-Namen:\n")
existing = []
for entity_name in test_entities:
try:
result = await espo.list_entities(entity_name, max_size=1)
print(f"{entity_name} - existiert ({len(result)} gefunden)")
existing.append(entity_name)
except Exception as e:
if '404' in str(e) or 'not found' in str(e).lower():
print(f"{entity_name} - existiert nicht")
else:
print(f" ⚠️ {entity_name} - Fehler: {str(e)[:50]}")
return existing
async def main():
print("\n" + "="*70)
print("ESPOCRM KOMMUNIKATION ANALYSE")
print("="*70)
print("\nZiel: Verstehen wie Kommunikationsdaten in EspoCRM gespeichert sind")
print("Frage: Gibt es separate Kommunikations-Entities oder nur Felder?\n")
try:
# Test 1: CBeteiligte Struktur
entity, comm_fields = await test_cbeteiligte_structure()
# Test 2: CKommunikation Entity
ckommunikation_exists, ckommunikation_data = await test_ckommunikation_entity()
# Test 3: Metadaten
# metadata = await test_entity_metadata()
# Test 4: Liste entities
existing_entities = await test_list_all_entities()
# Zusammenfassung
print_section("ZUSAMMENFASSUNG")
print("\n📊 Erkenntnisse:")
if comm_fields:
print(f"\n✅ CBeteiligte hat Kommunikationsfelder:")
for field, value in comm_fields.items():
vtype = type(value).__name__
print(f"{field} ({vtype})")
if ckommunikation_exists:
print(f"\n✅ CKommunikation Entity existiert")
print(f" → Separate Kommunikations-Entities möglich")
elif ckommunikation_exists == False:
print(f"\n❌ CKommunikation Entity existiert NICHT")
print(f" → Kommunikation nur als Felder in CBeteiligte")
if existing_entities:
print(f"\n📋 Gefundene Kommunikations-Entities:")
for ename in existing_entities:
print(f"{ename}")
print("\n💡 Empfehlung:")
if not comm_fields and not ckommunikation_exists:
print(" ⚠️ Keine Kommunikationsstruktur gefunden")
print(" → Eventuell müssen Custom Fields erst angelegt werden")
elif comm_fields and not ckommunikation_exists:
print(" → Verwende vorhandene Felder in CBeteiligte (phoneNumber, emailAddress, etc.)")
print(" → Sync als Teil des Beteiligte-Syncs (nicht separat)")
elif ckommunikation_exists:
print(" → Verwende CKommunikation Entity für separaten Kommunikations-Sync")
print(" → Ermöglicht mehrere Kommunikationseinträge pro Beteiligten")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,202 @@
"""
Detail-Analyse: emailAddressData und phoneNumberData Struktur
Erkenntnisse:
- CKommunikation Entity existiert NICHT in EspoCRM
- CBeteiligte hat phoneNumberData und emailAddressData Arrays
- PhoneNumber und EmailAddress Entities existieren (aber 403 Forbidden - nur intern)
Jetzt: Analysiere die Data-Arrays im Detail
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def analyze_communication_data():
"""Detaillierte Analyse der Communication-Data Felder"""
print_section("DETAIL-ANALYSE: emailAddressData und phoneNumberData")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Beteiligten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter: {entity.get('name')}")
print(f" ID: {entity.get('id')}")
# emailAddressData
print("\n" + "="*50)
print("emailAddressData")
print("="*50)
email_data = entity.get('emailAddressData', [])
if email_data:
print(f"\n📧 {len(email_data)} Email-Adresse(n):\n")
for i, email in enumerate(email_data):
print(f"[{i+1}] {json.dumps(email, indent=2, ensure_ascii=False)}")
# Analysiere Struktur
if i == 0:
print(f"\n📊 Feld-Struktur:")
for key, value in email.items():
print(f"{key:20s}: {type(value).__name__:10s} = {value}")
else:
print("\n❌ Keine Email-Adressen vorhanden")
# phoneNumberData
print("\n" + "="*50)
print("phoneNumberData")
print("="*50)
phone_data = entity.get('phoneNumberData', [])
if phone_data:
print(f"\n📞 {len(phone_data)} Telefonnummer(n):\n")
for i, phone in enumerate(phone_data):
print(f"[{i+1}] {json.dumps(phone, indent=2, ensure_ascii=False)}")
# Analysiere Struktur
if i == 0:
print(f"\n📊 Feld-Struktur:")
for key, value in phone.items():
print(f"{key:20s}: {type(value).__name__:10s} = {value}")
else:
print("\n❌ Keine Telefonnummern vorhanden")
# Prüfe andere Beteiligten mit mehr Kommunikationsdaten
print_section("SUCHE: Beteiligter mit mehr Kommunikationsdaten")
print("\n🔍 Liste erste 20 Beteiligte und prüfe Kommunikationsdaten...\n")
beteiligte_list = await espo.list_entities('CBeteiligte', max_size=20)
best_example = None
max_comm_count = 0
for bet in beteiligte_list:
# list_entities kann Strings oder Dicts zurückgeben
if isinstance(bet, str):
continue
email_count = len(bet.get('emailAddressData', []))
phone_count = len(bet.get('phoneNumberData', []))
total = email_count + phone_count
if total > 0:
print(f"{bet.get('name', 'N/A')[:40]:40s} | "
f"Email: {email_count} | Phone: {phone_count}")
if total > max_comm_count:
max_comm_count = total
best_example = bet
if best_example and max_comm_count > 0:
print(f"\n✅ Bester Beispiel-Beteiligter: {best_example.get('name')}")
print(f" Gesamt: {max_comm_count} Kommunikationseinträge")
print("\n📧 emailAddressData:")
for i, email in enumerate(best_example.get('emailAddressData', [])):
print(f"\n [{i+1}] {json.dumps(email, indent=6, ensure_ascii=False)}")
print("\n📞 phoneNumberData:")
for i, phone in enumerate(best_example.get('phoneNumberData', [])):
print(f"\n [{i+1}] {json.dumps(phone, indent=6, ensure_ascii=False)}")
return entity, email_data, phone_data, best_example
async def main():
print("\n" + "="*70)
print("ESPOCRM KOMMUNIKATION - DETAIL-ANALYSE")
print("="*70)
print("\nZiel: Verstehe die Struktur von emailAddressData und phoneNumberData")
print("Frage: Haben diese Arrays IDs für Matching mit Advoware?\n")
try:
entity, emails, phones, best = await analyze_communication_data()
print_section("ZUSAMMENFASSUNG")
print("\n📊 Erkenntnisse:")
print("\n1⃣ EspoCRM Standard-Struktur:")
print(" • emailAddressData: Array von Email-Objekten")
print(" • phoneNumberData: Array von Telefon-Objekten")
print(" • Keine separate CKommunikation Entity")
if emails:
print("\n2⃣ emailAddressData Felder:")
sample = emails[0]
for key in sample.keys():
print(f"{key}")
if 'id' in sample:
print("\n ✅ Hat 'id' Feld → Kann für Matching verwendet werden!")
else:
print("\n ❌ Kein 'id' Feld → Matching via Wert (emailAddress)")
if phones:
print("\n3⃣ phoneNumberData Felder:")
sample = phones[0]
for key in sample.keys():
print(f"{key}")
if 'id' in sample:
print("\n ✅ Hat 'id' Feld → Kann für Matching verwendet werden!")
else:
print("\n ❌ Kein 'id' Feld → Matching via Wert (phoneNumber)")
print("\n💡 Sync-Strategie:")
print("\n Option A: Kommunikation als Teil von Beteiligte-Sync")
print(" ────────────────────────────────────────────────────")
print(" • emailAddressData → Advoware Kommunikation (kommKz=4)")
print(" • phoneNumberData → Advoware Kommunikation (kommKz=1)")
print(" • Sync innerhalb von beteiligte_sync.py")
print(" • Kein separates Entity in EspoCRM nötig")
print("\n Option B: Custom CKommunikation Entity erstellen")
print(" ────────────────────────────────────────────────────")
print(" • Neues Custom Entity in EspoCRM anlegen")
print(" • Many-to-One Beziehung zu CBeteiligte")
print(" • Separater kommunikation_sync.py")
print(" • Ermöglicht mehr Flexibilität (Fax, BeA, etc.)")
print("\n ⚠️ WICHTIG:")
print(" • Standard EspoCRM hat NUR Email und Phone")
print(" • Advoware hat 12 verschiedene Kommunikationstypen")
print(" • Für vollständigen Sync → Custom Entity empfohlen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,297 @@
"""
Test: PhoneNumber und EmailAddress als System-Entities
Hypothese:
- PhoneNumber und EmailAddress sind separate Entities mit IDs
- CBeteiligte hat Links/Relations zu diesen Entities
- Wir können über related entries an die IDs kommen
Ziele:
1. Hole CBeteiligte mit expanded relationships
2. Prüfe ob phoneNumbers/emailAddresses als Links verfügbar sind
3. Extrahiere IDs der verknüpften PhoneNumber/EmailAddress Entities
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_related_entities():
"""Test 1: Hole CBeteiligte mit allen verfügbaren Feldern"""
print_section("TEST 1: CBeteiligte - Alle Felder")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Entity
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter: {entity.get('name')}")
print(f"\n📋 Alle Top-Level Felder:")
for key in sorted(entity.keys()):
value = entity[key]
value_type = type(value).__name__
# Zeige nur ersten Teil von langen Werten
if isinstance(value, str) and len(value) > 60:
display = f"{value[:60]}..."
elif isinstance(value, list):
display = f"[{len(value)} items]"
elif isinstance(value, dict):
display = f"{{dict with {len(value)} keys}}"
else:
display = value
print(f"{key:30s}: {value_type:10s} = {display}")
# Suche nach ID-Feldern für Kommunikation
print(f"\n🔍 Suche nach ID-Feldern für Email/Phone:")
potential_id_fields = [k for k in entity.keys() if 'email' in k.lower() or 'phone' in k.lower()]
for field in potential_id_fields:
print(f"{field}: {entity.get(field)}")
return entity
async def test_list_with_select():
"""Test 2: Nutze select Parameter um spezifische Felder zu holen"""
print_section("TEST 2: CBeteiligte mit select Parameter")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche verschiedene Feld-Namen
potential_fields = [
'emailAddresses',
'phoneNumbers',
'emailAddressId',
'phoneNumberId',
'emailAddressIds',
'phoneNumberIds',
'emailAddressList',
'phoneNumberList'
]
print(f"\n📋 Teste verschiedene Feld-Namen:")
for field in potential_fields:
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': field}
)
if result and field in result:
print(f"{field:30s}: {result[field]}")
else:
print(f"{field:30s}: Nicht im Response")
except Exception as e:
print(f"{field:30s}: Error - {e}")
async def test_entity_relationships():
"""Test 3: Hole Links/Relationships über dedizierte Endpoints"""
print_section("TEST 3: Entity Relationships")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Test verschiedene Relationship-Endpoints
relationship_names = [
'emailAddresses',
'phoneNumbers',
'emails',
'phones'
]
for rel_name in relationship_names:
print(f"\n🔗 Teste Relationship: {rel_name}")
try:
# EspoCRM API Format: /Entity/{id}/relationship-name
result = await espo.api_call(f'CBeteiligte/{TEST_BETEILIGTE_ID}/{rel_name}')
if result:
print(f" ✅ Success! Type: {type(result)}")
if isinstance(result, dict):
print(f" 📋 Response Keys: {list(result.keys())}")
# Häufige EspoCRM Response-Strukturen
if 'list' in result:
items = result['list']
print(f" 📊 {len(items)} Einträge in 'list'")
if items:
print(f"\n Erster Eintrag:")
print(json.dumps(items[0], indent=6, ensure_ascii=False))
if 'total' in result:
print(f" 📊 Total: {result['total']}")
elif isinstance(result, list):
print(f" 📊 {len(result)} Einträge direkt als Liste")
if result:
print(f"\n Erster Eintrag:")
print(json.dumps(result[0], indent=6, ensure_ascii=False))
else:
print(f" ⚠️ Empty response")
except Exception as e:
error_msg = str(e)
if '404' in error_msg:
print(f" ❌ 404 Not Found - Relationship existiert nicht")
elif '403' in error_msg:
print(f" ❌ 403 Forbidden - Kein Zugriff")
else:
print(f" ❌ Error: {error_msg}")
async def test_direct_entity_access():
"""Test 4: Direkter Zugriff auf PhoneNumber/EmailAddress Entities"""
print_section("TEST 4: Direkte Entity-Abfrage")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche die Entities direkt zu listen
for entity_type in ['PhoneNumber', 'EmailAddress']:
print(f"\n📋 Liste {entity_type} Entities:")
try:
# Mit Filter für unseren Beteiligten
result = await espo.api_call(
entity_type,
params={
'maxSize': 5,
'where': json.dumps([{
'type': 'equals',
'attribute': 'parentId',
'value': TEST_BETEILIGTE_ID
}])
}
)
if result and 'list' in result:
items = result['list']
print(f"{len(items)} Einträge gefunden")
for item in items:
print(f"\n 📧/📞 {entity_type}:")
print(json.dumps(item, indent=6, ensure_ascii=False))
else:
print(f" ⚠️ Keine Einträge oder unerwartetes Format")
print(f" Response: {result}")
except Exception as e:
error_msg = str(e)
if '403' in error_msg:
print(f" ❌ 403 Forbidden")
print(f" → Versuche ohne Filter...")
try:
# Ohne Filter
result = await espo.api_call(entity_type, params={'maxSize': 3})
print(f" ✅ Ohne Filter: {result.get('total', 0)} total existieren")
except Exception as e2:
print(f" ❌ Auch ohne Filter: {e2}")
else:
print(f" ❌ Error: {error_msg}")
async def test_espocrm_metadata():
"""Test 5: Prüfe EspoCRM Metadata für CBeteiligte"""
print_section("TEST 5: EspoCRM Metadata")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n📋 Hole Metadata für CBeteiligte:")
try:
# EspoCRM bietet manchmal Metadata-Endpoints
result = await espo.api_call('Metadata')
if result and 'entityDefs' in result:
if 'CBeteiligte' in result['entityDefs']:
bet_meta = result['entityDefs']['CBeteiligte']
print(f"\n ✅ CBeteiligte Metadata gefunden")
if 'links' in bet_meta:
print(f"\n 🔗 Links/Relationships:")
for link_name, link_def in bet_meta['links'].items():
if 'email' in link_name.lower() or 'phone' in link_name.lower():
print(f"{link_name}: {link_def}")
if 'fields' in bet_meta:
print(f"\n 📋 Relevante Felder:")
for field_name, field_def in bet_meta['fields'].items():
if 'email' in field_name.lower() or 'phone' in field_name.lower():
print(f"{field_name}: {field_def.get('type', 'unknown')}")
else:
print(f" ⚠️ Unerwartetes Format")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("ESPOCRM PHONENUMBER/EMAILADDRESS - ENTITIES & IDS")
print("="*70)
print("\nZiel: Finde IDs für PhoneNumber/EmailAddress über Relationships\n")
try:
# Test 1: Alle Felder inspizieren
entity = await test_related_entities()
# Test 2: Select Parameter
await test_list_with_select()
# Test 3: Relationships
await test_entity_relationships()
# Test 4: Direkte Entity-Abfrage
await test_direct_entity_access()
# Test 5: Metadata
await test_espocrm_metadata()
print_section("ZUSAMMENFASSUNG")
print("\n🎯 Erkenntnisse:")
print("\n Wenn PhoneNumber/EmailAddress System-Entities sind:")
print(" 1. ✅ Sie haben eigene IDs")
print(" 2. ✅ Stabiles Matching möglich")
print(" 3. ✅ Bidirektionaler Sync machbar")
print(" 4. ✅ Change Detection via ID")
print("\n Wenn wir IDs haben:")
print(" • Können Advoware-ID zu EspoCRM-ID mappen")
print(" • Können Änderungen tracken")
print(" • Kein Problem bei Wert-Änderungen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())