3.9 KiB
3.9 KiB
CRM Step 12 — Integration: FreePBX AMI Call Logging
Context
Read .claude/crm-build-plan.md for full context and IMPORTANT NOTES.
Steps 01–11 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: Valuelines terminated by\r\n\r\n - The
Hangupevent 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
DestCallerIDNuminstead. 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