#!/usr/bin/env python3 """Audita lacunas Chatwoot -> ClientFlow usando payload.message_type. A versão anterior gerava falsos positivos porque assumia sender.type=contact; nos webhooks de email do Chatwoot, inbound chega como message_type=incoming e sender.type pode estar vazio. """ from __future__ import annotations import argparse import csv from pathlib import Path from typing import Any from sqlalchemy import text from app.db import engine OUT_CSV = Path("/tmp/clientflow_chatwoot_ingestion_gap.csv") OUT_MD = Path("/tmp/clientflow_chatwoot_ingestion_gap.md") def clean(value: Any, limit: int = 240) -> str: txt = " ".join(str(value or "").replace("\r", " ").replace("\n", " ").split()) return txt if len(txt) <= limit else txt[: limit - 1].rstrip() + "…" def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--hours", type=int, default=168) parser.add_argument("--include-ok", action="store_true") args = parser.parse_args() with engine.begin() as conn: rows = conn.execute(text(""" SELECT re.id::text AS raw_event_id, re.source_event_id, re.created_at, re.processed, re.ignored, re.processing_error, re.payload #>> '{message_type}' AS message_type, re.payload #>> '{conversation,id}' AS conversation_id, re.payload #>> '{sender,email}' AS sender_email, re.payload #>> '{conversation,meta,sender,email}' AS meta_sender_email, left(coalesce(re.payload #>> '{content}', ''), 600) AS content, m.id IS NOT NULL AS has_message, c.id IS NOT NULL AS has_communication, o.id IS NOT NULL AS has_opportunity FROM raw_events re LEFT JOIN messages m ON m.source_system = 'chatwoot' AND m.source_event_id = re.source_event_id LEFT JOIN communications c ON c.source_system = 'chatwoot' AND c.source_message_id = re.source_event_id LEFT JOIN opportunities o ON o.conversation_id = re.payload #>> '{conversation,id}' WHERE re.source_system = 'chatwoot' AND re.event_type = 'message_created' AND re.created_at >= now() - (:hours * interval '1 hour') ORDER BY re.created_at DESC """), {"hours": int(args.hours)}).mappings().all() report: list[dict[str, Any]] = [] counts: dict[str, int] = {} for row in rows: message_type = str(row.get("message_type") or "").lower() if message_type == "outgoing": status = "OUTGOING_IGNORED_FOR_INBOUND_AUDIT" if not args.include_ok: continue elif message_type != "incoming": status = "NON_INCOMING_OR_UNKNOWN" if not args.include_ok: continue elif not row.get("processed") and not row.get("ignored") and not row.get("processing_error"): status = "PENDING_INCOMING_RAW_EVENT" elif row.get("processing_error"): status = "INCOMING_PROCESSING_ERROR" elif row.get("ignored"): status = "INCOMING_IGNORED" elif not row.get("has_message") and not row.get("has_communication"): status = "PROCESSED_BUT_NOT_VISIBLE" elif not row.get("has_opportunity"): status = "VISIBLE_BUT_UNLINKED_CONVERSATION" else: status = "OK" if not args.include_ok: continue counts[status] = counts.get(status, 0) + 1 report.append({ "status": status, "created_at": row.get("created_at"), "raw_event_id": row.get("raw_event_id"), "source_event_id": row.get("source_event_id"), "conversation_id": row.get("conversation_id"), "email": row.get("sender_email") or row.get("meta_sender_email") or "", "processed": row.get("processed"), "ignored": row.get("ignored"), "processing_error": row.get("processing_error") or "", "has_message": row.get("has_message"), "has_communication": row.get("has_communication"), "has_opportunity": row.get("has_opportunity"), "content": clean(row.get("content")), }) fieldnames = list(report[0].keys()) if report else [ "status", "created_at", "raw_event_id", "source_event_id", "conversation_id", "email", "processed", "ignored", "processing_error", "has_message", "has_communication", "has_opportunity", "content", ] with OUT_CSV.open("w", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(report) with OUT_MD.open("w", encoding="utf-8") as f: f.write("# Auditoria Chatwoot → ClientFlow\n\n") f.write(f"Janela: últimas {args.hours} horas\n\n") f.write("## Resumo\n\n") for status, total in sorted(counts.items(), key=lambda item: (-item[1], item[0])): f.write(f"- {status}: {total}\n") f.write("\n## Casos\n\n") for row in report[:200]: f.write(f"### {row['status']} · conv #{row['conversation_id']} · msg {row['source_event_id']}\n\n") f.write(f"- Email: {row['email'] or '-'}\n") f.write(f"- Data: {row['created_at']}\n") f.write(f"- Erro: {row['processing_error'] or '-'}\n") f.write(f"- Excerto: {row['content'] or '-'}\n\n") print("Auditoria Chatwoot inbound concluída.") print(f"CSV: {OUT_CSV}") print(f"Markdown: {OUT_MD}") print("Resumo:") if not counts: print("OK: sem gaps inbound encontrados") for status, total in sorted(counts.items(), key=lambda item: (-item[1], item[0])): print(f"{status}: {total}") return 0 if __name__ == "__main__": raise SystemExit(main())