Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
78
scripts/audit_contact_identity_collisions.py
Executable file
78
scripts/audit_contact_identity_collisions.py
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user