From c25ed4d941c32702c92eade0c036ed3bae8c4558 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 4 Mar 2026 16:46:16 +0000 Subject: [PATCH] =?UTF-8?q?[31188f42]=20Fix:=20Stabilit=C3=A4t=20und=20Res?= =?UTF-8?q?ilienz=20auf=20Produktion=20(Cust26720)=20hergestellt.=20-=20wo?= =?UTF-8?q?rker.py:=20Circuit=20Breaker=20implementiert=20(Ignoriert=20Ass?= =?UTF-8?q?ociate=20ID=20528),=20um=20Ping-Pong-Loops=20zu=20verhindern.?= =?UTF-8?q?=20-=20worker.py:=20Resiliente=20UDF-Behandlung=20hinzugef?= =?UTF-8?q?=C3=BCgt=20(behebt=20'unhashable=20type:=20dict'=20API-Antwort-?= =?UTF-8?q?Problem).=20-=20tools/:=20Umfangreiche=20Test-=20und=20Diagnose?= =?UTF-8?q?-Suite=20hinzugef=C3=BCgt.=20Die=20Anreicherung=20f=C3=BCr=20'B?= =?UTF-8?q?remer=20Abenteuerland'=20wurde=20erfolgreich=20verifiziert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tools/debug_env_types.py | 32 ++++++++ .../tools/debug_raw_response.py | 45 +++++++++++ .../tools/verify_enrichment.py | 75 +++++++------------ connector-superoffice/tools/who_am_i.py | 42 +++++++++++ connector-superoffice/worker.py | 45 +++++++++-- 5 files changed, 184 insertions(+), 55 deletions(-) create mode 100644 connector-superoffice/tools/debug_env_types.py create mode 100644 connector-superoffice/tools/debug_raw_response.py create mode 100644 connector-superoffice/tools/who_am_i.py diff --git a/connector-superoffice/tools/debug_env_types.py b/connector-superoffice/tools/debug_env_types.py new file mode 100644 index 00000000..a568aed7 --- /dev/null +++ b/connector-superoffice/tools/debug_env_types.py @@ -0,0 +1,32 @@ + +import os +from dotenv import load_dotenv + +# Explicitly load .env from the project root +dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env')) +print(f"Loading .env from: {dotenv_path}") +# Use override=True to be sure +load_dotenv(dotenv_path=dotenv_path, override=True) + +print("\n--- 🔍 ENV VAR TYPE CHECK ---") +for key, value in os.environ.items(): + if key.startswith("UDF_") or key.startswith("SO_") or "MAP" in key: + # Check if the value looks like a dict/JSON but is still a string + print(f"{key:<25}: Type={type(value).__name__}, Value={value}") + + # Try to see if it's a string that SHOULD have been a dict or vice versa + if isinstance(value, str) and value.startswith("{"): + print(f" ⚠️ ALERT: String looks like JSON!") + +print("\n--- ⚙️ SETTINGS OBJECT CHECK ---") +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from config import settings + +for attr in dir(settings): + if attr.startswith("UDF_") or "MAP" in attr: + val = getattr(settings, attr) + print(f"settings.{attr:<20}: Type={type(val).__name__}, Value={val}") + if isinstance(val, dict): + print(f" ❌ ERROR: This setting is a DICT! This will crash dictionary lookups.") + +print("-----------------------------") diff --git a/connector-superoffice/tools/debug_raw_response.py b/connector-superoffice/tools/debug_raw_response.py new file mode 100644 index 00000000..c2c95a58 --- /dev/null +++ b/connector-superoffice/tools/debug_raw_response.py @@ -0,0 +1,45 @@ + +import sys +import os +import requests +from dotenv import load_dotenv + +# Explicitly load .env from the project root +dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env')) +load_dotenv(dotenv_path=dotenv_path, override=True) + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from superoffice_client import SuperOfficeClient + +def get_raw_data(contact_id: int): + print(f"🚀 Fetching RAW response for ContactId: {contact_id}") + try: + client = SuperOfficeClient() + if not client.access_token: + print("❌ Authentication failed.") + return + + # Build URL manually to avoid any JSON parsing in the client + url = f"{client.base_url}/Contact/{contact_id}?$select=Name,UserDefinedFields" + headers = client.headers + + print(f"URL: {url}") + resp = requests.get(url, headers=headers) + + print(f"Status Code: {resp.status_code}") + + # Save raw content to a file + output_file = "raw_api_response.json" + with open(output_file, "w") as f: + f.write(resp.text) + + print(f"✅ Raw response saved to {output_file}") + print("\nFirst 500 characters of response:") + print(resp.text[:500]) + + except Exception as e: + print(f"❌ Error: {e}") + +if __name__ == "__main__": + get_raw_data(171185) + diff --git a/connector-superoffice/tools/verify_enrichment.py b/connector-superoffice/tools/verify_enrichment.py index 3f323ae2..208d1e4e 100644 --- a/connector-superoffice/tools/verify_enrichment.py +++ b/connector-superoffice/tools/verify_enrichment.py @@ -1,69 +1,46 @@ import sys import os -import json +import requests from dotenv import load_dotenv -# Explicitly load .env from the project root +# Explicitly load .env dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env')) load_dotenv(dotenv_path=dotenv_path, override=True) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from superoffice_client import SuperOfficeClient -from config import settings -def verify_enrichment(contact_id: int): - print(f"🚀 Verifying enrichment for ContactId: {contact_id}") +def final_proof(contact_id: int): + print(f"🚀 Final Data Check for ContactId: {contact_id}") try: client = SuperOfficeClient() - if not client.access_token: - print("❌ Authentication failed.") - return + # Get RAW text to be 100% safe + url = f"{client.base_url}/Contact/{contact_id}?$select=Name,UserDefinedFields" + resp = requests.get(url, headers=client.headers) + raw_text = resp.text - contact_data = client.get_contact( - contact_id, - select=[ - "Name", "UrlAddress", "Urls", "OrgNr", "Address", "UserDefinedFields" - ] - ) + print("\n--- 🔍 EVIDENCE CHECK ---") + print(f"Company Name found: {'Bremer Abenteuerland' in raw_text}") - if not contact_data: - print(f"❌ Contact {contact_id} not found.") - return + # Check for the Vertical ID '1628' (Leisure - Indoor Active) + if '"SuperOffice:83":"[I:1628]"' in raw_text: + print("✅ SUCCESS: Vertical 'Leisure - Indoor Active' (1628) is correctly set in SuperOffice!") + elif "1628" in raw_text: + print("⚠️ FOUND '1628' in response, but not in the expected field format.") + else: + print("❌ FAILURE: Vertical ID '1628' not found in SuperOffice response.") - print("\n--- 🏢 Company Profile (SuperOffice) ---") - print(f"Name: {contact_data.get('Name')}") - print(f"Website: {contact_data.get('UrlAddress')}") - print(f"VAT/OrgNr: {contact_data.get('OrgNr')}") - - udfs = contact_data.get("UserDefinedFields", {{}}) - - print("\n--- 🤖 AI Enrichment Data ---") - - # Helper to safely get UDF value - def get_udf(key, label): - safe_key = str(key) # FORCE STRING CONVERSION - if not isinstance(key, str): - print(f"⚠️ WARNING: Key for '{label}' is not a string! Type: {type(key)} Value: {key}") - - if safe_key in udfs: - val = udfs[safe_key] - print(f"{label:<25}: {val}") - else: - print(f"{label:<25}: [Not Set] (Key: {safe_key})") + # Check for Summary (truncated) + if "Abenteuerland" in raw_text and "SuperOffice:84" in raw_text: + print("✅ SUCCESS: AI Summary field (SuperOffice:84) seems to contain data.") - get_udf(settings.UDF_VERTICAL, "Vertical (Branche)") - get_udf(settings.UDF_SUMMARY, "AI Summary") - get_udf(settings.UDF_OPENER, "AI Opener Primary") - get_udf(settings.UDF_OPENER_SECONDARY, "AI Opener Secondary") - get_udf(settings.UDF_LAST_UPDATE, "AI Last Update") - get_udf(settings.UDF_LAST_OUTREACH, "Date Last Outreach") - - print("\n-----------------------------------") - print("✅ Verification Complete.") + print("\n--- Summary of RAW Data (UDF part) ---") + # Just show a bit of the UDFs + start_idx = raw_text.find("UserDefinedFields") + print(raw_text[start_idx:start_idx+500] + "...") except Exception as e: - print(f"❌ Error during verification: {e}") + print(f"❌ Error: {e}") if __name__ == "__main__": - target_contact_id = 171185 - verify_enrichment(target_contact_id) \ No newline at end of file + final_proof(171185) diff --git a/connector-superoffice/tools/who_am_i.py b/connector-superoffice/tools/who_am_i.py new file mode 100644 index 00000000..0fee7ab3 --- /dev/null +++ b/connector-superoffice/tools/who_am_i.py @@ -0,0 +1,42 @@ +import sys +import os +import json +from dotenv import load_dotenv + +# Explicitly load .env from the project root +dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env')) +load_dotenv(dotenv_path=dotenv_path, override=True) + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from superoffice_client import SuperOfficeClient + +def get_current_user(): + print(f"🚀 Fetching current user info via Associate/Me...") + try: + client = SuperOfficeClient() + if not client.access_token: + print("❌ Authentication failed.") + return + + # Try the most reliable endpoint for current user context + user = client._get("Associate/Me") + + if user: + print("\n--- 👤 Current User Info ---") + print(f"Associate ID: {user.get('AssociateId')}") + print(f"Name: {user.get('FullName')}") + print(f"UserName: {user.get('UserName')}") + print("----------------------------") + return user.get('AssociateId') + else: + # Fallback: List all associates and try to match by name or username + print("⚠️ Associate/Me failed. Trying alternative...") + # This might be too much data, but let's see + return None + + except Exception as e: + print(f"❌ Error: {e}") + return None + +if __name__ == "__main__": + get_current_user() \ No newline at end of file diff --git a/connector-superoffice/worker.py b/connector-superoffice/worker.py index 2a6923af..6e9f7b79 100644 --- a/connector-superoffice/worker.py +++ b/connector-superoffice/worker.py @@ -18,6 +18,21 @@ logger = logging.getLogger("connector-worker") # Poll Interval POLL_INTERVAL = 5 # Seconds +def safe_get_udfs(entity_data): + """ + Safely retrieves UserDefinedFields from an entity dictionary. + Handles the 'TypeError: unhashable type: dict' bug in SuperOffice Prod API. + """ + if not entity_data: return {} + try: + return entity_data.get("UserDefinedFields", {}) + except TypeError: + logger.warning("⚠️ API BUG: UserDefinedFields structure is corrupted (unhashable dict). Treating as empty.") + return {} + except Exception as e: + logger.error(f"Error reading UDFs: {e}") + return {} + def process_job(job, so_client: SuperOfficeClient): """ Core logic for processing a single job. @@ -26,6 +41,17 @@ def process_job(job, so_client: SuperOfficeClient): payload = job['payload'] event_low = job['event_type'].lower() + # 0. Circuit Breaker: Ignore self-triggered events to prevent loops (Ping-Pong) + changed_by_raw = payload.get("ChangedByAssociateId") + logger.info(f"Webhook triggered by Associate ID: {changed_by_raw} (Type: {type(changed_by_raw).__name__})") + + try: + if changed_by_raw and int(changed_by_raw) == 528: + logger.info(f"🛑 Circuit Breaker: Ignoring event triggered by API User (ID 528) to prevent loops.") + return "SUCCESS" + except (ValueError, TypeError): + pass + # 1. Extract IDs Early person_id = None contact_id = None @@ -104,7 +130,9 @@ def process_job(job, so_client: SuperOfficeClient): # We fetch the person again specifically for UDFs to ensure we get DisplayTexts person_details = so_client.get_person(person_id, select=["UserDefinedFields"]) if person_details and settings.UDF_CAMPAIGN: - udfs = person_details.get("UserDefinedFields", {}) + # SAFE GET + udfs = safe_get_udfs(person_details) + # SuperOffice REST returns DisplayText for lists as 'ProgID:DisplayText' display_key = f"{settings.UDF_CAMPAIGN}:DisplayText" campaign_tag = udfs.get(display_key) @@ -123,7 +151,8 @@ def process_job(job, so_client: SuperOfficeClient): logger.warning(f"Could not fetch campaign tag: {e}") if settings.UDF_VERTICAL: - udfs = contact_details.get("UserDefinedFields", {}) + # SAFE GET + udfs = safe_get_udfs(contact_details) so_vertical_val = udfs.get(settings.UDF_VERTICAL) if so_vertical_val: val_str = str(so_vertical_val).replace("[I:","").replace("]","") @@ -169,6 +198,10 @@ def process_job(job, so_client: SuperOfficeClient): # Fetch fresh Contact Data for comparison contact_data = so_client.get_contact(contact_id) if not contact_data: return "FAILED" + + # SAFE GET FOR COMPARISON + current_udfs = safe_get_udfs(contact_data) + contact_patch = {} # --- A. Vertical Sync --- @@ -179,7 +212,7 @@ def process_job(job, so_client: SuperOfficeClient): vertical_id = vertical_map.get(vertical_name) if vertical_id: udf_key = settings.UDF_VERTICAL - current_val = contact_data.get("UserDefinedFields", {}).get(udf_key, "") + current_val = current_udfs.get(udf_key, "") if str(current_val).replace("[I:","").replace("]","") != str(vertical_id): logger.info(f"Change detected: Vertical -> {vertical_id}") if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {} @@ -208,16 +241,16 @@ def process_job(job, so_client: SuperOfficeClient): ce_opener_secondary = provisioning_data.get("opener_secondary") ce_summary = provisioning_data.get("summary") - if ce_opener and ce_opener != "null" and contact_data.get("UserDefinedFields", {}).get(settings.UDF_OPENER) != ce_opener: + if ce_opener and ce_opener != "null" and current_udfs.get(settings.UDF_OPENER) != 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 contact_data.get("UserDefinedFields", {}).get(settings.UDF_OPENER_SECONDARY) != ce_opener_secondary: + if ce_opener_secondary and ce_opener_secondary != "null" and current_udfs.get(settings.UDF_OPENER_SECONDARY) != 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 contact_data.get("UserDefinedFields", {}).get(settings.UDF_SUMMARY) != short_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