import json import datetime def should_skip_update(entity_data): """ Prüft ob Update gefiltert werden soll (verhindert Webhook-Loop) SKIP wenn: - Nur Sync-Felder geändert UND - syncStatus ist "clean" oder "syncing" (normale Sync-Completion) EMIT wenn: - Echte Datenänderung (nicht nur Sync-Felder) ODER - syncStatus ist "dirty", "failed", "pending_sync" (braucht Sync) """ if not isinstance(entity_data, dict): return False # Felder die von Sync-Handler gesetzt werden sync_fields = {'syncStatus', 'advowareLastSync', 'syncErrorMessage', 'syncRetryCount'} # Meta-Felder die immer vorhanden sind meta_fields = {'id', 'modifiedAt', 'modifiedById', 'modifiedByName'} # Alle ignorierbaren Felder ignorable = sync_fields | meta_fields # Prüfe ob es relevante (nicht-sync) Felder gibt entity_keys = set(entity_data.keys()) relevant_keys = entity_keys - ignorable # Wenn echte Datenänderung → Emit (nicht skippen) if len(relevant_keys) > 0: return False # Nur Sync-Felder vorhanden → Prüfe syncStatus sync_status = entity_data.get('syncStatus') # Skip nur wenn Status "clean" oder "syncing" (normale Completion) # Emit wenn "dirty", "failed", "pending_sync" (braucht Sync) should_skip = sync_status in ['clean', 'syncing'] return should_skip config = { 'type': 'api', 'name': 'VMH Webhook Beteiligte Update', 'description': 'Empfängt Update-Webhooks von EspoCRM für Beteiligte', 'path': '/vmh/webhook/beteiligte/update', 'method': 'POST', 'flows': ['vmh'], 'emits': ['vmh.beteiligte.update'] } async def handler(req, context): try: payload = req.get('body', []) context.logger.info("VMH Webhook Beteiligte Update empfangen") context.logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}") # Sammle alle IDs aus dem Batch (filtere Sync-Only-Updates) entity_ids = set() filtered_count = 0 if isinstance(payload, list): for entity in payload: if isinstance(entity, dict) and 'id' in entity: # Prüfe ob Update gefiltert werden soll (verhindert Loop) if should_skip_update(entity): context.logger.info(f"Sync-Completion gefiltert: {entity['id']} (syncStatus={entity.get('syncStatus')})") filtered_count += 1 continue entity_ids.add(entity['id']) elif isinstance(payload, dict) and 'id' in payload: if not should_skip_update(payload): entity_ids.add(payload['id']) else: context.logger.info(f"Sync-Completion gefiltert: {payload['id']} (syncStatus={payload.get('syncStatus')})") filtered_count += 1 if filtered_count > 0: context.logger.info(f"{len(entity_ids)} IDs zum Update-Sync gefunden ({filtered_count} Sync-Completions gefiltert)") else: context.logger.info(f"{len(entity_ids)} IDs zum Update-Sync gefunden") # Emittiere Events direkt (Deduplizierung erfolgt im Event-Handler via Lock) for entity_id in entity_ids: await context.emit({ 'topic': 'vmh.beteiligte.update', 'data': { 'entity_id': entity_id, 'action': 'update', 'source': 'webhook', 'timestamp': req.get('timestamp') or datetime.datetime.now().isoformat() } }) context.logger.info(f"VMH Update Webhook verarbeitet: {len(entity_ids)} Events emittiert") return { 'status': 200, 'body': { 'status': 'received', 'action': 'update', 'ids_count': len(entity_ids) } } except Exception as e: context.logger.error(f"Fehler beim Verarbeiten des VMH Update Webhooks: {e}") context.logger.error(f"Request: {req}") return { 'status': 500, 'body': { 'error': 'Internal server error', 'details': str(e) } }