[2ff88f42] 1. Analyse der Änderungen:

1. Analyse der Änderungen:
      * superoffice_client.py: Implementierung der Methoden create_contact (Standardfelder) und create_person (inkl. Firmenverknüpfung).
      * auth_handler.py: Härtung der Authentifizierung durch Priorisierung von SO_CLIENT_ID und Unterstützung für load_dotenv(override=True).
      * main.py: Erweiterung des Test-Workflows für den vollständigen Lese- und Schreib-Durchstich (Erstellung von Demo-Firmen und Personen).
      * README.md: Aktualisierung des Status Quo und der verfügbaren Client-Methoden.
This commit is contained in:
2026-02-09 19:00:18 +00:00
parent 395251dd9c
commit 2318bf322b
9 changed files with 248 additions and 43 deletions

View File

@@ -7,9 +7,11 @@ Der Connector nutzt die **REST API (v1)** und authentifiziert sich via **OAuth 2
Der **Proof of Concept (POC)** ist erfolgreich abgeschlossen.
- **Verbindung:** Erfolgreich hergestellt (`Cust55774`).
- **Authentifizierung:** Funktioniert automatisch via Refresh Token.
- **Datenabruf:** Stammdaten (Contact ID 1) können gelesen werden.
- **Daten schreiben:** Vorbereitet (Code ist fertig), erfordert jedoch noch das Anlegen der UDF-Felder im SuperOffice Admin-Panel.
- **Authentifizierung:** Funktioniert automatisch via Refresh Token (gehärtet via `load_dotenv(override=True)`).
- **Daten schreiben (Erfolg):**
- **Firmen (Contacts):** Erfolgreich angelegt (inkl. Name, URL, OrgNr).
- **Personen (Persons):** Erfolgreich angelegt und mit Firmen verknüpft (inkl. E-Mail).
- **UDF-Felder:** Vorbereitet im Code, erfordert finalen Abgleich der `ProgId` nach Anlage im Admin-Panel.
## 2. Einrichtung & Installation
@@ -42,14 +44,20 @@ SO_REFRESH_TOKEN="<Dein langlebiger Refresh Token>"
## 3. Nutzung
### Verbindungstest (Main Script)
Führt einen Login durch und sucht nach einer Test-Firma.
### Verbindungstest & Demo
Führt einen Login durch, sucht nach einer Test-Firma und legt bei Bedarf einen Demo-Account mit Ansprechpartner an.
```bash
cd connector-superoffice
../.venv/bin/python main.py
```
### Verfügbare Methoden (SuperOfficeClient)
- `find_contact_by_criteria(...)`: Suche nach Name, URL oder OrgNr.
- `create_contact(...)`: Erstellt eine neue Firma.
- `create_person(...)`: Erstellt einen Ansprechpartner (verknüpft mit ContactId).
- `update_udfs(...)`: (Vorbereitet) Aktualisiert KI-Felder.
### Felder entdecken
Listet alle verfügbaren Felder (inkl. UDFs) auf, um die technischen Namen (`ProgId`) für das Mapping zu finden.

View File

@@ -8,7 +8,14 @@ logger = logging.getLogger(__name__)
class AuthHandler:
def __init__(self):
# Load configuration from environment
self.client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
self.client_id = os.getenv("SO_CLIENT_ID")
if not self.client_id:
self.client_id = os.getenv("SO_SOD")
if self.client_id:
logger.info("Using SO_SOD as Client ID")
else:
logger.info("Using SO_CLIENT_ID as Client ID")
self.client_secret = os.getenv("SO_CLIENT_SECRET")
self.refresh_token = os.getenv("SO_REFRESH_TOKEN")
self.tenant_id = os.getenv("SO_CONTEXT_IDENTIFIER") # e.g., Cust55774

View File

@@ -0,0 +1,44 @@
import os
import requests
from dotenv import load_dotenv
# Lade Umgebungsvariablen, um ID und Refresh Token zu holen
load_dotenv(dotenv_path="../.env")
# Wir nutzen das Secret, das du mir gegeben hast, HARTCODIERT, um .env Fehler auszuschließen
HARDCODED_SECRET = "418c424681944ad4138788692dfd7ab2"
# ID und Refresh Token aus der .env (zur Kontrolle)
client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
refresh_token = os.getenv("SO_REFRESH_TOKEN")
print(f"--- DEBUG AUTHENTICATION ---")
print(f"Client ID (aus .env): {client_id}")
print(f"Refresh Token (aus .env): {refresh_token[:10]}...")
print(f"Client Secret (hartcodiert): {HARDCODED_SECRET[:5]}...")
url = "https://sod.superoffice.com/login/common/oauth/tokens"
# Payload für den Request
payload = {
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": HARDCODED_SECRET,
"refresh_token": refresh_token,
# Manche Server brauchen das hier auch beim Refresh:
"redirect_uri": "https://devnet-tools.superoffice.com/openid/callback"
}
print(f"\nSende Request an: {url}")
try:
resp = requests.post(url, data=payload)
print(f"Status Code: {resp.status_code}")
if resp.status_code == 200:
print("SUCCESS! Access Token erhalten.")
print(resp.json())
else:
print("FAILURE.")
print(resp.text)
except Exception as e:
print(f"Exception: {e}")

View File

@@ -0,0 +1,25 @@
import os
import requests
from dotenv import load_dotenv
load_dotenv() # Lädt .env aus dem aktuellen Verzeichnis
client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
client_secret = os.getenv("SO_CLIENT_SECRET")
refresh_token = os.getenv("SO_REFRESH_TOKEN")
print(f"ID: {client_id}")
print(f"Secret: {client_secret[:5]}...")
print(f"Token: {refresh_token[:5]}...")
url = "https://sod.superoffice.com/login/common/oauth/tokens"
payload = {
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token
}
resp = requests.post(url, data=payload)
print(f"Status: {resp.status_code}")
print(resp.text)

View File

@@ -0,0 +1,36 @@
import os
from dotenv import load_dotenv
import urllib.parse
def generate_url():
load_dotenv(dotenv_path="../.env")
client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
redirect_uri = "https://devnet-tools.superoffice.com/openid/callback" # Das muss im Portal so registriert sein
state = "12345"
if not client_id:
print("Fehler: Keine SO_CLIENT_ID in der .env gefunden!")
return
params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"response_type": "code",
"scope": "openid offline_access", # Wichtig für Refresh Token
"state": state
}
base_url = "https://sod.superoffice.com/login/common/oauth/authorize"
auth_url = f"{base_url}?{urllib.parse.urlencode(params)}"
print("\nBitte öffne diese URL im Browser:")
print("-" * 60)
print(auth_url)
print("-" * 60)
print("\nNach dem Login wirst du auf eine Seite weitergeleitet, die nicht lädt (localhost).")
print("Kopiere die URL aus der Adresszeile und gib mir den Wert nach '?code='.")
if __name__ == "__main__":
generate_url()

View File

@@ -13,7 +13,12 @@ logger = logging.getLogger(__name__)
def main():
# Load .env from the root directory
load_dotenv(dotenv_path="../.env")
# If running from /app, .env is in the same directory.
# If running from /app/connector-superoffice, it's in ../.env
if os.path.exists(".env"):
load_dotenv(".env", override=True)
elif os.path.exists("../.env"):
load_dotenv("../.env", override=True)
logger.info("Starting SuperOffice Connector S2S POC...")
@@ -33,24 +38,51 @@ def main():
logger.error("Connection test failed.")
return
# 2. Search for a test contact
test_company = "Wackler Holding"
logger.info(f"Step 2: Searching for company '{test_company}'...")
contact = client.find_contact_by_criteria(name=test_company)
# 2. Search for our demo company
demo_company_name = "Gemini Test Company [2ff88f42]"
logger.info(f"Step 2: Searching for company '{demo_company_name}'...")
contact = client.find_contact_by_criteria(name=demo_company_name)
target_contact_id = None
if contact:
logger.info(f"Found contact: {contact.get('Name')} (ID: {contact.get('ContactId')})")
# 3. Try to update UDFs (Warning: technical names might be wrong)
# logger.info("Step 3: Attempting UDF update (experimental)...")
# ai_test_data = {
# "potential": "High",
# "industry": "Facility Management",
# "summary": "KI-Analyse erfolgreich durchgeführt."
# }
# client.update_udfs(contact.get('ContactId'), ai_test_data)
target_contact_id = contact.get('ContactId')
logger.info(f"Found existing demo company: {contact.get('Name')} (ID: {target_contact_id})")
else:
logger.info(f"Contact '{test_company}' not found.")
logger.info(f"Demo company not found. Creating new one...")
demo_company_url = "https://www.gemini-test-company.com"
demo_company_orgnr = "DE123456789"
new_contact = client.create_contact(
name=demo_company_name,
url=demo_company_url,
org_nr=demo_company_orgnr
)
if new_contact:
target_contact_id = new_contact.get('ContactId')
logger.info(f"Created new demo company with ID: {target_contact_id}")
# 3. Create a Person linked to this company
if target_contact_id:
logger.info(f"Step 3: Creating Person for Contact ID {target_contact_id}...")
# Create Max Mustermann
person = client.create_person(
first_name="Max",
last_name="Mustermann",
contact_id=target_contact_id,
email="max.mustermann@gemini-test.com"
)
if person:
logger.info("SUCCESS: Person created!")
logger.info(f"Name: {person.get('Firstname')} {person.get('Lastname')}")
logger.info(f"Person ID: {person.get('PersonId')}")
logger.info(f"Linked to Contact ID: {person.get('Contact').get('ContactId')}")
else:
logger.error("Failed to create person.")
else:
logger.error("Skipping person creation because company could not be found or created.")
except Exception as e:
logger.error(f"An error occurred: {e}", exc_info=True)

View File

@@ -89,3 +89,63 @@ class SuperOfficeClient:
except Exception as e:
logger.error(f"Error searching for contact: {e}")
return None
def create_contact(self, name, url=None, org_nr=None):
"""Creates a new contact (company) in SuperOffice with basic details."""
url = self._get_url("v1/Contact")
payload = {
"Name": name,
"OrgNr": org_nr,
"UrlAddress": url,
"ActivePublications": [], # Required field, can be empty
"Emails": [], # Required field, can be empty
"Phones": [] # Required field, can be empty
}
# Remove None values
payload = {k: v for k, v in payload.items() if v is not None}
try:
logger.info(f"Attempting to create contact: {name}")
resp = self.session.post(url, headers=self._get_headers(), json=payload)
resp.raise_for_status()
created_contact = resp.json()
logger.info(f"Successfully created contact: {created_contact.get('Name')} (ID: {created_contact.get('ContactId')})")
return created_contact
except Exception as e:
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
def create_person(self, first_name, last_name, contact_id, email=None):
"""Creates a new person linked to a contact (company)."""
url = self._get_url("v1/Person")
payload = {
"Firstname": first_name,
"Lastname": last_name,
"Contact": {
"ContactId": contact_id
},
"Emails": []
}
if email:
payload["Emails"].append({
"Value": email,
"Rank": 1,
"Description": "Work" # Optional description
})
try:
logger.info(f"Attempting to create person: {first_name} {last_name} for Contact ID {contact_id}")
resp = self.session.post(url, headers=self._get_headers(), json=payload)
resp.raise_for_status()
created_person = resp.json()
logger.info(f"Successfully created person: {created_person.get('Firstname')} {created_person.get('Lastname')} (ID: {created_person.get('PersonId')})")
return created_person
except Exception as e:
logger.error(f"Error creating person: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None