v2.1: docs + boot_session sms_enabled + legacy api header: api.py
This commit is contained in:
parent
f68cae0c4a
commit
b977800868
1 changed files with 26 additions and 37 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue