Enhance EspoCRM API and Webhook Handling

- Improved logging for file uploads in EspoCRMAPI to include upload parameters and error details.
- Updated cron job configurations for calendar sync and participant sync to trigger every 15 minutes on the first minute of the hour.
- Enhanced document create, delete, and update webhook handlers to determine and log the entity type.
- Refactored document sync event handler to include entity type in sync operations and logging.
- Added a new test script for uploading preview images to EspoCRM and verifying the upload process.
- Created a test script for document thumbnail generation, including document creation, file upload, webhook triggering, and preview verification.
This commit is contained in:
bsiggel
2026-03-03 16:53:55 +00:00
parent 0e521f22f8
commit c45bfb7233
10 changed files with 738 additions and 84 deletions

View File

@@ -17,7 +17,7 @@ config = {
'description': 'Runs calendar sync automatically every 15 minutes',
'flows': ['advoware-calendar-sync'],
'triggers': [
cron("0 */15 * * * *") # Every 15 minutes at second 0 (6-field: sec min hour day month weekday)
cron("0 */15 1o * * *") # Every 15 minutes at second 0 (6-field: sec min hour day month weekday)
],
'enqueues': ['calendar_sync_all']
}

View File

@@ -151,17 +151,27 @@ async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse:
)
# Emit events für Queue-Processing (Deduplizierung erfolgt im Event-Handler via Lock)
# Versuche Entity-Type zu ermitteln
entity_type = 'CDokumente' # Default für VMH
if isinstance(payload, list) and payload:
entity_type = payload[0].get('entityType') or payload[0].get('_scope') or 'CDokumente'
elif isinstance(payload, dict):
entity_type = payload.get('entityType') or payload.get('_scope') or 'CDokumente'
ctx.logger.info(f"📝 Entity-Type: {entity_type}")
for entity_id in entity_ids:
await ctx.enqueue({
'topic': 'vmh.document.create',
'data': {
'entity_id': entity_id,
'entity_type': entity_type,
'action': 'create',
'source': 'webhook',
'timestamp': datetime.datetime.now().isoformat()
}
})
ctx.logger.info(f"✅ Event emittiert: vmh.document.create für ID {entity_id}")
ctx.logger.info(f"✅ Event emittiert: vmh.document.create für ID {entity_id} (Type: {entity_type})")
ctx.logger.info("\n" + "=" * 80)
ctx.logger.info(f"✅ WEBHOOK VERARBEITUNG ABGESCHLOSSEN")

View File

@@ -128,17 +128,27 @@ async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse:
)
# Emit events für Queue-Processing
# Versuche Entity-Type zu ermitteln
entity_type = 'CDokumente' # Default für VMH
if isinstance(payload, list) and payload:
entity_type = payload[0].get('entityType') or payload[0].get('_scope') or 'CDokumente'
elif isinstance(payload, dict):
entity_type = payload.get('entityType') or payload.get('_scope') or 'CDokumente'
ctx.logger.info(f"📝 Entity-Type: {entity_type}")
for entity_id in entity_ids:
await ctx.enqueue({
'topic': 'vmh.document.delete',
'data': {
'entity_id': entity_id,
'entity_type': entity_type,
'action': 'delete',
'source': 'webhook',
'timestamp': datetime.datetime.now().isoformat()
}
})
ctx.logger.info(f"✅ Event emittiert: vmh.document.delete für ID {entity_id}")
ctx.logger.info(f"✅ Event emittiert: vmh.document.delete für ID {entity_id} (Type: {entity_type})")
ctx.logger.info("\n" + "=" * 80)
ctx.logger.info(f"✅ WEBHOOK VERARBEITUNG ABGESCHLOSSEN")

View File

@@ -150,17 +150,27 @@ async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse:
)
# Emit events für Queue-Processing
# Versuche Entity-Type zu ermitteln
entity_type = 'CDokumente' # Default für VMH
if isinstance(payload, list) and payload:
entity_type = payload[0].get('entityType') or payload[0].get('_scope') or 'CDokumente'
elif isinstance(payload, dict):
entity_type = payload.get('entityType') or payload.get('_scope') or 'CDokumente'
ctx.logger.info(f"📝 Entity-Type: {entity_type}")
for entity_id in entity_ids:
await ctx.enqueue({
'topic': 'vmh.document.update',
'data': {
'entity_id': entity_id,
'entity_type': entity_type,
'action': 'update',
'source': 'webhook',
'timestamp': datetime.datetime.now().isoformat()
}
})
ctx.logger.info(f"✅ Event emittiert: vmh.document.update für ID {entity_id}")
ctx.logger.info(f"✅ Event emittiert: vmh.document.update für ID {entity_id} (Type: {entity_type})")
ctx.logger.info("\n" + "=" * 80)
ctx.logger.info(f"✅ WEBHOOK VERARBEITUNG ABGESCHLOSSEN")

View File

@@ -19,7 +19,7 @@ config = {
"description": "Prüft alle 15 Minuten welche Beteiligte synchronisiert werden müssen",
"flows": ["vmh-beteiligte"],
"triggers": [
cron("0 */15 * * * *") # Alle 15 Minuten (6-field format!)
cron("0 */15 1 * * *") # Alle 15 Minuten (6-field format!)
],
"enqueues": ["vmh.beteiligte.sync_check"]
}

View File

@@ -33,6 +33,7 @@ config = {
async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]):
"""Zentraler Sync-Handler für Documents"""
entity_id = event_data.get('entity_id')
entity_type = event_data.get('entity_type', 'CDokumente') # Default: CDokumente
action = event_data.get('action')
source = event_data.get('source')
@@ -43,6 +44,7 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]):
ctx.logger.info("=" * 80)
ctx.logger.info(f"🔄 DOCUMENT SYNC HANDLER GESTARTET")
ctx.logger.info("=" * 80)
ctx.logger.info(f"Entity Type: {entity_type}")
ctx.logger.info(f"Action: {action.upper()}")
ctx.logger.info(f"Document ID: {entity_id}")
ctx.logger.info(f"Source: {source}")
@@ -70,39 +72,40 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]):
try:
# 1. ACQUIRE LOCK (verhindert parallele Syncs)
lock_acquired = await sync_utils.acquire_sync_lock(entity_id)
lock_acquired = await sync_utils.acquire_sync_lock(entity_id, entity_type)
if not lock_acquired:
ctx.logger.warn(f"⏸️ Sync bereits aktiv für Document {entity_id}, überspringe")
ctx.logger.warn(f"⏸️ Sync bereits aktiv für {entity_type} {entity_id}, überspringe")
return
# Lock erfolgreich acquired - MUSS im finally block released werden!
try:
# 2. FETCH VOLLSTÄNDIGES DOCUMENT VON ESPOCRM
try:
document = await espocrm.get_entity('Document', entity_id)
document = await espocrm.get_entity(entity_type, entity_id)
except Exception as e:
ctx.logger.error(f"❌ Fehler beim Laden von Document: {e}")
await sync_utils.release_sync_lock(entity_id, success=False, error_message=str(e))
ctx.logger.error(f"❌ Fehler beim Laden von {entity_type}: {e}")
await sync_utils.release_sync_lock(entity_id, success=False, error_message=str(e), entity_type=entity_type)
return
ctx.logger.info(f"📋 Document geladen:")
ctx.logger.info(f"📋 {entity_type} geladen:")
ctx.logger.info(f" Name: {document.get('name', 'N/A')}")
ctx.logger.info(f" Type: {document.get('type', 'N/A')}")
ctx.logger.info(f" xaiFileId: {document.get('xaiFileId', 'N/A')}")
ctx.logger.info(f" fileStatus: {document.get('fileStatus', 'N/A')}")
ctx.logger.info(f" xaiFileId: {document.get('xaiFileId') or document.get('xaiId', 'N/A')}")
ctx.logger.info(f" xaiCollections: {document.get('xaiCollections', [])}")
# 3. BESTIMME SYNC-AKTION BASIEREND AUF ACTION
if action == 'delete':
await handle_delete(entity_id, document, sync_utils, ctx)
await handle_delete(entity_id, document, sync_utils, ctx, entity_type)
elif action in ['create', 'update']:
await handle_create_or_update(entity_id, document, sync_utils, ctx)
await handle_create_or_update(entity_id, document, sync_utils, ctx, entity_type)
else:
ctx.logger.warn(f"⚠️ Unbekannte Action: {action}")
await sync_utils.release_sync_lock(entity_id, success=False, error_message=f"Unbekannte Action: {action}")
await sync_utils.release_sync_lock(entity_id, success=False, error_message=f"Unbekannte Action: {action}", entity_type=entity_type)
except Exception as e:
# Unerwarteter Fehler während Sync - GARANTIERE Lock-Release
@@ -114,7 +117,8 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]):
await sync_utils.release_sync_lock(
entity_id,
success=False,
error_message=str(e)[:2000]
error_message=str(e)[:2000],
entity_type=entity_type
)
except Exception as release_error:
# Selbst Lock-Release failed - logge kritischen Fehler
@@ -134,7 +138,7 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]):
ctx.logger.error(traceback.format_exc())
async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync_utils: DocumentSync, ctx: FlowContext[Any]):
async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync_utils: DocumentSync, ctx: FlowContext[Any], entity_type: str = 'CDokumente'):
"""
Behandelt Create/Update von Documents
@@ -146,15 +150,15 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
ctx.logger.info("🔍 ANALYSE: Braucht dieses Document xAI-Sync?")
ctx.logger.info("=" * 80)
# Datei-Status für Preview-Generierung
datei_status = document.get('dateiStatus') or document.get('fileStatus')
# Datei-Status für Preview-Generierung (verschiedene Feld-Namen unterstützen)
datei_status = document.get('fileStatus') or document.get('dateiStatus')
# Entscheidungslogik: Soll dieses Document zu xAI?
needs_sync, collection_ids, reason = await sync_utils.should_sync_to_xai(document)
ctx.logger.info(f"📊 Entscheidung: {'✅ SYNC NÖTIG' if needs_sync else '⏭️ KEIN SYNC NÖTIG'}")
ctx.logger.info(f" Grund: {reason}")
ctx.logger.info(f" Datei-Status: {datei_status or 'N/A'}")
ctx.logger.info(f" File-Status: {datei_status or 'N/A'}")
if collection_ids:
ctx.logger.info(f" Collections: {collection_ids}")
@@ -163,7 +167,9 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
# PREVIEW-GENERIERUNG bei neuen/geänderten Dateien
# ═══════════════════════════════════════════════════════════════
if datei_status in ['Neu', 'Geändert', 'neu', 'geändert', 'New', 'Changed']:
# Case-insensitive check für Datei-Status
datei_status_lower = (datei_status or '').lower()
if datei_status_lower in ['neu', 'geändert', 'new', 'changed']:
ctx.logger.info("")
ctx.logger.info("=" * 80)
ctx.logger.info("🖼️ PREVIEW-GENERIERUNG STARTEN")
@@ -172,7 +178,7 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
try:
# 1. Hole Download-Informationen
download_info = await sync_utils.get_document_download_info(entity_id)
download_info = await sync_utils.get_document_download_info(entity_id, entity_type)
if not download_info:
ctx.logger.warn("⚠️ Keine Download-Info verfügbar - überspringe Preview")
@@ -213,7 +219,8 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
ctx.logger.info(f"📤 Uploading preview to EspoCRM...")
await sync_utils.update_sync_metadata(
entity_id,
preview_data=preview_data
preview_data=preview_data,
entity_type=entity_type
# Keine xaiFileId/collections - nur Preview update
)
ctx.logger.info(f"✅ Preview uploaded successfully")
@@ -244,7 +251,7 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
if not needs_sync:
ctx.logger.info("✅ Kein xAI-Sync erforderlich, Lock wird released")
await sync_utils.release_sync_lock(entity_id, success=True)
await sync_utils.release_sync_lock(entity_id, success=True, entity_type=entity_type)
return
# ═══════════════════════════════════════════════════════════════
@@ -315,9 +322,9 @@ async def handle_create_or_update(entity_id: str, document: Dict[str, Any], sync
await sync_utils.release_sync_lock(entity_id, success=False, error_message=str(e))
async def handle_delete(entity_id: str, document: Dict[str, Any], sync_utils: DocumentSync, ctx: FlowContext[Any]):
async def handle_delete(entity_id: str, document: Dict[str, Any], sync_utils: DocumentSync, ctx: FlowContext[Any], entity_type: str = 'CDokumente'):
"""
Behandelt Deletion von Documents
Behandelt Delete von Documents
Entfernt Document aus xAI Collections (aber löscht File nicht - kann in anderen Collections sein)
"""
@@ -327,12 +334,12 @@ async def handle_delete(entity_id: str, document: Dict[str, Any], sync_utils: Do
ctx.logger.info("🗑️ DOCUMENT DELETE - xAI CLEANUP")
ctx.logger.info("=" * 80)
xai_file_id = document.get('xaiFileId')
xai_file_id = document.get('xaiFileId') or document.get('xaiId')
xai_collections = document.get('xaiCollections') or []
if not xai_file_id or not xai_collections:
ctx.logger.info("⏭️ Document war nicht in xAI gesynct, nichts zu tun")
await sync_utils.release_sync_lock(entity_id, success=True)
await sync_utils.release_sync_lock(entity_id, success=True, entity_type=entity_type)
return
ctx.logger.info(f"📋 Document Info:")
@@ -354,7 +361,7 @@ async def handle_delete(entity_id: str, document: Dict[str, Any], sync_utils: Do
#
# ctx.logger.info(f"✅ File aus {len(xai_collections)} Collection(s) entfernt")
await sync_utils.release_sync_lock(entity_id, success=True)
await sync_utils.release_sync_lock(entity_id, success=True, entity_type=entity_type)
ctx.logger.info("=" * 80)
ctx.logger.info("✅ DELETE ABGESCHLOSSEN (PLACEHOLDER)")
@@ -364,4 +371,4 @@ async def handle_delete(entity_id: str, document: Dict[str, Any], sync_utils: Do
ctx.logger.error(f"❌ Fehler bei Delete: {e}")
import traceback
ctx.logger.error(traceback.format_exc())
await sync_utils.release_sync_lock(entity_id, success=False, error_message=str(e))
await sync_utils.release_sync_lock(entity_id, success=False, error_message=str(e), entity_type=entity_type)