[31588f42] feat: implement SuperOffice Sales API prototype and fix auth docs

This commit is contained in:
2026-02-28 18:06:12 +00:00
parent 4eccb55eab
commit 30c5172ed0
3 changed files with 214 additions and 0 deletions

View File

@@ -249,6 +249,29 @@ The full list of 25 verticals with their internal SuperOffice IDs (List `udlist3
### 4. Campaign Trigger Logic
The `worker.py` (v1.8) now extracts the `campaign_tag` from `SuperOffice:23:DisplayText`. This tag is passed to the Company Explorer's provisioning API. If a matching entry exists in the `MarketingMatrix` for that tag, specific texts are used; otherwise, it falls back to the "standard" Kaltakquise texts.
### 5. SuperOffice Authentication (Critical Update Feb 28, 2026)
**Problem:** Authentication failures ("Invalid refresh token" or "Invalid client_id") occurred because standard `load_dotenv()` did not override stale environment variables present in the shell process.
**Solution:** Always use `load_dotenv(override=True)` in Python scripts to force loading the actual values from the `.env` file.
**Correct Authentication Pattern (Python):**
```python
from dotenv import load_dotenv
import os
# CRITICAL: override=True ensures we read from .env even if env vars are already set
load_dotenv(override=True)
client_id = os.getenv("SO_CLIENT_ID")
# ...
```
**Known Working Config (Production):**
* **Environment:** `online3`
* **Tenant:** `Cust26720`
* **Token Logic:** The `AuthHandler` implementation in `health_check_so.py` is the reference standard. Avoid using legacy `superoffice_client.py` without verifying it uses `override=True`.
---
Here are the available functions:
[

View File

@@ -273,6 +273,29 @@ python3 connector-superoffice/create_email_test.py 193036
* `POST /Document`: Creates the email body and metadata.
* `POST /Appointment`: Creates the activity record linked to the document.
### 17. Sales & Opportunities Implementation (Feb 28, 2026)
We have successfully prototyped the creation of "Sales" (Opportunities) via the API. This allows the AI to not only enrich contacts but also create tangible pipeline objects for sales representatives.
**Prototype Script:** `connector-superoffice/create_sale_test.py`
**Key Findings:**
1. **Authentication:** Must use `load_dotenv(override=True)` to ensure production credentials are used (see GEMINI.md).
2. **Required Fields for `POST /Sale`:**
* `Heading`: The title of the opportunity.
* `Amount` & `Currency`: Use `Currency: { "Id": 33 }` (for EUR) instead of string "EUR".
* `SaleType`: `{ "Id": 1 }` (General Sale / Allgemeiner Verkauf).
* `Stage`: `{ "Id": 10 }` (5% Prospect / Angebot prospektiv).
* `Source`: `{ "Id": 2 }` (Proposal Center/Lead Source).
* `Contact`: `{ "ContactId": 123 }`.
* `Person`: `{ "PersonId": 456 }` (Optional but recommended).
**Usage:**
```bash
python3 connector-superoffice/create_sale_test.py
```
This script finds the first available contact in the CRM and creates a test opportunity for them.
---
This is the core logic used to generate the company-specific opener.

View File

@@ -0,0 +1,168 @@
import os
import sys
import json
import logging
import requests
from dotenv import load_dotenv
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("create_sale_test")
# --- Inline AuthHandler & SuperOfficeClient (Proven Working Logic) ---
class AuthHandler:
def __init__(self):
# CRITICAL: override=True ensures we read from .env even if env vars are already set
load_dotenv(override=True)
self.client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
self.client_secret = os.getenv("SO_CLIENT_SECRET")
self.refresh_token = os.getenv("SO_REFRESH_TOKEN")
self.redirect_uri = os.getenv("SO_REDIRECT_URI", "http://localhost")
self.env = os.getenv("SO_ENVIRONMENT", "sod")
self.cust_id = os.getenv("SO_CONTEXT_IDENTIFIER", "Cust55774")
if not all([self.client_id, self.client_secret, self.refresh_token]):
raise ValueError("SuperOffice credentials missing in .env file.")
def get_access_token(self):
return self._refresh_access_token()
def _refresh_access_token(self):
token_domain = "online.superoffice.com" if "online" in self.env.lower() else "sod.superoffice.com"
url = f"https://{token_domain}/login/common/oauth/tokens"
data = {
"grant_type": "refresh_token",
"client_id": self.client_id,
"client_secret": self.client_secret,
"refresh_token": self.refresh_token,
"redirect_uri": self.redirect_uri
}
try:
resp = requests.post(url, data=data)
if resp.status_code != 200:
logger.error(f"❌ Token Refresh Failed (Status {resp.status_code}): {resp.text}")
return None
return resp.json().get("access_token")
except Exception as e:
logger.error(f"❌ Connection Error during token refresh: {e}")
return None
class SuperOfficeClient:
def __init__(self, auth_handler):
self.auth_handler = auth_handler
self.env = auth_handler.env
self.cust_id = auth_handler.cust_id
self.base_url = f"https://{self.env}.superoffice.com/{self.cust_id}/api/v1"
self.access_token = self.auth_handler.get_access_token()
if not self.access_token:
raise Exception("Failed to obtain access token.")
self.headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
def _get(self, endpoint):
resp = requests.get(f"{self.base_url}/{endpoint}", headers=self.headers)
resp.raise_for_status()
return resp.json()
def _post(self, endpoint, payload):
resp = requests.post(f"{self.base_url}/{endpoint}", headers=self.headers, json=payload)
resp.raise_for_status()
return resp.json()
def main():
try:
# Initialize Auth and Client
auth = AuthHandler()
client = SuperOfficeClient(auth)
print("\n--- 0. Pre-Flight: Finding Currency ID for EUR ---")
currencies = client._get("List/Currency/Items")
eur_id = None
for curr in currencies:
if curr.get('Name') == 'EUR' or curr.get('value') == 'EUR': # Check keys carefully
eur_id = curr.get('Id')
print(f"✅ Found EUR Currency ID: {eur_id}")
break
if not eur_id:
print("⚠️ EUR Currency not found. Defaulting to ID 33 (from discovery).")
eur_id = 33
print("\n--- 1. Finding a Target Contact (Company) ---")
contacts = client._get("Contact?$top=1&$select=ContactId,Name")
if not contacts or 'value' not in contacts or len(contacts['value']) == 0:
logger.error("No contacts found in CRM. Cannot create sale.")
return
target_contact = contacts['value'][0]
# API returns lowercase keys in this OData format
contact_id = target_contact.get('contactId') or target_contact.get('ContactId')
contact_name = target_contact.get('name') or target_contact.get('Name')
print(f"✅ Found Target Contact: {contact_name} (ID: {contact_id})")
print("\n--- 2. Finding a Person (Optional but recommended) ---")
persons_endpoint = f"Contact/{contact_id}/Persons?$top=1&$select=PersonId,FirstName,LastName"
persons = client._get(persons_endpoint)
person_id = None
if persons and 'value' in persons and len(persons['value']) > 0:
target_person = persons['value'][0]
# Fix keys here too
person_id = target_person.get('personId') or target_person.get('PersonId')
first_name = target_person.get('firstName') or target_person.get('FirstName')
last_name = target_person.get('lastName') or target_person.get('LastName')
print(f"✅ Found Target Person: {first_name} {last_name} (ID: {person_id})")
else:
print("⚠️ No person found for this contact. Creating sale without person link.")
print("\n--- 3. Creating Sale (Opportunity) ---")
# Construct the payload for a new Sale
sale_payload = {
"Heading": "AI Prospect: Automation Potential Detected",
"Description": "Automated opportunity created by Gemini AI based on high automation potential score.\n\nKey Insights:\n- High robot density potential\n- Manual processes identified",
"Amount": 5000.0,
# FIXED: Use Object with ID for Currency
"Currency": { "Id": eur_id },
"SaleType": { "Id": 1 }, # Allgemeiner Verkauf
"Stage": { "Id": 10 }, # 5% Angebot prospektiv
"Contact": { "ContactId": contact_id },
"Source": { "Id": 2 } # Proposal Center
}
if person_id:
sale_payload["Person"] = { "PersonId": person_id }
print(f"Payload Preview: {json.dumps(sale_payload, indent=2)}")
# Send POST request to create the Sale
new_sale = client._post("Sale", sale_payload)
print("\n--- ✅ SUCCESS: Sale Created! ---")
sale_id = new_sale.get('SaleId')
sale_number = new_sale.get('SaleNumber')
print(f"Sale ID: {sale_id}")
print(f"Sale Number: {sale_number}")
sale_link = f"https://{auth.env}.superoffice.com/{auth.cust_id}/default.aspx?sale_id={sale_id}"
print(f"Direct Link: {sale_link}")
except requests.exceptions.HTTPError as e:
logger.error(f"❌ API Error: {e}")
if e.response is not None:
logger.error(f"Response Body: {e.response.text}")
except Exception as e:
logger.error(f"Fatal Error: {e}")
if __name__ == "__main__":
main()