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