"""Operational feed mock store. Spec 041 · OF-API-001.""" from __future__ import annotations import json import sqlite3 from datetime import datetime, timezone, timedelta from typing import Any CHANNELS = ( ("all", "Todos os canais"), ("email", "Email"), ("whatsapp", "WhatsApp API"), ("voice", "Telefonia / Voz"), ("sms", "SMS"), ("telegram", "Telegram"), ("tickets", "Tickets"), ("agents", "Agentes IA"), ("internal", "Solicitações internas"), ("clients", "Clientes"), ("alerts", "Alertas sistema"), ) PRIORITIES = ("normal", "high", "critical") def _now() -> str: return datetime.now(timezone.utc).isoformat() def _minutes_ago(minutes: int) -> str: return (datetime.now(timezone.utc) - timedelta(minutes=minutes)).isoformat() def init_inbox_schema(conn: sqlite3.Connection) -> None: conn.executescript( """ CREATE TABLE IF NOT EXISTS ops_inbox_events ( id TEXT PRIMARY KEY, channel TEXT NOT NULL, event_type TEXT NOT NULL, priority TEXT NOT NULL DEFAULT 'normal', title TEXT NOT NULL, preview TEXT NOT NULL, tags_json TEXT NOT NULL DEFAULT '[]', assignee TEXT, status TEXT NOT NULL DEFAULT 'open', contact_name TEXT, contact_company TEXT, contact_cnpj TEXT, contact_client_id TEXT, sla_minutes INTEGER NOT NULL DEFAULT 60, sla_remaining_sec INTEGER NOT NULL DEFAULT 3600, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS ops_inbox_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id TEXT NOT NULL, author_type TEXT NOT NULL, author_label TEXT NOT NULL, body TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY (event_id) REFERENCES ops_inbox_events(id) ); CREATE INDEX IF NOT EXISTS idx_ops_inbox_events_channel ON ops_inbox_events(channel); CREATE INDEX IF NOT EXISTS idx_ops_inbox_events_status ON ops_inbox_events(status); """ ) count = conn.execute("SELECT COUNT(*) c FROM ops_inbox_events").fetchone()["c"] if count == 0: _seed_inbox(conn) def _seed_inbox(conn: sqlite3.Connection) -> None: now = _now() seeds = [ { "id": "evt-wa-001", "channel": "whatsapp", "event_type": "message", "priority": "high", "title": "Cliente: Empresa Alpha — Não consigo acessar o painel financeiro", "preview": "Bom dia, após o login aparece erro 403 no módulo billing…", "tags": ["Cliente", "Acesso", "Infraestrutura"], "assignee": "Editor", "status": "open", "contact_name": "Empresa Alpha", "contact_company": "Empresa Alpha Ltda", "contact_cnpj": "12.345.678/0001-90", "contact_client_id": "CLI-8842", "sla_minutes": 15, "sla_remaining_sec": 750, "created_at": _minutes_ago(2), }, { "id": "evt-int-002", "channel": "internal", "event_type": "request", "priority": "normal", "title": "Carlos — Financeiro: Aprovar reembolso #8821", "preview": "Solicitação interna aguardando aprovação do Chefe Ops", "tags": ["Interno", "Financeiro"], "assignee": "Super Admin", "status": "pending", "contact_name": "Carlos Mendes", "contact_company": "Ligbox Ops", "contact_cnpj": "", "contact_client_id": "", "sla_minutes": 120, "sla_remaining_sec": 5400, "created_at": _minutes_ago(18), }, { "id": "evt-ai-003", "channel": "agents", "event_type": "alert", "priority": "critical", "title": "Agente A3 — CPU VM122 acima de 92%", "preview": "Alerta automático: load average 4.2 — recomendação de escala", "tags": ["Agente IA", "Infra", "VM122"], "assignee": "AI Agent", "status": "open", "contact_name": "Watchman A3", "contact_company": "Ligbox Platform", "contact_cnpj": "", "contact_client_id": "", "sla_minutes": 30, "sla_remaining_sec": 1200, "created_at": _minutes_ago(5), }, { "id": "evt-em-004", "channel": "email", "event_type": "message", "priority": "normal", "title": "Empresa Beta — Pedido de upgrade de plano", "preview": "Gostaríamos de migrar para o plano enterprise…", "tags": ["Cliente", "Comercial"], "assignee": "Sales Admin", "status": "open", "contact_name": "Empresa Beta", "contact_company": "Beta Serviços SA", "contact_cnpj": "98.765.432/0001-10", "contact_client_id": "CLI-1201", "sla_minutes": 240, "sla_remaining_sec": 12000, "created_at": _minutes_ago(45), }, { "id": "evt-tk-005", "channel": "tickets", "event_type": "ticket", "priority": "high", "title": "Ticket #4412 — Erro importação DNS", "preview": "Falha ao sincronizar zona dns.example.com", "tags": ["Ticket", "DNS"], "assignee": "NOC", "status": "open", "contact_name": "Suporte N1", "contact_company": "Cliente Gamma", "contact_cnpj": "", "contact_client_id": "CLI-3300", "sla_minutes": 60, "sla_remaining_sec": 2100, "created_at": _minutes_ago(12), }, { "id": "evt-vc-006", "channel": "voice", "event_type": "missed_call", "priority": "normal", "title": "Chamada perdida — +55 11 98765-4321", "preview": "Duração 0s · fila comercial", "tags": ["Telefonia"], "assignee": None, "status": "open", "contact_name": "Desconhecido", "contact_company": "", "contact_cnpj": "", "contact_client_id": "", "sla_minutes": 30, "sla_remaining_sec": 900, "created_at": _minutes_ago(8), }, ] for s in seeds: conn.execute( """ INSERT INTO ops_inbox_events (id, channel, event_type, priority, title, preview, tags_json, assignee, status, contact_name, contact_company, contact_cnpj, contact_client_id, sla_minutes, sla_remaining_sec, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( s["id"], s["channel"], s["event_type"], s["priority"], s["title"], s["preview"], json.dumps(s["tags"]), s["assignee"], s["status"], s["contact_name"], s["contact_company"], s["contact_cnpj"], s["contact_client_id"], s["sla_minutes"], s["sla_remaining_sec"], s["created_at"], s["created_at"], ), ) msgs = [ ("evt-wa-001", "user", "Empresa Alpha", "Bom dia, não consigo acessar o painel financeiro."), ("evt-wa-001", "agent", "AI Agent", "Detectei erro 403 no módulo billing. Encaminhei para suporte."), ("evt-wa-001", "system", "Sistema", "SLA iniciado — 15 min"), ] for event_id, author_type, author_label, body in msgs: conn.execute( """ INSERT INTO ops_inbox_messages (event_id, author_type, author_label, body, created_at) VALUES (?, ?, ?, ?, ?) """, (event_id, author_type, author_label, body, now), ) def inbox_stats(conn: sqlite3.Connection) -> dict: total = conn.execute("SELECT COUNT(*) c FROM ops_inbox_events").fetchone()["c"] pending = conn.execute( "SELECT COUNT(*) c FROM ops_inbox_events WHERE status IN ('open','pending')" ).fetchone()["c"] critical = conn.execute( "SELECT COUNT(*) c FROM ops_inbox_events WHERE priority = 'critical'" ).fetchone()["c"] channels = [] for cid, label in CHANNELS: if cid == "all": cnt = total else: cnt = conn.execute( "SELECT COUNT(*) c FROM ops_inbox_events WHERE channel = ?", (cid,), ).fetchone()["c"] channels.append({"id": cid, "label": label, "count": cnt}) return { "events_today": total, "pending": pending, "critical": critical, "awaiting_you": conn.execute( "SELECT COUNT(*) c FROM ops_inbox_events WHERE assignee IS NOT NULL AND status = 'open'" ).fetchone()["c"], "sla_avg_pct": 96, "channels": channels, } def list_events( conn: sqlite3.Connection, *, channel: str | None = None, priority: str | None = None, status: str | None = None, q: str | None = None, limit: int = 128, ) -> list[dict]: sql = "SELECT * FROM ops_inbox_events WHERE 1=1" params: list[Any] = [] if channel and channel != "all": sql += " AND channel = ?" params.append(channel) if priority: sql += " AND priority = ?" params.append(priority) if status: sql += " AND status = ?" params.append(status) if q: sql += " AND (title LIKE ? OR preview LIKE ? OR contact_name LIKE ?)" like = f"%{q}%" params.extend([like, like, like]) sql += " ORDER BY datetime(created_at) DESC LIMIT ?" params.append(limit) rows = conn.execute(sql, params).fetchall() out = [] for row in rows: item = dict(row) try: item["tags"] = json.loads(item.pop("tags_json") or "[]") except json.JSONDecodeError: item["tags"] = [] out.append(item) return out def get_event(conn: sqlite3.Connection, event_id: str) -> dict | None: row = conn.execute("SELECT * FROM ops_inbox_events WHERE id = ?", (event_id,)).fetchone() if not row: return None item = dict(row) item["tags"] = json.loads(item.pop("tags_json") or "[]") msgs = conn.execute( """ SELECT id, author_type, author_label, body, created_at FROM ops_inbox_messages WHERE event_id = ? ORDER BY id ASC """, (event_id,), ).fetchall() item["messages"] = [dict(m) for m in msgs] return item def add_message(conn: sqlite3.Connection, event_id: str, author_type: str, author_label: str, body: str) -> dict: now = _now() cur = conn.execute( """ INSERT INTO ops_inbox_messages (event_id, author_type, author_label, body, created_at) VALUES (?, ?, ?, ?, ?) """, (event_id, author_type, author_label, body.strip(), now), ) conn.execute( "UPDATE ops_inbox_events SET updated_at = ? WHERE id = ?", (now, event_id), ) return {"id": cur.lastrowid, "event_id": event_id, "author_type": author_type, "author_label": author_label, "body": body.strip(), "created_at": now} def patch_event(conn: sqlite3.Connection, event_id: str, **fields: Any) -> dict | None: allowed = {"status", "assignee", "priority"} updates = [] params: list[Any] = [] for k, v in fields.items(): if k in allowed and v is not None: updates.append(f"{k} = ?") params.append(v) if not updates: ev = get_event(conn, event_id) return ev updates.append("updated_at = ?") params.append(_now()) params.append(event_id) conn.execute( f"UPDATE ops_inbox_events SET {', '.join(updates)} WHERE id = ?", params, ) return get_event(conn, event_id)