feat(email-api): Implement email document creation via SuperOffice API (no SMTP)
This commit is contained in:
@@ -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.
|
||||
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
|
||||
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**.
|
||||
* **Betroffene Felder:** `Address` (Postal & Street), `OrgNr` (VAT), `Urls` (Website) und alle `UserDefinedFields`.
|
||||
* **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.
|
||||
**Features:**
|
||||
* **Document Creation:** Creates a document of type "Ausg. E-Mail" (Template ID 157).
|
||||
* **Activity Tracking:** Automatically creates a linked "Appointment" (Task ID 6 - Document Out) to ensure the email appears in the contact's activity timeline.
|
||||
* **Direct Link:** Outputs a direct URL to open the created document in SuperOffice Online.
|
||||
|
||||
#### B. REST Website-Sync (The `Urls` Array)
|
||||
SuperOffice REST akzeptiert kein direktes Update auf `UrlAddress` via PATCH. Stattdessen muss das `Urls` Array manipuliert werden.
|
||||
* **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.
|
||||
* **Format:** `"Urls": [{"Value": "https://...", "Description": "AI Discovered"}]`.
|
||||
**Usage:**
|
||||
```bash
|
||||
python3 connector-superoffice/create_email_test.py <PersonID>
|
||||
# Example:
|
||||
python3 connector-superoffice/create_email_test.py 193036
|
||||
```
|
||||
|
||||
#### C. Kampagnen-Auflösung via `:DisplayText`
|
||||
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.
|
||||
* **Technik:** Im `$select` Parameter wird das Feld `SuperOffice:23:DisplayText` angefordert.
|
||||
* **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.
|
||||
**Key API Endpoints Used:**
|
||||
* `POST /Document`: Creates the email body and metadata.
|
||||
* `POST /Appointment`: Creates the activity record linked to the document.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Appendix: The "First Sentence" Prompt
|
||||
This is the core logic used to generate the company-specific opener.
|
||||
|
||||
**Goal:** Prove understanding of the business model + imply the pain (positive observation).
|
||||
|
||||
131
connector-superoffice/create_email_test.py
Normal file
131
connector-superoffice/create_email_test.py
Normal file
@@ -0,0 +1,131 @@
|
||||
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
|
||||
subject = "Test E-Mail from Gemini CLI (API)"
|
||||
body = f"""Hallo {person.get('Firstname')},
|
||||
|
||||
Dies ist ein Test für die Erstellung eines E-Mail-Dokuments direkt über die SuperOffice API.
|
||||
Wir nutzen das Template 'Ausg. E-Mail' (ID 157).
|
||||
|
||||
Viele Grüße,
|
||||
Gemini"""
|
||||
|
||||
# 3. Create Document Payload
|
||||
# Note: DocumentTemplateId 157 = "Ausg. E-Mail"
|
||||
template_id = 157
|
||||
|
||||
payload = {
|
||||
"Name": subject, # Internal Name
|
||||
"Header": subject, # Subject Line
|
||||
# "OurRef": {"AssociateId": my_associate_id}, # Omitted, hoping SO uses API User context
|
||||
"Contact": {"ContactId": contact_id},
|
||||
"Person": {"PersonId": person_id},
|
||||
"DocumentTemplate": {"DocumentTemplateId": template_id},
|
||||
"Content": body
|
||||
}
|
||||
|
||||
print(f"📤 Sending POST /Document payload...")
|
||||
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" Name: {doc.get('Name')}")
|
||||
print(f" Template: {doc.get('DocumentTemplate', {}).get('Name')}")
|
||||
|
||||
# Construct direct link
|
||||
# Format: https://online3.superoffice.com/Cust26720/default.aspx?document_id=334050
|
||||
env = settings.SO_ENVIRONMENT
|
||||
cust_id = settings.SO_CONTEXT_IDENTIFIER
|
||||
# Note: This is a best-guess link format for SO Online
|
||||
link = f"https://{env}.superoffice.com/{cust_id}/default.aspx?document_id={doc_id}"
|
||||
print(f"🔗 Direct Link: {link}")
|
||||
|
||||
# 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:
|
||||
print(f"✅ Appointment Created: {appt.get('AppointmentId')}")
|
||||
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)
|
||||
Reference in New Issue
Block a user