Compare commits
5 Commits
7971a02d1a
...
4eccb55eab
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eccb55eab | |||
| 176e1f89f4 | |||
| 76452df6ab | |||
| 22c1daa7d7 | |||
| bbab1d70ff |
@@ -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"}
|
||||||
12
GEMINI.md
12
GEMINI.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
25
connector-superoffice/EMAIL_WORKAROUND_REPORT.md
Normal file
25
connector-superoffice/EMAIL_WORKAROUND_REPORT.md
Normal 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.
|
||||||
@@ -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).
|
||||||
|
|||||||
37
connector-superoffice/SENDING_STRATEGY_ANALYSIS.md
Normal file
37
connector-superoffice/SENDING_STRATEGY_ANALYSIS.md
Normal 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.
|
||||||
38
connector-superoffice/attempt_agent_send.py
Normal file
38
connector-superoffice/attempt_agent_send.py
Normal 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")
|
||||||
36
connector-superoffice/check_crmscript.py
Normal file
36
connector-superoffice/check_crmscript.py
Normal 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()
|
||||||
173
connector-superoffice/create_email_test.py
Normal file
173
connector-superoffice/create_email_test.py
Normal 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)
|
||||||
71
connector-superoffice/create_mailing_test.py
Normal file
71
connector-superoffice/create_mailing_test.py
Normal 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)
|
||||||
87
connector-superoffice/diagnose_email_capability.py
Normal file
87
connector-superoffice/diagnose_email_capability.py
Normal 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()
|
||||||
@@ -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)...")
|
||||||
|
|
||||||
client = SuperOfficeClient()
|
try:
|
||||||
|
client = SuperOfficeClient()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to init client: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
20
connector-superoffice/inspect_person_full.py
Normal file
20
connector-superoffice/inspect_person_full.py
Normal 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)
|
||||||
@@ -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.")
|
||||||
|
|||||||
Reference in New Issue
Block a user