Files
clientflow_backend/scripts/audit_chatwoot_ingestion_gap.py

147 lines
5.9 KiB
Python
Executable File

#!/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())