98 lines
3.9 KiB
Markdown
98 lines
3.9 KiB
Markdown
# 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
|