# 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` ```python 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). ```python 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 ```python 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