Add voxtelesys_integration/api.py
This commit is contained in:
parent
f77b855a15
commit
ac64d86e99
1 changed files with 127 additions and 0 deletions
127
voxtelesys_integration/api.py
Normal file
127
voxtelesys_integration/api.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
"""
|
||||||
|
voxtelesys_integration.api
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Single whitelisted endpoint called by 3CX Call Flow Designer (CF_URLFetch)
|
||||||
|
when an inbound call arrives.
|
||||||
|
|
||||||
|
3CX sends a POST with these variables set in the Call Flow:
|
||||||
|
caller_id - the inbound caller's number (e.g. +12025551234)
|
||||||
|
called_did - the DID that was dialled (optional, for routing info)
|
||||||
|
call_id - 3CX internal call ID (optional, for deduplication)
|
||||||
|
|
||||||
|
The endpoint creates an HD Ticket in Frappe Helpdesk and returns JSON.
|
||||||
|
|
||||||
|
Endpoint URL to put in 3CX:
|
||||||
|
https://erp.broadcastmgmt.cloud/api/method/voxtelesys_integration.api.inbound_call
|
||||||
|
|
||||||
|
No authentication required (allow_guest=True) — protect via 3CX secret token
|
||||||
|
in the request if desired (see WEBHOOK_SECRET below).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
# Optional: set this in Site Config as voxtelesys_webhook_secret = "yourtoken"
|
||||||
|
# 3CX should pass it as ?secret=yourtoken in the URL.
|
||||||
|
# Leave blank to accept all requests (fine if ERPNext is behind a firewall).
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def inbound_call(caller_id: str = "", called_did: str = "", call_id: str = "", **kwargs):
|
||||||
|
"""
|
||||||
|
Called by 3CX CF_URLFetch on every inbound call.
|
||||||
|
Creates an HD Ticket and returns the ticket name.
|
||||||
|
"""
|
||||||
|
_validate_secret()
|
||||||
|
|
||||||
|
caller_id = (caller_id or "").strip()
|
||||||
|
if not caller_id:
|
||||||
|
frappe.local.response["http_status_code"] = 400
|
||||||
|
return {"ok": False, "error": "caller_id is required"}
|
||||||
|
|
||||||
|
# Deduplication: if a ticket was already created for this call_id, return it
|
||||||
|
if call_id:
|
||||||
|
existing = frappe.db.get_value(
|
||||||
|
"HD Ticket",
|
||||||
|
{"custom_3cx_call_id": call_id},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return {"ok": True, "ticket": existing, "duplicate": True}
|
||||||
|
|
||||||
|
# Look up a Contact matching the caller number so we can pre-fill the ticket
|
||||||
|
contact_name, contact_email = _find_contact(caller_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ticket = frappe.new_doc("HD Ticket")
|
||||||
|
ticket.subject = f"Inbound Call from {caller_id}"
|
||||||
|
ticket.description = (
|
||||||
|
f"An inbound call was received from <b>{frappe.utils.escape_html(caller_id)}</b>."
|
||||||
|
+ (f"<br>DID dialled: {frappe.utils.escape_html(called_did)}" if called_did else "")
|
||||||
|
+ (f"<br>3CX Call ID: {frappe.utils.escape_html(call_id)}" if call_id else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pre-fill contact/email if we found a match
|
||||||
|
if contact_email:
|
||||||
|
ticket.raised_by = contact_email
|
||||||
|
if contact_name:
|
||||||
|
ticket.contact = contact_name
|
||||||
|
|
||||||
|
# Store the 3CX call ID for deduplication (requires custom field — see README)
|
||||||
|
if call_id and frappe.db.has_column("HD Ticket", "custom_3cx_call_id"):
|
||||||
|
ticket.custom_3cx_call_id = call_id
|
||||||
|
|
||||||
|
ticket.insert(ignore_permissions=True)
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
frappe.logger().info(
|
||||||
|
f"[Voxtelesys] Created HD Ticket {ticket.name} for inbound call from {caller_id}"
|
||||||
|
)
|
||||||
|
return {"ok": True, "ticket": ticket.name}
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(frappe.get_traceback(), "Voxtelesys: Failed to create HD Ticket")
|
||||||
|
frappe.local.response["http_status_code"] = 500
|
||||||
|
return {"ok": False, "error": "Failed to create ticket — check Error Log"}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_secret():
|
||||||
|
"""
|
||||||
|
If voxtelesys_webhook_secret is set in site_config.json, require it
|
||||||
|
to be passed as ?secret= in the request URL.
|
||||||
|
"""
|
||||||
|
expected = frappe.conf.get("voxtelesys_webhook_secret")
|
||||||
|
if not expected:
|
||||||
|
return # no secret configured — allow all
|
||||||
|
provided = frappe.request.args.get("secret") or frappe.form_dict.get("secret") or ""
|
||||||
|
if provided != expected:
|
||||||
|
frappe.throw(_("Unauthorized"), frappe.PermissionError)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_contact(phone_number: str):
|
||||||
|
"""
|
||||||
|
Look up a Contact by phone number. Returns (contact_name, email) or (None, None).
|
||||||
|
Strips +, spaces, and dashes for a loose suffix match.
|
||||||
|
"""
|
||||||
|
if not phone_number:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
normalised = phone_number.replace("+", "").replace(" ", "").replace("-", "")
|
||||||
|
|
||||||
|
result = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT cp.parent, c.email_id
|
||||||
|
FROM `tabContact Phone` cp
|
||||||
|
JOIN `tabContact` c ON c.name = cp.parent
|
||||||
|
WHERE REPLACE(REPLACE(REPLACE(cp.phone, '+', ''), ' ', ''), '-', '') LIKE %(num)s
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
{"num": f"%{normalised}"},
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result[0].parent, result[0].email_id
|
||||||
|
|
||||||
|
return None, None
|
||||||
Loading…
Reference in a new issue