[31188f42] Fix: Stabilität und Resilienz auf Produktion (Cust26720) hergestellt.
- worker.py: Circuit Breaker implementiert (Ignoriert Associate ID 528), um Ping-Pong-Loops zu verhindern. - worker.py: Resiliente UDF-Behandlung hinzugefügt (behebt 'unhashable type: dict' API-Antwort-Problem). - tools/: Umfangreiche Test- und Diagnose-Suite hinzugefügt. Die Anreicherung für 'Bremer Abenteuerland' wurde erfolgreich verifiziert.
This commit is contained in:
32
connector-superoffice/tools/debug_env_types.py
Normal file
32
connector-superoffice/tools/debug_env_types.py
Normal file
@@ -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("-----------------------------")
|
||||||
45
connector-superoffice/tools/debug_raw_response.py
Normal file
45
connector-superoffice/tools/debug_raw_response.py
Normal file
@@ -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)
|
||||||
|
|
||||||
@@ -1,69 +1,46 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import requests
|
||||||
from dotenv import load_dotenv
|
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'))
|
dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '.env'))
|
||||||
load_dotenv(dotenv_path=dotenv_path, override=True)
|
load_dotenv(dotenv_path=dotenv_path, override=True)
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||||
from superoffice_client import SuperOfficeClient
|
from superoffice_client import SuperOfficeClient
|
||||||
from config import settings
|
|
||||||
|
|
||||||
def verify_enrichment(contact_id: int):
|
def final_proof(contact_id: int):
|
||||||
print(f"🚀 Verifying enrichment for ContactId: {contact_id}")
|
print(f"🚀 Final Data Check for ContactId: {contact_id}")
|
||||||
try:
|
try:
|
||||||
client = SuperOfficeClient()
|
client = SuperOfficeClient()
|
||||||
if not client.access_token:
|
# Get RAW text to be 100% safe
|
||||||
print("❌ Authentication failed.")
|
url = f"{client.base_url}/Contact/{contact_id}?$select=Name,UserDefinedFields"
|
||||||
return
|
resp = requests.get(url, headers=client.headers)
|
||||||
|
raw_text = resp.text
|
||||||
|
|
||||||
contact_data = client.get_contact(
|
print("\n--- 🔍 EVIDENCE CHECK ---")
|
||||||
contact_id,
|
print(f"Company Name found: {'Bremer Abenteuerland' in raw_text}")
|
||||||
select=[
|
|
||||||
"Name", "UrlAddress", "Urls", "OrgNr", "Address", "UserDefinedFields"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not contact_data:
|
# Check for the Vertical ID '1628' (Leisure - Indoor Active)
|
||||||
print(f"❌ Contact {contact_id} not found.")
|
if '"SuperOffice:83":"[I:1628]"' in raw_text:
|
||||||
return
|
print("✅ SUCCESS: Vertical 'Leisure - Indoor Active' (1628) is correctly set in SuperOffice!")
|
||||||
|
elif "1628" in raw_text:
|
||||||
print("\n--- 🏢 Company Profile (SuperOffice) ---")
|
print("⚠️ FOUND '1628' in response, but not in the expected field format.")
|
||||||
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:
|
else:
|
||||||
print(f"{label:<25}: [Not Set] (Key: {safe_key})")
|
print("❌ FAILURE: Vertical ID '1628' not found in SuperOffice response.")
|
||||||
|
|
||||||
get_udf(settings.UDF_VERTICAL, "Vertical (Branche)")
|
# Check for Summary (truncated)
|
||||||
get_udf(settings.UDF_SUMMARY, "AI Summary")
|
if "Abenteuerland" in raw_text and "SuperOffice:84" in raw_text:
|
||||||
get_udf(settings.UDF_OPENER, "AI Opener Primary")
|
print("✅ SUCCESS: AI Summary field (SuperOffice:84) seems to contain data.")
|
||||||
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("\n--- Summary of RAW Data (UDF part) ---")
|
||||||
print("✅ Verification Complete.")
|
# 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:
|
except Exception as e:
|
||||||
print(f"❌ Error during verification: {e}")
|
print(f"❌ Error: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
target_contact_id = 171185
|
final_proof(171185)
|
||||||
verify_enrichment(target_contact_id)
|
|
||||||
|
|||||||
42
connector-superoffice/tools/who_am_i.py
Normal file
42
connector-superoffice/tools/who_am_i.py
Normal file
@@ -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()
|
||||||
@@ -18,6 +18,21 @@ logger = logging.getLogger("connector-worker")
|
|||||||
# Poll Interval
|
# Poll Interval
|
||||||
POLL_INTERVAL = 5 # Seconds
|
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):
|
def process_job(job, so_client: SuperOfficeClient):
|
||||||
"""
|
"""
|
||||||
Core logic for processing a single job.
|
Core logic for processing a single job.
|
||||||
@@ -26,6 +41,17 @@ def process_job(job, so_client: SuperOfficeClient):
|
|||||||
payload = job['payload']
|
payload = job['payload']
|
||||||
event_low = job['event_type'].lower()
|
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
|
# 1. Extract IDs Early
|
||||||
person_id = None
|
person_id = None
|
||||||
contact_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
|
# We fetch the person again specifically for UDFs to ensure we get DisplayTexts
|
||||||
person_details = so_client.get_person(person_id, select=["UserDefinedFields"])
|
person_details = so_client.get_person(person_id, select=["UserDefinedFields"])
|
||||||
if person_details and settings.UDF_CAMPAIGN:
|
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'
|
# SuperOffice REST returns DisplayText for lists as 'ProgID:DisplayText'
|
||||||
display_key = f"{settings.UDF_CAMPAIGN}:DisplayText"
|
display_key = f"{settings.UDF_CAMPAIGN}:DisplayText"
|
||||||
campaign_tag = udfs.get(display_key)
|
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}")
|
logger.warning(f"Could not fetch campaign tag: {e}")
|
||||||
|
|
||||||
if settings.UDF_VERTICAL:
|
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)
|
so_vertical_val = udfs.get(settings.UDF_VERTICAL)
|
||||||
if so_vertical_val:
|
if so_vertical_val:
|
||||||
val_str = str(so_vertical_val).replace("[I:","").replace("]","")
|
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
|
# Fetch fresh Contact Data for comparison
|
||||||
contact_data = so_client.get_contact(contact_id)
|
contact_data = so_client.get_contact(contact_id)
|
||||||
if not contact_data: return "FAILED"
|
if not contact_data: return "FAILED"
|
||||||
|
|
||||||
|
# SAFE GET FOR COMPARISON
|
||||||
|
current_udfs = safe_get_udfs(contact_data)
|
||||||
|
|
||||||
contact_patch = {}
|
contact_patch = {}
|
||||||
|
|
||||||
# --- A. Vertical Sync ---
|
# --- A. Vertical Sync ---
|
||||||
@@ -179,7 +212,7 @@ def process_job(job, so_client: SuperOfficeClient):
|
|||||||
vertical_id = vertical_map.get(vertical_name)
|
vertical_id = vertical_map.get(vertical_name)
|
||||||
if vertical_id:
|
if vertical_id:
|
||||||
udf_key = settings.UDF_VERTICAL
|
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):
|
if str(current_val).replace("[I:","").replace("]","") != str(vertical_id):
|
||||||
logger.info(f"Change detected: Vertical -> {vertical_id}")
|
logger.info(f"Change detected: Vertical -> {vertical_id}")
|
||||||
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
|
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_opener_secondary = provisioning_data.get("opener_secondary")
|
||||||
ce_summary = provisioning_data.get("summary")
|
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"] = {}
|
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
|
||||||
contact_patch["UserDefinedFields"][settings.UDF_OPENER] = ce_opener
|
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"] = {}
|
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
|
||||||
contact_patch["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary
|
contact_patch["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary
|
||||||
|
|
||||||
if ce_summary and ce_summary != "null":
|
if ce_summary and ce_summary != "null":
|
||||||
short_summary = (ce_summary[:132] + "...") if len(ce_summary) > 135 else ce_summary
|
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")
|
logger.info("Change detected: AI Summary")
|
||||||
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
|
if "UserDefinedFields" not in contact_patch: contact_patch["UserDefinedFields"] = {}
|
||||||
contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = short_summary
|
contact_patch["UserDefinedFields"][settings.UDF_SUMMARY] = short_summary
|
||||||
|
|||||||
Reference in New Issue
Block a user