feat: Enhance Advoware API integration with backward compatibility for data payloads and improve logging for sync events

This commit is contained in:
2026-02-07 15:44:56 +00:00
parent b5abe6cf00
commit 8550107b89
4 changed files with 447 additions and 94 deletions

View File

@@ -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())

View File

@@ -130,6 +130,9 @@ class AdvowareAPI:
effective_headers = headers.copy() if headers else {} effective_headers = headers.copy() if headers else {}
effective_headers['Authorization'] = f'Bearer {token}' effective_headers['Authorization'] = f'Bearer {token}'
effective_headers.setdefault('Content-Type', 'application/json') 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: async with aiohttp.ClientSession(timeout=effective_timeout) as session:
try: try:
@@ -137,13 +140,13 @@ class AdvowareAPI:
self.context.logger.debug(f"Making API call: {method} {url}") self.context.logger.debug(f"Making API call: {method} {url}")
else: else:
logger.debug(f"Making API call: {method} {url}") 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() response.raise_for_status()
if response.status == 401: if response.status == 401:
self._log("401 Unauthorized, refreshing token") self._log("401 Unauthorized, refreshing token")
token = self.get_access_token(force_refresh=True) token = self.get_access_token(force_refresh=True)
effective_headers['Authorization'] = f'Bearer {token}' 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() response.raise_for_status()
return await response.json() if response.content_type == 'application/json' else None return await response.json() if response.content_type == 'application/json' else None
response.raise_for_status() response.raise_for_status()

View File

@@ -17,21 +17,24 @@ class BeteiligteMapper:
@staticmethod @staticmethod
def map_cbeteiligte_to_advoware(espo_entity: Dict[str, Any]) -> Dict[str, Any]: 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: Args:
espo_entity: CBeteiligte Entity von EspoCRM espo_entity: CBeteiligte Entity von EspoCRM
Returns: 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 # Bestimme ob Person oder Firma
is_firma = bool(espo_entity.get('firmenname')) is_firma = bool(espo_entity.get('firmenname'))
rechtsform = espo_entity.get('rechtsform', '') rechtsform = espo_entity.get('rechtsform', '')
# Basis-Struktur # Basis-Struktur (nur Stammdaten, keine Kontaktdaten!)
advo_data = { advo_data = {
'rechtsform': rechtsform, 'rechtsform': rechtsform,
} }
@@ -56,43 +59,15 @@ class BeteiligteMapper:
if date_of_birth: if date_of_birth:
advo_data['geburtsdatum'] = 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) # HANDELSREGISTER (nur für Firmen)
if is_firma: if is_firma:
hr_nummer = espo_entity.get('handelsregisterNummer') hr_nummer = espo_entity.get('handelsregisterNummer')
if hr_nummer: if hr_nummer:
advo_data['handelsRegisterNummer'] = hr_nummer advo_data['handelsRegisterNummer'] = hr_nummer
# DISGTYP (EspoCRM spezifisch - falls vorhanden) # TODO: Weitere Stammdaten-Felder hier ergänzen (Steuernummer, etc.)
disgtyp = espo_entity.get('disgTyp')
if disgtyp:
advo_data['disgTyp'] = disgtyp
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 return advo_data
@@ -143,63 +118,16 @@ class BeteiligteMapper:
if geburtsdatum: if geburtsdatum:
espo_data['dateOfBirth'] = 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) # HANDELSREGISTER (nur für Firmen)
if not is_person: if not is_person:
hr_nummer = advo_entity.get('handelsRegisterNummer') hr_nummer = advo_entity.get('handelsRegisterNummer')
if hr_nummer: if hr_nummer:
espo_data['handelsregisterNummer'] = hr_nummer espo_data['handelsregisterNummer'] = hr_nummer
# DISGTYP # TODO: Weitere Stammdaten-Felder hier ergänzen
disgtyp = advo_entity.get('disgTyp') # HINWEIS: Kontaktdaten (Telefon, Email, Fax) werden über separate Endpoints gesynct
if disgtyp:
espo_data['disgTyp'] = disgtyp
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 return espo_data

View File

@@ -195,6 +195,30 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
context.logger.info(f"⏱️ Timestamp-Vergleich: {comparison}") 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 # KEIN SYNC NÖTIG
if comparison == 'no_change': if comparison == 'no_change':
context.logger.info(f"✅ Keine Änderungen, Sync übersprungen") 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 # ESPOCRM NEUER → Update Advoware
if comparison == 'espocrm_newer': 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( await advoware.api_call(
f'api/v1/advonet/Beteiligte/{betnr}', f'api/v1/advonet/Beteiligte/{betnr}',
method='PUT', method='PUT',
data=advo_data data=merged_data
) )
await sync_utils.release_sync_lock(entity_id, 'clean') 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 # ADVOWARE NEUER → Update EspoCRM
elif comparison == 'advoware_newer': 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) 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 # KONFLIKT → EspoCRM WINS
elif comparison == 'conflict': elif comparison == 'conflict':
context.logger.warning(f"⚠️ KONFLIKT erkannt → EspoCRM WINS") context.logger.warning(f"⚠️ KONFLIKT erkannt → EspoCRM WINS (STAMMDATEN)")
# Überschreibe Advoware mit EspoCRM # Überschreibe Advoware mit EspoCRM (merge mit aktuellen Daten)
advo_data = mapper.map_cbeteiligte_to_advoware(espo_entity) 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( await advoware.api_call(
f'api/v1/advonet/Beteiligte/{betnr}', f'api/v1/advonet/Beteiligte/{betnr}',
method='PUT', method='PUT',
data=advo_data data=merged_data
) )
conflict_msg = ( conflict_msg = (