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,41 @@
# Analysis Scripts
Scripts für Analyse und Debugging von Sync-Problemen.
## Scripts
### analyze_beteiligte_endpoint.py
Analysiert Beteiligte-Endpoint in Advoware.
**Features:**
- Field-Analyse (funktionierende vs. ignorierte Felder)
- Response-Structure Analyse
- Edge-Case Testing
### analyze_sync_issues_104860.py
Spezifische Analyse für Entity 104860 Sync-Probleme.
**Analysiert:**
- Sync-Status Historie
- Timestamp-Vergleiche
- Konflikt-Erkennung
- Hash-Berechnung
### compare_entities_104860.py
Detaillierter Vergleich von Entity 104860 zwischen Systemen.
**Features:**
- Field-by-Field Diff
- Kommunikation-Arrays Vergleich
- Marker-Analyse
## Verwendung
```bash
cd /opt/motia-app/bitbylaw
python scripts/analysis/analyze_sync_issues_104860.py
```
## Zweck
Diese Scripts wurden erstellt, um spezifische Sync-Probleme zu debuggen und die API-Charakteristiken zu verstehen.

View File

@@ -0,0 +1,152 @@
"""
Detaillierte Analyse: Was liefert /api/v1/advonet/Beteiligte/{id}?
Prüfe:
1. Kommunikation-Array: Alle Felder
2. kommKz und kommArt Werte
3. Adressen-Array (falls enthalten)
4. Vollständige Struktur
"""
import asyncio
import json
from services.advoware import AdvowareAPI
TEST_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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def main():
print("\n" + "="*70)
print("DETAILLIERTE ANALYSE: Beteiligte Endpoint")
print("="*70)
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole kompletten Beteiligte
print(f"\n📋 GET /api/v1/advonet/Beteiligte/{TEST_BETNR}")
result = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_BETNR}')
print(f"\nResponse Type: {type(result)}")
if isinstance(result, list):
print(f"Response Length: {len(result)}")
beteiligte = result[0]
else:
beteiligte = result
# Zeige Top-Level Struktur
print_section("TOP-LEVEL FELDER")
print(f"\nVerfügbare Keys:")
for key in sorted(beteiligte.keys()):
value = beteiligte[key]
if isinstance(value, list):
print(f"{key:30s}: [{len(value)} items]")
elif isinstance(value, dict):
print(f"{key:30s}: {{dict}}")
else:
value_str = str(value)[:50]
print(f"{key:30s}: {value_str}")
# Kommunikationen
print_section("KOMMUNIKATION ARRAY")
kommunikationen = beteiligte.get('kommunikation', [])
print(f"\n{len(kommunikationen)} Kommunikationen gefunden")
if kommunikationen:
print(f"\n📋 Erste Kommunikation - ALLE Felder:")
first = kommunikationen[0]
print(json.dumps(first, indent=2, ensure_ascii=False))
print(f"\n📊 Übersicht aller Kommunikationen:")
print(f"\n{'ID':>8s} | {'kommKz':>6s} | {'kommArt':>7s} | {'online':>6s} | {'Wert':40s} | {'Bemerkung'}")
print("-" * 120)
for k in kommunikationen:
komm_id = k.get('id', 'N/A')
kommkz = k.get('kommKz', 'N/A')
kommart = k.get('kommArt', 'N/A')
online = k.get('online', False)
wert = (k.get('tlf') or '')[:40]
bemerkung = (k.get('bemerkung') or '')[:20]
# Highlighting
kommkz_str = f"{kommkz}" if kommkz not in [0, 'N/A'] else f"{kommkz}"
kommart_str = f"{kommart}" if kommart not in [0, 'N/A'] else f"{kommart}"
print(f"{komm_id:8} | {kommkz_str:>6s} | {kommart_str:>7s} | {str(online):>6s} | {wert:40s} | {bemerkung}")
# Adressen
print_section("ADRESSEN ARRAY")
adressen = beteiligte.get('adressen', [])
print(f"\n{len(adressen)} Adressen gefunden")
if adressen:
print(f"\n📋 Erste Adresse - Struktur:")
first_addr = adressen[0]
print(json.dumps(first_addr, indent=2, ensure_ascii=False))
# Bankverbindungen
print_section("BANKVERBINDUNGEN")
bankverb = beteiligte.get('bankkverbindungen', []) # Typo im API?
if not bankverb:
bankverb = beteiligte.get('bankverbindungen', [])
print(f"\n{len(bankverb)} Bankverbindungen gefunden")
if bankverb:
print(f"\n📋 Erste Bankverbindung - Keys:")
print(list(bankverb[0].keys()))
# Analyse
print_section("ZUSAMMENFASSUNG")
print(f"\n📊 Verfügbare Daten:")
print(f" • Kommunikationen: {len(kommunikationen)}")
print(f" • Adressen: {len(adressen)}")
print(f" • Bankverbindungen: {len(bankverb)}")
print(f"\n🔍 kommKz/kommArt Status:")
if kommunikationen:
kommkz_values = [k.get('kommKz', 0) for k in kommunikationen]
kommart_values = [k.get('kommArt', 0) for k in kommunikationen]
kommkz_non_zero = [v for v in kommkz_values if v != 0]
kommart_non_zero = [v for v in kommart_values if v != 0]
print(f" • kommKz unique values: {set(kommkz_values)}")
print(f" • kommKz non-zero count: {len(kommkz_non_zero)} / {len(kommunikationen)}")
print(f" • kommArt unique values: {set(kommart_values)}")
print(f" • kommArt non-zero count: {len(kommart_non_zero)} / {len(kommunikationen)}")
if kommkz_non_zero:
print(f"\n ✅✅✅ JACKPOT! kommKz HAT WERTE im Beteiligte-Endpoint!")
print(f" → Wir können den Typ korrekt erkennen!")
elif kommart_non_zero:
print(f"\n ✅ kommArt hat Werte (Email/Phone unterscheidbar)")
else:
print(f"\n ❌ Beide sind 0 - müssen Typ aus Wert ableiten")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""
Detaillierte Analyse der Sync-Probleme für Entity 104860
"""
import asyncio
import sys
import json
from pathlib import Path
import base64
sys.path.insert(0, str(Path(__file__).parent.parent))
from services.advoware import AdvowareAPI
from services.espocrm import EspoCRMAPI
from services.kommunikation_mapper import parse_marker, should_sync_to_espocrm
class SimpleContext:
class Logger:
def info(self, msg): print(f" {msg}")
def debug(self, msg): pass # Suppress debug
def warn(self, msg): print(f"⚠️ {msg}")
def warning(self, msg): print(f"⚠️ {msg}")
def error(self, msg): print(f"{msg}")
def __init__(self):
self.logger = self.Logger()
async def analyze():
context = SimpleContext()
betnr = 104860
espo_id = "68e3e7eab49f09adb"
# Initialize APIs
advoware_api = AdvowareAPI(context)
espocrm = EspoCRMAPI(context)
# Fetch data
advo_result = await advoware_api.api_call(f'api/v1/advonet/Beteiligte/{betnr}', method='GET')
advo_entity = advo_result[0] if isinstance(advo_result, list) else advo_result
espo_entity = await espocrm.get_entity('CBeteiligte', espo_id)
print("\n" + "="*80)
print("DETAILLIERTE SYNC-PROBLEM ANALYSE")
print("="*80 + "\n")
# ========== PROBLEM 1: NAME MISMATCH ==========
print("🔴 PROBLEM 1: STAMMDATEN NICHT SYNCHRON")
print("-" * 80)
print(f"EspoCRM Name: '{espo_entity.get('name')}'")
print(f"Advoware Name: '{advo_entity.get('name')}'")
print(f"")
print(f"ANALYSE:")
print(f"- syncStatus: {espo_entity.get('syncStatus')}")
print(f"- advowareLastSync: {espo_entity.get('advowareLastSync')}")
print(f"- modifiedAt (EspoCRM): {espo_entity.get('modifiedAt')}")
print(f"- geaendertAm (Advoware): {advo_entity.get('geaendertAm')}")
print(f"")
print(f"💡 URSACHE:")
print(f" - Sync sagt 'clean' aber Daten sind NICHT identisch!")
print(f" - Dies ist Problem #13: Keine Validierung von Sync-Ergebnissen")
print(f" - Sync glaubt es war erfolgreich, aber Mapping oder API-Call fehlte")
print()
# ========== PROBLEM 2: KOMMUNIKATION COUNTS ==========
print("🟡 PROBLEM 2: KOMMUNIKATION ANZAHL-MISMATCH")
print("-" * 80)
advo_kommunikationen = advo_entity.get('kommunikation', [])
espo_emails = espo_entity.get('emailAddressData', [])
espo_phones = espo_entity.get('phoneNumberData', [])
# Analysiere Advoware Kommunikationen
advo_with_value = []
advo_empty_slots = []
advo_non_sync = []
for komm in advo_kommunikationen:
tlf = (komm.get('tlf') or '').strip()
bemerkung = komm.get('bemerkung', '')
marker = parse_marker(bemerkung)
if not should_sync_to_espocrm(komm):
advo_non_sync.append(komm)
elif not tlf or (marker and marker.get('is_slot')):
advo_empty_slots.append(komm)
else:
advo_with_value.append(komm)
print(f"Advoware Kommunikationen: {len(advo_kommunikationen)} total")
print(f" - Mit Wert (sollten in EspoCRM sein): {len(advo_with_value)}")
print(f" - Empty Slots: {len(advo_empty_slots)}")
print(f" - Nicht-sync-relevant: {len(advo_non_sync)}")
print()
print(f"EspoCRM Kommunikationen: {len(espo_emails) + len(espo_phones)} total")
print(f" - Emails: {len(espo_emails)}")
print(f" - Phones: {len(espo_phones)}")
print()
# Detaillierte Analyse der Empty Slots
print("📋 Empty Slots in Advoware:")
for i, slot in enumerate(advo_empty_slots, 1):
marker = parse_marker(slot.get('bemerkung', ''))
kommkz = marker.get('kommKz') if marker else 'N/A'
rowid = slot.get('rowId', 'N/A')[:20]
print(f" {i}. kommKz={kommkz} | rowId={rowid}... | bemerkung={slot.get('bemerkung', '')[:40]}")
print()
print("💡 URSACHE:")
print(f" - {len(advo_empty_slots)} Empty Slots werden NICHT aufgeräumt")
print(f" - Dies ist Problem #2: Empty Slot Accumulation")
print(f" - Nur {len(advo_with_value)} Einträge mit Wert, aber Hash beinhaltet ALLE {len(advo_kommunikationen)}")
print()
# ========== PROBLEM 3: MARKER ANALYSIS ==========
print("🟡 PROBLEM 3: MARKER VALIDIERUNG")
print("-" * 80)
marker_issues = []
for komm in advo_with_value:
tlf = (komm.get('tlf') or '').strip()
bemerkung = komm.get('bemerkung', '')
marker = parse_marker(bemerkung)
if marker:
synced_value = marker.get('synced_value', '')
if synced_value != tlf:
marker_issues.append({
'tlf': tlf,
'synced_value': synced_value,
'marker': bemerkung[:50]
})
if marker_issues:
print(f"{len(marker_issues)} Marker stimmen NICHT mit aktuellem Wert überein:")
for issue in marker_issues:
print(f" - Aktuell: '{issue['tlf']}'")
print(f" Marker: '{issue['synced_value']}'")
print(f" Marker-String: {issue['marker']}...")
print()
print("💡 URSACHE:")
print(" - Dies deutet auf Problem #6: Marker-Update fehlgeschlagen")
print(" - Oder Var6 wurde erkannt aber Marker nicht aktualisiert")
else:
print("✅ Alle Marker stimmen mit aktuellen Werten überein")
print()
# ========== PROBLEM 4: HASH COVERAGE ==========
print("🟡 PROBLEM 4: HASH-BERECHNUNG")
print("-" * 80)
import hashlib
# Aktueller Code (FALSCH - beinhaltet ALLE)
all_rowids = sorted([k.get('rowId', '') for k in advo_kommunikationen if k.get('rowId')])
wrong_hash = hashlib.md5(''.join(all_rowids).encode()).hexdigest()[:16]
# Korrekt (nur sync-relevante)
sync_relevant_komm = [k for k in advo_kommunikationen if should_sync_to_espocrm(k) and (k.get('tlf') or '').strip()]
sync_rowids = sorted([k.get('rowId', '') for k in sync_relevant_komm if k.get('rowId')])
correct_hash = hashlib.md5(''.join(sync_rowids).encode()).hexdigest()[:16]
stored_hash = espo_entity.get('kommunikationHash')
print(f"Hash-Vergleich:")
print(f" - Gespeichert: {stored_hash}")
print(f" - Aktuell (ALL): {wrong_hash} {'' if wrong_hash == stored_hash else ''}")
print(f" - Korrekt (nur sync-relevant): {correct_hash} {'' if correct_hash == stored_hash else ''}")
print()
print(f"Rowids einbezogen:")
print(f" - ALL: {len(all_rowids)} Kommunikationen")
print(f" - Sync-relevant: {len(sync_rowids)} Kommunikationen")
print()
print("💡 URSACHE:")
print(" - Dies ist Problem #3: Hash beinhaltet ALLE statt nur sync-relevante")
print(" - Empty Slots ändern Hash obwohl sie nicht in EspoCRM sind")
print()
# ========== ZUSAMMENFASSUNG ==========
print("="*80)
print("ZUSAMMENFASSUNG DER PROBLEME")
print("="*80)
print()
print("✅ BESTÄTIGT - Die folgenden Probleme existieren:")
print()
print("1. ❌ Problem #13: Keine Validierung von Sync-Ergebnissen")
print(" → Stammdaten sind NICHT synchron obwohl syncStatus='clean'")
print()
print("2. ❌ Problem #2: Empty Slot Accumulation")
print(f"{len(advo_empty_slots)} Empty Slots sammeln sich an")
print()
print("3. ❌ Problem #3: Hash-Berechnung inkorrekt")
print(f" → Hash beinhaltet {len(all_rowids)} statt {len(sync_rowids)} Kommunikationen")
print()
if marker_issues:
print("4. ❌ Problem #6: Marker-Update Failures")
print(f"{len(marker_issues)} Marker stimmen nicht mit aktuellem Wert überein")
print()
print("="*80)
if __name__ == '__main__':
asyncio.run(analyze())

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
Vergleicht Advoware Entity (betNr 104860) mit EspoCRM Entity (68e3e7eab49f09adb)
um zu prüfen ob sie synchron sind.
"""
import asyncio
import sys
import json
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from services.advoware import AdvowareAPI
from services.advoware_service import AdvowareService
from services.espocrm import EspoCRMAPI
from services.espocrm_mapper import BeteiligteMapper
from services.beteiligte_sync_utils import BeteiligteSync
import hashlib
class SimpleContext:
"""Minimal context for logging"""
class Logger:
def info(self, msg): print(f" {msg}")
def debug(self, msg): print(f"🔍 {msg}")
def warn(self, msg): print(f"⚠️ {msg}")
def warning(self, msg): print(f"⚠️ {msg}")
def error(self, msg): print(f"{msg}")
def __init__(self):
self.logger = self.Logger()
def calculate_komm_hash(kommunikationen):
"""Berechnet Hash wie im Code"""
komm_rowids = sorted([k.get('rowId', '') for k in kommunikationen if k.get('rowId')])
return hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
async def compare_entities():
context = SimpleContext()
# IDs
betnr = 104860
espo_id = "68e3e7eab49f09adb"
print(f"\n{'='*80}")
print(f"ENTITY COMPARISON")
print(f"{'='*80}")
print(f"Advoware betNr: {betnr}")
print(f"EspoCRM ID: {espo_id}")
print(f"{'='*80}\n")
# Initialize APIs
advoware_api = AdvowareAPI(context)
advoware_service = AdvowareService(context)
espocrm = EspoCRMAPI(context)
mapper = BeteiligteMapper()
sync_utils = BeteiligteSync(espocrm, None, context)
# ========== FETCH ADVOWARE ==========
print("\n📥 Fetching Advoware Entity...")
try:
advo_result = await advoware_api.api_call(
f'api/v1/advonet/Beteiligte/{betnr}',
method='GET'
)
if isinstance(advo_result, list):
advo_entity = advo_result[0] if advo_result else None
else:
advo_entity = advo_result
if not advo_entity:
print("❌ Advoware Entity nicht gefunden!")
return
print(f"✅ Advoware Entity geladen")
print(f" - Name: {advo_entity.get('name')}")
print(f" - rowId: {advo_entity.get('rowId', 'N/A')[:40]}...")
print(f" - geaendertAm: {advo_entity.get('geaendertAm')}")
print(f" - Kommunikationen: {len(advo_entity.get('kommunikation', []))}")
except Exception as e:
print(f"❌ Fehler beim Laden von Advoware: {e}")
import traceback
traceback.print_exc()
return
# ========== FETCH ESPOCRM ==========
print("\n📥 Fetching EspoCRM Entity...")
try:
espo_entity = await espocrm.get_entity('CBeteiligte', espo_id)
if not espo_entity:
print("❌ EspoCRM Entity nicht gefunden!")
return
print(f"✅ EspoCRM Entity geladen")
print(f" - Name: {espo_entity.get('name')}")
print(f" - betnr: {espo_entity.get('betnr')}")
print(f" - modifiedAt: {espo_entity.get('modifiedAt')}")
print(f" - syncStatus: {espo_entity.get('syncStatus')}")
print(f" - advowareLastSync: {espo_entity.get('advowareLastSync')}")
print(f" - advowareRowId: {espo_entity.get('advowareRowId', 'N/A')[:40]}...")
print(f" - kommunikationHash: {espo_entity.get('kommunikationHash')}")
print(f" - emailAddressData: {len(espo_entity.get('emailAddressData', []))}")
print(f" - phoneNumberData: {len(espo_entity.get('phoneNumberData', []))}")
except Exception as e:
print(f"❌ Fehler beim Laden von EspoCRM: {e}")
import traceback
traceback.print_exc()
return
# ========== COMPARISON ==========
print(f"\n{'='*80}")
print("STAMMDATEN VERGLEICH")
print(f"{'='*80}\n")
# Timestamp comparison
comparison = sync_utils.compare_entities(espo_entity, advo_entity)
print(f"🔍 Timestamp-Vergleich: {comparison}")
# Field-by-field comparison
print("\n📊 Feld-für-Feld Vergleich (Stammdaten):\n")
# Map Advoware → EspoCRM für Vergleich
advo_mapped = mapper.map_advoware_to_cbeteiligte(advo_entity)
fields_to_compare = [
'name', 'rechtsform', 'geburtsdatum', 'anrede',
'handelsregister', 'geschlecht', 'titel'
]
differences = []
for field in fields_to_compare:
espo_val = espo_entity.get(field)
advo_val = advo_mapped.get(field)
match = "" if espo_val == advo_val else ""
print(f"{match} {field:20} | EspoCRM: {str(espo_val)[:40]:40} | Advoware: {str(advo_val)[:40]:40}")
if espo_val != advo_val:
differences.append({
'field': field,
'espocrm': espo_val,
'advoware': advo_val
})
# ========== KOMMUNIKATION COMPARISON ==========
print(f"\n{'='*80}")
print("KOMMUNIKATION VERGLEICH")
print(f"{'='*80}\n")
advo_kommunikationen = advo_entity.get('kommunikation', [])
espo_emails = espo_entity.get('emailAddressData', [])
espo_phones = espo_entity.get('phoneNumberData', [])
# Hash Vergleich
current_hash = calculate_komm_hash(advo_kommunikationen)
stored_hash = espo_entity.get('kommunikationHash')
print(f"📊 Kommunikations-Hash:")
print(f" - Gespeichert in EspoCRM: {stored_hash}")
print(f" - Aktuell in Advoware: {current_hash}")
print(f" - Match: {'✅ JA' if current_hash == stored_hash else '❌ NEIN'}")
# Advoware Kommunikationen im Detail
print(f"\n📞 Advoware Kommunikationen ({len(advo_kommunikationen)}):")
for i, komm in enumerate(advo_kommunikationen, 1):
tlf = (komm.get('tlf') or '').strip()
kommkz = komm.get('kommKz', 0)
bemerkung = komm.get('bemerkung', '')[:50]
online = komm.get('online', False)
rowid = komm.get('rowId', 'N/A')[:20]
print(f" {i}. {tlf:30} | kommKz={kommkz:2} | online={online} | rowId={rowid}...")
if bemerkung:
print(f" Bemerkung: {bemerkung}...")
# EspoCRM Emails
print(f"\n📧 EspoCRM Emails ({len(espo_emails)}):")
for i, email in enumerate(espo_emails, 1):
addr = email.get('emailAddress', '')
primary = email.get('primary', False)
print(f" {i}. {addr:40} | primary={primary}")
# EspoCRM Phones
print(f"\n📱 EspoCRM Phones ({len(espo_phones)}):")
for i, phone in enumerate(espo_phones, 1):
num = phone.get('phoneNumber', '')
typ = phone.get('type', 'N/A')
primary = phone.get('primary', False)
print(f" {i}. {num:30} | type={typ:10} | primary={primary}")
# ========== SUMMARY ==========
print(f"\n{'='*80}")
print("ZUSAMMENFASSUNG")
print(f"{'='*80}\n")
if differences:
print(f"❌ STAMMDATEN NICHT SYNCHRON! {len(differences)} Unterschiede gefunden:")
for diff in differences:
print(f" - {diff['field']}: EspoCRM='{diff['espocrm']}' ≠ Advoware='{diff['advoware']}'")
else:
print("✅ Stammdaten sind synchron")
print()
if current_hash != stored_hash:
print(f"❌ KOMMUNIKATION NICHT SYNCHRON! Hash stimmt nicht überein")
else:
print("✅ Kommunikation-Hash stimmt überein (aber könnte trotzdem Unterschiede geben)")
print()
# Total count check
total_espo_komm = len(espo_emails) + len(espo_phones)
total_advo_komm = len([k for k in advo_kommunikationen if (k.get('tlf') or '').strip()])
if total_espo_komm != total_advo_komm:
print(f"⚠️ Anzahl-Unterschied: EspoCRM={total_espo_komm} ≠ Advoware={total_advo_komm}")
else:
print(f"✅ Anzahl stimmt überein: {total_espo_komm} Kommunikationen")
print(f"\n{'='*80}\n")
if __name__ == '__main__':
asyncio.run(compare_entities())