feat: Implement Akte webhook for EspoCRM to queue entity IDs for synchronization

fix: Refactor Akte sync logic to handle multiple Redis queues and improve logging
refactor: Enhance parameter flattening for EspoCRM API calls
This commit is contained in:
bsiggel
2026-03-26 09:48:46 +00:00
parent b4d35b1790
commit 3459b9342f
6 changed files with 278 additions and 127 deletions

View File

@@ -54,8 +54,8 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
lock_key = f"akte_sync:{akte_id}"
lock_acquired = redis_client.set(lock_key, datetime.now().isoformat(), nx=True, ex=1800)
if not lock_acquired:
ctx.logger.warn(f"⏸️ Lock busy for Akte {aktennummer} requeueing")
raise RuntimeError(f"Lock busy for {aktennummer}")
ctx.logger.warn(f"⏸️ Lock busy for Akte {akte_id} requeueing")
raise RuntimeError(f"Lock busy for akte_id={akte_id}")
espocrm = EspoCRMAPI(ctx)
@@ -64,9 +64,13 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
akte = await espocrm.get_entity('CAkten', akte_id)
if not akte:
ctx.logger.error(f"❌ Akte {akte_id} not found in EspoCRM")
redis_client.srem("akte:processing", aktennummer)
return
# aktennummer can come from the event payload OR from the entity
# (Akten without Advoware have no aktennummer)
if not aktennummer:
aktennummer = akte.get('aktennummer')
sync_schalter = akte.get('syncSchalter', False)
aktivierungsstatus = str(akte.get('aktivierungsstatus') or '').lower()
ai_aktivierungsstatus = str(akte.get('aiAktivierungsstatus') or '').lower()
@@ -76,7 +80,8 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
ctx.logger.info(f" aktivierungsstatus : {aktivierungsstatus}")
ctx.logger.info(f" aiAktivierungsstatus : {ai_aktivierungsstatus}")
advoware_enabled = sync_schalter and aktivierungsstatus in ('import', 'neu', 'new', 'aktiv', 'active')
# Advoware sync requires an aktennummer (Akten without Advoware won't have one)
advoware_enabled = bool(aktennummer) and sync_schalter and aktivierungsstatus in ('import', 'neu', 'new', 'aktiv', 'active')
xai_enabled = ai_aktivierungsstatus in ('new', 'neu', 'aktiv', 'active')
ctx.logger.info(f" Advoware sync : {'✅ ON' if advoware_enabled else '⏭️ OFF'}")
@@ -84,7 +89,6 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
if not advoware_enabled and not xai_enabled:
ctx.logger.info("⏭️ Both syncs disabled nothing to do")
redis_client.srem("akte:processing", aktennummer)
return
# ── ADVOWARE SYNC ──────────────────────────────────────────────────
@@ -102,12 +106,23 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
if advoware_enabled:
final_update['syncStatus'] = 'synced'
final_update['lastSync'] = now
# 'import' = erster Sync → danach auf 'aktiv' setzen
if aktivierungsstatus == 'import':
final_update['aktivierungsstatus'] = 'aktiv'
ctx.logger.info("🔄 aktivierungsstatus: import → aktiv")
if xai_enabled:
final_update['aiSyncStatus'] = 'synced'
final_update['aiLastSync'] = now
# 'new' = Collection wurde gerade erstmalig angelegt → auf 'aktiv' setzen
if ai_aktivierungsstatus == 'new':
final_update['aiAktivierungsstatus'] = 'aktiv'
ctx.logger.info("🔄 aiAktivierungsstatus: new → aktiv")
await espocrm.update_entity('CAkten', akte_id, final_update)
redis_client.srem("akte:processing", aktennummer)
# Clean up processing sets (both queues may have triggered this sync)
if aktennummer:
redis_client.srem("advoware:processing_aktennummern", aktennummer)
redis_client.srem("akte:processing_entity_ids", akte_id)
ctx.logger.info("=" * 80)
ctx.logger.info("✅ AKTE SYNC COMPLETE")
@@ -120,9 +135,12 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext) -> None:
import traceback
ctx.logger.error(traceback.format_exc())
# Requeue for retry
# Requeue for retry (into the appropriate queue(s))
import time
redis_client.zadd("akte:pending", {aktennummer: time.time()})
now_ts = time.time()
if aktennummer:
redis_client.zadd("advoware:pending_aktennummern", {aktennummer: now_ts})
redis_client.zadd("akte:pending_entity_ids", {akte_id: now_ts})
try:
await espocrm.update_entity('CAkten', akte_id, {
@@ -254,8 +272,8 @@ async def _run_advoware_sync(
'name': filename,
'dokumentId': attachment.get('id'),
'hnr': history_entry.get('hNr') if history_entry else None,
'advowareArt': history_entry.get('art', 'Schreiben') if history_entry else 'Schreiben',
'advowareBemerkung': history_entry.get('text', '') if history_entry else '',
'advowareArt': (history_entry.get('art', 'Schreiben') or 'Schreiben')[:100] if history_entry else 'Schreiben',
'advowareBemerkung': (history_entry.get('text', '') or '')[:255] if history_entry else '',
'dateipfad': windows_file.get('path', ''),
'blake3hash': blake3_hash,
'syncedHash': blake3_hash,
@@ -302,8 +320,8 @@ async def _run_advoware_sync(
}
if history_entry:
update_data['hnr'] = history_entry.get('hNr')
update_data['advowareArt'] = history_entry.get('art', 'Schreiben')
update_data['advowareBemerkung'] = history_entry.get('text', '')
update_data['advowareArt'] = (history_entry.get('art', 'Schreiben') or 'Schreiben')[:100]
update_data['advowareBemerkung'] = (history_entry.get('text', '') or '')[:255]
await espocrm.update_entity('CDokumente', espo_doc['id'], update_data)
results['updated'] += 1
@@ -324,8 +342,15 @@ async def _run_advoware_sync(
elif action.action == 'DELETE':
if espo_doc:
await espocrm.delete_entity('CDokumente', espo_doc['id'])
results['deleted'] += 1
# Only delete if the HNR is genuinely absent from Advoware History
# (not just absent from Windows avoids deleting docs whose file
# is temporarily unavailable on the Windows share)
if hnr in history_by_hnr:
ctx.logger.warn(f" ⚠️ SKIP DELETE hnr={hnr}: still in Advoware History, only missing from Windows")
results['skipped'] += 1
else:
await espocrm.delete_entity('CDokumente', espo_doc['id'])
results['deleted'] += 1
except Exception as e:
ctx.logger.error(f" ❌ Error for hnr {hnr} ({filename}): {e}")