Production files for contact form instance
This commit is contained in:
commit
a64a145f00
13
contact-api/Dockerfile
Normal file
13
contact-api/Dockerfile
Normal file
@ -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"]
|
||||||
|
|
38
contact-api/contact_api.py
Normal file
38
contact-api/contact_api.py
Normal file
@ -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)
|
3
contact-api/requirements.txt
Normal file
3
contact-api/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
requests
|
||||||
|
gunicorn
|
163
contact-api/ticket_handler.py
Normal file
163
contact-api/ticket_handler.py
Normal file
@ -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']}"
|
||||||
|
)
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user