From 1a0d936fdb912a9cddf0d448a96a5add373952b4 Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 8 Mar 2026 20:01:20 +0000 Subject: [PATCH] feat(lead-engine): Implement Teams notification and email enhancements [31988f42] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced Teams Adaptive Card with precise email send time and re-added emojis to action buttons ("✅ JETZT Aussenden", "❌ STOP Aussendung"). - Modified email sending logic to include HTML signature from `signature.html` and an inline banner image from `RoboPlanetBannerWebinarEinladung.png`. - Documented future enhancements in `lead-engine/README.md`: - Race-condition protection for calendar bookings with a live calendar check. - Integration of booking confirmation pages into the WordPress website (iFrame first, then API integration). --- lead-engine/README.md | 22 +++++++- .../RoboPlanetBannerWebinarEinladung.png | 0 lead-engine/trading_twins/manager.py | 24 +++++++-- lead-engine/trading_twins/signature.html | 53 +++++-------------- .../trading_twins/teams_notification.py | 18 +++---- 5 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 lead-engine/trading_twins/RoboPlanetBannerWebinarEinladung.png diff --git a/lead-engine/README.md b/lead-engine/README.md index fc894713..0a4f2dfd 100644 --- a/lead-engine/README.md +++ b/lead-engine/README.md @@ -76,7 +76,27 @@ docker exec lead-engine python /app/trading_twins/test_calendar_logic.py **Zugriff:** `https://floke-ai.duckdns.org/lead/` (Passwortgeschützt) -## 📝 Credentials (.env) +## 📝 Zukünftige Erweiterungen & Todos + +### Task: Race-Condition-Schutz bei Überbuchung +* **Problem:** Wenn mehrere Leads E-Mails mit denselben Terminvorschlägen erhalten, kann es zu "Race Conditions" kommen, bei denen mehrere Personen denselben Slot fast zeitgleich buchen. +* **Lösung:** Implementierung eines "Live-Checks" im Feedback-Server. + 1. **Trigger:** Ein Nutzer klickt auf einen Buchungslink. + 2. **Aktion:** Bevor der Termin im Kalender erstellt wird, sendet der Server eine *erneute* `getSchedule`-Anfrage an die Graph API für exakt diesen Zeit-Slot. + 3. **Logik:** + * **Slot frei:** Der Termin wird wie geplant gebucht und der Job-Status auf `booked` gesetzt. + * **Slot belegt:** Der Nutzer erhält eine freundliche Nachricht ("Dieser Termin wurde gerade vergeben."). Idealerweise werden ihm dynamisch zwei neue, freie Termine vorgeschlagen, die er direkt auf der Seite buchen kann. +* **Ziel:** Sicherstellen, dass der Kalender die "Single Source of Truth" ist und doppelte Buchungen zuverlässig verhindert werden. + +### Task: Integration der Buchungs-Seiten in WordPress +* **Ziel:** Eine nahtlose User Experience schaffen, bei der Termin-Bestätigungen auf der Haupt-Website (`robo-planet.de`) statt auf der direkten API-URL angezeigt werden. +* **Phase 1 (Kurzfristig): Einbettung via iFrame** + * **Umsetzung:** Eine Seite in WordPress anlegen und die URL des Feedback-Servers (z.B. `https://floke-ai.duckdns.org/feedback/book_slot/...`) in einem iFrame laden. + * **Vorteil:** Kein Programmieraufwand auf unserer Seite nötig, sofort umsetzbar. +* **Phase 2 (Langfristig): Native API-Integration** + * **Umsetzung:** Die Links in der E-Mail führen direkt zu einer WordPress-Seite (z.B. `robo-planet.de/termin-bestaetigen`). Ein Skript auf dieser Seite ruft im Hintergrund unsere `/book_slot` API auf. + * **Vorteil:** Perfekte Integration ins Corporate Design, volle Kontrolle über die Erfolgs- und Fehlermeldungen. Die API ist dafür bereits ausgelegt. + ```env # Info-Postfach (App 1 - Schreiben) diff --git a/lead-engine/trading_twins/RoboPlanetBannerWebinarEinladung.png b/lead-engine/trading_twins/RoboPlanetBannerWebinarEinladung.png new file mode 100644 index 00000000..e69de29b diff --git a/lead-engine/trading_twins/manager.py b/lead-engine/trading_twins/manager.py index f2273dcb..608ec520 100644 --- a/lead-engine/trading_twins/manager.py +++ b/lead-engine/trading_twins/manager.py @@ -28,6 +28,7 @@ DEFAULT_WAIT_MINUTES = 5 SENDER_EMAIL = os.getenv("SENDER_EMAIL", "info@robo-planet.de") TEST_RECEIVER_EMAIL = "floke.com@gmail.com" SIGNATURE_FILE_PATH = os.path.join(os.path.dirname(__file__), "signature.html") +BANNER_FILE_PATH = os.path.join(os.path.dirname(__file__), "RoboPlanetBannerWebinarEinladung.png") # Credentials AZURE_CLIENT_ID = os.getenv("INFO_Application_ID") @@ -156,12 +157,25 @@ def book_slot(job_uuid: str, ts: int): db.close(); return Response("Fehler bei Kalender.", 500) # --- Workflow Logic --- -def send_email(subject, body, to_email, signature): +def send_email(subject, body, to_email, signature, banner_path=None): + attachments = [] + if banner_path and os.path.exists(banner_path): + with open(banner_path, "rb") as f: + content_bytes = f.read() + content_b64 = base64.b64encode(content_bytes).decode("utf-8") + attachments.append({ + "@odata.type": "#microsoft.graph.fileAttachment", + "name": "RoboPlanetBannerWebinarEinladung.png", + "contentBytes": content_b64, + "isInline": True, + "contentId": "banner_image" + }) catchall = os.getenv("EMAIL_CATCHALL"); to_email = catchall if catchall else to_email token = get_access_token(AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID) if not token: return headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} payload = {"message": {"subject": subject, "body": {"contentType": "HTML", "content": body + signature}, "toRecipients": [{"emailAddress": {"address": to_email}}]}, "saveToSentItems": "true"} + if attachments: payload["message"]["attachments"] = attachments requests.post(f"{GRAPH_API_ENDPOINT}/users/{SENDER_EMAIL}/sendMail", headers=headers, json=payload) def process_lead(request_id, company, opener, receiver, name): @@ -186,8 +200,10 @@ def process_lead(request_id, company, opener, receiver, name): for s in suggestions: db.add(ProposedSlot(job_id=job.id, start_time=s, end_time=s+timedelta(minutes=15))) db.commit() - card = {"type": "message", "attachments": [{"contentType": "application/vnd.microsoft.card.adaptive", "content": {"type": "AdaptiveCard", "version": "1.4", "body": [{"type": "TextBlock", "text": f"🤖 E-Mail an {company}?"}], "actions": [{"type": "Action.OpenUrl", "title": "STOP", "url": f"{FEEDBACK_SERVER_BASE_URL}/stop/{request_id}"},{"type": "Action.OpenUrl", "title": "JETZT", "url": f"{FEEDBACK_SERVER_BASE_URL}/send_now/{request_id}"}]}}]} - requests.post(TEAMS_WEBHOOK_URL, json=card) + send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) + # Using the more detailed card from teams_notification.py + from .teams_notification import send_approval_card + send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL) send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) while datetime.now(TZ_BERLIN) < send_time: @@ -213,7 +229,7 @@ def process_lead(request_id, company, opener, receiver, name): {booking_html} """ - send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver, sig) + send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver, sig, BANNER_FILE_PATH) job.status = "sent"; db.commit(); db.close() if __name__ == "__main__": diff --git a/lead-engine/trading_twins/signature.html b/lead-engine/trading_twins/signature.html index 41a8cb2b..9bc9a3f1 100644 --- a/lead-engine/trading_twins/signature.html +++ b/lead-engine/trading_twins/signature.html @@ -1,40 +1,13 @@ - - - - - - E-Mail Signatur - - - -

Freundliche Grüße

-

- Elizabeta Melcer
- Inside Sales Managerin -

-

- - RoboPlanet GmbH
- Schatzbogen 39, 81829 München
- T: +49 89 420490-402 | M: +49 175 8334071
- e.melcer@robo-planet.de | www.robo-planet.de -

-

- LinkedIn | Instagram | Newsletteranmeldung -

-

- Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth
- Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410
- Hinweispflichten zum Datenschutz -

-

- RoboPlanet Webinar Einladung -

- - \ No newline at end of file +Freundliche Grüße
+Elizabeta Melcer
+Inside Sales Managerin
+Wackler Logo
+RoboPlanet GmbH
+Schatzbogen 39, 81829 München
+T: +49 89 420490-402 | M: +49 175 8334071
+e.melcer@robo-planet.de | www.robo-planet.de
+LinkedIn Instagram Newsletteranmeldung
+Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth
+Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410
+Hinweispflichten zum Datenschutz
+RoboPlanet Webinar Einladung \ No newline at end of file diff --git a/lead-engine/trading_twins/teams_notification.py b/lead-engine/trading_twins/teams_notification.py index 0529e579..0cc1fddd 100644 --- a/lead-engine/trading_twins/teams_notification.py +++ b/lead-engine/trading_twins/teams_notification.py @@ -6,13 +6,13 @@ from datetime import datetime # Default-Webhook (Platzhalter) - sollte in .env stehen DEFAULT_WEBHOOK_URL = os.getenv("TEAMS_WEBHOOK_URL", "") -def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT_WEBHOOK_URL): +def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT_WEBHOOK_URL, api_base_url="http://localhost:8004"): """ Sendet eine Adaptive Card an Teams mit Approve/Deny Buttons. """ # Die URL unserer API (muss von außen erreichbar sein, z.B. via ngrok oder Server-IP) - api_base_url = os.getenv("API_BASE_URL", "http://localhost:8004") + card_payload = { "type": "message", @@ -27,33 +27,27 @@ def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT "body": [ { "type": "TextBlock", - "text": f"🤖 Automatisierte E-Mail an {customer_name}", + "text": f"🤖 Automatisierte E-Mail an {customer_name} (via Trading Twins) wird um {time_string} Uhr ausgesendet.", "weight": "Bolder", "size": "Medium" }, { "type": "TextBlock", - "text": f"(via Trading Twins) wird um {time_string} Uhr ausgesendet.", + "text": "Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.", "isSubtle": True, "wrap": True - }, - { - "type": "TextBlock", - "text": "Wenn Du bis dahin NICHT reagierst, wird die E-Mail automatisch gesendet.", - "color": "Attention", - "wrap": True } ], "actions": [ { "type": "Action.OpenUrl", "title": "✅ JETZT Aussenden", - "url": f"{api_base_url}/action/approve/{job_uuid}" + "url": f"{api_base_url}/send_now/{job_uuid}" }, { "type": "Action.OpenUrl", "title": "❌ STOP Aussendung", - "url": f"{api_base_url}/action/cancel/{job_uuid}" + "url": f"{api_base_url}/stop/{job_uuid}" } ] }