Files
Brancheneinstufung2/connector-superoffice/superoffice_client.py
Floke 538e42b5ea [2ff88f42] feat(connector-superoffice): Implement Sale and Project entities, refine workflow
This commit extends the SuperOffice connector to support the creation and linking of Sale \(Opportunity\) and Project \(Campaign\) entities, providing a comprehensive foundation for both sales and marketing automation workflows.

Key achievements:
- **`SUPEROFFICE_INTEGRATION_PLAN.md`**: Updated to include strategic mapping of D365 concepts \(Opportunity, Campaign\) to SuperOffice entities \(Sale, Project\).
- **`connector-superoffice/superoffice_client.py`**:
    - Implemented `create_sale` method to generate new opportunities, correctly mapping `Title` to SuperOffices
2026-02-10 07:57:11 +00:00

223 lines
8.1 KiB
Python

import requests
import logging
from auth_handler import AuthHandler
logger = logging.getLogger(__name__)
class SuperOfficeClient:
"""
A client for interacting with the SuperOffice REST API using OAuth 2.0 Bearer tokens.
"""
def __init__(self, auth_handler: AuthHandler):
self.auth_handler = auth_handler
self.session = requests.Session()
# Mapping for UDF fields (These are typical technical names, but might need adjustment)
self.udf_mapping = {
"robotics_potential": "x_robotics_potential",
"industry": "x_ai_industry",
"summary": "x_ai_summary",
"last_update": "x_ai_last_update",
"status": "x_ai_status"
}
# Mapping for list values (Explorer -> SO ID)
self.potential_id_map = {
"High": 1,
"Medium": 2,
"Low": 3,
"None": 4
}
def _get_headers(self):
"""Returns the authorization headers with Bearer token."""
access_token, _ = self.auth_handler.get_ticket()
return {
'Authorization': f'Bearer {access_token}',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def _get_url(self, path):
"""Constructs the full URL for a given API path."""
_, webapi_url = self.auth_handler.get_ticket()
base = webapi_url.rstrip('/')
p = path.lstrip('/')
return f"{base}/api/{p}"
def test_connection(self):
"""Tests the connection by fetching the current user."""
url = self._get_url("v1/User/currentPrincipal")
try:
resp = self.session.get(url, headers=self._get_headers())
resp.raise_for_status()
return resp.json()
except Exception as e:
logger.error(f"Connection test failed: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
def find_contact_by_criteria(self, name=None, url=None, org_nr=None):
"""Searches for a contact by OrgNr, URL, or Name."""
filters = []
if org_nr:
filters.append(f"orgNr eq '{org_nr}'")
if url:
filters.append(f"urlAddress eq '{url}'")
if not filters and name:
filters.append(f"name contains '{name}'")
if not filters:
return None
query = " and ".join(filters)
path = f"v1/Contact?$filter={query}"
try:
full_url = self._get_url(path)
resp = self.session.get(full_url, headers=self._get_headers())
resp.raise_for_status()
data = resp.json()
results = data.get("value", [])
if results:
logger.info(f"Found {len(results)} matching contacts.")
return results[0]
return None
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:
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
def create_sale(self, title, contact_id, person_id=None, amount=0.0):
"""Creates a new Sale (Opportunity) linked to a contact and optionally a person."""
url = self._get_url("v1/Sale")
payload = {
"Heading": title,
"Contact": {
"ContactId": contact_id
},
"Amount": amount,
"SaleType": { # Assuming default ID 1 exists
"Id": 1
},
"SaleStage": { # Assuming default ID for the first stage is 1
"Id": 1
},
"Probability": 10 # Default probability
}
if person_id:
payload["Person"] = {
"PersonId": person_id
}
try:
logger.info(f"Attempting to create sale: '{title}' for Contact ID {contact_id}")
resp = self.session.post(url, headers=self._get_headers(), json=payload)
resp.raise_for_status()
created_sale = resp.json()
logger.info(f"Successfully created sale: {created_sale.get('Heading')} (ID: {created_sale.get('SaleId')})")
return created_sale
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_project(self, name, contact_id, person_id=None):
"""Creates a new Project linked to a contact and optionally adds a person as a member."""
url = self._get_url("v1/Project")
payload = {
"Name": name,
"Contact": {
"ContactId": contact_id
},
"ProjectType": { # Assuming default ID 1 exists
"Id": 1
},
"ProjectStatus": { # Assuming default ID 1 for 'In progress' exists
"Id": 1
},
"ProjectMembers": []
}
if person_id:
payload["ProjectMembers"].append({
"PersonId": person_id
})
try:
logger.info(f"Attempting to create project: '{name}' for Contact ID {contact_id}")
resp = self.session.post(url, headers=self._get_headers(), json=payload)
resp.raise_for_status()
created_project = resp.json()
logger.info(f"Successfully created project: {created_project.get('Name')} (ID: {created_project.get('ProjectId')})")
return created_project
except Exception as e:
logger.error(f"Error creating project: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None