Files
bellsystems-cp/.claude/crm-step-12.md

3.9 KiB
Raw Blame History

CRM Step 12 — Integration: FreePBX AMI Call Logging

Context

Read .claude/crm-build-plan.md for full context and IMPORTANT NOTES. Steps 0111 must be complete.

Prerequisites (manual setup before this step)

  • FreePBX server with AMI (Asterisk Manager Interface) enabled
  • An AMI user created in FreePBX: Admin → Asterisk Manager Users
    • Username + password (set these in config)
    • Permissions needed: read = "call,cdr" (minimum)
  • Network access from VPS to FreePBX AMI port (default: 5038)
  • Values ready: AMI_HOST, AMI_PORT (5038), AMI_USERNAME, AMI_PASSWORD

Task

Connect to FreePBX AMI over TCP, listen for call events, and auto-log them to crm_comms_log matched against customer phone numbers.

Backend changes

1. Add to backend/config.py

ami_host: str = ""
ami_port: int = 5038
ami_username: str = ""
ami_password: str = ""

2. Create backend/crm/ami_listener.py

AMI uses a plain TCP socket with a text protocol (key: value\r\n pairs, events separated by \r\n\r\n).

import asyncio
from config import settings
from mqtt import database as mqtt_db

async def ami_connect_and_listen():
    """
    1. Open TCP connection to ami_host:ami_port
    2. Read the banner line
    3. Send login action:
         Action: Login\r\n
         Username: {ami_username}\r\n
         Secret: {ami_password}\r\n\r\n
    4. Read response — check for "Response: Success"
    5. Loop reading events. Parse each event block into a dict.
    6. Handle Event: Hangup:
         - CallerID: the phone number (field: CallerIDNum)
         - Duration: call duration seconds (field: Duration, may not always be present)
         - Channel direction: inbound if DestChannel starts with "PJSIP/" or "SIP/",
           outbound if Channel starts with "PJSIP/" or "SIP/"
         - Normalize CallerIDNum: strip leading + and spaces
         - Look up customer by normalized phone
         - Create crm_comms_log entry: type=call, direction=inbound|outbound,
           body=f"Call duration: {duration}s", ext_message_id=Uniqueid field
    7. On disconnect: wait 30s, reconnect. Infinite retry loop.
    """

async def start_ami_listener():
    """Entry point — only starts if ami_host is set."""
    if not settings.ami_host:
        return
    asyncio.create_task(ami_connect_and_listen())

3. Add to backend/main.py startup

from crm.ami_listener import start_ami_listener
# in startup():
await start_ami_listener()

4. Add manual log endpoint to backend/crm/router.py

POST /api/crm/calls/log Body: { customer_id, direction, duration_seconds, notes, occurred_at } Requires auth. → create crm_comms_log entry (type=call) manually → useful if auto-logging misses a call or for logging calls made outside the office

Frontend changes

Update Comms tab in CustomerDetail.jsx

  • Call entries: amber/yellow color, phone icon
  • Show duration if available (parse from body)
  • "Log Call" button → quick modal with: direction (inbound/outbound), duration (minutes + seconds), notes, occurred_at
  • On save: POST /api/crm/calls/log

Update InboxPage.jsx

  • Add "Call" to type filter options
  • Call entries show customer name, direction arrow, duration

Notes

  • AMI protocol reference: each event/response is a block of Key: Value lines terminated by \r\n\r\n
  • The Hangup event fires at end of call and includes Duration in seconds
  • CallerIDNum for inbound calls is the caller's number. For outbound it's typically the extension — may need to use DestCallerIDNum instead. Test against your FreePBX setup.
  • Phone matching uses the same normalization as WhatsApp step (strip +, spaces, leading zeros if needed)
  • If AMI connection drops (FreePBX restart, network blip), the reconnect loop handles it silently
  • This gives you: auto-logged inbound calls matched to customers, duration recorded, plus a manual log option for anything missed