v2.1: docs + boot_session sms_enabled + legacy api header: api.py

This commit is contained in:
Zac Gaetano 2026-05-12 00:14:30 -04:00
parent f68cae0c4a
commit b977800868

View file

@ -1,37 +1,35 @@
"""
voxtelesys_integration.api
==========================
voxtelesys_integration.api (LEGACY 3CX bridge path)
====================================================
Single whitelisted endpoint called by 3CX Call Flow Designer (CF_URLFetch)
when an inbound call arrives.
This module implements the v1.x "3CX webhook" architecture where 3CX is the
PBX call broker, the Voxtelesys SIP trunk is just the carrier, and ERPNext
is a passive ticket creator notified by 3CX's CF_URLFetch block.
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)
For the v2.x direct-API architecture (ERPNext talks to Voxtelesys APIs and
brokers calls itself), see api/voxtelesys.py and api/sms.py.
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).
Both paths can coexist. Use this one if your existing 3CX deployment is
already routing calls and you only want auto-ticketing in ERPNext; use the
direct path if you want click-to-call, agent routing, SMS, and call
recording driven from ERPNext.
"""
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.
"""3CX CF_URLFetch entrypoint — creates an HD Ticket on every inbound call.
Endpoint URL:
https://<site>/api/method/voxtelesys_integration.api.inbound_call
3CX passes:
caller_id inbound caller's number (e.g. +12025551234)
called_did DID that was dialled (optional, for routing info)
call_id 3CX internal call ID (optional, for deduplication)
"""
_validate_secret()
@ -40,7 +38,6 @@ def inbound_call(caller_id: str = "", called_did: str = "", call_id: str = "", *
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",
@ -50,7 +47,6 @@ def inbound_call(caller_id: str = "", called_did: str = "", call_id: str = "", *
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:
@ -62,13 +58,11 @@ def inbound_call(caller_id: str = "", called_did: str = "", call_id: str = "", *
+ (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
@ -76,34 +70,29 @@ def inbound_call(caller_id: str = "", called_did: str = "", call_id: str = "", *
frappe.db.commit()
frappe.logger().info(
f"[Voxtelesys] Created HD Ticket {ticket.name} for inbound call from {caller_id}"
f"[Voxtelesys/3CX] 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.log_error(frappe.get_traceback(), "Voxtelesys/3CX: 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.
"""
"""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
return
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.
"""
"""Loose suffix match on Contact Phone — strips +, spaces, dashes."""
if not phone_number:
return None, None