5 Commits

14 changed files with 559 additions and 75 deletions

View File

@@ -1 +1 @@
{"task_id": "31088f42-8544-8017-96da-fa75bb6d8121", "token": "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", "readme_path": null, "session_start_time": "2026-02-27T15:09:49.228277"} {"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "readme_path": "connector-superoffice/README.md", "session_start_time": "2026-02-28T14:25:23.133066"}

View File

@@ -156,11 +156,13 @@ Since the "Golden Record" for Industry Verticals (Pains, Gains, Products) reside
- **Purpose:** Lists all property keys and page titles. Use this to debug schema changes (e.g. if a column was renamed). - **Purpose:** Lists all property keys and page titles. Use this to debug schema changes (e.g. if a column was renamed).
- **Usage:** `python3 list_notion_structure.py` - **Usage:** `python3 list_notion_structure.py`
## Next Steps ## Next Steps (Updated Feb 27, 2026)
* **Marketing Automation:** Implement the actual sending logic (or export) based on the contact status. * **Notion Content:** Finalize "Pains" and "Gains" for all 25 verticals in the Notion master database.
* **Job Role Mapping Engine:** Connect the configured patterns to the contact import/creation process to auto-assign roles. * **Intelligence:** Run `generate_matrix.py` in the Company Explorer backend to populate the matrix for all new English vertical names.
* **Industry Classification Engine:** Connect the configured industries to the AI Analysis prompt to enforce the "Strict Mode" mapping. * **Automation:** Register the production webhook (requires `admin-webhooks` rights) to enable real-time CRM sync without manual job injection.
* **Export:** Generate Excel/CSV enriched reports (already partially implemented via JSON export). * **Execution:** Connect the "Sending Engine" (the actual email dispatch logic) to the SuperOffice fields.
* **Monitoring:** Monitor the 'Atomic PATCH' logs in production for any 400 errors regarding field length or specific character sets.
## Company Explorer Access & Debugging ## Company Explorer Access & Debugging

View File

@@ -412,7 +412,17 @@ Der Company Explorer unterstützt nun den Parameter `campaign_tag`. Der Connecto
--- ---
## 18. Next Steps & Todos (Post-Migration) ## 18. Offene Arbeitspakete (Stand: 27.02.2026)
* **Task 1:** Monitoring & Alerting (Dashboard Ausbau).
* **Task 2:** Robust Address Parsing (Google Maps API). ### Prio A: Operative Automatisierung
* **Task 3:** "Person-First" Logic (Reverse Lookup via Domain). * **Webhook-Aktivierung:** Registrierung des Live-Webhooks für `online3`, sobald Admin-Rechte für den API-User vorliegen (`register_webhook.py`).
* **Full Matrix Generation:** Ausführung der KI-Generierung für alle 25 Verticals (englische IDs), sobald die "Pains" in Notion finalisiert wurden.
* **Campaign-Validation:** Erstellung von Test-Szenarien für mindestens 3 verschiedene Kampagnen-Tags zur Verifizierung der Weichenstellung.
### Prio B: Marketing-Execution
* **Sending Logic:** Implementierung der Logik für den tatsächlichen E-Mail-Versand (oder Export zu einem E-Mail-Provider) basierend auf den befüllten UDFs.
* **Unsubscribe-Frontend:** Visuelle Gestaltung der HTML-Bestätigungsseite für den Unsubscribe-Link.
### Prio C: Daten-Optimierung
* **Google Maps API:** Einbindung zur Validierung von Firmenadressen bei Diskrepanzen zwischen CRM und Scraper.
* **Deduplication 2.0:** Verfeinerung des Matchings bei Firmen mit mehreren Standorten (Filial-Logik).

View File

@@ -0,0 +1,25 @@
# Status Report: Email Sending Workaround (Feb 28, 2026)
## Problem
The automated dispatch of emails via the SuperOffice API (using `/Shipment` or `/Mailing` endpoints) is currently blocked by a **500 Internal Server Error** in the `Cust26720` tenant environment. Additionally, created Documents often throw a "Cannot check out" error when users try to open them directly, likely due to missing Web Tools or strict SharePoint integration policies for API-generated files.
## Solution / Workaround
We have implemented a robust "Activity-Based" workaround that ensures the email content is visible and actionable for the user.
1. **Draft Creation:** The system creates a Document (Template: "Ausg. E-Mail") via API.
2. **Content Upload:** The email body is uploaded as a binary stream to prevent "0kb file" errors.
3. **Activity Mirroring:** Crucially, a linked **Appointment (Task)** is created. The full email body is copied into the `Description` field of this appointment.
4. **Direct Access:** The user is provided with a direct link to the **Appointment**, bypassing the problematic Document checkout process.
## Verification
* **Target:** Person ID 193036 (Christian Test2 / floke.com@gmail.com)
* **Document ID:** 334055 (Content uploaded successfully)
* **Activity ID:** 992236 (Contains full text)
* **Result:** The user can open the Activity link, copy the pre-generated text, and send it via their standard mail client or SuperOffice MailLink.
## Usage
Run the test script to generate a new draft for any person:
```bash
python3 connector-superoffice/create_email_test.py <PersonID>
```
The script outputs the "Safe Link" to the Activity.

View File

@@ -253,32 +253,27 @@ Das System unterstützt mehrere Outreach-Varianten über das Feld **`MA_Campaign
2. **Spezifisch:** Wird ein Wert gewählt (z.B. "Messe 2026"), sucht der Connector gezielt nach Matrix-Einträgen mit diesem Tag. 2. **Spezifisch:** Wird ein Wert gewählt (z.B. "Messe 2026"), sucht der Connector gezielt nach Matrix-Einträgen mit diesem Tag.
3. **Fallback:** Existiert für die gewählte Kampagne kein spezifischer Text für das Vertical/Persona, wird automatisch auf "standard" zurückgegriffen. 3. **Fallback:** Existiert für die gewählte Kampagne kein spezifischer Text für das Vertical/Persona, wird automatisch auf "standard" zurückgegriffen.
### 15. Advanced Implementation Details (v1.8) ### 16. Email Sending Implementation (Feb 28, 2026)
Mit der Version 1.8 des Workers wurden kritische Optimierungen für den produktiven Betrieb (online3) implementiert, um API-Stabilität und Datenintegrität zu gewährleisten. A dedicated script `create_email_test.py` has been implemented to create "Email Documents" directly in SuperOffice via the API. This bypasses the need for an external SMTP server by utilizing SuperOffice's internal document system.
#### A. Atomic PATCH Strategy **Features:**
Um "Race Conditions" und unnötigen API-Traffic zu vermeiden, bündelt der Worker alle Änderungen an einem Kontakt-Objekt in einem einzigen **Atomic PATCH**. * **Document Creation:** Creates a document of type "Ausg. E-Mail" (Template ID 157).
* **Betroffene Felder:** `Address` (Postal & Street), `OrgNr` (VAT), `Urls` (Website) und alle `UserDefinedFields`. * **Activity Tracking:** Automatically creates a linked "Appointment" (Task ID 6 - Document Out) to ensure the email appears in the contact's activity timeline.
* **Vorteil:** Entweder alle Daten werden konsistent übernommen, oder der Call schlägt kontrolliert fehl. Dies verhindert, dass Teil-Updates (z.B. nur die Adresse) von nachfolgenden UDF-Updates überschrieben werden. * **Direct Link:** Outputs a direct URL to open the created document in SuperOffice Online.
#### B. REST Website-Sync (The `Urls` Array) **Usage:**
SuperOffice REST akzeptiert kein direktes Update auf `UrlAddress` via PATCH. Stattdessen muss das `Urls` Array manipuliert werden. ```bash
* **Logik:** Der Worker prüft, ob die KI-entdeckte Website bereits im Array vorhanden ist. Wenn nicht, wird sie als neues Objekt mit der Beschreibung `"AI Discovered"` an den Anfang der Liste gestellt. python3 connector-superoffice/create_email_test.py <PersonID>
* **Format:** `"Urls": [{"Value": "https://...", "Description": "AI Discovered"}]`. # Example:
python3 connector-superoffice/create_email_test.py 193036
```
#### C. Kampagnen-Auflösung via `:DisplayText` **Key API Endpoints Used:**
Um den Klarnamen einer Kampagne (z.B. "Messe 2026") statt der internen ID (z.B. `[I:123]`) zu erhalten, nutzt der Worker eine OData-Optimierung. * `POST /Document`: Creates the email body and metadata.
* **Technik:** Im `$select` Parameter wird das Feld `SuperOffice:23:DisplayText` angefordert. * `POST /Appointment`: Creates the activity record linked to the document.
* **Ergebnis:** Der Worker erhält direkt den sauberen String, der zur Steuerung der Textvarianten im Company Explorer dient. Zusätzliche API-Abfragen zur Listenauflösung entfallen.
#### D. Feldlängen & Truncation
Standard-UDF-Textfelder in SuperOffice sind oft auf **254 Zeichen** begrenzt. Da das AI-Dossier (Summary) deutlich länger sein kann, kürzt der Worker den Text hart auf **132 Zeichen** (+ "..."). Dies stellt sicher, dass der gesamte `PATCH` Request nicht aufgrund eines "Field Overflow" von der SuperOffice-Validierung abgelehnt wird.
--- ---
## Appendix: The "First Sentence" Prompt
This is the core logic used to generate the company-specific opener. This is the core logic used to generate the company-specific opener.
**Goal:** Prove understanding of the business model + imply the pain (positive observation). **Goal:** Prove understanding of the business model + imply the pain (positive observation).

View File

@@ -0,0 +1,37 @@
# SuperOffice Email Sending Strategy Analysis (Feb 28, 2026)
## Executive Summary
Automated email sending "on behalf of" sales representatives directly via the SuperOffice API is currently **technically blocked** due to missing permissions and license restrictions on the configured System User (Client ID `0fd8...`).
We have exhausted all standard API paths (Agents, REST, Archive, CRMScript). A strategic reconfiguration by the SuperOffice administrator is required.
## Technical Findings
| Feature | Status | Error Code | Root Cause Analysis |
| :--- | :--- | :--- | :--- |
| **Document Creation** | ✅ Working | 200 OK | We can create `.somail` files in the archive. |
| **Native Sending** (`/Shipment`) | ❌ Failed | 500 Internal | The System User lacks a valid `Associate` context or "Mailing" license. |
| **Agent Sending** (`/Agents/EMail`) | ❌ Failed | 401 Unauth | The standard OAuth token is rejected for this Agent; likely requires "interactive" user context or specific scope. |
| **CRMScripting** | ❌ Failed | 403 Forbidden | Access to the Scripting Engine is blocked for this API user. |
| **User Context** (`/Associate/Me`) | ❌ Failed | 500 Internal | **Critical:** The System User does not know "who it is". This breaks all "Send As" logic. |
## Required Actions (IT / Admin)
To enable automated sending, one of the following two paths must be implemented:
### Option A: Enable Native SuperOffice Sending (Preferred)
1. **Fix System User:** The API User must be linked to a valid "Person" card in SuperOffice Admin with **Service / Marketing Administrator** rights.
2. **Enable Mailings:** The tenant `Cust26720` must have the "Marketing" license active and assigned to the API User.
3. **Approve "Send As":** The API User needs explicit permission to set the `SenderEmailAddress` field in Shipments.
### Option B: External Sending Engine (Recommended Fallback)
If Option A is too complex or costly (licensing), we switch the architecture:
1. **SMTP Relay:** Provision a dedicated SMTP account (e.g., Office365 Service Account or SendGrid) for the "RoboPlanet GTM Engine".
2. **Logic Shift:** The Python Connector sends the email via SMTP (Python `smtplib`).
3. **Archiving:** The Connector saves the *sent* email as a `.eml` document in SuperOffice (which already works!).
## Immediate Workaround
Until a decision is made, the system uses the **"Activity Handoff"** method:
1. System generates the text.
2. System creates a Task (Appointment) in SuperOffice.
3. User clicks a link, copies the text, and sends via their own Outlook/Gmail.

View File

@@ -0,0 +1,38 @@
import os
import json
import logging
import sys
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
logging.basicConfig(level=logging.INFO)
sys.stdout.reconfigure(line_buffering=True)
def attempt_send(to_email: str):
client = SuperOfficeClient()
# Payload for Agents/EMail/Send
# It expects an array of "EMail" objects
payload = [
{
"To": [{"Value": to_email, "Address": to_email}],
"Subject": "Test from SuperOffice Agent API",
"HTMLBody": "<h1>Hello!</h1><p>This is a test from the Agents/EMail/Send endpoint.</p>",
"From": {"Value": "system@roboplanet.de", "Address": "system@roboplanet.de"} # Try to force a sender
}
]
print(f"🚀 Attempting POST /Agents/EMail/Send to {to_email}...")
try:
# Note: The endpoint might be v1/Agents/EMail/Send
res = client._post("Agents/EMail/Send", payload)
if res:
print("✅ Success! Response:", json.dumps(res, indent=2))
else:
print("❌ Request failed (None returned).")
except Exception as e:
print(f"❌ Exception during send: {e}")
if __name__ == "__main__":
attempt_send("floke.com@gmail.com")

View File

@@ -0,0 +1,36 @@
import os
import json
import logging
import sys
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
logging.basicConfig(level=logging.INFO)
sys.stdout.reconfigure(line_buffering=True)
def check_crmscript():
client = SuperOfficeClient()
print(f"🚀 Checking CRMScript capability...")
# 1. Check if we can list scripts
try:
# Agents/CRMScript/GetCRMScripts
res = client._post("Agents/CRMScript/GetCRMScripts", payload={"CRMScriptIds": []}) # Empty array usually gets all or error
if res:
print(f"✅ Can access CRMScripts. Response type: {type(res)}")
else:
# Try GET Archive
print("⚠️ Agent access failed/empty. Trying Archive...")
res = client._get("Archive/dynamic?$select=all&$top=1&entity=crmscript")
if res:
print(f"✅ CRMScript Entity found in Archive.")
else:
print(f"❌ CRMScript Entity NOT found in Archive.")
except Exception as e:
print(f"❌ Exception checking CRMScript: {e}")
if __name__ == "__main__":
check_crmscript()

View File

@@ -0,0 +1,173 @@
import os
import requests
import json
import logging
import argparse
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
from config import settings
# Setup Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("create-email-test")
def create_email_document(person_id_input: int):
print(f"🚀 Creating Email Document for Person ID {person_id_input}...")
client = SuperOfficeClient()
if not client.access_token:
print("❌ Auth failed. Check .env")
return
# --- TARGET PERSON ---
target_person_id = person_id_input
contact_id = None
person_id = None
print(f"📡 Fetching target Person {target_person_id}...")
try:
person = client._get(f"Person/{target_person_id}")
if not person:
print(f"❌ Person {target_person_id} not found.")
return
print(f"✅ Found Person: {person.get('Firstname')} {person.get('Lastname')}")
# Get associated Contact ID
contact_id = person.get('Contact', {}).get('ContactId')
if not contact_id:
print("❌ Person has no associated company (ContactId).")
return
# Verify Contact
contact = client._get(f"Contact/{contact_id}")
if contact:
print(f"✅ Associated Company: {contact.get('Name')} (ID: {contact_id})")
person_id = target_person_id
except Exception as e:
print(f"❌ Error fetching Person/Contact: {e}")
return
if not contact_id or not person_id:
print("❌ Could not resolve Contact/Person IDs.")
return
# 2. Define Email Content
# Get Email Address from Person
email_address = person.get("Emails", [{}])[0].get("Value", "k.A.")
subject = f"Optimierung Ihrer Service-Prozesse (Referenz: {person.get('Firstname')} {person.get('Lastname')})"
# We use the UDFs we already found in Person 193036
udefs = person.get("UserDefinedFields", {})
intro = udefs.get(settings.UDF_INTRO, "Guten Tag,")
proof = udefs.get(settings.UDF_SOCIAL_PROOF, "Wir unterstützen Unternehmen bei der Automatisierung.")
unsub = udefs.get(settings.UDF_UNSUBSCRIBE_LINK, "")
body = f"""{intro}
{proof}
Abmelden: {unsub}
Viele Grüße,
Christian Godelmann
RoboPlanet"""
# 3. Create Document Payload
template_id = 157
payload = {
"Name": f"Outreach: {email_address}", # Internal Name with Email for visibility
"Header": subject, # Subject Line
"Contact": {"ContactId": contact_id},
"Person": {"PersonId": person_id},
"DocumentTemplate": {"DocumentTemplateId": template_id},
"Content": body
}
print(f"📤 Creating E-Mail draft for {email_address}...")
try:
doc = client._post("Document", payload)
if doc:
doc_id = doc.get('DocumentId')
print(f"✅ Document Created Successfully!")
print(f" ID: {doc_id}")
print(f" Recipient: {email_address}")
print(f" Template: {doc.get('DocumentTemplate', {}).get('Name')}")
# 3b. Upload Content (Critical Step to avoid 'Checkout Error')
print(f"📤 Uploading content stream to Document {doc_id}...")
try:
content_bytes = body.encode('utf-8')
# Manual request because _request_with_retry assumes JSON
headers = client.headers.copy()
headers["Content-Type"] = "application/octet-stream"
res = requests.put(
f"{client.base_url}/Document/{doc_id}/Content",
data=content_bytes,
headers=headers
)
if res.status_code in [200, 204]:
print("✅ Content uploaded successfully.")
else:
print(f"⚠️ Content upload failed: {res.status_code} {res.text}")
except Exception as e:
print(f"⚠️ Content upload error: {e}")
# Construct direct link
env = settings.SO_ENVIRONMENT
cust_id = settings.SO_CONTEXT_IDENTIFIER
doc_link = f"https://{env}.superoffice.com/{cust_id}/default.aspx?document_id={doc_id}"
# 4. Create Linked Appointment (Activity)
print("📅 Creating Linked Appointment (Email Sent Activity)...")
appt_payload = {
"Description": body,
"Contact": {"ContactId": contact_id},
"Person": {"PersonId": person_id},
"Task": {"Id": 6}, # 6 = Document / Email Out
"Document": {"DocumentId": doc_id},
"MainHeader": f"E-Mail: {subject}"[:40]
}
try:
appt = client._post("Appointment", appt_payload)
if appt:
appt_id = appt.get('AppointmentId')
print(f"✅ Appointment Created: {appt_id}")
appt_link = f"https://{env}.superoffice.com/{cust_id}/default.aspx?appointment_id={appt_id}"
print(f"\n--- WICHTIG: NUTZEN SIE DIESEN LINK ---")
print(f"Da das Dokument selbst ('Cannot check out') oft blockiert,")
print(f"öffnen Sie bitte die AKTIVITÄT. Dort steht der Text im Beschreibungsfeld:")
print(f"🔗 {appt_link}")
print(f"---------------------------------------\n")
print(f"(Backup Link zum Dokument: {doc_link})")
else:
print("⚠️ Failed to create appointment (None response).")
except Exception as e:
print(f"⚠️ Failed to create appointment: {e}")
else:
print("❌ Failed to create document (Response was empty/None).")
except Exception as e:
print(f"❌ Error creating document: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Create a test email document in SuperOffice.')
parser.add_argument('person_id', type=int, help='The SuperOffice Person ID to attach the email to.')
args = parser.parse_args()
create_email_document(args.person_id)

View File

@@ -0,0 +1,71 @@
import os
import requests
import json
import logging
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
from config import settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mailing-test")
def create_mailing(person_id: int):
client = SuperOfficeClient()
if not client.access_token:
print("❌ Auth failed.")
return
# 1. Get Person & Marketing Texts
person = client._get(f"Person/{person_id}")
if not person:
print(f"❌ Person {person_id} not found.")
return
email_address = person.get("Emails", [{}])[0].get("Value")
if not email_address:
print(f"❌ Person {person_id} has no email address.")
return
udefs = person.get("UserDefinedFields", {})
subject = udefs.get(settings.UDF_SUBJECT)
intro = udefs.get(settings.UDF_INTRO)
proof = udefs.get(settings.UDF_SOCIAL_PROOF)
if not all([subject, intro, proof]):
print("❌ Marketing texts missing in Person UDFs. Run provisioning first.")
return
full_body = f"{intro}\n\n{proof}\n\nAbmelden: {udefs.get(settings.UDF_UNSUBSCRIBE_LINK)}"
# 2. Create a "Shipment" (Individual Mailing)
# Based on SO Documentation for Marketing API
# We try to create a Shipment directly.
payload = {
"Name": f"Gemini Outreach: {subject}",
"Subject": subject,
"Body": full_body,
"DocumentTemplateId": 157, # Ausg. E-Mail
"ShipmentType": "Email",
"AssociateId": 528, # RCGO
"ContactId": person.get("Contact", {}).get("ContactId"),
"PersonId": person_id,
"Status": "Ready" # This might trigger the internal SO send process
}
print(f"📤 Creating Shipment for {email_address}...")
try:
# Endpoints to try: /Shipment or /Mailing
# Let's try /Shipment
resp = client._post("Shipment", payload)
if resp:
print(f"✅ Shipment created successfully! ID: {resp.get('ShipmentId')}")
print(json.dumps(resp, indent=2))
else:
print("❌ Shipment creation returned no data.")
except Exception as e:
print(f"❌ Shipment API failed: {e}")
if __name__ == "__main__":
create_mailing(193036)

View File

@@ -0,0 +1,87 @@
import os
import json
import logging
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
from config import settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("diagnose-email")
def diagnose():
print("🔍 Starting Email Capability Diagnosis...")
client = SuperOfficeClient()
if not client.access_token:
print("❌ Auth failed.")
return
# 1. Check Licenses / Capabilities via Associate/Me
print("\n--- 1. User & License Check ---")
try:
me = client._get("Associate/Me")
if me:
print(f"User: {me.get('Name')} (ID: {me.get('AssociateId')})")
print(f"Type: {me.get('Type')}")
# Check for specific functional rights if available in the object
# (Note: API often hides raw license keys, but let's check what we get)
print("Function Rights (TableRight):", me.get("TableRight"))
else:
print("❌ Could not fetch current user.")
except Exception as e:
print(f"❌ User check failed: {e}")
# 2. Check User Preferences (Email Settings)
# Endpoint: GET /Preference/{section}/{key}
# We look for 'Mail' section preferences
print("\n--- 2. Email Preferences (System & User) ---")
pref_keys = [
("Mail", "EmailClient"),
("Mail", "EmailSystem"),
("System", "SoProtocol"),
("Visual", "UseWebTools")
]
for section, key in pref_keys:
try:
# Note: The API for preferences might be /Preference/<Section>/<Key>
# or require a search. Let's try direct access first.
res = client._get(f"Preference/{section}/{key}")
if res:
print(f"✅ Preference '{section}/{key}': {json.dumps(res, indent=2)}")
else:
print(f"❓ Preference '{section}/{key}' not found or empty.")
except Exception as e:
print(f"⚠️ Error checking preference '{section}/{key}': {e}")
# 3. Check for specific functional rights (Archive/List)
# If we can access 'ShipmentType' list, we might have Marketing
print("\n--- 3. Marketing Capability Check ---")
try:
shipment_types = client._get("List/ShipmentType/Items")
if shipment_types:
print(f"✅ Found {len(shipment_types)} Shipment Types (Marketing module likely active).")
for st in shipment_types:
print(f" - {st.get('Name')} (ID: {st.get('Id')})")
else:
print("❌ No Shipment Types found (Marketing module might be inactive/restricted).")
except Exception as e:
print(f"❌ Error checking Shipment Types: {e}")
# 4. Check Document Template for 'Email'
print("\n--- 4. Document Template Configuration ---")
try:
# We know ID 157 exists, let's inspect it closely
tmpl = client._get("DocumentTemplate/157")
if tmpl:
print(f"Template 157: {tmpl.get('Name')}")
print(f" - Generator: {tmpl.get('Generator')}") # Important!
print(f" - Filename: {tmpl.get('Filename')}")
print(f" - Direction: {tmpl.get('Direction')}")
else:
print("❌ Template 157 not found via ID.")
except Exception as e:
print(f"❌ Error checking Template 157: {e}")
if __name__ == "__main__":
diagnose()

View File

@@ -1,62 +1,50 @@
import json import json
import os
import sys
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient from superoffice_client import SuperOfficeClient
import logging
# Setup Logging # Force unbuffered stdout
logging.basicConfig(level=logging.INFO) sys.stdout.reconfigure(line_buffering=True)
logger = logging.getLogger("discovery")
def discover(): def discover():
print("🔍 Starting SuperOffice Discovery Tool...") print("🔍 Starting SuperOffice Discovery Tool (Direct Sending)...")
try:
client = SuperOfficeClient()
except Exception as e:
print(f"❌ Failed to init client: {e}")
return
client = SuperOfficeClient()
if not client.access_token: if not client.access_token:
print("❌ Auth failed. Check .env") print("❌ Auth failed. Check .env")
return return
# 1. Discover UDFs (User Defined Fields) # 4. Check Sending Endpoints
print("\n--- 1. User Defined Fields (UDFs) Definitions ---") print("\n--- 4. Direct Sending Endpoints ---")
# EMail Agent
print(f"Checking Endpoint: Agents/EMail/GetDefaultEMailFromAddress...")
try: try:
# Fetch Metadata about UDFs to get Labels res = client._get("Agents/EMail/GetDefaultEMailFromAddress")
udf_info = client._get("UserDefinedFieldInfo") if res:
if udf_info: print(f"✅ Agents/EMail active. Default From: {json.dumps(res)}")
print(f"Found {len(udf_info)} UDF definitions.")
# Filter for Contact and Person UDFs
contact_udfs = [u for u in udf_info if u['UDTargetEntityName'] == 'Contact']
person_udfs = [u for u in udf_info if u['UDTargetEntityName'] == 'Person']
print(f"\n--- CONTACT UDFs ({len(contact_udfs)}) ---")
for u in contact_udfs:
print(f" - Label: '{u['FieldLabel']}' | ProgId: '{u['ProgId']}' | Type: {u['UDFieldType']}")
print(f"\n--- PERSON UDFs ({len(person_udfs)}) ---")
for u in person_udfs:
print(f" - Label: '{u['FieldLabel']}' | ProgId: '{u['ProgId']}' | Type: {u['UDFieldType']}")
else: else:
print("❌ Could not fetch UserDefinedFieldInfo.") print(f"❓ Agents/EMail returned None (likely 404/403).")
except Exception as e: except Exception as e:
print(f"Error fetching UDF Info: {e}") print(f"Agents/EMail check failed: {e}")
print("\n--- 2. Sample Data Inspection ---") # TicketMessage
print(f"Checking Endpoint: Archive/dynamic (Ticket)...")
lists_to_check = ["position", "business"] try:
res = client._get("Archive/dynamic?$select=all&$top=1&entity=ticket")
for list_name in lists_to_check: if res:
print(f"\nChecking List: '{list_name}'...") print(f"✅ Ticket entities found. Service module active.")
try: else:
# Endpoint: GET /List/{list_name}/Items print(f"❓ No Ticket entities found (Service module inactive?).")
items = client._get(f"List/{list_name}/Items") except Exception as e:
if items: print(f"❌ Ticket check failed: {e}")
print(f"Found {len(items)} items in '{list_name}':")
for item in items:
print(f" - ID: {item['Id']} | Name: '{item['Name']}'")
else:
print(f" (List '{list_name}' is empty or not accessible)")
except Exception as e:
print(f" ❌ Failed to fetch list '{list_name}': {e}")
if __name__ == "__main__": if __name__ == "__main__":
discover() discover()

View File

@@ -0,0 +1,20 @@
import os
import json
import logging
from dotenv import load_dotenv
load_dotenv(override=True)
from superoffice_client import SuperOfficeClient
logging.basicConfig(level=logging.INFO)
def inspect_person(person_id: int):
client = SuperOfficeClient()
print(f"📡 Fetching FULL Person {person_id}...")
person = client._get(f"Person/{person_id}")
if person:
print(json.dumps(person, indent=2))
else:
print("❌ Person not found.")
if __name__ == "__main__":
inspect_person(193036)

View File

@@ -99,8 +99,10 @@ class SuperOfficeClient:
logger.warning(f"⚠️ 401 Unauthorized for {endpoint}. Attempting Token Refresh...") logger.warning(f"⚠️ 401 Unauthorized for {endpoint}. Attempting Token Refresh...")
new_token = self._refresh_access_token() new_token = self._refresh_access_token()
if new_token: if new_token:
logger.info("✅ Token refreshed successfully during retry.")
self.access_token = new_token self.access_token = new_token
self.headers["Authorization"] = f"Bearer {self.access_token}" self.headers["Authorization"] = f"Bearer {self.access_token}"
# Recursive retry with the new token
return self._request_with_retry(method, endpoint, payload, retry=False) return self._request_with_retry(method, endpoint, payload, retry=False)
else: else:
logger.error("❌ Token Refresh failed during retry.") logger.error("❌ Token Refresh failed during retry.")