#!/usr/bin/env python3 """Audit possible polluted Chatwoot contact names in pending work items. Read-only. It highlights sender names reused across several different sender emails, which can make the Operator Workbench display the wrong person name. """ from __future__ import annotations import re from collections import defaultdict from sqlalchemy import text from app.db import engine def norm(value: object) -> str: return " ".join(re.sub(r"[^0-9a-zA-ZÀ-ÿ]+", " ", str(value or "").casefold()).split()) SQL = """ SELECT t.id::text AS task_id, t.created_at, t.status, t.action_code, t.conversation_id, COALESCE(re.payload->'sender'->>'name', '') AS sender_name, COALESCE(re.payload->'sender'->>'email', '') AS sender_email, COALESCE(o.title, '') AS opportunity_title, COALESCE(cu.name, '') AS fiscal_customer_name FROM tasks t LEFT JOIN raw_events re ON re.id = t.raw_event_id LEFT JOIN opportunities o ON o.id = t.opportunity_id LEFT JOIN customers cu ON cu.id = o.local_customer_id WHERE t.status = 'pending' AND t.source_system = 'chatwoot' ORDER BY t.created_at DESC LIMIT :limit """ def main() -> int: with engine.begin() as conn: rows = [dict(r) for r in conn.execute(text(SQL), {"limit": 250}).mappings().all()] by_name: dict[str, list[dict]] = defaultdict(list) for row in rows: key = norm(row.get("sender_name")) if key: by_name[key].append(row) suspects = [] for key, items in by_name.items(): emails = {str(i.get("sender_email") or "").casefold() for i in items if i.get("sender_email")} if len(emails) > 1: suspects.append((key, emails, items)) print("Contact identity collision audit") print("=" * 80) print(f"pending_chatwoot_tasks={len(rows)}") print(f"suspect_reused_names={len(suspects)}") print() for key, emails, items in sorted(suspects, key=lambda x: (-len(x[1]), x[0])): display = items[0].get("sender_name") or key print("=" * 80) print(f"sender_name={display!r} reused_with_{len(emails)}_emails") for item in items[:20]: print( f"- task={item['task_id']} conv={item.get('conversation_id')} " f"email={item.get('sender_email') or '-'} action={item.get('action_code')} " f"opportunity={item.get('opportunity_title') or '-'} fiscal={item.get('fiscal_customer_name') or '-'}" ) return 1 if suspects else 0 if __name__ == "__main__": raise SystemExit(main())