""" 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