Files
motia/bitbylaw/services/kommunikation_sync_utils.py

704 lines
33 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Kommunikation Sync Utilities
Bidirektionale Synchronisation: Advoware ↔ EspoCRM
Strategie:
- Emails: emailAddressData[] ↔ Advoware Kommunikationen (kommKz: 4,8,11,12)
- Phones: phoneNumberData[] ↔ Advoware Kommunikationen (kommKz: 1,2,3,6,7,9,10)
- Matching: Hash-basiert via bemerkung-Marker
- Type Detection: Marker > Top-Level > Value Pattern > Default
"""
import logging
from typing import Dict, List, Optional, Tuple, Any
from services.kommunikation_mapper import (
parse_marker, create_marker, create_slot_marker,
detect_kommkz, encode_value, decode_value,
is_email_type, is_phone_type,
advoware_to_espocrm_email, advoware_to_espocrm_phone,
find_matching_advoware, find_empty_slot,
should_sync_to_espocrm, get_user_bemerkung,
calculate_hash,
EMAIL_KOMMKZ, PHONE_KOMMKZ
)
from services.advoware_service import AdvowareService
from services.espocrm import EspoCRMAPI
logger = logging.getLogger(__name__)
class KommunikationSyncManager:
"""Manager für Kommunikation-Synchronisation"""
def __init__(self, advoware: AdvowareService, espocrm: EspoCRMAPI, context=None):
self.advoware = advoware
self.espocrm = espocrm
self.context = context
self.logger = context.logger if context else logger
# ========== BIDIRECTIONAL SYNC ==========
async def sync_bidirectional(self, beteiligte_id: str, betnr: int,
direction: str = 'both') -> Dict[str, Any]:
"""
Bidirektionale Synchronisation mit intelligentem Diffing
Optimiert:
- Lädt Daten nur 1x von jeder Seite
- Echtes 3-Way Diffing (Advoware, EspoCRM, Marker)
- Handhabt alle 6 Szenarien korrekt
Args:
direction: 'both', 'to_espocrm', 'to_advoware'
Returns:
Combined results mit detaillierten Änderungen
"""
result = {
'advoware_to_espocrm': {'emails_synced': 0, 'phones_synced': 0, 'errors': []},
'espocrm_to_advoware': {'created': 0, 'updated': 0, 'deleted': 0, 'errors': []},
'summary': {'total_changes': 0}
}
try:
# ========== LADE DATEN NUR 1X ==========
self.logger.info(f"[KOMM] Bidirectional Sync: betnr={betnr}, bet_id={beteiligte_id}")
# Advoware Daten
advo_result = await self.advoware.get_beteiligter(betnr)
if isinstance(advo_result, list):
advo_bet = advo_result[0] if advo_result else None
else:
advo_bet = advo_result
if not advo_bet:
result['advoware_to_espocrm']['errors'].append("Advoware Beteiligte nicht gefunden")
result['espocrm_to_advoware']['errors'].append("Advoware Beteiligte nicht gefunden")
return result
# EspoCRM Daten
espo_bet = await self.espocrm.get_entity('CBeteiligte', beteiligte_id)
if not espo_bet:
result['advoware_to_espocrm']['errors'].append("EspoCRM Beteiligte nicht gefunden")
result['espocrm_to_advoware']['errors'].append("EspoCRM Beteiligte nicht gefunden")
return result
advo_kommunikationen = advo_bet.get('kommunikation', [])
espo_emails = espo_bet.get('emailAddressData', [])
espo_phones = espo_bet.get('phoneNumberData', [])
self.logger.info(f"[KOMM] Geladen: {len(advo_kommunikationen)} Advoware, {len(espo_emails)} EspoCRM emails, {len(espo_phones)} EspoCRM phones")
# Check ob initialer Sync
stored_komm_hash = espo_bet.get('kommunikationHash')
is_initial_sync = not stored_komm_hash
# ========== 3-WAY DIFFING MIT HASH-BASIERTER KONFLIKT-ERKENNUNG ==========
diff = self._compute_diff(advo_kommunikationen, espo_emails, espo_phones, advo_bet, espo_bet)
espo_wins = diff.get('espo_wins', False)
self.logger.info(f"[KOMM] ===== DIFF RESULTS =====")
self.logger.info(f"[KOMM] Diff: {len(diff['advo_changed'])} Advoware changed, {len(diff['espo_changed'])} EspoCRM changed, "
f"{len(diff['advo_new'])} Advoware new, {len(diff['espo_new'])} EspoCRM new, "
f"{len(diff['advo_deleted'])} Advoware deleted, {len(diff['espo_deleted'])} EspoCRM deleted")
self.logger.info(f"[KOMM] ===== CONFLICT STATUS: espo_wins={espo_wins} =====")
# ========== APPLY CHANGES ==========
# 1. Advoware → EspoCRM (Var4: Neu in Advoware, Var6: Geändert in Advoware)
# WICHTIG: Bei Konflikt (espo_wins=true) KEINE Advoware-Änderungen übernehmen!
if direction in ['both', 'to_espocrm'] and not espo_wins:
self.logger.info(f"[KOMM] ✅ Applying Advoware→EspoCRM changes...")
espo_result = await self._apply_advoware_to_espocrm(
beteiligte_id, diff, advo_bet
)
result['advoware_to_espocrm'] = espo_result
elif direction in ['both', 'to_espocrm'] and espo_wins:
self.logger.info(f"[KOMM] ⚠️ CONFLICT: EspoCRM wins - skipping Advoware→EspoCRM sync")
else:
self.logger.info(f"[KOMM] Skipping Advoware→EspoCRM (direction={direction})")
# 2. EspoCRM → Advoware (Var1: Neu in EspoCRM, Var2: Gelöscht in EspoCRM, Var5: Geändert in EspoCRM)
if direction in ['both', 'to_advoware']:
advo_result = await self._apply_espocrm_to_advoware(
betnr, diff, advo_bet
)
result['espocrm_to_advoware'] = advo_result
total_changes = (
result['advoware_to_espocrm']['emails_synced'] +
result['advoware_to_espocrm']['phones_synced'] +
result['espocrm_to_advoware']['created'] +
result['espocrm_to_advoware']['updated'] +
result['espocrm_to_advoware']['deleted']
)
result['summary']['total_changes'] = total_changes
# Speichere neuen Kommunikations-Hash in EspoCRM (für nächsten Sync)
# WICHTIG: Auch beim initialen Sync oder wenn keine Änderungen
if total_changes > 0 or is_initial_sync:
# Re-berechne Hash nach allen Änderungen
advo_result_final = await self.advoware.get_beteiligter(betnr)
if isinstance(advo_result_final, list):
advo_bet_final = advo_result_final[0]
else:
advo_bet_final = advo_result_final
import hashlib
final_kommunikationen = advo_bet_final.get('kommunikation', [])
komm_rowids = sorted([k.get('rowId', '') for k in final_kommunikationen if k.get('rowId')])
new_komm_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
await self.espocrm.update_entity('CBeteiligte', beteiligte_id, {
'kommunikationHash': new_komm_hash
})
self.logger.info(f"[KOMM] ✅ Updated kommunikationHash: {new_komm_hash}")
self.logger.info(f"[KOMM] ✅ Bidirectional Sync complete: {total_changes} total changes")
except Exception as e:
self.logger.error(f"[KOMM] Fehler bei Bidirectional Sync: {e}", exc_info=True)
result['advoware_to_espocrm']['errors'].append(str(e))
result['espocrm_to_advoware']['errors'].append(str(e))
return result
# ========== 3-WAY DIFFING ==========
def _compute_diff(self, advo_kommunikationen: List[Dict], espo_emails: List[Dict],
espo_phones: List[Dict], advo_bet: Dict, espo_bet: Dict) -> Dict[str, List]:
"""
Berechnet Diff zwischen Advoware und EspoCRM mit Kommunikations-Hash-basierter Konflikt-Erkennung
Da die Beteiligte-rowId sich NICHT bei Kommunikations-Änderungen ändert,
nutzen wir einen Hash aus allen Kommunikations-rowIds + EspoCRM modifiedAt.
Returns:
{
'advo_changed': [(komm, old_value, new_value)], # Var6: In Advoware geändert
'advo_new': [komm], # Var4: Neu in Advoware (ohne Marker)
'advo_deleted': [(value, item)], # Var3: In Advoware gelöscht (via Hash)
'espo_changed': [(value, advo_komm)], # Var5: In EspoCRM geändert
'espo_new': [(value, item)], # Var1: Neu in EspoCRM (via Hash)
'espo_deleted': [advo_komm], # Var2: In EspoCRM gelöscht
'no_change': [(value, komm, item)] # Keine Änderung
}
"""
diff = {
'advo_changed': [],
'advo_new': [],
'advo_deleted': [], # NEU: Var3
'espo_changed': [],
'espo_new': [],
'espo_deleted': [],
'no_change': [],
'espo_wins': False # Default
}
# Hole Sync-Metadaten für Konflikt-Erkennung
espo_modified = espo_bet.get('modifiedAt')
last_sync = espo_bet.get('advowareLastSync')
# Berechne Hash aus Kommunikations-rowIds
import hashlib
komm_rowids = sorted([k.get('rowId', '') for k in advo_kommunikationen if k.get('rowId')])
current_advo_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
stored_komm_hash = espo_bet.get('kommunikationHash')
# Parse Timestamps
from services.beteiligte_sync_utils import BeteiligteSync
espo_modified_ts = BeteiligteSync.parse_timestamp(espo_modified)
last_sync_ts = BeteiligteSync.parse_timestamp(last_sync)
# Bestimme wer geändert hat
espo_changed_since_sync = espo_modified_ts and last_sync_ts and espo_modified_ts > last_sync_ts
advo_changed_since_sync = stored_komm_hash and current_advo_hash != stored_komm_hash
# Initial Sync: Wenn kein Hash gespeichert ist, behandle als "keine Änderung in Advoware"
is_initial_sync = not stored_komm_hash
self.logger.info(f"[KOMM] 🔍 Konflikt-Check:")
self.logger.info(f"[KOMM] - EspoCRM changed: {espo_changed_since_sync} (modified={espo_modified}, lastSync={last_sync})")
self.logger.info(f"[KOMM] - Advoware changed: {advo_changed_since_sync} (stored_hash={stored_komm_hash}, current_hash={current_advo_hash})")
self.logger.info(f"[KOMM] - Initial sync: {is_initial_sync}")
self.logger.info(f"[KOMM] - Kommunikation rowIds count: {len(komm_rowids)}")
if espo_changed_since_sync and advo_changed_since_sync:
self.logger.warn(f"[KOMM] ⚠️ KONFLIKT: Beide Seiten geändert seit letztem Sync - EspoCRM WINS")
espo_wins = True
else:
espo_wins = False
# Speichere espo_wins im diff für spätere Verwendung
diff['espo_wins'] = espo_wins
# Baue EspoCRM Value Map
espo_values = {}
for email in espo_emails:
val = email.get('emailAddress', '').strip()
if val:
espo_values[val] = {'value': val, 'is_email': True, 'primary': email.get('primary', False), 'type': 'email'}
for phone in espo_phones:
val = phone.get('phoneNumber', '').strip()
if val:
espo_values[val] = {'value': val, 'is_email': False, 'primary': phone.get('primary', False), 'type': phone.get('type', 'Office')}
# Baue Advoware Maps
advo_with_marker = {} # synced_value -> (komm, current_value)
advo_without_marker = [] # Einträge ohne Marker (von Advoware angelegt)
for komm in advo_kommunikationen:
if not should_sync_to_espocrm(komm):
continue
tlf = (komm.get('tlf') or '').strip()
if not tlf: # Leere Einträge ignorieren
continue
bemerkung = komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
if marker and not marker['is_slot']:
# Hat Marker → Von EspoCRM synchronisiert
synced_value = marker['synced_value']
advo_with_marker[synced_value] = (komm, tlf)
else:
# Kein Marker → Von Advoware angelegt (Var4)
advo_without_marker.append(komm)
# ========== ANALYSE ==========
# 1. Prüfe Advoware-Einträge MIT Marker
for synced_value, (komm, current_value) in advo_with_marker.items():
if synced_value != current_value:
# Var6: In Advoware geändert
self.logger.info(f"[KOMM] ✏️ Var6: Changed in Advoware - synced='{synced_value[:30]}...', current='{current_value[:30]}...'")
diff['advo_changed'].append((komm, synced_value, current_value))
elif synced_value in espo_values:
espo_item = espo_values[synced_value]
# Prüfe ob primary geändert wurde (Var5 könnte auch sein)
current_online = komm.get('online', False)
espo_primary = espo_item['primary']
if current_online != espo_primary:
# Var5: EspoCRM hat primary geändert
self.logger.info(f"[KOMM] 🔄 Var5: Primary changed in EspoCRM - value='{synced_value}', advo_online={current_online}, espo_primary={espo_primary}")
diff['espo_changed'].append((synced_value, komm, espo_item))
else:
# Keine Änderung
self.logger.info(f"[KOMM] ✓ No change: '{synced_value[:30]}...'")
diff['no_change'].append((synced_value, komm, espo_item))
else:
# Eintrag war mal in EspoCRM (hat Marker), ist jetzt aber nicht mehr da
# → Var2: In EspoCRM gelöscht
self.logger.info(f"[KOMM] 🗑️ Var2: Deleted in EspoCRM - synced_value='{synced_value}', komm_id={komm.get('id')}")
diff['espo_deleted'].append(komm)
# 2. Prüfe Advoware-Einträge OHNE Marker
for komm in advo_without_marker:
# Var4: Neu in Advoware angelegt
tlf = (komm.get('tlf') or '').strip()
self.logger.info(f"[KOMM] Var4: New in Advoware - value='{tlf[:30]}...', komm_id={komm.get('id')}")
diff['advo_new'].append(komm)
# 3. Prüfe EspoCRM-Einträge die NICHT in Advoware sind (oder nur mit altem Marker)
for value, espo_item in espo_values.items():
if value not in advo_with_marker:
# HASH-BASIERTE KONFLIKT-LOGIK: Unterscheide Var1 von Var3
if espo_wins or (espo_changed_since_sync and not advo_changed_since_sync):
# Var1: Neu in EspoCRM (EspoCRM geändert, Advoware nicht)
self.logger.info(f"[KOMM] Var1: New in EspoCRM '{value}' (espo changed, advo unchanged)")
diff['espo_new'].append((value, espo_item))
elif advo_changed_since_sync and not espo_changed_since_sync:
# Var3: In Advoware gelöscht (Advoware geändert, EspoCRM nicht)
self.logger.info(f"[KOMM] Var3: Deleted in Advoware '{value}' (advo changed, espo unchanged)")
diff['advo_deleted'].append((value, espo_item))
else:
# Kein klarer Hinweis - Default: Behandle als Var1 (neu in EspoCRM)
self.logger.info(f"[KOMM] Var1 (default): '{value}' - no clear indication, treating as new in EspoCRM")
diff['espo_new'].append((value, espo_item))
return diff
# ========== APPLY CHANGES ==========
async def _apply_advoware_to_espocrm(self, beteiligte_id: str, diff: Dict,
advo_bet: Dict) -> Dict[str, Any]:
"""
Wendet Advoware-Änderungen auf EspoCRM an (Var4, Var6)
"""
result = {'emails_synced': 0, 'phones_synced': 0, 'markers_updated': 0, 'errors': []}
try:
# Lade aktuelle EspoCRM Daten
espo_bet = await self.espocrm.get_entity('CBeteiligte', beteiligte_id)
espo_emails = list(espo_bet.get('emailAddressData', []))
espo_phones = list(espo_bet.get('phoneNumberData', []))
# Var6: Advoware-Änderungen → Update Marker + Sync zu EspoCRM
for komm, old_value, new_value in diff['advo_changed']:
self.logger.info(f"[KOMM] Var6: Advoware changed '{old_value}''{new_value}'")
# Update Marker in Advoware
bemerkung = komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
user_text = marker.get('user_text', '') if marker else ''
kommkz = marker['kommKz'] if marker else detect_kommkz(new_value, advo_bet)
new_marker = create_marker(new_value, kommkz, user_text)
await self.advoware.update_kommunikation(advo_bet['betNr'], komm['id'], {
'bemerkung': new_marker
})
result['markers_updated'] += 1
# Update in EspoCRM: Finde alten Wert und ersetze mit neuem
if is_email_type(kommkz):
for i, email in enumerate(espo_emails):
if email.get('emailAddress') == old_value:
espo_emails[i] = {
'emailAddress': new_value,
'lower': new_value.lower(),
'primary': komm.get('online', False),
'optOut': False,
'invalid': False
}
result['emails_synced'] += 1
break
else:
for i, phone in enumerate(espo_phones):
if phone.get('phoneNumber') == old_value:
type_map = {1: 'Office', 2: 'Fax', 3: 'Mobile', 6: 'Home', 7: 'Fax', 9: 'Mobile', 10: 'Other'}
espo_phones[i] = {
'phoneNumber': new_value,
'type': type_map.get(kommkz, 'Other'),
'primary': komm.get('online', False),
'optOut': False,
'invalid': False
}
result['phones_synced'] += 1
break
# Var4: Neu in Advoware → Zu EspoCRM hinzufügen + Marker setzen
for komm in diff['advo_new']:
tlf = (komm.get('tlf') or '').strip()
kommkz = detect_kommkz(tlf, advo_bet, komm.get('bemerkung'))
self.logger.info(f"[KOMM] Var4: New in Advoware '{tlf}', syncing to EspoCRM")
# Setze Marker in Advoware
new_marker = create_marker(tlf, kommkz)
await self.advoware.update_kommunikation(advo_bet['betNr'], komm['id'], {
'bemerkung': new_marker
})
# Zu EspoCRM hinzufügen
if is_email_type(kommkz):
espo_emails.append({
'emailAddress': tlf,
'lower': tlf.lower(),
'primary': komm.get('online', False),
'optOut': False,
'invalid': False
})
result['emails_synced'] += 1
else:
type_map = {1: 'Office', 2: 'Fax', 3: 'Mobile', 6: 'Home', 7: 'Fax', 9: 'Mobile', 10: 'Other'}
espo_phones.append({
'phoneNumber': tlf,
'type': type_map.get(kommkz, 'Other'),
'primary': komm.get('online', False),
'optOut': False,
'invalid': False
})
result['phones_synced'] += 1
# Var3: In Advoware gelöscht → Aus EspoCRM entfernen
for value, espo_item in diff.get('advo_deleted', []):
self.logger.info(f"[KOMM] Var3: Deleted in Advoware '{value}', removing from EspoCRM")
if espo_item['is_email']:
espo_emails = [e for e in espo_emails if e.get('emailAddress') != value]
result['emails_synced'] += 1 # Zählt als "synced" (gelöscht)
else:
espo_phones = [p for p in espo_phones if p.get('phoneNumber') != value]
result['phones_synced'] += 1
# Update EspoCRM wenn Änderungen
if result['emails_synced'] > 0 or result['phones_synced'] > 0:
await self.espocrm.update_entity('CBeteiligte', beteiligte_id, {
'emailAddressData': espo_emails,
'phoneNumberData': espo_phones
})
self.logger.info(f"[KOMM] ✅ Updated EspoCRM: {result['emails_synced']} emails, {result['phones_synced']} phones")
except Exception as e:
self.logger.error(f"[KOMM] Fehler bei Advoware→EspoCRM Apply: {e}", exc_info=True)
result['errors'].append(str(e))
return result
async def _apply_espocrm_to_advoware(self, betnr: int, diff: Dict,
advo_bet: Dict) -> Dict[str, Any]:
"""
Wendet EspoCRM-Änderungen auf Advoware an (Var1, Var2, Var3, Var5)
"""
result = {'created': 0, 'updated': 0, 'deleted': 0, 'errors': []}
try:
advo_kommunikationen = advo_bet.get('kommunikation', [])
# Var2: In EspoCRM gelöscht → Empty Slot in Advoware
for komm in diff['espo_deleted']:
komm_id = komm.get('id')
tlf = (komm.get('tlf') or '').strip()
self.logger.info(f"[KOMM] 🗑️ Var2: Deleted in EspoCRM - komm_id={komm_id}, value='{tlf[:30]}...'")
await self._create_empty_slot(betnr, komm)
self.logger.info(f"[KOMM] ✅ Empty slot created for komm_id={komm_id}")
result['deleted'] += 1
# Var5: In EspoCRM geändert (z.B. primary Flag)
for value, advo_komm, espo_item in diff['espo_changed']:
self.logger.info(f"[KOMM] ✏️ Var5: EspoCRM changed '{value[:30]}...', primary={espo_item.get('primary')}")
bemerkung = advo_komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
user_text = marker.get('user_text', '') if marker else ''
# Erkenne kommKz mit espo_type
if marker:
kommkz = marker['kommKz']
self.logger.info(f"[KOMM] kommKz from marker: {kommkz}")
else:
espo_type = espo_item.get('type', 'email' if '@' in value else None)
kommkz = detect_kommkz(value, advo_bet, espo_type=espo_type)
self.logger.info(f"[KOMM] kommKz detected: espo_type={espo_type}, kommKz={kommkz}")
# Update in Advoware
await self.advoware.update_kommunikation(betnr, advo_komm['id'], {
'tlf': value,
'online': espo_item['primary'],
'bemerkung': create_marker(value, kommkz, user_text)
})
self.logger.info(f"[KOMM] ✅ Updated komm_id={advo_komm['id']}, kommKz={kommkz}")
result['updated'] += 1
# Var1: Neu in EspoCRM → Create oder reuse Slot in Advoware
for value, espo_item in diff['espo_new']:
self.logger.info(f"[KOMM] Var1: New in EspoCRM '{value[:30]}...', type={espo_item.get('type')}")
# Erkenne kommKz mit espo_type
espo_type = espo_item.get('type', 'email' if '@' in value else None)
kommkz = detect_kommkz(value, advo_bet, espo_type=espo_type)
self.logger.info(f"[KOMM] 🔍 kommKz detected: espo_type={espo_type}, kommKz={kommkz}")
# Suche leeren Slot
empty_slot = find_empty_slot(kommkz, advo_kommunikationen)
if empty_slot:
# Reuse Slot
self.logger.info(f"[KOMM] ♻️ Reusing empty slot: slot_id={empty_slot['id']}, kommKz={kommkz}")
await self.advoware.update_kommunikation(betnr, empty_slot['id'], {
'tlf': value,
'online': espo_item['primary'],
'bemerkung': create_marker(value, kommkz)
})
self.logger.info(f"[KOMM] ✅ Slot reused successfully")
else:
# Create new
self.logger.info(f"[KOMM] Creating new kommunikation: kommKz={kommkz}")
await self.advoware.create_kommunikation(betnr, {
'tlf': value,
'kommKz': kommkz,
'online': espo_item['primary'],
'bemerkung': create_marker(value, kommkz)
})
self.logger.info(f"[KOMM] ✅ Created new kommunikation with kommKz={kommkz}")
result['created'] += 1
except Exception as e:
self.logger.error(f"[KOMM] Fehler bei EspoCRM→Advoware Apply: {e}", exc_info=True)
result['errors'].append(str(e))
return result
# ========== HELPER METHODS ==========
async def _create_empty_slot(self, betnr: int, advo_komm: Dict) -> None:
"""Erstellt leeren Slot für gelöschten Eintrag"""
try:
komm_id = advo_komm['id']
bemerkung = advo_komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
if not marker:
self.logger.warning(f"[KOMM] Kein Marker gefunden für gelöschten Eintrag: {komm_id}")
return
kommkz = marker['kommKz']
slot_marker = create_slot_marker(kommkz)
update_data = {
'tlf': '',
'bemerkung': slot_marker,
'online': False
}
await self.advoware.update_kommunikation(betnr, komm_id, update_data)
self.logger.info(f"[KOMM] ✅ Created empty slot: komm_id={komm_id}, kommKz={kommkz}")
except Exception as e:
self.logger.error(f"[KOMM] Fehler beim Erstellen von Empty Slot: {e}", exc_info=True)
def _needs_update(self, advo_komm: Dict, espo_item: Dict) -> bool:
"""Prüft ob Update nötig ist"""
current_value = (advo_komm.get('tlf') or '').strip()
new_value = espo_item['value'].strip()
current_online = advo_komm.get('online', False)
new_online = espo_item.get('primary', False)
return current_value != new_value or current_online != new_online
async def _update_kommunikation(self, betnr: int, advo_komm: Dict, espo_item: Dict) -> None:
"""Updated Advoware Kommunikation"""
try:
komm_id = advo_komm['id']
value = espo_item['value']
# Erkenne kommKz (sollte aus Marker kommen)
bemerkung = advo_komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
kommkz = marker['kommKz'] if marker else detect_kommkz(value, espo_type=espo_item.get('type'))
# Behalte User-Bemerkung
user_text = get_user_bemerkung(advo_komm)
new_marker = create_marker(value, kommkz, user_text)
update_data = {
'tlf': value,
'bemerkung': new_marker,
'online': espo_item.get('primary', False)
}
await self.advoware.update_kommunikation(betnr, komm_id, update_data)
self.logger.info(f"[KOMM] ✅ Updated: komm_id={komm_id}, value={value[:30]}...")
except Exception as e:
self.logger.error(f"[KOMM] Fehler beim Update: {e}", exc_info=True)
async def _create_or_reuse_kommunikation(self, betnr: int, espo_item: Dict,
advo_kommunikationen: List[Dict]) -> bool:
"""
Erstellt neue Kommunikation oder nutzt leeren Slot
Returns:
True wenn erfolgreich erstellt/reused
"""
try:
value = espo_item['value']
# Erkenne kommKz mit EspoCRM type
espo_type = espo_item.get('type', 'email' if '@' in value else None)
kommkz = detect_kommkz(value, espo_type=espo_type)
self.logger.info(f"[KOMM] 🔍 kommKz detection: value='{value[:30]}...', espo_type={espo_type}, kommKz={kommkz}")
# Suche leeren Slot mit passendem kommKz
empty_slot = find_empty_slot(kommkz, advo_kommunikationen)
new_marker = create_marker(value, kommkz)
if empty_slot:
# ========== REUSE SLOT ==========
komm_id = empty_slot['id']
self.logger.info(f"[KOMM] ♻️ Reusing empty slot: komm_id={komm_id}, kommKz={kommkz}")
update_data = {
'tlf': value,
'bemerkung': new_marker,
'online': espo_item.get('primary', False)
}
await self.advoware.update_kommunikation(betnr, komm_id, update_data)
self.logger.info(f"[KOMM] ✅ Slot reused successfully: value='{value[:30]}...'")
else:
# ========== CREATE NEW ==========
self.logger.info(f"[KOMM] Creating new kommunikation entry: kommKz={kommkz}")
create_data = {
'tlf': value,
'bemerkung': new_marker,
'kommKz': kommkz,
'online': espo_item.get('primary', False)
}
await self.advoware.create_kommunikation(betnr, create_data)
self.logger.info(f"[KOMM] ✅ Created new: value='{value[:30]}...', kommKz={kommkz}")
return True
except Exception as e:
self.logger.error(f"[KOMM] Fehler beim Erstellen/Reuse: {e}", exc_info=True)
return False
# ========== CHANGE DETECTION ==========
def detect_kommunikation_changes(old_bet: Dict, new_bet: Dict) -> bool:
"""
Erkennt Änderungen in Kommunikationen via rowId
Args:
old_bet: Alte Beteiligte-Daten (mit kommunikation[])
new_bet: Neue Beteiligte-Daten (mit kommunikation[])
Returns:
True wenn Änderungen erkannt
"""
old_komm = old_bet.get('kommunikation', [])
new_komm = new_bet.get('kommunikation', [])
# Check Count
if len(old_komm) != len(new_komm):
return True
# Check rowIds
old_row_ids = {k.get('rowId') for k in old_komm}
new_row_ids = {k.get('rowId') for k in new_komm}
return old_row_ids != new_row_ids
def detect_espocrm_kommunikation_changes(old_data: Dict, new_data: Dict) -> bool:
"""
Erkennt Änderungen in EspoCRM emailAddressData/phoneNumberData
Returns:
True wenn Änderungen erkannt
"""
old_emails = old_data.get('emailAddressData', [])
new_emails = new_data.get('emailAddressData', [])
old_phones = old_data.get('phoneNumberData', [])
new_phones = new_data.get('phoneNumberData', [])
# Einfacher Vergleich: Count und Values
if len(old_emails) != len(new_emails) or len(old_phones) != len(new_phones):
return True
old_email_values = {e.get('emailAddress') for e in old_emails}
new_email_values = {e.get('emailAddress') for e in new_emails}
old_phone_values = {p.get('phoneNumber') for p in old_phones}
new_phone_values = {p.get('phoneNumber') for p in new_phones}
return old_email_values != new_email_values or old_phone_values != new_phone_values