commit a64a145f001baf83b36befb447f9583d80b3a79a Author: root Date: Thu Jul 24 09:17:20 2025 +0000 Production files for contact form instance diff --git a/contact-api/Dockerfile b/contact-api/Dockerfile new file mode 100644 index 0000000..de1edaa --- /dev/null +++ b/contact-api/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY contact_api.py ticket_handler.py . + +EXPOSE 8000 + +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "contact_api:app"] + diff --git a/contact-api/contact_api.py b/contact-api/contact_api.py new file mode 100644 index 0000000..d799680 --- /dev/null +++ b/contact-api/contact_api.py @@ -0,0 +1,38 @@ +from flask import Flask, request, jsonify +from ticket_handler import process_submission +import logging + +app = Flask(__name__) +logging.basicConfig(level=logging.INFO) + +@app.route("/status", methods=["GET"]) +def status(): + return jsonify({"status": "ok"}), 200 + +@app.route("/contact", methods=["POST"]) +def contact(): + data = request.get_json() or {} + try: + full_name = data.get("name", "").strip() + email = data.get("email", "").strip() + message = data.get("message", "").strip() + + # split full name + parts = full_name.split() + first_name = parts[0] if parts else "" + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + + process_submission({ + "first_name": first_name, + "last_name": last_name, + "email": email, + "message": message + }) + + return jsonify({"status": "success"}), 200 + except Exception as e: + logging.exception("❌ Error in /contact:") + return jsonify({"status": "error", "message": str(e)}), 500 + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) diff --git a/contact-api/requirements.txt b/contact-api/requirements.txt new file mode 100644 index 0000000..e3562d5 --- /dev/null +++ b/contact-api/requirements.txt @@ -0,0 +1,3 @@ +flask +requests +gunicorn diff --git a/contact-api/ticket_handler.py b/contact-api/ticket_handler.py new file mode 100644 index 0000000..6d7a4c7 --- /dev/null +++ b/contact-api/ticket_handler.py @@ -0,0 +1,163 @@ +import requests +import json + +# ————————————— Environment / Credentials ————————————— +TWENTY_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmY2UyYjc4OS1iY2Y4LTQzMjAtYjEyZS0yMjUyZDZkNzhlZjUiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiZmNlMmI3ODktYmNmOC00MzIwLWIxMmUtMjI1MmQ2ZDc4ZWY1IiwiaWF0IjoxNzUwODU4OTI4LCJleHAiOjQ5MDQ0NTg5MjcsImp0aSI6ImNiOTcxMjEyLWExYjAtNGU5NS1hYjI5LWU3YjdiN2E4YWUwMyJ9.mujCJqNnnk0xcQ0VP8m9uLzzXDtjH9AGi0RF2I8VXHA" +TWENTY_API_BASE = "https://crm.toothfairyai.com/rest" +ZAMMAD_URL = "https://zammad.toothfairyai.com" +ZAMMAD_TOKEN = "Ljej6w4vyEp4P4ES95Fi8bxL4Yj6h1G5y7SXMnWnx-EFKDPE6q3wkMdWXSKHy062" +ZAMMAD_GROUP = "Users" + +GRAPH_TENANT_ID = "3ef5b73c-24f4-4f88-b1dd-c136f2ceaf1f" +GRAPH_CLIENT_ID = "6a426d19-7f5b-4ab3-9fe8-b6b457f3375b" +GRAPH_CLIENT_SECRET = "Vpu8Q~z4qFxIcFDIWM664bcxN5KU1PgDMk91kaNm" +GRAPH_FROM_ADDRESS = "support@toothfairyai.com" + +CUSTOMER_REPLY_SUBJECT = "Thanks for contacting ToothFairyAI" +SALES_EMAIL = "daniel.grozdanovic@icloud.com" +SUPPORT_EMAIL = "daniel.grozdanovic@toothfairyai.com" + +# ————————————— Helpers ————————————— + +def is_business_opportunity(text): + keywords = ["partner", "quote", "enterprise", "demo", "pricing", "ai", "onboarding"] + return any(kw in text.lower() for kw in keywords) + +def push_to_twenty(sub): + url = f"{TWENTY_API_BASE}/people" + headers = { + "Authorization": f"Bearer {TWENTY_API_KEY}", + "Content-Type": "application/json" + } + payload = { + "name": { + "firstName": sub["first_name"], + "lastName": sub["last_name"] + }, + "position": "first", + "emails": { + "primaryEmail": sub["email"], + "additionalEmails": [] + } + } + res = requests.post(url, headers=headers, json=payload) + res.raise_for_status() + return res.json()["data"]["createPerson"]["id"] + +def create_zammad_ticket(submission, is_lead): + import json + + url = f"{ZAMMAD_URL}/api/v1/tickets" + headers = { + "Authorization": f"Token token={ZAMMAD_TOKEN}", + "Content-Type": "application/json" + } + + name = f"{submission['first_name']} {submission['last_name']}".strip() + ticket_title = f"{'Sales' if is_lead else 'Other'}: {name}" + message = submission["message"] + real_email = submission["email"] + customer = "support@toothfairyai.com" # hardcoded Zammad user + + article = { + "subject": ticket_title, + "body": f"Name: {name}\nEmail: {real_email}\n\nMessage:\n{message}", + "note": message, + "type": "note", # ✅ NOT "email" + "internal": False + } + + payload = { + "title": ticket_title, + "group": ZAMMAD_GROUP, + "customer": customer, + "article": article + } + + res = requests.post(url, headers=headers, json=payload) + if res.status_code not in (200, 201): + print("→ Zammad create-ticket ERROR:", res.status_code, res.text) + print("→ Payload was:", json.dumps(payload, indent=2)) + res.raise_for_status() + + return res.json() + +def tag_zammad_ticket(ticket_id, tag): + url = f"{ZAMMAD_URL}/api/v1/tickets/{ticket_id}" + headers = { + "Authorization": f"Token token={ZAMMAD_TOKEN}", + "Content-Type": "application/json" + } + res = requests.put(url, headers=headers, json={"tags": [tag]}) + res.raise_for_status() + return res.json() + +def get_graph_token(): + url = f"https://login.microsoftonline.com/{GRAPH_TENANT_ID}/oauth2/v2.0/token" + data = { + "client_id": GRAPH_CLIENT_ID, + "client_secret": GRAPH_CLIENT_SECRET, + "grant_type": "client_credentials", + "scope": "https://graph.microsoft.com/.default" + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + res = requests.post(url, data=data, headers=headers) + res.raise_for_status() + return res.json()["access_token"] + +def send_email(to, subject, body): + token = get_graph_token() + url = f"https://graph.microsoft.com/v1.0/users/{GRAPH_FROM_ADDRESS}/sendMail" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + payload = { + "message": { + "subject": subject, + "body": { + "contentType": "Text", + "content": body + }, + "toRecipients": [ + {"emailAddress": {"address": to}} + ] + } + } + res = requests.post(url, headers=headers, json=payload) + if res.status_code != 202: + print(f"❌ Graph email failed: {res.status_code} {res.text}") + +# ————————————— Orchestrator ————————————— + +def process_submission(sub): + is_lead = is_business_opportunity(sub["message"]) + crm_id = push_to_twenty(sub) + tag = "sales" if is_lead else "other" + + ticket = create_zammad_ticket(sub, is_lead) + tag_zammad_ticket(ticket["id"], tag) + + send_email( + to=sub["email"], + subject=CUSTOMER_REPLY_SUBJECT, + body=( + f"Hi {sub['first_name']},\n\n" + f"Your ticket “{ticket['title']}” (ID #{ticket['id']}) is in our system—" + "we'll be in touch soon.\n\nCheers,\nThe ToothFairyAI Team" + ) + ) + + internal_to = SALES_EMAIL if is_lead else SUPPORT_EMAIL + internal_subj = "🚀 New Business Lead" if is_lead else "ℹ️ New Inquiry" + send_email( + to=internal_to, + subject=internal_subj, + body=( + f"CRM ID: {crm_id}\n" + f"Zammad Ticket: #{ticket['id']} (Title: {ticket['title']}, Tag: {tag})\n" + f"Name: {sub['first_name']} {sub['last_name']}\n" + f"Email: {sub['email']}\n" + f"Message:\n{sub['message']}" + ) + )