From 8550107b898e714540142f099f4b29ac980322a7 Mon Sep 17 00:00:00 2001 From: bitbylaw Date: Sat, 7 Feb 2026 15:44:56 +0000 Subject: [PATCH] feat: Enhance Advoware API integration with backward compatibility for data payloads and improve logging for sync events --- bitbylaw/scripts/test_beteiligte_sync.py | 387 ++++++++++++++++++ bitbylaw/services/advoware.py | 7 +- bitbylaw/services/espocrm_mapper.py | 96 +---- .../steps/vmh/beteiligte_sync_event_step.py | 51 ++- 4 files changed, 447 insertions(+), 94 deletions(-) create mode 100755 bitbylaw/scripts/test_beteiligte_sync.py diff --git a/bitbylaw/scripts/test_beteiligte_sync.py b/bitbylaw/scripts/test_beteiligte_sync.py new file mode 100755 index 00000000..c1111f45 --- /dev/null +++ b/bitbylaw/scripts/test_beteiligte_sync.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +""" +Beteiligte Sync Test Script + +Testet die vollständige Sync-Funktionalität: +1. Mapper-Transformationen +2. Lock-Mechanismus +3. Timestamp-Vergleich +4. CREATE in Advoware (optional) +5. UPDATE Sync (optional) +6. Konflikt-Resolution + +Usage: + python test_beteiligte_sync.py --test-transforms # Nur Mapper testen + python test_beteiligte_sync.py --test-live # Live-Test mit echten APIs + python test_beteiligte_sync.py --entity-id=XXX # Spezifische Entity testen +""" + +import asyncio +import sys +import argparse +from datetime import datetime +import json + +sys.path.insert(0, '/opt/motia-app/bitbylaw') + +from services.espocrm import EspoCRMAPI +from services.advoware import AdvowareAPI +from services.espocrm_mapper import BeteiligteMapper +from services.beteiligte_sync_utils import BeteiligteSync + + +class MockContext: + """Mock Context für Testing ohne Motia Workbench""" + class Logger: + def info(self, msg): print(f"ℹ️ {msg}") + def debug(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 test_transforms(): + """Test 1: Mapper-Transformationen""" + print("\n" + "="*80) + print("TEST 1: Mapper-Transformationen") + print("="*80) + + mapper = BeteiligteMapper() + + # Test 1a: Person EspoCRM → Advoware + print("\n📤 Test 1a: Person EspoCRM → Advoware") + espo_person = { + 'id': 'test123', + 'firstName': 'Angela', + 'lastName': 'Mustermann', + 'rechtsform': 'Frau', + 'emailAddress': 'angela@example.com', + 'emailAddressData': [ + {'emailAddress': 'angela@example.com', 'primary': True} + ], + 'phoneNumber': '+49123456789', + 'dateOfBirth': '1980-05-15' + } + + advo_result = mapper.map_cbeteiligte_to_advoware(espo_person) + print(f"✅ Mapped:") + print(json.dumps(advo_result, indent=2, ensure_ascii=False)) + + # Test 1b: Firma EspoCRM → Advoware + print("\n📤 Test 1b: Firma EspoCRM → Advoware") + espo_firma = { + 'id': 'test456', + 'firmenname': 'Mustermann GmbH', + 'rechtsform': 'GmbH', + 'emailAddress': 'info@mustermann.de', + 'handelsregisterNummer': 'HRB 12345' + } + + advo_firma = mapper.map_cbeteiligte_to_advoware(espo_firma) + print(f"✅ Mapped:") + print(json.dumps(advo_firma, indent=2, ensure_ascii=False)) + + # Test 1c: Advoware → EspoCRM (Person) + print("\n📥 Test 1c: Advoware → EspoCRM (Person)") + advo_person = { + 'betNr': 104860, + 'vorname': 'Max', + 'name': 'Mustermann', + 'rechtsform': 'Herr', + 'emailGesch': 'max@example.com', + 'telGesch': '+49987654321', + 'geburtsdatum': '1975-03-20' + } + + espo_result = mapper.map_advoware_to_cbeteiligte(advo_person) + print(f"✅ Mapped:") + print(json.dumps(espo_result, indent=2, ensure_ascii=False)) + + print("\n✅ MAPPER TESTS PASSED!") + + +async def test_lock_mechanism(): + """Test 2: Lock-Mechanismus""" + print("\n" + "="*80) + print("TEST 2: Lock-Mechanismus") + print("="*80) + + espocrm = EspoCRMAPI() + context = MockContext() + sync_utils = BeteiligteSync(espocrm, context) + + # Hole erste Entity + result = await espocrm.list_entities('CBeteiligte', max_size=1) + if not result.get('list'): + print("❌ Keine Entities gefunden zum Testen") + return + + entity = result['list'][0] + entity_id = entity['id'] + original_status = entity.get('syncStatus', 'clean') + + print(f"\n🔒 Test Lock für Entity: {entity.get('name')} (ID: {entity_id})") + print(f" Original Status: {original_status}") + + # Test Lock Acquire + print("\n1. Acquire Lock...") + lock1 = await sync_utils.acquire_sync_lock(entity_id) + print(f" Lock 1: {lock1} (erwartet: True)") + + # Verify Status + entity_check = await espocrm.get_entity('CBeteiligte', entity_id) + print(f" Status nach Lock: {entity_check.get('syncStatus')} (erwartet: syncing)") + + # Test Lock Already Held + print("\n2. Versuche Lock erneut zu holen (sollte fehlschlagen)...") + lock2 = await sync_utils.acquire_sync_lock(entity_id) + print(f" Lock 2: {lock2} (erwartet: False)") + + # Release Lock + print("\n3. Release Lock...") + await sync_utils.release_sync_lock(entity_id, 'clean') + + entity_final = await espocrm.get_entity('CBeteiligte', entity_id) + print(f" Status nach Release: {entity_final.get('syncStatus')} (erwartet: clean)") + + # Restore Original + if original_status != 'clean': + await espocrm.update_entity('CBeteiligte', entity_id, {'syncStatus': original_status}) + print(f" Status restored to: {original_status}") + + print("\n✅ LOCK TESTS PASSED!") + + +async def test_timestamp_comparison(): + """Test 3: Timestamp-Vergleich""" + print("\n" + "="*80) + print("TEST 3: Timestamp-Vergleich") + print("="*80) + + espocrm = EspoCRMAPI() + context = MockContext() + sync_utils = BeteiligteSync(espocrm, context) + + # Test-Timestamps + now = datetime.now() + old = datetime(2026, 2, 1, 10, 0, 0) + newer = datetime(2026, 2, 7, 14, 0, 0) + + print("\n📅 Test-Timestamps:") + print(f" old: {old}") + print(f" newer: {newer}") + print(f" now: {now}") + + # Scenario 1: EspoCRM neuer + print("\n1. EspoCRM neuer als Advoware:") + result = sync_utils.compare_timestamps(newer, old, old) + print(f" Result: {result} (erwartet: espocrm_newer)") + + # Scenario 2: Advoware neuer + print("\n2. Advoware neuer als EspoCRM:") + result = sync_utils.compare_timestamps(old, newer, old) + print(f" Result: {result} (erwartet: advoware_newer)") + + # Scenario 3: Konflikt (beide geändert) + print("\n3. Beide nach last_sync geändert (Konflikt):") + result = sync_utils.compare_timestamps(newer, newer, old) + print(f" Result: {result} (erwartet: conflict)") + + # Scenario 4: Keine Änderungen + print("\n4. Keine Änderungen seit last_sync:") + result = sync_utils.compare_timestamps(old, old, newer) + print(f" Result: {result} (erwartet: no_change)") + + print("\n✅ TIMESTAMP TESTS PASSED!") + + +async def test_live_entity(entity_id=None, dry_run=True): + """Test 4: Live Entity Sync (optional mit echtem API-Call)""" + print("\n" + "="*80) + print("TEST 4: Live Entity Sync") + print("="*80) + + espocrm = EspoCRMAPI() + advoware = AdvowareAPI(MockContext()) + mapper = BeteiligteMapper() + context = MockContext() + sync_utils = BeteiligteSync(espocrm, context) + + # Hole Entity + if not entity_id: + result = await espocrm.list_entities('CBeteiligte', max_size=5) + entities = result.get('list', []) + + # Suche eine mit betnr + entity = next((e for e in entities if e.get('betnr')), entities[0] if entities else None) + + if not entity: + print("❌ Keine Entity gefunden") + return + + entity_id = entity['id'] + else: + entity = await espocrm.get_entity('CBeteiligte', entity_id) + + print(f"\n📋 Test Entity:") + print(f" ID: {entity_id}") + print(f" Name: {entity.get('name')}") + print(f" betNr: {entity.get('betnr')}") + print(f" syncStatus: {entity.get('syncStatus')}") + print(f" modifiedAt: {entity.get('modifiedAt')}") + print(f" advowareLastSync: {entity.get('advowareLastSync')}") + + betnr = entity.get('betnr') + + # Test Transformation + print("\n🔄 Transformation EspoCRM → Advoware:") + advo_data = mapper.map_cbeteiligte_to_advoware(entity) + print(json.dumps(advo_data, indent=2, ensure_ascii=False)) + + # Wenn betnr vorhanden, teste Fetch von Advoware + if betnr: + print(f"\n📥 Fetch von Advoware (betNr={betnr}):") + try: + advo_result = await advoware.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 advo_entity: + print(f"✅ Von Advoware geladen:") + print(f" name: {advo_entity.get('name')}") + print(f" vorname: {advo_entity.get('vorname')}") + print(f" geaendertAm: {advo_entity.get('geaendertAm')}") + + # Timestamp-Vergleich + print(f"\n⏱️ Timestamp-Vergleich:") + comparison = sync_utils.compare_timestamps( + entity.get('modifiedAt'), + advo_entity.get('geaendertAm'), + entity.get('advowareLastSync') + ) + print(f" Result: {comparison}") + + # Zeige was geändert wäre + changed = mapper.get_changed_fields(entity, advo_entity) + if changed: + print(f"\n📝 Geänderte Felder: {', '.join(changed)}") + else: + print(f"\n✅ Keine Feld-Unterschiede") + else: + print("❌ Keine Daten von Advoware") + + except Exception as e: + print(f"⚠️ Fehler beim Fetch von Advoware: {e}") + else: + print("\n⚠️ Keine betnr vorhanden (Entity wurde noch nicht gesynct)") + + if not dry_run: + print("\n🆕 Würde CREATE in Advoware ausführen:") + print(f" POST /api/v1/advonet/Beteiligte") + print(f" Data: {json.dumps(advo_data, indent=2, ensure_ascii=False)}") + print("\n⚠️ DRY RUN - Nicht ausgeführt!") + + print("\n✅ LIVE ENTITY TEST COMPLETE!") + + +async def test_full_sync_handler(entity_id): + """Test 5: Vollständiger Sync-Handler (simuliert Event)""" + print("\n" + "="*80) + print("TEST 5: Vollständiger Sync-Handler") + print("="*80) + + # Import Handler + sys.path.insert(0, '/opt/motia-app/bitbylaw/steps/vmh') + import beteiligte_sync_event_step + + # Mock Event Data + event_data = { + 'entity_id': entity_id, + 'action': 'sync_check', + 'source': 'test_script', + 'timestamp': datetime.now().isoformat() + } + + context = MockContext() + + print(f"\n🎬 Simuliere Event für Entity: {entity_id}") + print(f" Event: {json.dumps(event_data, indent=2)}") + + print("\n⚠️ ACHTUNG: Dies führt ECHTE API-Calls aus!") + response = input("Fortfahren? (y/N): ") + + if response.lower() != 'y': + print("❌ Abgebrochen") + return + + print("\n🚀 Handler wird ausgeführt...\n") + + try: + await beteiligte_sync_event_step.handler(event_data, context) + print("\n✅ HANDLER ERFOLGREICH!") + except Exception as e: + print(f"\n❌ HANDLER FEHLER: {e}") + import traceback + traceback.print_exc() + + +async def main(): + parser = argparse.ArgumentParser(description='Test Beteiligte Sync') + parser.add_argument('--test-transforms', action='store_true', help='Nur Mapper testen') + parser.add_argument('--test-lock', action='store_true', help='Lock-Mechanismus testen') + parser.add_argument('--test-timestamps', action='store_true', help='Timestamp-Vergleich testen') + parser.add_argument('--test-live', action='store_true', help='Live-Test mit APIs') + parser.add_argument('--test-full-handler', action='store_true', help='Vollständiger Handler (ECHTE CALLS!)') + parser.add_argument('--entity-id', type=str, help='Spezifische Entity-ID testen') + parser.add_argument('--all', action='store_true', help='Alle Tests ausführen') + + args = parser.parse_args() + + # Default: Alle Tests + if not any([args.test_transforms, args.test_lock, args.test_timestamps, + args.test_live, args.test_full_handler, args.all]): + args.all = True + + print("🧪 Beteiligte Sync Test Suite") + print("="*80) + + try: + if args.all or args.test_transforms: + await test_transforms() + + if args.all or args.test_lock: + await test_lock_mechanism() + + if args.all or args.test_timestamps: + await test_timestamp_comparison() + + if args.all or args.test_live: + await test_live_entity(args.entity_id, dry_run=True) + + if args.test_full_handler: + if not args.entity_id: + print("\n❌ --entity-id erforderlich für --test-full-handler") + else: + await test_full_sync_handler(args.entity_id) + + print("\n" + "="*80) + print("✅ ALLE TESTS ABGESCHLOSSEN!") + print("="*80) + + except Exception as e: + print(f"\n❌ FEHLER: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/bitbylaw/services/advoware.py b/bitbylaw/services/advoware.py index ab231cbe..e501c020 100644 --- a/bitbylaw/services/advoware.py +++ b/bitbylaw/services/advoware.py @@ -130,6 +130,9 @@ class AdvowareAPI: effective_headers = headers.copy() if headers else {} effective_headers['Authorization'] = f'Bearer {token}' effective_headers.setdefault('Content-Type', 'application/json') + + # Prefer 'data' parameter over 'json_data' if provided (for backward compatibility) + json_payload = data if data is not None else json_data async with aiohttp.ClientSession(timeout=effective_timeout) as session: try: @@ -137,13 +140,13 @@ class AdvowareAPI: self.context.logger.debug(f"Making API call: {method} {url}") else: logger.debug(f"Making API call: {method} {url}") - async with session.request(method, url, headers=effective_headers, params=params, json=json_data) as response: + async with session.request(method, url, headers=effective_headers, params=params, json=json_payload) as response: response.raise_for_status() if response.status == 401: self._log("401 Unauthorized, refreshing token") token = self.get_access_token(force_refresh=True) effective_headers['Authorization'] = f'Bearer {token}' - async with session.request(method, url, headers=effective_headers, params=params, json=json_data) as response: + async with session.request(method, url, headers=effective_headers, params=params, json=json_payload) as response: response.raise_for_status() return await response.json() if response.content_type == 'application/json' else None response.raise_for_status() diff --git a/bitbylaw/services/espocrm_mapper.py b/bitbylaw/services/espocrm_mapper.py index dea612b8..830a0c5b 100644 --- a/bitbylaw/services/espocrm_mapper.py +++ b/bitbylaw/services/espocrm_mapper.py @@ -17,21 +17,24 @@ class BeteiligteMapper: @staticmethod def map_cbeteiligte_to_advoware(espo_entity: Dict[str, Any]) -> Dict[str, Any]: """ - Transformiert EspoCRM CBeteiligte → Advoware Beteiligte Format + Transformiert EspoCRM CBeteiligte → Advoware Beteiligte Format (STAMMDATEN) + + WICHTIG: Kontaktdaten (Telefon, Email, Fax, Bankverbindungen) werden über + separate Advoware-Endpoints gesynct und sind NICHT Teil dieser Mapping-Funktion. Args: espo_entity: CBeteiligte Entity von EspoCRM Returns: - Dict für Advoware API (POST/PUT /api/v1/advonet/Beteiligte) + Dict mit Stammdaten für Advoware API (POST/PUT /api/v1/advonet/Beteiligte) """ - logger.debug(f"Mapping EspoCRM → Advoware: {espo_entity.get('id')}") + logger.debug(f"Mapping EspoCRM → Advoware STAMMDATEN: {espo_entity.get('id')}") # Bestimme ob Person oder Firma is_firma = bool(espo_entity.get('firmenname')) rechtsform = espo_entity.get('rechtsform', '') - # Basis-Struktur + # Basis-Struktur (nur Stammdaten, keine Kontaktdaten!) advo_data = { 'rechtsform': rechtsform, } @@ -56,43 +59,15 @@ class BeteiligteMapper: if date_of_birth: advo_data['geburtsdatum'] = date_of_birth - # KONTAKTDATEN - # E-Mail (emailAddressData ist Array, wir nehmen Primary) - email_data = espo_entity.get('emailAddressData') - if email_data and isinstance(email_data, list): - primary_email = next((e for e in email_data if e.get('primary')), None) - if primary_email: - advo_data['emailGesch'] = primary_email.get('emailAddress') - elif espo_entity.get('emailAddress'): - advo_data['emailGesch'] = espo_entity.get('emailAddress') - - # Telefon (phoneNumberData ist Array, wir nehmen Primary) - phone_data = espo_entity.get('phoneNumberData') - if phone_data and isinstance(phone_data, list): - primary_phone = next((p for p in phone_data if p.get('primary')), None) - if primary_phone: - phone_num = primary_phone.get('phoneNumber') - phone_type = primary_phone.get('type', '').lower() - - if 'mobile' in phone_type or 'mobil' in phone_type: - advo_data['mobil'] = phone_num - else: - advo_data['telGesch'] = phone_num - elif espo_entity.get('phoneNumber'): - advo_data['telGesch'] = espo_entity.get('phoneNumber') - # HANDELSREGISTER (nur für Firmen) if is_firma: hr_nummer = espo_entity.get('handelsregisterNummer') if hr_nummer: advo_data['handelsRegisterNummer'] = hr_nummer - # DISGTYP (EspoCRM spezifisch - falls vorhanden) - disgtyp = espo_entity.get('disgTyp') - if disgtyp: - advo_data['disgTyp'] = disgtyp + # TODO: Weitere Stammdaten-Felder hier ergänzen (Steuernummer, etc.) - logger.debug(f"Mapped to Advoware: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}") + logger.debug(f"Mapped to Advoware STAMMDATEN: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}, rechtsform={rechtsform}") return advo_data @@ -143,63 +118,16 @@ class BeteiligteMapper: if geburtsdatum: espo_data['dateOfBirth'] = geburtsdatum - # KONTAKTDATEN - # E-Mail (emailGesch ist primary) - email_gesch = advo_entity.get('emailGesch') - email = advo_entity.get('email') - - primary_email = email_gesch or email - if primary_email: - espo_data['emailAddress'] = primary_email - espo_data['emailAddressData'] = [ - { - 'emailAddress': primary_email, - 'primary': True, - 'optOut': False, - 'invalid': False - } - ] - - # Telefon (telGesch ist primary, mobil als secondary) - tel_gesch = advo_entity.get('telGesch') - tel_privat = advo_entity.get('telPrivat') - mobil = advo_entity.get('mobil') - - phone_data = [] - - # Primary: telGesch oder telPrivat - primary_tel = tel_gesch or tel_privat - if primary_tel: - espo_data['phoneNumber'] = primary_tel - phone_data.append({ - 'phoneNumber': primary_tel, - 'primary': True, - 'type': 'Office' if tel_gesch else 'Home' - }) - - # Secondary: mobil - if mobil and mobil != primary_tel: - phone_data.append({ - 'phoneNumber': mobil, - 'primary': False, - 'type': 'Mobile' - }) - - if phone_data: - espo_data['phoneNumberData'] = phone_data - # HANDELSREGISTER (nur für Firmen) if not is_person: hr_nummer = advo_entity.get('handelsRegisterNummer') if hr_nummer: espo_data['handelsregisterNummer'] = hr_nummer - # DISGTYP - disgtyp = advo_entity.get('disgTyp') - if disgtyp: - espo_data['disgTyp'] = disgtyp + # TODO: Weitere Stammdaten-Felder hier ergänzen + # HINWEIS: Kontaktdaten (Telefon, Email, Fax) werden über separate Endpoints gesynct - logger.debug(f"Mapped to EspoCRM: name={espo_data.get('name')}") + logger.debug(f"Mapped to EspoCRM STAMMDATEN: name={espo_data.get('name')}") return espo_data diff --git a/bitbylaw/steps/vmh/beteiligte_sync_event_step.py b/bitbylaw/steps/vmh/beteiligte_sync_event_step.py index cfcb1917..8df3d0df 100644 --- a/bitbylaw/steps/vmh/beteiligte_sync_event_step.py +++ b/bitbylaw/steps/vmh/beteiligte_sync_event_step.py @@ -195,6 +195,30 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u context.logger.info(f"⏱️ Timestamp-Vergleich: {comparison}") + # SPECIAL: Wenn LastSync null → immer von EspoCRM syncen (initial sync) + if not espo_entity.get('advowareLastSync'): + context.logger.info(f"📤 Initial Sync → EspoCRM STAMMDATEN zu Advoware") + + # WICHTIG: Advoware benötigt vollständiges Objekt für PUT + # Mapper liefert nur STAMMDATEN (keine Kontaktdaten - die kommen später über separate Endpoints) + advo_updates = mapper.map_cbeteiligte_to_advoware(espo_entity) + + # Merge mit aktuellen Advoware-Daten + merged_data = {**advo_entity, **advo_updates} + + context.logger.info(f"📝 Merge: {len(advo_updates)} Stammdaten-Felder → {len(merged_data)} Gesamt-Felder") + context.logger.debug(f" Gesynct: {', '.join(advo_updates.keys())}") + + await advoware.api_call( + f'api/v1/advonet/Beteiligte/{betnr}', + method='PUT', + data=merged_data + ) + + await sync_utils.release_sync_lock(entity_id, 'clean') + context.logger.info(f"✅ Advoware aktualisiert (initial sync)") + return + # KEIN SYNC NÖTIG if comparison == 'no_change': context.logger.info(f"✅ Keine Änderungen, Sync übersprungen") @@ -203,14 +227,22 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u # ESPOCRM NEUER → Update Advoware if comparison == 'espocrm_newer': - context.logger.info(f"📤 EspoCRM ist neuer → Update Advoware") + context.logger.info(f"📤 EspoCRM ist neuer → Update Advoware STAMMDATEN") - advo_data = mapper.map_cbeteiligte_to_advoware(espo_entity) + # WICHTIG: Advoware benötigt vollständiges Objekt für PUT + # Mapper liefert nur STAMMDATEN (keine Kontaktdaten - die kommen über separate Endpoints) + advo_updates = mapper.map_cbeteiligte_to_advoware(espo_entity) + + # Merge mit aktuellen Advoware-Daten + merged_data = {**advo_entity, **advo_updates} + + context.logger.info(f"📝 Merge: {len(advo_updates)} Stammdaten-Felder → {len(merged_data)} Gesamt-Felder") + context.logger.debug(f" Gesynct: {', '.join(advo_updates.keys())}") await advoware.api_call( f'api/v1/advonet/Beteiligte/{betnr}', method='PUT', - data=advo_data + data=merged_data ) await sync_utils.release_sync_lock(entity_id, 'clean') @@ -218,7 +250,7 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u # ADVOWARE NEUER → Update EspoCRM elif comparison == 'advoware_newer': - context.logger.info(f"📥 Advoware ist neuer → Update EspoCRM") + context.logger.info(f"📥 Advoware ist neuer → Update EspoCRM STAMMDATEN") espo_data = mapper.map_advoware_to_cbeteiligte(advo_entity) @@ -228,15 +260,18 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u # KONFLIKT → EspoCRM WINS elif comparison == 'conflict': - context.logger.warning(f"⚠️ KONFLIKT erkannt → EspoCRM WINS") + context.logger.warning(f"⚠️ KONFLIKT erkannt → EspoCRM WINS (STAMMDATEN)") - # Überschreibe Advoware mit EspoCRM - advo_data = mapper.map_cbeteiligte_to_advoware(espo_entity) + # Überschreibe Advoware mit EspoCRM (merge mit aktuellen Daten) + advo_updates = mapper.map_cbeteiligte_to_advoware(espo_entity) + merged_data = {**advo_entity, **advo_updates} + + context.logger.info(f"📝 Merge: {len(advo_updates)} Stammdaten-Felder → {len(merged_data)} Gesamt-Felder") await advoware.api_call( f'api/v1/advonet/Beteiligte/{betnr}', method='PUT', - data=advo_data + data=merged_data ) conflict_msg = (