[30e88f42] ✦ In dieser Sitzung haben wir den End-to-End-Test der SuperOffice-Schnittstelle erfolgreich von der automatisierten Simulation bis zum produktiven Live-Lauf
✦ In dieser Sitzung haben wir den End-to-End-Test der SuperOffice-Schnittstelle erfolgreich von der automatisierten Simulation bis zum produktiven Live-Lauf mit Echtdaten abgeschlossen.
This commit is contained in:
@@ -141,7 +141,10 @@ def process_job(job, so_client: SuperOfficeClient):
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch contact details for {contact_id}: {e}")
|
||||
|
||||
# 2. Call Company Explorer Provisioning API
|
||||
# --- 2. PREPARE UPDATES (Atomic Strategy) ---
|
||||
# We will fetch the contact ONCE, calculate all needed changes (Standard + UDFs),
|
||||
# and push them in a single operation if possible to avoid race conditions.
|
||||
|
||||
ce_url = f"{settings.COMPANY_EXPLORER_URL}/api/provision/superoffice-contact"
|
||||
ce_req = {
|
||||
"so_contact_id": contact_id,
|
||||
@@ -152,7 +155,6 @@ def process_job(job, so_client: SuperOfficeClient):
|
||||
"crm_industry_name": crm_industry_name
|
||||
}
|
||||
|
||||
# Simple Basic Auth for internal API
|
||||
ce_auth = (os.getenv("API_USER", "admin"), os.getenv("API_PASSWORD", "gemini"))
|
||||
|
||||
try:
|
||||
@@ -173,82 +175,116 @@ def process_job(job, so_client: SuperOfficeClient):
|
||||
|
||||
logger.info(f"CE Response for Contact {contact_id}: {json.dumps(provisioning_data)}")
|
||||
|
||||
# 2b. Sync Vertical to SuperOffice (Company Level)
|
||||
vertical_name = provisioning_data.get("vertical_name")
|
||||
# Fetch fresh Contact Data for comparison
|
||||
try:
|
||||
contact_data = so_client.get_contact(contact_id)
|
||||
if not contact_data:
|
||||
logger.error(f"Contact {contact_id} not found in SuperOffice.")
|
||||
return "FAILED"
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch contact {contact_id}: {e}")
|
||||
return "RETRY"
|
||||
|
||||
dirty_standard = False
|
||||
dirty_udfs = False
|
||||
|
||||
# Ensure nested dicts exist
|
||||
if "UserDefinedFields" not in contact_data: contact_data["UserDefinedFields"] = {}
|
||||
if "PostalAddress" not in contact_data or contact_data["PostalAddress"] is None: contact_data["PostalAddress"] = {}
|
||||
|
||||
# --- A. Vertical Sync ---
|
||||
vertical_name = provisioning_data.get("vertical_name")
|
||||
if vertical_name:
|
||||
try:
|
||||
vertical_map = json.loads(settings.VERTICAL_MAP_JSON)
|
||||
except:
|
||||
vertical_map = {}
|
||||
logger.error("Failed to parse VERTICAL_MAP_JSON from config.")
|
||||
|
||||
vertical_id = vertical_map.get(vertical_name)
|
||||
|
||||
if vertical_id:
|
||||
logger.info(f"Identified Vertical '{vertical_name}' -> ID {vertical_id}")
|
||||
try:
|
||||
# Check current value to avoid loops
|
||||
current_contact = so_client.get_contact(contact_id)
|
||||
current_udfs = current_contact.get("UserDefinedFields", {})
|
||||
|
||||
# Use Config UDF key
|
||||
vertical_id = vertical_map.get(vertical_name)
|
||||
if vertical_id:
|
||||
udf_key = settings.UDF_VERTICAL
|
||||
current_val = current_udfs.get(udf_key, "")
|
||||
|
||||
# Normalize SO list ID format (e.g., "[I:26]" -> "26")
|
||||
if current_val and current_val.startswith("[I:"):
|
||||
current_val = current_val.split(":")[1].strip("]")
|
||||
current_val = contact_data["UserDefinedFields"].get(udf_key, "")
|
||||
# Normalize "[I:26]" -> "26"
|
||||
if current_val and str(current_val).startswith("[I:"):
|
||||
current_val = str(current_val).split(":")[1].strip("]")
|
||||
|
||||
if str(current_val) != str(vertical_id):
|
||||
logger.info(f"Updating Contact {contact_id} Vertical: {current_val} -> {vertical_id}")
|
||||
so_client.update_entity_udfs(contact_id, "Contact", {udf_key: str(vertical_id)})
|
||||
else:
|
||||
logger.info(f"Vertical for Contact {contact_id} already in sync ({vertical_id}).")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sync vertical for Contact {contact_id}: {e}")
|
||||
else:
|
||||
logger.warning(f"Vertical '{vertical_name}' not found in internal mapping.")
|
||||
logger.info(f"Change detected: Vertical {current_val} -> {vertical_id}")
|
||||
contact_data["UserDefinedFields"][udf_key] = str(vertical_id)
|
||||
dirty_udfs = True
|
||||
except Exception as e:
|
||||
logger.error(f"Vertical sync error: {e}")
|
||||
|
||||
# 2b.2 Sync Address & VAT (Standard Fields)
|
||||
# Check if we have address data to sync
|
||||
# --- B. Address & VAT Sync ---
|
||||
ce_city = provisioning_data.get("address_city")
|
||||
ce_country = provisioning_data.get("address_country") # Assuming 'DE' code or similar
|
||||
ce_street = provisioning_data.get("address_street")
|
||||
ce_zip = provisioning_data.get("address_zip")
|
||||
ce_vat = provisioning_data.get("vat_id")
|
||||
|
||||
if ce_city or ce_vat:
|
||||
# Check if ANY address component is present
|
||||
if ce_city or ce_street or ce_zip:
|
||||
# Initialize Address object if missing
|
||||
if "Address" not in contact_data or contact_data["Address"] is None:
|
||||
contact_data["Address"] = {"Street": {}, "Postal": {}}
|
||||
|
||||
addr_obj = contact_data["Address"]
|
||||
if "Postal" not in addr_obj or addr_obj["Postal"] is None: addr_obj["Postal"] = {}
|
||||
if "Street" not in addr_obj or addr_obj["Street"] is None: addr_obj["Street"] = {}
|
||||
|
||||
# Update Helper
|
||||
def update_addr_field(field_name, new_val, log_name):
|
||||
nonlocal dirty_standard
|
||||
if new_val:
|
||||
# Sync to both Postal and Street for best visibility
|
||||
for type_key in ["Postal", "Street"]:
|
||||
cur = addr_obj[type_key].get(field_name, "")
|
||||
if cur != new_val:
|
||||
logger.info(f"Change detected: {type_key} {log_name} '{cur}' -> '{new_val}'")
|
||||
addr_obj[type_key][field_name] = new_val
|
||||
dirty_standard = True
|
||||
|
||||
update_addr_field("City", ce_city, "City")
|
||||
update_addr_field("Address1", ce_street, "Street")
|
||||
update_addr_field("Zipcode", ce_zip, "Zip")
|
||||
|
||||
if ce_vat:
|
||||
# Field is 'OrgNr' in WebAPI, not 'OrgNumber'
|
||||
current_vat = contact_data.get("OrgNr", "")
|
||||
if current_vat != ce_vat:
|
||||
logger.info(f"Change detected: VAT '{current_vat}' -> '{ce_vat}'")
|
||||
contact_data["OrgNr"] = ce_vat
|
||||
dirty_standard = True
|
||||
|
||||
# --- C. AI Openers Sync ---
|
||||
ce_opener = provisioning_data.get("opener")
|
||||
ce_opener_secondary = provisioning_data.get("opener_secondary")
|
||||
|
||||
if ce_opener and ce_opener != "null":
|
||||
current_opener = contact_data["UserDefinedFields"].get(settings.UDF_OPENER, "")
|
||||
if current_opener != ce_opener:
|
||||
logger.info("Change detected: Primary Opener")
|
||||
contact_data["UserDefinedFields"][settings.UDF_OPENER] = ce_opener
|
||||
dirty_udfs = True
|
||||
|
||||
if ce_opener_secondary and ce_opener_secondary != "null":
|
||||
current_opener_sec = contact_data["UserDefinedFields"].get(settings.UDF_OPENER_SECONDARY, "")
|
||||
if current_opener_sec != ce_opener_secondary:
|
||||
logger.info("Change detected: Secondary Opener")
|
||||
contact_data["UserDefinedFields"][settings.UDF_OPENER_SECONDARY] = ce_opener_secondary
|
||||
dirty_udfs = True
|
||||
|
||||
# --- D. Apply Updates (Single Transaction) ---
|
||||
if dirty_standard or dirty_udfs:
|
||||
logger.info(f"Pushing combined updates for Contact {contact_id} (Standard={dirty_standard}, UDFs={dirty_udfs})...")
|
||||
try:
|
||||
# Re-fetch contact to be safe (or use cached if optimal)
|
||||
contact_data = so_client.get_contact(contact_id)
|
||||
changed = False
|
||||
|
||||
# City (PostalAddress)
|
||||
if ce_city:
|
||||
# SuperOffice Address structure is complex. Simplified check on PostalAddress.
|
||||
# Address: { "PostalAddress": { "City": "..." } }
|
||||
current_city = contact_data.get("PostalAddress", {}).get("City", "")
|
||||
if current_city != ce_city:
|
||||
if "PostalAddress" not in contact_data: contact_data["PostalAddress"] = {}
|
||||
contact_data["PostalAddress"]["City"] = ce_city
|
||||
changed = True
|
||||
logger.info(f"Updating City: {current_city} -> {ce_city}")
|
||||
|
||||
# VAT (OrgNumber)
|
||||
if ce_vat:
|
||||
current_vat = contact_data.get("OrgNumber", "")
|
||||
if current_vat != ce_vat:
|
||||
contact_data["OrgNumber"] = ce_vat
|
||||
changed = True
|
||||
logger.info(f"Updating VAT: {current_vat} -> {ce_vat}")
|
||||
|
||||
if changed:
|
||||
logger.info(f"Pushing standard field updates for Contact {contact_id}...")
|
||||
so_client._put(f"Contact/{contact_id}", contact_data)
|
||||
|
||||
# We PUT the whole modified contact object back
|
||||
# This handles both standard fields and UDFs in one atomic-ish go
|
||||
so_client._put(f"Contact/{contact_id}", contact_data)
|
||||
logger.info("✅ Contact Update Successful.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sync Address/VAT for Contact {contact_id}: {e}")
|
||||
logger.error(f"Failed to update Contact {contact_id}: {e}")
|
||||
raise
|
||||
else:
|
||||
logger.info("No changes needed for Contact.")
|
||||
|
||||
# 2c. Sync Website (Company Level)
|
||||
# 2d. Sync Person Position (Role) - if Person exists
|
||||
# TEMPORARILY DISABLED TO PREVENT LOOP (SO API Read-after-Write latency or field mapping issue)
|
||||
# Re-enable via config if needed
|
||||
if settings.ENABLE_WEBSITE_SYNC:
|
||||
|
||||
Reference in New Issue
Block a user