From bb306c77176e90b76526a734eb4a6a430ddbe12a Mon Sep 17 00:00:00 2001 From: Moltbot-Jarvis Date: Mon, 16 Feb 2026 13:54:51 +0000 Subject: [PATCH] feat(so-sync): bidirectional round-trip for company data established [lessons-learned] --- connector-superoffice/README.md | 13 +++-- connector-superoffice/inspect_contact.py | 38 ++++--------- connector-superoffice/inspect_person.py | 16 +++--- round_trip_final.py | 68 ++++++++++++++++++++++++ so_one_shot_fix.py | 66 +++++++++++++++++++++++ so_surgical_update_v2.py | 44 +++++++++++++++ 6 files changed, 202 insertions(+), 43 deletions(-) create mode 100644 round_trip_final.py create mode 100644 so_one_shot_fix.py create mode 100644 so_surgical_update_v2.py diff --git a/connector-superoffice/README.md b/connector-superoffice/README.md index a6b929e8..329591b6 100644 --- a/connector-superoffice/README.md +++ b/connector-superoffice/README.md @@ -133,10 +133,13 @@ Folgende IDs werden in den Skripten als Referenz genutzt. Diese müssen für PRO | `26` | Leisure - Indoor Active | | `...` | *tbd* | -## 5. "Gotchas" & Lessons Learned (POC) +## 5. "Gotchas" & Lessons Learned (Update Feb 16, 2026) -* **API-URL:** Der `sod` Tenant `Cust55774` ist nur über `https://app-sod.superoffice.com` erreichbar, nicht `sod.superoffice.com`. +* **API-URL:** Der `sod` Tenant `Cust55774` ist nur über `https://app-sod.superoffice.com` erreichbar. * **Listen-IDs:** Die API gibt IDs von Listenfeldern im Format `[I:26]` zurück. Der String muss vor der DB-Abfrage auf den Integer `26` geparst werden. -* **Dev-System Limits:** Die Textfelder im DEV-System sind auf 40 Zeichen limitiert. Die generierten Texte müssen vor dem Senden gekürzt werden. -* **Y-Tabellen:** Der direkte API-Zugriff auf Zusatz-Tabellen ist in diesem Mandanten blockiert (`403 Forbidden`). Daher der Workaround mit UDFs. -* **CRMScript Trigger:** Die Erstellung von Triggern ist im DEV-System nicht möglich. Daher die Umstellung auf den externen Polling-Daemon. +* **Write-Back (Stammfelder):** + * **UrlAddress & Phones:** Das einfache Root-Feld `UrlAddress` ist beim `PUT` oft schreibgeschützt. Um die Website oder Telefonnummern zu setzen, muss die entsprechende Liste (`Urls` oder `Phones`) als Array von Objekten gesendet werden (z.B. `{"Value": "...", "Description": "..."}`). + * **Mandatory Fields:** Beim Update eines `Contact` Objekts müssen Pflichtfelder wie `Name` und `Number2` (oder `Number1`) zwingend im Payload enthalten sein, sonst schlägt die Validierung serverseitig fehl. + * **Full Object PUT:** SuperOffice REST überschreibt das gesamte Objekt. Felder, die im `PUT`-Payload fehlen, werden im CRM geleert. Es empfiehlt sich, das Objekt erst per `GET` zu laden, die Änderungen vorzunehmen und dann das gesamte Objekt zurückzusenden. +* **Dev-System Limits:** Die Textfelder im DEV-System sind auf 40 Zeichen limitiert. +* **Y-Tabellen & Trigger:** Direkter Zugriff auf Zusatz-Tabellen und CRMScript-Trigger sind im SOD-DEV Mandanten blockiert. diff --git a/connector-superoffice/inspect_contact.py b/connector-superoffice/inspect_contact.py index db956136..b0f927da 100644 --- a/connector-superoffice/inspect_contact.py +++ b/connector-superoffice/inspect_contact.py @@ -9,37 +9,17 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def discover_contact_structure(): - load_dotenv(dotenv_path="../.env") - auth = AuthHandler() - client = SuperOfficeClient(auth) + load_dotenv(dotenv_path="/home/node/clawd/.env", override=True) + client = SuperOfficeClient() - logger.info("Fetching a single contact to inspect structure...") + logger.info("Fetching contact ID 2 to inspect structure...") - # Try to get the first contact (usually ID 1 exists or can be found) - # If not, we try to create a dummy and then inspect it. - url = client._get_url("v1/Contact/1") - try: - resp = client.session.get(url, headers=client._get_headers()) - if resp.status_code == 200: - contact = resp.json() - print("\n--- CONTACT STRUCTURE ---") - print(json.dumps(contact, indent=2)) - else: - logger.warning(f"Contact 1 not found (Status {resp.status_code}). Trying to list contacts...") - url = client._get_url("v1/Contact?$top=1") - resp = client.session.get(url, headers=client._get_headers()) - resp.raise_for_status() - contacts = resp.json().get("value", []) - if contacts: - print("\n--- CONTACT STRUCTURE (from list) ---") - print(json.dumps(contacts[0], indent=2)) - else: - print("\nNo contacts found in tenant. Please create one manually in the UI or stay tuned.") - - except Exception as e: - logger.error(f"Failed to fetch contact: {e}") - if hasattr(e, 'response') and e.response is not None: - print(f"Details: {e.response.text}") + contact = client._get("Contact/2") + if contact: + print("\n--- CONTACT STRUCTURE ---") + print(json.dumps(contact, indent=2)) + else: + logger.error("Failed to fetch contact.") if __name__ == "__main__": discover_contact_structure() diff --git a/connector-superoffice/inspect_person.py b/connector-superoffice/inspect_person.py index c6f3e182..dc3c2787 100644 --- a/connector-superoffice/inspect_person.py +++ b/connector-superoffice/inspect_person.py @@ -1,3 +1,4 @@ +from dotenv import load_dotenv import json from config import Config from logging_config import setup_logging @@ -7,18 +8,15 @@ from superoffice_client import SuperOfficeClient logger = setup_logging("inspector") def inspect_person(person_id): - auth = AuthHandler() - client = SuperOfficeClient(auth) + load_dotenv(dotenv_path="/home/node/clawd/.env", override=True) + client = SuperOfficeClient() logger.info(f"Fetching Person with ID {person_id} to inspect structure...") - url = client._get_url(f"v1/Person/{person_id}") - try: - resp = client.session.get(url, headers=client._get_headers()) - resp.raise_for_status() - person_data = resp.json() + person_data = client._get(f"Person/{person_id}") + if person_data: print(f"\n--- PERSON STRUCTURE (ID: {person_id}) ---") print(json.dumps(person_data, indent=2)) - except Exception as e: - logger.error(f"Failed to fetch person data: {e}") + else: + logger.error(f"Failed to fetch person data.") if __name__ == "__main__": target_person_id = 9 diff --git a/round_trip_final.py b/round_trip_final.py new file mode 100644 index 00000000..0144e66e --- /dev/null +++ b/round_trip_final.py @@ -0,0 +1,68 @@ +import os +import json +import sys +from dotenv import load_dotenv + +# Path gymnastics +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "connector-superoffice")) + +from company_explorer_connector import get_company_details +from superoffice_client import SuperOfficeClient + +# Load ENV +load_dotenv(dotenv_path="/home/node/clawd/.env", override=True) + +def perform_final_round_trip(ce_id): + client = SuperOfficeClient() + print(f"--- Final Round-Trip: CE {ce_id} -> SuperOffice ---") + + # 1. Get enriched data from CE + ce_data = get_company_details(ce_id) + if not ce_data or "error" in ce_data: + print("❌ Could not fetch CE data.") + return + + so_id = ce_data.get("crm_id") + if not so_id: + print("❌ No SO ID found in CE.") + return + + # 2. Fetch current SO contact + contact = client._get(f"Contact/{so_id}") + if not contact: + print(f"❌ Could not fetch SO Contact {so_id}") + return + + # 3. Intelligent Mapping (Full Object) + print(f"Mapping data for {ce_data.get('name')}...") + + # Simple Fields + contact["UrlAddress"] = ce_data.get("website", "") + contact["Department"] = "KI-Enriched via CE" + + # Address Object + if "Address" not in contact: contact["Address"] = {} + if "Street" not in contact["Address"]: contact["Address"]["Street"] = {} + + contact["Address"]["Street"]["Address1"] = ce_data.get("address", "") + contact["Address"]["Street"]["City"] = ce_data.get("city", "") + contact["Address"]["Street"]["Zipcode"] = ce_data.get("zip", "") + + # Phones (List) + if ce_data.get("phone"): + contact["Phones"] = [{"Number": ce_data.get("phone"), "Description": "Main"}] + + # 4. Write back + print(f"Sending full update to SO Contact {so_id}...") + result = client._put(f"Contact/{so_id}", contact) + + if result: + print("🚀 SUCCESS! Round-trip for Robo-Planet complete.") + print(f"Website: {contact['UrlAddress']}") + print(f"City: {contact['Address']['Street']['City']}") + else: + print("❌ Update failed.") + +if __name__ == "__main__": + perform_final_round_trip(53) diff --git a/so_one_shot_fix.py b/so_one_shot_fix.py new file mode 100644 index 00000000..751861a2 --- /dev/null +++ b/so_one_shot_fix.py @@ -0,0 +1,66 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv(dotenv_path="/home/node/clawd/.env", override=True) + +def fix_all_now_v2(): + # 1. Refresh Token + token_url = "https://sod.superoffice.com/login/common/oauth/tokens" + token_data = { + "grant_type": "refresh_token", + "client_id": os.getenv("SO_CLIENT_ID"), + "client_secret": os.getenv("SO_CLIENT_SECRET"), + "refresh_token": os.getenv("SO_REFRESH_TOKEN"), + "redirect_uri": "http://localhost" + } + t_resp = requests.post(token_url, data=token_data) + access_token = t_resp.json().get("access_token") + + if not access_token: + print("❌ Token Refresh failed.") + return + + # 2. Dual-Url Payload (Root + Array) + payload = { + "contactId": 2, + "Name": "RoboPlanet GmbH-SOD", + "Number2": "123", + "UrlAddress": "http://robo-planet.de", + "Urls": [ + { + "Value": "http://robo-planet.de", + "Description": "Website" + } + ], + "OrgNr": "DE400464410", + "Department": "Website Final Fix 13:42", + "Address": { + "Postal": { + "Address1": "Schatzbogen 39", + "City": "München", + "Zipcode": "81829" + } + }, + "UserDefinedFields": { + "SuperOffice:5": "[I:23]" + } + } + + # 3. Update Call + url = "https://app-sod.superoffice.com/Cust55774/api/v1/Contact/2" + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + + resp = requests.put(url, headers=headers, json=payload) + + if resp.status_code == 200: + print("🚀 SUCCESS! Website should now be visible via the Urls list.") + else: + print(f"❌ Error: {resp.text}") + +if __name__ == "__main__": + fix_all_now_v2() diff --git a/so_surgical_update_v2.py b/so_surgical_update_v2.py new file mode 100644 index 00000000..3eea6a60 --- /dev/null +++ b/so_surgical_update_v2.py @@ -0,0 +1,44 @@ +import os +import json +import sys +import requests +from dotenv import load_dotenv + +# Path gymnastics +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "connector-superoffice")) + +from superoffice_client import SuperOfficeClient + +# Load ENV +load_dotenv(dotenv_path="/home/node/clawd/.env", override=True) + +def surgical_update_v2(contact_id): + client = SuperOfficeClient() + print(f"--- Surgical Update V2: Contact {contact_id} ---") + + # We now use the proper 'Urls' list format + payload = { + "contactId": int(contact_id), + "Department": "Final Round-Trip 13:20", + "Urls": [ + { + "Value": "http://robo-planet.de", + "Description": "Website" + } + ] + } + + url = f"{client.base_url}/Contact/{contact_id}" + print(f"Sending PUT to {url} with proper URL list...") + + resp = requests.put(url, headers=client.headers, json=payload) + + print(f"Status Code: {resp.status_code}") + if resp.status_code == 200: + print("✅ SUCCESS! Website should be visible now.") + else: + print(f"❌ Error: {resp.text}") + +if __name__ == "__main__": + surgical_update_v2(2)