fix: [30388f42] Stoppe Webhook-Loop durch Idempotenz und Truncation

- Führt clean_text_for_so Hilfsfunktion ein, die KI-Texte auf 200 Zeichen kürzt.
- Vergleicht und sendet nur noch gekürzte Texte an SuperOffice, um Differenzen durch serverseitige Kürzung zu vermeiden.
- Fügt detailliertes Logging für erkannte Änderungen hinzu.
- Verhindert so die Endlosschleife bei Benutzern mit ID 528 (Bot-ID).
This commit is contained in:
2026-03-06 14:46:41 +00:00
parent 735cd77b68
commit 2783e51f44

View File

@@ -33,6 +33,12 @@ def safe_get_udfs(entity_data):
logger.error(f"Error reading UDFs: {e}")
return {}
def clean_text_for_so(text, limit=200):
"""Clean and truncate text for SuperOffice UDF compatibility."""
if not text or text == "null": return ""
# Strip whitespace and truncate to safe limit
return str(text).strip()[:limit]
def process_job(job, so_client: SuperOfficeClient, queue: JobQueue):
"""
Core logic for processing a single job.
@@ -46,9 +52,10 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue):
# --- CIRCUIT BREAKER: DETECT ECHOES ---
# We log if the event was triggered by our own API user (Associate 528)
# but we NO LONGER SKIP IT, to allow manual changes by the user who shares the same ID.
# The idempotency logic below will prevent infinite loops.
changed_by = payload.get("ChangedByAssociateId")
if changed_by == 528:
logger.info(f" Potential Echo: Event triggered by Associate 528. Proceeding to allow manual user updates.")
logger.info(f" Potential Echo: Event triggered by Associate 528. Proceeding to check for meaningful changes.")
# --------------------------------------------
# 0. ID Extraction & Early Exit for irrelevant jobs
@@ -287,23 +294,25 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue):
contact_patch["OrgNr"] = ce_vat
# --- C. AI Openers & Summary Sync ---
ce_opener = provisioning_data.get("opener")
ce_opener_secondary = provisioning_data.get("opener_secondary")
ce_summary = provisioning_data.get("summary")
# TRUNCATION TO 200 CHARS TO PREVENT INFINITE LOOPS (SO UDF LIMITS)
ce_opener = clean_text_for_so(provisioning_data.get("opener"), limit=200)
ce_opener_secondary = clean_text_for_so(provisioning_data.get("opener_secondary"), limit=200)
ce_summary = clean_text_for_so(provisioning_data.get("summary"), limit=132) # Summary is very short in SO
if ce_opener and ce_opener != "null" and current_udfs.get(settings.UDF_OPENER) != ce_opener:
if ce_opener and current_udfs.get(settings.UDF_OPENER) != ce_opener:
logger.info(f"Change detected: Opener Primary (Length: {len(ce_opener)})")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_OPENER] = ce_opener
if ce_opener_secondary and ce_opener_secondary != "null" and current_udfs.get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary:
if ce_opener_secondary and current_udfs.get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary:
logger.info(f"Change detected: Opener Secondary (Length: {len(ce_opener_secondary)})")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary
if ce_summary and ce_summary != "null":
short_summary = (ce_summary[:132] + "...") if len(ce_summary) > 135 else ce_summary
if current_udfs.get(settings.UDF_SUMMARY) != short_summary:
logger.info("Change detected: AI Summary")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = short_summary
if ce_summary and current_udfs.get(settings.UDF_SUMMARY) != ce_summary:
logger.info(f"Change detected: AI Summary (Length: {len(ce_summary)})")
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = ce_summary
# --- D. Timestamps & Website Sync ---
# CRITICAL: We only update the timestamp if we actually have OTHER changes to push.
@@ -326,6 +335,8 @@ def process_job(job, so_client: SuperOfficeClient, queue: JobQueue):
logger.info(f"Pushing combined PATCH for Contact {contact_id}: {list(contact_patch.keys())}")
so_client.patch_contact(contact_id, contact_patch)
logger.info("✅ Contact Update Successful.")
else:
logger.info(f" No changes detected for Contact {contact_id}. Skipping PATCH.")
# 2d. Sync Person Position
role_name = provisioning_data.get("role_name")