# CRM Step 04 — Backend: Comms Log + Media (SQLite) ## Context Read `.claude/crm-build-plan.md` for full schema, conventions, and IMPORTANT NOTES. Steps 01–03 must be complete. ## Task Add `crm_comms_log` and `crm_media` tables to the existing SQLite DB, plus CRUD endpoints. ## What to build ### 1. Add tables to `backend/mqtt/database.py` Inside `init_db()`, add these CREATE TABLE IF NOT EXISTS statements alongside existing tables: ```sql CREATE TABLE IF NOT EXISTS crm_comms_log ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL, type TEXT NOT NULL, direction TEXT NOT NULL, subject TEXT, body TEXT, attachments TEXT DEFAULT '[]', ext_message_id TEXT, logged_by TEXT, occurred_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS crm_media ( id TEXT PRIMARY KEY, customer_id TEXT, order_id TEXT, filename TEXT NOT NULL, nextcloud_path TEXT NOT NULL, mime_type TEXT, direction TEXT, tags TEXT DEFAULT '[]', uploaded_by TEXT, created_at TEXT NOT NULL ); ``` ### 2. Add to `backend/crm/models.py` **Comms:** - `CommType` enum — email, whatsapp, call, sms, note, in_person - `CommDirection` enum — inbound, outbound, internal - `CommAttachment` — filename (str), nextcloud_path (str) - `CommCreate` — customer_id, type (CommType), direction (CommDirection), subject (Optional[str]), body (Optional[str]), attachments (List[CommAttachment] default []), ext_message_id (Optional[str]), logged_by (Optional[str]), occurred_at (str ISO — default to now if not provided) - `CommUpdate` — subject, body, occurred_at all Optional - `CommInDB` — all fields + id, created_at - `CommListResponse` — entries: List[CommInDB], total: int **Media:** - `MediaDirection` enum — received, sent, internal - `MediaCreate` — customer_id (Optional[str]), order_id (Optional[str]), filename, nextcloud_path, mime_type (Optional), direction (MediaDirection optional), tags (List[str] default []), uploaded_by (Optional[str]) - `MediaInDB` — all fields + id, created_at - `MediaListResponse` — items: List[MediaInDB], total: int ### 3. Add to `backend/crm/service.py` Import `from mqtt import database as mqtt_db` for aiosqlite access. **Comms functions (all async):** - `list_comms(customer_id, type=None, direction=None, limit=100) -> List[CommInDB]` — SELECT ... WHERE customer_id=? ORDER BY occurred_at DESC - `get_comm(comm_id) -> CommInDB` — 404 if not found - `create_comm(data: CommCreate) -> CommInDB` — uuid id, created_at now, store attachments as JSON string - `update_comm(comm_id, data: CommUpdate) -> CommInDB` - `delete_comm(comm_id) -> None` **Media functions (all async):** - `list_media(customer_id=None, order_id=None) -> List[MediaInDB]` - `create_media(data: MediaCreate) -> MediaInDB` - `delete_media(media_id) -> None` Parse `attachments` and `tags` JSON strings back to lists when returning models. ### 4. Add to `backend/crm/router.py` Prefix `/api/crm/comms`: - `GET /` — list_comms (query: customer_id required, type, direction) - `POST /` — create_comm - `PUT /{comm_id}` — update_comm - `DELETE /{comm_id}` — delete_comm Prefix `/api/crm/media`: - `GET /` — list_media (query: customer_id or order_id) - `POST /` — create_media (metadata only — no file upload here, that's Step 9) - `DELETE /{media_id}` — delete_media Register both in `backend/main.py`. ## Notes - Use `mqtt_db.db` — it is an aiosqlite connection, use `async with mqtt_db.db.execute(...)` pattern - Look at `backend/mqtt/database.py` for exact aiosqlite usage pattern - attachments and tags are stored as JSON strings in SQLite, deserialized to lists in the Pydantic model