[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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
44
connector-superoffice/debug_auth_manual.py
Normal file
44
connector-superoffice/debug_auth_manual.py
Normal 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}")
|
||||
25
connector-superoffice/final_env_test.py
Normal file
25
connector-superoffice/final_env_test.py
Normal 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)
|
||||
36
connector-superoffice/generate_auth_url.py
Normal file
36
connector-superoffice/generate_auth_url.py
Normal 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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user