diff --git a/bitbylaw/services/beteiligte_sync_utils.py b/bitbylaw/services/beteiligte_sync_utils.py index 40836d23..2b8e80f6 100644 --- a/bitbylaw/services/beteiligte_sync_utils.py +++ b/bitbylaw/services/beteiligte_sync_utils.py @@ -217,6 +217,65 @@ class BeteiligteSync: return None + def compare_entities( + self, + espo_entity: Dict[str, Any], + advo_entity: Dict[str, Any] + ) -> TimestampResult: + """ + Vergleicht Änderungen zwischen EspoCRM und Advoware + + PRIMÄR: rowId-Vergleich (Advoware rowId ändert sich bei jedem Update - SEHR zuverlässig!) + FALLBACK: Timestamp-Vergleich (wenn rowId nicht verfügbar) + + Args: + espo_entity: EspoCRM CBeteiligte + advo_entity: Advoware Beteiligte + + Returns: + "espocrm_newer": EspoCRM wurde geändert + "advoware_newer": Advoware wurde geändert + "conflict": Beide wurden geändert + "no_change": Keine Änderungen + """ + # PRIMÄR: rowId-basierte Änderungserkennung (zuverlässiger!) + espo_rowid = espo_entity.get('advowareRowId') + advo_rowid = advo_entity.get('rowId') + + if espo_rowid and advo_rowid: + if espo_rowid != advo_rowid: + # rowId unterschiedlich → Advoware wurde geändert + self._log(f"Advoware rowId geändert: {espo_rowid[:20]}... → {advo_rowid[:20]}...") + return 'advoware_newer' + else: + # rowId gleich → keine Änderung in Advoware + # Prüfe ob EspoCRM geändert wurde (via modifiedAt) + espo_modified = espo_entity.get('modifiedAt') + last_sync = espo_entity.get('advowareLastSync') + + if espo_modified and last_sync: + try: + espo_ts = self.parse_timestamp(espo_modified) + sync_ts = self.parse_timestamp(last_sync) + + if espo_ts and sync_ts and espo_ts > sync_ts: + self._log(f"EspoCRM neuer (rowId gleich, aber modifiedAt > lastSync)") + return 'espocrm_newer' + except Exception as e: + self._log(f"Timestamp-Parse-Fehler: {e}", level='debug') + + # Keine Änderungen + self._log("Keine Änderungen (rowId identisch)") + return 'no_change' + + # FALLBACK: Timestamp-Vergleich (wenn rowId nicht verfügbar) + self._log("rowId nicht verfügbar, fallback auf Timestamp-Vergleich", level='debug') + return self.compare_timestamps( + espo_entity.get('modifiedAt'), + advo_entity.get('geaendertAm'), + espo_entity.get('advowareLastSync') + ) + def compare_timestamps( self, espo_modified_at: Any, @@ -224,7 +283,7 @@ class BeteiligteSync: last_sync_ts: Any ) -> TimestampResult: """ - Vergleicht Timestamps und bestimmt Sync-Richtung + Vergleicht Timestamps und bestimmt Sync-Richtung (FALLBACK wenn rowId nicht verfügbar) Args: espo_modified_at: EspoCRM modifiedAt @@ -412,7 +471,8 @@ class BeteiligteSync: entity_id: str, espo_entity: Dict[str, Any], advo_entity: Dict[str, Any], - conflict_details: str + conflict_details: str, + extra_fields: Optional[Dict[str, Any]] = None ) -> None: """ Löst Konflikt auf: EspoCRM wins (überschreibt Advoware) @@ -422,13 +482,26 @@ class BeteiligteSync: espo_entity: EspoCRM Entity-Daten advo_entity: Advoware Entity-Daten conflict_details: Details zum Konflikt + extra_fields: Zusätzliche Felder (z.B. advowareRowId) """ try: - now = datetime.now(pytz.UTC).isoformat() + # EspoCRM datetime format + now_utc = datetime.now(pytz.UTC) + espo_datetime = now_utc.strftime('%Y-%m-%d %H:%M:%S') # Markiere als gelöst mit Konflikt-Info - await self.espocrm.update_entity('CBeteiligte', entity_id, { + update_data = { 'syncStatus': 'clean', # Gelöst! + 'advowareLastSync': espo_datetime, + 'syncErrorMessage': f'Konflikt: {conflict_details}', + 'syncRetryCount': 0 + } + + # Merge extra fields (z.B. advowareRowId) + if extra_fields: + update_data.update(extra_fields) + + await self.espocrm.update_entity('CBeteiligte', entity_id, update_data) 'advowareLastSync': now, 'syncErrorMessage': f"Konflikt am {now}: {conflict_details}. EspoCRM hat gewonnen.", 'syncRetryCount': 0 diff --git a/bitbylaw/services/espocrm_mapper.py b/bitbylaw/services/espocrm_mapper.py index 830a0c5b..9e290bcb 100644 --- a/bitbylaw/services/espocrm_mapper.py +++ b/bitbylaw/services/espocrm_mapper.py @@ -64,6 +64,11 @@ class BeteiligteMapper: hr_nummer = espo_entity.get('handelsregisterNummer') if hr_nummer: advo_data['handelsRegisterNummer'] = hr_nummer + + # Registergericht + registergericht = espo_entity.get('registergericht') + if registergericht: + advo_data['registergericht'] = registergericht # TODO: Weitere Stammdaten-Felder hier ergänzen (Steuernummer, etc.) @@ -92,6 +97,7 @@ class BeteiligteMapper: espo_data = { 'rechtsform': advo_entity.get('rechtsform', ''), 'betnr': advo_entity.get('betNr'), # Link zu Advoware + 'advowareRowId': advo_entity.get('rowId'), # Änderungserkennung } # NAME: Person vs. Firma @@ -123,6 +129,11 @@ class BeteiligteMapper: hr_nummer = advo_entity.get('handelsRegisterNummer') if hr_nummer: espo_data['handelsregisterNummer'] = hr_nummer + + # Registergericht + registergericht = advo_entity.get('registergericht') + if registergericht: + espo_data['registergericht'] = registergericht # TODO: Weitere Stammdaten-Felder hier ergänzen # HINWEIS: Kontaktdaten (Telefon, Email, Fax) werden über separate Endpoints gesynct @@ -153,7 +164,8 @@ class BeteiligteMapper: 'name', 'firstName', 'lastName', 'firmenname', 'emailAddress', 'phoneNumber', 'dateOfBirth', 'rechtsform', - 'handelsregisterNummer' + 'handelsregisterNummer', 'handelsregisterArt', 'registergericht', + 'betnr', 'advowareRowId' ] for field in compare_fields: diff --git a/bitbylaw/steps/vmh/beteiligte_sync_event_step.py b/bitbylaw/steps/vmh/beteiligte_sync_event_step.py index b5351509..df2c28d7 100644 --- a/bitbylaw/steps/vmh/beteiligte_sync_event_step.py +++ b/bitbylaw/steps/vmh/beteiligte_sync_event_step.py @@ -184,14 +184,10 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u context.logger.info(f"📥 Von Advoware geladen: {advo_entity.get('name')}") - # TIMESTAMP-VERGLEICH - comparison = sync_utils.compare_timestamps( - espo_entity.get('modifiedAt'), - advo_entity.get('geaendertAm'), - espo_entity.get('advowareLastSync') - ) + # ÄNDERUNGSERKENNUNG (Primary: rowId, Fallback: Timestamps) + comparison = sync_utils.compare_entities(espo_entity, advo_entity) - context.logger.info(f"⏱️ Timestamp-Vergleich: {comparison}") + context.logger.info(f"⏱️ Vergleich: {comparison}") # SPECIAL: Wenn LastSync null → immer von EspoCRM syncen (initial sync) if not espo_entity.get('advowareLastSync'): @@ -206,7 +202,12 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u data=merged_data ) - await sync_utils.release_sync_lock(entity_id, 'clean') + # Speichere rowId für zukünftige Vergleiche + await sync_utils.release_sync_lock( + entity_id, + 'clean', + extra_fields={'advowareRowId': advo_entity.get('rowId')} + ) context.logger.info(f"✅ Advoware aktualisiert (initial sync)") return @@ -229,7 +230,16 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u data=merged_data ) - await sync_utils.release_sync_lock(entity_id, 'clean') + # Hole aktualisierte Entity um neue rowId zu bekommen + updated_advo = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}', method='GET') + if isinstance(updated_advo, list): + updated_advo = updated_advo[0] + + await sync_utils.release_sync_lock( + entity_id, + 'clean', + extra_fields={'advowareRowId': updated_advo.get('rowId')} + ) context.logger.info(f"✅ Advoware aktualisiert") # ADVOWARE NEUER → Update EspoCRM @@ -239,7 +249,11 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u espo_data = mapper.map_advoware_to_cbeteiligte(advo_entity) await espocrm.update_entity('CBeteiligte', entity_id, espo_data) - await sync_utils.release_sync_lock(entity_id, 'clean') + await sync_utils.release_sync_lock( + entity_id, + 'clean', + extra_fields={'advowareRowId': advo_entity.get('rowId')} + ) context.logger.info(f"✅ EspoCRM aktualisiert") # KONFLIKT → EspoCRM WINS @@ -255,6 +269,11 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u data=merged_data ) + # Hole aktualisierte Entity um neue rowId zu bekommen + updated_advo = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}', method='GET') + if isinstance(updated_advo, list): + updated_advo = updated_advo[0] + conflict_msg = ( f"EspoCRM: {espo_entity.get('modifiedAt')}, " f"Advoware: {advo_entity.get('geaendertAm')}. " @@ -265,7 +284,8 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u entity_id, espo_entity, advo_entity, - conflict_msg + conflict_msg, + extra_fields={'advowareRowId': updated_advo.get('rowId')} ) context.logger.info(f"✅ Konflikt gelöst: EspoCRM → Advoware")