diff --git a/voxtelesys_integration/api.py b/voxtelesys_integration/api.py index 7368fb8..5083b0a 100644 --- a/voxtelesys_integration/api.py +++ b/voxtelesys_integration/api.py @@ -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:///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"
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