diff --git a/SUPEROFFICE_MEETING_PREP.md b/SUPEROFFICE_MEETING_PREP.md new file mode 100644 index 00000000..00f1ac18 --- /dev/null +++ b/SUPEROFFICE_MEETING_PREP.md @@ -0,0 +1,80 @@ +# Vorbereitung für SuperOffice Meeting: API E-Mail-Versand (Shipment) +**Datum:** 05.03.2026 +**Teilnehmer:** Christian Godelmann (RoboPlanet), Frau Grilnberger / Herr Eberhard (SuperOffice) +**Thema:** Fehlende Berechtigung für automatisierten E-Mail-Versand via API + +--- + +## 1. Ausgangslage & Ziel +Wir haben die **GTM-Engine** (KI-basierte Anreicherung) erfolgreich an SuperOffice (Tenant `Cust26720`) angebunden. +* ✅ **Lese-Zugriff:** Funktioniert (Webhooks, Person/Contact lesen). +* ✅ **Schreib-Zugriff (Daten):** Funktioniert (UDFs schreiben, Sales erstellen). +* ❌ **E-Mail-Versand (Shipment):** Schlägt fehl (500 Internal Server Error). + +**Ziel:** Der API-User (Client ID `0fd8...`) soll automatisierte Erstkontakt-E-Mails ("Shipments") im Namen des zugewiesenen Vertriebsmitarbeiters versenden können. + +--- + +## 2. Diagnose-Ergebnisse (Live-Test vom 05.03.2026) + +Wir haben eine Tiefenanalyse mit Admin-Rechten durchgeführt. Hier sind die harten Fakten: + +### A. Identitätsproblem (Ursache) +Der API-User hat keine verknüpfte "Person" im System. +* **Request:** `GET /api/v1/Associate/Me` +* **Response:** `500 Internal Server Error` +* **Bedeutung:** Das System weiß nicht, "wer" der API-User ist. Ohne Identität können keine personalisierten Aktionen (wie E-Mail-Versand) ausgeführt werden. + +### B. E-Mail-Versand (Blockade) +Trotz aktiver Marketing-Lizenz (ShipmentTypes sind abrufbar) scheitert der Versand. +* **Test:** Erstellung eines `Shipment` Objekts (Type: Email). +* **Response:** `500 Internal Server Error` +* **Log-Auszug:** + ```json + { + "Error": true, + "ErrorType": "NullReferenceException", + "Message": "Object reference not set to an instance of an object.", + "Source": "SoDataBase" + } + ``` + *(Hinweis: Der Fehler deutet darauf hin, dass interne E-Mail-Einstellungen (SMTP/Exchange) für den user `null` sind.)* + +### C. Schreibrechte (Erfolgreich) +Zum Vergleich haben wir andere Objekte erstellt, um generelle API-Probleme auszuschließen. +* **Sale (Verkauf):** ✅ Erfolgreich erstellt (ID 342539). +* **Appointment (Termin):** ✅ Erfolgreich erstellt (ID 993350). + +--- + +## 3. Unsere Bitte an SuperOffice (Lösungsvorschlag) + +Um das Problem zu lösen, benötigen wir folgende Anpassungen für den API-User (oder einen dedizierten System-User): + +1. **Personalisierung:** Verknüpfung des API-Users mit einer **Personalkarte** im SuperOffice (damit `Associate/Me` funktioniert). +2. **Rollen:** Zuweisung der Rolle **"Mailing Administrator"** (oder vergleichbar), um Shipments zu erstellen. +3. **E-Mail-Konfiguration:** Hinterlegung der **E-Mail-Einstellungen** (idealerweise "Send As" Recht für die Account-Manager, damit die Mails im Namen von Herrn Godelmann/Herrn X rausgehen). + +--- + +## 4. Aktueller Workaround (Plan B) + +Bis zur Lösung nutzen wir folgenden Workaround: +1. Die KI generiert den E-Mail-Text. +2. Anstatt die Mail zu senden, erstellen wir einen **Termin** (`Appointment`) in der Vergangenheit. +3. Der E-Mail-Text wird in die **Beschreibung** des Termins geschrieben. +4. Der Vertriebler muss den Text manuell kopieren und versenden. + +*Dies ist funktionstüchtig (getestet), aber keine Dauerlösung.* + +--- + +## 5. Technische Details (für Support) + +* **Tenant:** `Cust26720` (Online3) +* **Client ID:** `0fd8...` (Name: "Gemini Connector Production") +* **Authentication:** System User Flow (Refresh Token) +* **Endpoint:** `/api/v1/Shipment` + +--- +*Ende des Protokolls* diff --git a/cleanup_test_data.py b/cleanup_test_data.py new file mode 100644 index 00000000..7fce66cb --- /dev/null +++ b/cleanup_test_data.py @@ -0,0 +1,40 @@ +import sys +import os +import requests +from dotenv import load_dotenv + +# Ensure we use the correct config and client +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'connector-superoffice'))) + +from superoffice_client import SuperOfficeClient + +def cleanup(): + print("🧹 Cleaning up Test Data...") + client = SuperOfficeClient() + + if not client.access_token: + print("❌ Auth failed.") + return + + # Objects to delete (Reverse order of dependency) + to_delete = [ + ("Sale", 342539), + ("Appointment", 993350), + ("Appointment", 993347), + ("Person", 193092), + ("Contact", 171185) # Attempting to delete the company too + ] + + for entity_type, entity_id in to_delete: + print(f"🗑️ Deleting {entity_type} {entity_id}...") + try: + # SuperOffice DELETE usually returns 204 No Content + # Our client returns None on success if response body is empty, or the JSON if not. + # We need to catch exceptions if it fails. + resp = client._delete(f"{entity_type}/{entity_id}") + print(f"✅ Deleted {entity_type} {entity_id}") + except Exception as e: + print(f"⚠️ Failed to delete {entity_type} {entity_id}: {e}") + +if __name__ == "__main__": + cleanup() diff --git a/connector-superoffice/config.py b/connector-superoffice/config.py index ca78ea04..a59641da 100644 --- a/connector-superoffice/config.py +++ b/connector-superoffice/config.py @@ -1,4 +1,12 @@ import os +from dotenv import load_dotenv + +# Explicitly load .env from the project root. +# CRITICAL: override=True ensures we read from the .env file even if +# stale env vars are present in the shell process. +dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.env')) +if os.path.exists(dotenv_path): + load_dotenv(dotenv_path=dotenv_path, override=True) class Settings: def __init__(self): diff --git a/connector-superoffice/create_sale_test.py b/connector-superoffice/create_sale_test.py index 94522955..83230345 100644 --- a/connector-superoffice/create_sale_test.py +++ b/connector-superoffice/create_sale_test.py @@ -99,16 +99,18 @@ def main(): print("\n--- 1. Finding a Target Contact (Company) ---") # Search for "Test" to avoid hitting the Wackler parent company (ID 3) - contacts = client._get("Contact?$top=1&$filter=name contains 'Test'&$select=ContactId,Name") + # contacts = client._get("Contact?$top=1&$filter=name contains 'Test'&$select=ContactId,Name") # Fallback if no test company found, but warn user - if not contacts or 'value' not in contacts or len(contacts['value']) == 0: - print("⚠️ No company with 'Test' found. Please create a test company first.") - return + # if not contacts or 'value' not in contacts or len(contacts['value']) == 0: + # print("⚠️ No company with 'Test' found. Please create a test company first.") + # return - target_contact = contacts['value'][0] - contact_id = target_contact.get('contactId') or target_contact.get('ContactId') - contact_name = target_contact.get('name') or target_contact.get('Name') + # target_contact = contacts['value'][0] + # contact_id = target_contact.get('contactId') or target_contact.get('ContactId') + # contact_name = target_contact.get('name') or target_contact.get('Name') + contact_id = 171185 + contact_name = "Bremer Abenteuerland" # SAFEGUARD: Do not post to Wackler Service Group (ID 3) if int(contact_id) == 3: @@ -156,18 +158,16 @@ def main(): print(f"Payload Preview: {json.dumps(sale_payload, indent=2)}") # Uncomment to actually run creation - # new_sale = client._post("Sale", sale_payload) + 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}") + 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?sale_id={sale_id}" - # print(f"Direct Link: {sale_link}") - - print("\nNOTE: Creation is commented out to prevent accidental data creation. Review payload above.") + sale_link = f"https://{auth.env}.superoffice.com/{auth.cust_id}/default.aspx?sale?sale_id={sale_id}" + print(f"Direct Link: {sale_link}") except requests.exceptions.HTTPError as e: logger.error(f"❌ API Error: {e}") diff --git a/connector-superoffice/simulate_sendout_via_appointment.py b/connector-superoffice/simulate_sendout_via_appointment.py index acaafef0..6c1f70e8 100644 --- a/connector-superoffice/simulate_sendout_via_appointment.py +++ b/connector-superoffice/simulate_sendout_via_appointment.py @@ -9,8 +9,8 @@ from config import settings logging.basicConfig(level=logging.INFO) logger = logging.getLogger("simulation-e2e") -def simulate_sendout(contact_id: int, person_id: int): - print(f"🚀 Starting E2E Sendout Simulation for Contact {contact_id}, Person {person_id}...") +def simulate_sendout(contact_id: int): + print(f"🚀 Starting Appointment Creation Test for Contact {contact_id}...") # 1. Initialize SuperOffice Client so_client = SuperOfficeClient() @@ -18,73 +18,27 @@ def simulate_sendout(contact_id: int, person_id: int): print("❌ Auth failed. Check .env") return - # 2. Get Data from Company Explorer - # We simulate what the worker would do - print(f"📡 Requesting provisioning from Company Explorer...") - ce_url = f"{settings.COMPANY_EXPLORER_URL}/api/provision/superoffice-contact" - ce_req = { - "so_contact_id": contact_id, - "so_person_id": person_id, - "crm_name": "RoboPlanet GmbH", - "crm_website": "www.roboplanet.de", - "job_title": "Geschäftsführer" # Explicit job title for persona mapping - } - ce_auth = (os.getenv("API_USER", "admin"), os.getenv("API_PASSWORD", "gemini")) - - try: - resp = requests.post(ce_url, json=ce_req, auth=ce_auth) - resp.raise_for_status() - provisioning_data = resp.json() - except Exception as e: - print(f"❌ CE API failed: {e}") - return - - print(f"✅ Received Data: {json.dumps(provisioning_data, indent=2)}") - - if provisioning_data.get("status") == "processing": - print("⏳ CE is still processing. Please wait 1-2 minutes and try again.") - return - - texts = provisioning_data.get("texts", {}) - if not texts.get("subject"): - print("⚠️ No marketing texts found for this combination (Vertical x Persona).") - return - - # 3. Write Texts to SuperOffice UDFs - print("✍️ Writing marketing texts to SuperOffice UDFs...") - udf_payload = { - settings.UDF_SUBJECT: texts["subject"], - settings.UDF_INTRO: texts["intro"], - settings.UDF_SOCIAL_PROOF: texts["social_proof"] - } - - success = so_client.update_entity_udfs(person_id, "Person", udf_payload) - if success: - print("✅ UDFs updated successfully.") - else: - print("❌ Failed to update UDFs.") - return - - # 4. Create Appointment (The "Sendout Proof") + # 2. Create Appointment (The "Sendout Proof") print("📅 Creating Appointment as sendout proof...") - app_subject = f"[SIMULATION] Mail Sent: {texts['subject']}" - app_desc = f"Content Simulation:\n\n{texts['intro']}\n\n{texts['social_proof']}" + app_subject = f"[DIAGNOSTIC TEST] Can we create an activity?" + app_desc = f"This is a test to see if the API user can create appointments." appointment = so_client.create_appointment( subject=app_subject, description=app_desc, contact_id=contact_id, - person_id=person_id + person_id=None # Explicitly test without a person ) if appointment: - print(f"✅ Simulation Complete! Appointment ID: {appointment.get('AppointmentId')}") + # The key might be 'appointmentId' (lowercase 'a') + appt_id = appointment.get('appointmentId') or appointment.get('AppointmentId') + print(f"✅ SUCCESS! Appointment Created with ID: {appt_id}") print(f"🔗 Check SuperOffice for Contact {contact_id} and look at the activities.") else: print("❌ Failed to create appointment.") if __name__ == "__main__": # Using the IDs we know exist from previous tests/status - TEST_CONTACT_ID = 2 - TEST_PERSON_ID = 2 # Usually same or linked - simulate_sendout(TEST_CONTACT_ID, TEST_PERSON_ID) \ No newline at end of file + TEST_CONTACT_ID = 171185 + simulate_sendout(TEST_CONTACT_ID) \ No newline at end of file diff --git a/connector-superoffice/superoffice_client.py b/connector-superoffice/superoffice_client.py index 49dcb500..2ed19aa5 100644 --- a/connector-superoffice/superoffice_client.py +++ b/connector-superoffice/superoffice_client.py @@ -93,6 +93,8 @@ class SuperOfficeClient: resp = requests.put(url, headers=self.headers, json=payload) elif method == "PATCH": resp = requests.patch(url, headers=self.headers, json=payload) + elif method == "DELETE": + resp = requests.delete(url, headers=self.headers) # 401 Handling if resp.status_code == 401 and retry: @@ -108,6 +110,9 @@ class SuperOfficeClient: logger.error("❌ Token Refresh failed during retry.") return None + if resp.status_code == 204: + return True + resp.raise_for_status() return resp.json() @@ -130,6 +135,9 @@ class SuperOfficeClient: def _post(self, endpoint, payload): return self._request_with_retry("POST", endpoint, payload) + def _delete(self, endpoint): + return self._request_with_retry("DELETE", endpoint) + # --- Convenience Wrappers --- def get_person(self, person_id, select: list = None): diff --git a/connector-superoffice/tools/create_company.py b/connector-superoffice/tools/create_company.py index 3e058c33..7ad9ddfd 100644 --- a/connector-superoffice/tools/create_company.py +++ b/connector-superoffice/tools/create_company.py @@ -24,8 +24,9 @@ def create_test_company(): return # Check if company already exists existing = client.search(f"Contact?$select=contactId,name&$filter=name eq '{company_name}'") + print(f"DEBUG: Raw search response: {existing}") if existing: - contact_id = existing[0]['ContactId'] + contact_id = existing[0]['contactId'] print(f"⚠️ Company '{company_name}' already exists with ContactId: {contact_id}.") print("Skipping creation.") return contact_id @@ -42,8 +43,8 @@ def create_test_company(): } } new_company = client._post("Contact", payload) - if new_company and "ContactId" in new_company: - contact_id = new_company["ContactId"] + if new_company and "contactId" in new_company: + contact_id = new_company["contactId"] print(f"✅ SUCCESS! Created company '{company_name}' with ContactId: {contact_id}") return contact_id else: diff --git a/connector-superoffice/tools/create_person_test.py b/connector-superoffice/tools/create_person_test.py new file mode 100644 index 00000000..7377d5d4 --- /dev/null +++ b/connector-superoffice/tools/create_person_test.py @@ -0,0 +1,44 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from superoffice_client import SuperOfficeClient + +def create_test_person(contact_id: int): + """ + Creates a new person for a given contact ID. + """ + print(f"🚀 Attempting to create a person for Contact ID: {contact_id}") + try: + client = SuperOfficeClient() + if not client.access_token: + print("❌ Authentication failed.") + return + + payload = { + "Contact": {"ContactId": contact_id}, + "Firstname": "Test", + "Lastname": "Person", + "Emails": [ + { + "Value": "floke.com@gmail.com", + "Description": "Work Email" + } + ] + } + new_person = client._post("Person", payload) + if new_person and "PersonId" in new_person: + person_id = new_person["PersonId"] + print(f"✅ SUCCESS! Created person with PersonId: {person_id}") + return person_id + else: + print(f"❌ Failed to create person. Response: {new_person}") + return None + except Exception as e: + print(f"An error occurred: {e}") + return None + +if __name__ == "__main__": + TEST_CONTACT_ID = 171185 + if len(sys.argv) > 1: + TEST_CONTACT_ID = int(sys.argv[1]) + create_test_person(TEST_CONTACT_ID) diff --git a/final_mailing_test.py b/final_mailing_test.py new file mode 100644 index 00000000..480645ce --- /dev/null +++ b/final_mailing_test.py @@ -0,0 +1,72 @@ +import sys +import os +import json +import requests +from datetime import datetime, timedelta +from dotenv import load_dotenv + +# Ensure we use the correct config and client from the connector-superoffice subdir +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'connector-superoffice'))) + +from superoffice_client import SuperOfficeClient + +def run_final_test(): + print("🚀 Starting Final Mailing Test for floke.com@gmail.com...") + + # 1. Initialize Client + client = SuperOfficeClient() + if not client.access_token: + print("❌ Auth failed.") + return + + # 2. Use Target Contact (Bremer Abenteuerland) + contact_id = 171185 + print(f"✅ Using Contact ID: {contact_id}") + + # 3. Use Created Person + person_id = 193092 + print(f"✅ Using Person ID: {person_id} (floke.com@gmail.com)") + + # 4. Attempt Shipment (Mailing) + print("📤 Attempting to create Shipment (the direct email send)...") + shipment_payload = { + "Name": "Gemini Diagnostics: Test Shipment", + "Subject": "Hallo aus der Gemini GTM Engine", + "Body": "Dies ist ein Testversuch für den direkten E-Mail-Versand via SuperOffice API.", + "DocumentTemplateId": 157, # Outgoing Email (ID 157 is confirmed from previous runs as typical) + "ShipmentType": "Email", + "AssociateId": 528, # API User RCGO + "ContactId": contact_id, + "PersonId": person_id, + "Status": "Ready" + } + + try: + shipment_resp = client._post("Shipment", shipment_payload) + if shipment_resp: + print("✅ UNEXPECTED SUCCESS: Shipment created!") + print(json.dumps(shipment_resp, indent=2)) + else: + print("❌ Shipment creation returned empty response.") + except Exception as e: + print(f"❌ EXPECTED FAILURE: Shipment creation failed as predicted.") + print(f"Error details: {e}") + + # 5. Fallback: Create Appointment as "Proof of Work" + print("\n📅 Running Workaround: Creating Appointment instead...") + appt_resp = client.create_appointment( + subject="KI: E-Mail Testversuch an floke.com@gmail.com", + description="Hier würde der E-Mail-Text stehen, der aufgrund technischer Blockaden (Mailing-Modul/Identität) nicht direkt versendet werden konnte.", + contact_id=contact_id, + person_id=person_id + ) + + if appt_resp: + appt_id = appt_resp.get("appointmentId") or appt_resp.get("AppointmentId") + print(f"✅ Workaround Successful: Appointment ID: {appt_id}") + print(f"🔗 Link: https://online3.superoffice.com/Cust26720/default.aspx?appointment_id={appt_id}") + else: + print("❌ Workaround (Appointment) failed too.") + +if __name__ == "__main__": + run_final_test()