feat(so-sync): bidirectional round-trip for company data established [lessons-learned]
This commit is contained in:
@@ -133,10 +133,13 @@ Folgende IDs werden in den Skripten als Referenz genutzt. Diese müssen für PRO
|
|||||||
| `26` | Leisure - Indoor Active |
|
| `26` | Leisure - Indoor Active |
|
||||||
| `...` | *tbd* |
|
| `...` | *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.
|
* **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.
|
* **Write-Back (Stammfelder):**
|
||||||
* **Y-Tabellen:** Der direkte API-Zugriff auf Zusatz-Tabellen ist in diesem Mandanten blockiert (`403 Forbidden`). Daher der Workaround mit UDFs.
|
* **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": "..."}`).
|
||||||
* **CRMScript Trigger:** Die Erstellung von Triggern ist im DEV-System nicht möglich. Daher die Umstellung auf den externen Polling-Daemon.
|
* **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.
|
||||||
|
|||||||
@@ -9,37 +9,17 @@ logging.basicConfig(level=logging.INFO)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def discover_contact_structure():
|
def discover_contact_structure():
|
||||||
load_dotenv(dotenv_path="../.env")
|
load_dotenv(dotenv_path="/home/node/clawd/.env", override=True)
|
||||||
auth = AuthHandler()
|
client = SuperOfficeClient()
|
||||||
client = SuperOfficeClient(auth)
|
|
||||||
|
|
||||||
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)
|
contact = client._get("Contact/2")
|
||||||
# If not, we try to create a dummy and then inspect it.
|
if contact:
|
||||||
url = client._get_url("v1/Contact/1")
|
print("\n--- CONTACT STRUCTURE ---")
|
||||||
try:
|
print(json.dumps(contact, indent=2))
|
||||||
resp = client.session.get(url, headers=client._get_headers())
|
else:
|
||||||
if resp.status_code == 200:
|
logger.error("Failed to fetch contact.")
|
||||||
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}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
discover_contact_structure()
|
discover_contact_structure()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
import json
|
import json
|
||||||
from config import Config
|
from config import Config
|
||||||
from logging_config import setup_logging
|
from logging_config import setup_logging
|
||||||
@@ -7,18 +8,15 @@ from superoffice_client import SuperOfficeClient
|
|||||||
logger = setup_logging("inspector")
|
logger = setup_logging("inspector")
|
||||||
|
|
||||||
def inspect_person(person_id):
|
def inspect_person(person_id):
|
||||||
auth = AuthHandler()
|
load_dotenv(dotenv_path="/home/node/clawd/.env", override=True)
|
||||||
client = SuperOfficeClient(auth)
|
client = SuperOfficeClient()
|
||||||
logger.info(f"Fetching Person with ID {person_id} to inspect structure...")
|
logger.info(f"Fetching Person with ID {person_id} to inspect structure...")
|
||||||
url = client._get_url(f"v1/Person/{person_id}")
|
person_data = client._get(f"Person/{person_id}")
|
||||||
try:
|
if person_data:
|
||||||
resp = client.session.get(url, headers=client._get_headers())
|
|
||||||
resp.raise_for_status()
|
|
||||||
person_data = resp.json()
|
|
||||||
print(f"\n--- PERSON STRUCTURE (ID: {person_id}) ---")
|
print(f"\n--- PERSON STRUCTURE (ID: {person_id}) ---")
|
||||||
print(json.dumps(person_data, indent=2))
|
print(json.dumps(person_data, indent=2))
|
||||||
except Exception as e:
|
else:
|
||||||
logger.error(f"Failed to fetch person data: {e}")
|
logger.error(f"Failed to fetch person data.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
target_person_id = 9
|
target_person_id = 9
|
||||||
|
|||||||
68
round_trip_final.py
Normal file
68
round_trip_final.py
Normal file
@@ -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)
|
||||||
66
so_one_shot_fix.py
Normal file
66
so_one_shot_fix.py
Normal file
@@ -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()
|
||||||
44
so_surgical_update_v2.py
Normal file
44
so_surgical_update_v2.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user