diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index 0be6459d..15457e57 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-09T18:59:42.953882"} \ No newline at end of file +{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-10T07:08:50.264533"} \ No newline at end of file diff --git a/SUPEROFFICE_INTEGRATION_PLAN.md b/SUPEROFFICE_INTEGRATION_PLAN.md index 84c272f7..1cf68d34 100644 --- a/SUPEROFFICE_INTEGRATION_PLAN.md +++ b/SUPEROFFICE_INTEGRATION_PLAN.md @@ -61,6 +61,15 @@ Folgende Felder sollten am Objekt `Company` (bzw. `Contact` in SuperOffice-Termi | :--- | :--- | :--- | | `external_crm_id` | String/Int | Speichert die `ContactId` aus SuperOffice zur eindeutigen Zuordnung (Primary Key Mapping). | +## 2.1. Mapping of CRM Concepts (SuperOffice vs. D365) + +Um die Integration effizient zu gestalten, wurde eine strategische Entscheidung bezüglich der Abbildung von Kern-CRM-Konzepten getroffen: + +| D365 Konzept | SuperOffice Entität | Zweck & Begründung | +| :--- | :--- | :--- | +| **Opportunity** | `Sale` | Die `Sale`-Entität in SuperOffice ist das direkte Äquivalent zu einer Opportunity. Hier werden potenzielle Umsätze, Vertriebsphasen und Wahrscheinlichkeiten erfasst. Dies ist das primäre Zielobjekt, sobald eine konkrete Verkaufschance durch den Company Explorer identifiziert wird. | +| **Campaign** | `Project` | Für Marketing-Automatisierung und die Bündelung von Kontakten für Kampagnen dient die `Project`-Entität als idealer Container. Sie ermöglicht es, Kampagnen-Teilnehmer zu gruppieren, Aktivitäten zuzuordnen und den ROI durch Verknüpfung mit `Sale`-Objekten zu messen. | + --- ## 3. Phasenplan diff --git a/connector-superoffice/README.md b/connector-superoffice/README.md index 22debb5f..c1499088 100644 --- a/connector-superoffice/README.md +++ b/connector-superoffice/README.md @@ -11,6 +11,8 @@ Der **Proof of Concept (POC)** ist erfolgreich abgeschlossen. - **Daten schreiben (Erfolg):** - **Firmen (Contacts):** Erfolgreich angelegt (inkl. Name, URL, OrgNr). - **Personen (Persons):** Erfolgreich angelegt und mit Firmen verknüpft (inkl. E-Mail). + - **Verkauf (Sale):** Erfolgreich angelegt. + - **Projekt (Project):** Erfolgreich angelegt und Person als Mitglied hinzugefügt. - **UDF-Felder:** Vorbereitet im Code, erfordert finalen Abgleich der `ProgId` nach Anlage im Admin-Panel. ## 2. Einrichtung & Installation @@ -56,6 +58,8 @@ cd connector-superoffice - `find_contact_by_criteria(...)`: Suche nach Name, URL oder OrgNr. - `create_contact(...)`: Erstellt eine neue Firma. - `create_person(...)`: Erstellt einen Ansprechpartner (verknüpft mit ContactId). +- `create_sale(...)`: Erstellt einen Verkauf/Opportunity. +- `create_project(...)`: Erstellt ein Projekt und fügt optional eine Person als Mitglied hinzu. - `update_udfs(...)`: (Vorbereitet) Aktualisiert KI-Felder. ### Felder entdecken @@ -78,6 +82,52 @@ Ursprünglich war ein "Server-to-Server" (S2S) Flow mittels RSA-Zertifikaten gep * **Nachteil:** Muss einmalig manuell via Browser generiert werden (bereits erledigt). ## 5. Nächste Schritte -1. **Felder anlegen:** In SuperOffice Admin -> Felder -> Firma die UDFs (`AI Robotics Potential`, `AI Summary` etc.) anlegen. -2. **Mapping aktualisieren:** Die technischen Namen (`ProgId`) in `superoffice_client.py` eintragen. -3. **Schreib-Test:** `main.py` ausführen, um Daten zurückzuschreiben. + +### Detaillierter Plan: Marketing Automation (SuperOffice-zentrisch) + +Der Versand von hyper-personalisierten E-Mails aus SuperOffice heraus ist ein zentrales Ziel. Das Vorgehen wird sich auf die Vorbereitung der E-Mails durch den Connector konzentrieren, während der Versand und die finale Kontrolle im SuperOffice-Client durch den Endbenutzer erfolgen. + +#### Anwenderplan für die Marketing Automation (Ihre Beschreibung) + +1. **Wichtig: Versand aus SuperOffice heraus:** Der Versand sollte aus SuperOffice heraus erfolgen (wir haben alle technischen Erforderungen ergriffen, um die E-Mail aus SuperOffice heraus in unserem Namen zu senden und vertrauenswürdig erscheinen zu lassen). Dies ist wichtig, um Transparenz zu haben (man sieht im System genau, was passiert ist). +2. **Versand im NAMEN des angemeldeten CRM-Users:** Der Versand erfolgt im NAMEN des angemeldeten CRM-Users / Account-Verantwortlichen. Ggf. muss er sogar manuell einen Knopf drücken, damit etwas passiert. +3. **Statuswechsel bei Antwort:** Der Statuswechsel bei Antwort _kann_ ein manueller Prozess sein – wenn wir dies sinnvoll automatisieren können (bei eingehender Antwort automatisch Status auf "hat geantwortet" setzen wäre auch denkbar, wenn dies technisch möglich ist). +4. **Crafting der E-Mail (Vorab-Generierung):** Das "Crafting" der E-Mail ist für mich wichtiger als die reine Automation des im Wochenabstand Aussendens: Eine E-Mail besteht aus einem unternehmensspezifischen Satz, der gezielt die Herausforderungen des Unternehmens in Bezug auf Roboter als Herausforderung formuliert. Dieser Satz wird VORAB am Account gespeichert, nicht erst zur Laufzeit. Der zweite Teil der E-Mail besteht aus einer rollen- und branchenspezifischen "Antwort" auf die in Satz 1 formulierte Herausforderung, die als logische Schlussfolgerung den Einsatz unserer Roboter ins Feld führt. Im weiteren wird ein persönlicher Buchungslink zu einer Erstberatung des ABSENDERS ergänzt. (Dies ist unsere CTA, es gibt nichts anderes, was er klicken soll). Abgeschlossen wird die E-Mail durch einen Social Proof, der wiederum branchen- und rollenspezifische KPIs, die die Referenz durch Einsatz unserer Lösungen erreicht hat, aufführt. Die rollen- und branchenspezifischen Texte werden ebenfalls VORAB generiert. Sie liegen in einem eigenen Entitätstypen "marketingansprache". Der unternehmensspezifische Satz wird am Contact (Firma) gespeichert. +5. **Zusammenführen im E-Mail-Template:** Im E-Mail-Template werden dann alle Felder zusammengebracht. Da Satz 1 immer eine Herausforderung formuliert (egal welche dies ist) und Satz 2 immer eine Auflösung dieser Herausforderung formuliert, passt es am Ende so gut zusammen, dass niemand merkt, dass diese beiden Sätze nie voneinander wussten. +6. **E-Mail-Signatur:** Die E-Mail-Signatur (Name, Telefon, E-Mail) werden vom USER gezogen, der den Prozess anstößt. + +#### Architekturentwurf: "Der Butler-Service" + +Dieser Plan wird durch ein "Butler-Service"-Modell umgesetzt, bei dem der Connector die E-Mail-Inhalte intelligent vorbereitet und SuperOffice als Sendeplattform und "Single Source of Truth" für den Status dient. + +**Phase 1: Die Vorbereitung (Unser Connector, der Butler)** +Der Connector läuft im Hintergrund (z.B. alle Stunde) und führt folgende Aufgaben aus: + +1. **Identifiziere Kandidaten:** Er fragt die SuperOffice API: "Gib mir alle Firmen (`Contact`), die für eine Ansprache vorgesehen sind (z.B. in einem bestimmten `Project` oder mit einem Status `Ready_for_Crafting`)." +2. **Generiere Satz 1 (Unternehmens-spezifisch):** Für jede Firma ruft der Connector die Company Explorer API auf und generiert den hyper-personalisierten "Herausforderungs-Satz". +3. **Speichere Satz 1:** Der Connector speichert diesen Satz in einem neuen, benutzerdefinierten Text-Feld (UDF) direkt an der Firma (`Contact`) in SuperOffice. Vorschlag: `AI_Challenge_Sentence`. +4. **Hole Satz 2 & Social Proof (Rollen/Branchen-spezifisch):** Der Connector weiß, welche Rolle und Branche der Ansprechpartner hat. Er greift auf die "marketingansprache"-Bibliothek zu (diese kann in einer einfachen Datenbank oder sogar einer JSON-Datei liegen, die der Connector verwaltet) und holt die passenden Textbausteine. +5. **Stelle die E-Mail zusammen:** Der Connector kombiniert nun alles: + * Satz 1 (vom `Contact`-Feld) + * Satz 2 (aus der Bibliothek) + * Social Proof (aus der Bibliothek) +6. **Speichere den E-Mail-Entwurf:** Der komplette, fertige E-Mail-Text wird in einem zweiten, großen UDF-Textfeld gespeichert, diesmal aber an der **Person**, dem Ansprechpartner. Vorschlag: `AI_Email_Draft`. +7. **Setze den Status:** Der Connector aktualisiert den `MA-Status` der Person auf `Ready_to_Send`. + +**Phase 2: Die Ausführung (Der SuperOffice User)** +Der Vertriebsmitarbeiter hat eine extrem einfache Aufgabe: + +1. **Dashboard-Ansicht:** Er öffnet in SuperOffice eine Selektion oder ein Projekt, das ihm alle Kontakte mit dem Status `Ready_to_Send` anzeigt. +2. **Klick, Klick, Senden:** Er öffnet den Kontakt. Im E-Mail-Editor von SuperOffice gibt es eine Vorlage. Diese Vorlage tut nichts anderes, als den Inhalt des Feldes `AI_Email_Draft` zu laden, den persönlichen Buchungslink des Mitarbeiters und seine Signatur hinzuzufügen. +3. **Manuelle Kontrolle:** Er kann den Text kurz überfliegen, wenn er möchte, und klickt auf **"Senden"**. +4. **Status-Update:** Nach dem Senden ändert er den `MA-Status` manuell auf `Sent_Week1`. + +#### Erforderliche UDFs für die Marketing Automation + +Um diesen Plan umzusetzen, werden die folgenden UDFs in SuperOffice benötigt (Anlage durch den Admin): + +1. Am Objekt **`Contact` (Firma):** Ein Textfeld. Vorschlag: `AI_Challenge_Sentence` +2. Am Objekt **`Person` (Ansprechpartner):** Ein großes Textfeld (Memo/Long Text). Vorschlag: `AI_Email_Draft` +3. Am Objekt **`Person` (Ansprechpartner):** Ein Listenfeld. Vorschlag: `MA_Status` mit den Werten `Init`, `Ready_to_Craft`, `Ready_to_Send`, `Sent_Week1`, `Sent_Week2`, `Replied`, `Paused`. + +Sobald diese Felder angelegt sind, können die `ProgId`s in den Connector integriert werden. \ No newline at end of file diff --git a/connector-superoffice/discover_fields.py b/connector-superoffice/discover_fields.py index cabd807e..16485042 100644 --- a/connector-superoffice/discover_fields.py +++ b/connector-superoffice/discover_fields.py @@ -13,18 +13,53 @@ def discover_fields(): auth = AuthHandler() client = SuperOfficeClient(auth) - logger.info("Fetching metadata to discover UDF fields...") - - # Get metadata for Contact (Company) - url = client._get_url("v1/Metadata/Contact/UserDefinedFields") try: + logger.info("Fetching general Contact metadata...") + general_metadata_url = client._get_url("v1/Metadata/Contact") + resp = client.session.get(general_metadata_url, headers=client._get_headers()) + resp.raise_for_status() + general_metadata = resp.json() + print("\n--- GENERAL CONTACT METADATA (partial) ---") + print(f"DisplayName: {general_metadata.get('DisplayName')}") + print(f"HasUserDefinedFields: {general_metadata.get('HasUserDefinedFields')}") + print("----------------------------------------") + + logger.info("Attempting to fetch UDF metadata...") + # Get metadata for Contact (Company) + url = client._get_url("v1/Metadata/Contact/UserDefinedFields") resp = client.session.get(url, headers=client._get_headers()) resp.raise_for_status() fields = resp.json() print("\n--- AVAILABLE UDF FIELDS ---") for field in fields.get("value", []): - print(f"Label: {field.get('FieldLabel')} -> Technical Name: {field.get('ProgId')} (Type: {field.get('FieldType')})") + label = field.get('FieldLabel') + prog_id = field.get('ProgId') + field_type = field.get('FieldType') + print(f"Label: {label} -> Technical Name: {prog_id} (Type: {field_type})") + + # Check if the field is a list type + if field_type in ["List", "DropDown", "UserDefinedList"]: # Common types, might need adjustment + logger.info(f" -> Fetching list items for {label} (ProgId: {prog_id})...") + # Attempt to get the list items. The actual endpoint might vary. + # Assuming a pattern like 'v1/List/UserDefinedField/{ProgId}' or 'v1/UserDefinedFieldList/{ProgId}/Items' + list_url = client._get_url(f"v1/List/UserDefinedField/{prog_id}") + try: + list_resp = client.session.get(list_url, headers=client._get_headers()) + list_resp.raise_for_status() + list_items = list_resp.json() + + if list_items.get("value"): + print(" --- List Items ---") + for item in list_items["value"]: + print(f" ID: {item.get('Id')}, Name: {item.get('Name')}") + else: + print(" (No list items found or unexpected response structure)") + + except Exception as list_e: + logger.warning(f" Failed to fetch list items for {label}: {list_e}") + if hasattr(list_e, 'response') and list_e.response is not None: + logger.warning(f" List fetch details: {list_e.response.text}") except Exception as e: logger.error(f"Failed to fetch metadata: {e}") @@ -32,5 +67,4 @@ def discover_fields(): print(f"Details: {e.response.text}") if __name__ == "__main__": - discover_fields() - + discover_fields() \ No newline at end of file diff --git a/connector-superoffice/main.py b/connector-superoffice/main.py index c62e9168..8de8d6b7 100644 --- a/connector-superoffice/main.py +++ b/connector-superoffice/main.py @@ -33,7 +33,7 @@ def main(): logger.info("Step 1: Testing connection...") user_info = client.test_connection() if user_info: - logger.info(f"Connected successfully as: {user_info.get('Name')}") + logger.info(f"Connected successfully as: {user_info.get('FullName')}") else: logger.error("Connection test failed.") return @@ -76,9 +76,40 @@ def main(): if person: logger.info("SUCCESS: Person created!") + person_id = person.get('PersonId') logger.info(f"Name: {person.get('Firstname')} {person.get('Lastname')}") - logger.info(f"Person ID: {person.get('PersonId')}") + logger.info(f"Person ID: {person_id}") logger.info(f"Linked to Contact ID: {person.get('Contact').get('ContactId')}") + + # 4. Create a Sale for this company + logger.info(f"Step 4: Creating Sale for Contact ID {target_contact_id}...") + sale = client.create_sale( + title=f"Robotics Automation Opportunity - {demo_company_name}", + contact_id=target_contact_id, + person_id=person_id, + amount=50000.0 + ) + if sale: + logger.info("SUCCESS: Sale created!") + logger.info(f"Sale Title: {sale.get('Heading')}") + logger.info(f"Sale ID: {sale.get('SaleId')}") + else: + logger.error("Failed to create sale.") + + # 5. Create a Project for this company and add the person to it + logger.info(f"Step 5: Creating Project for Contact ID {target_contact_id} and adding Person ID {person_id}...") + project = client.create_project( + name=f"Marketing Campaign Q1 [2ff88f42]", + contact_id=target_contact_id, + person_id=person_id + ) + if project: + logger.info("SUCCESS: Project created and person added!") + logger.info(f"Project Name: {project.get('Name')}") + logger.info(f"Project ID: {project.get('ProjectId')}") + else: + logger.error("Failed to create project.") + else: logger.error("Failed to create person.") else: diff --git a/connector-superoffice/superoffice_client.py b/connector-superoffice/superoffice_client.py index eda9d737..ef692440 100644 --- a/connector-superoffice/superoffice_client.py +++ b/connector-superoffice/superoffice_client.py @@ -145,7 +145,78 @@ class SuperOfficeClient: logger.info(f"Successfully created person: {created_person.get('Firstname')} {created_person.get('Lastname')} (ID: {created_person.get('PersonId')})") return created_person except Exception as e: - logger.error(f"Error creating person: {e}") + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response: {e.response.text}") + return None + + def create_sale(self, title, contact_id, person_id=None, amount=0.0): + """Creates a new Sale (Opportunity) linked to a contact and optionally a person.""" + url = self._get_url("v1/Sale") + + payload = { + "Heading": title, + "Contact": { + "ContactId": contact_id + }, + "Amount": amount, + "SaleType": { # Assuming default ID 1 exists + "Id": 1 + }, + "SaleStage": { # Assuming default ID for the first stage is 1 + "Id": 1 + }, + "Probability": 10 # Default probability + } + + if person_id: + payload["Person"] = { + "PersonId": person_id + } + + try: + logger.info(f"Attempting to create sale: '{title}' for Contact ID {contact_id}") + resp = self.session.post(url, headers=self._get_headers(), json=payload) + resp.raise_for_status() + created_sale = resp.json() + logger.info(f"Successfully created sale: {created_sale.get('Heading')} (ID: {created_sale.get('SaleId')})") + return created_sale + except Exception as e: + if hasattr(e, 'response') and e.response is not None: + logger.error(f"Response: {e.response.text}") + return None + + def create_project(self, name, contact_id, person_id=None): + """Creates a new Project linked to a contact and optionally adds a person as a member.""" + url = self._get_url("v1/Project") + + payload = { + "Name": name, + "Contact": { + "ContactId": contact_id + }, + "ProjectType": { # Assuming default ID 1 exists + "Id": 1 + }, + "ProjectStatus": { # Assuming default ID 1 for 'In progress' exists + "Id": 1 + }, + "ProjectMembers": [] + } + + if person_id: + payload["ProjectMembers"].append({ + "PersonId": person_id + }) + + try: + logger.info(f"Attempting to create project: '{name}' for Contact ID {contact_id}") + resp = self.session.post(url, headers=self._get_headers(), json=payload) + resp.raise_for_status() + created_project = resp.json() + logger.info(f"Successfully created project: {created_project.get('Name')} (ID: {created_project.get('ProjectId')})") + return created_project + except Exception as e: + logger.error(f"Error creating project: {e}") if hasattr(e, 'response') and e.response is not None: logger.error(f"Response: {e.response.text}") return None