Files
clientflow_backend/scripts/archive_spam_opportunities.py

91 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Archive spam/falso-positivo opportunities without counting them as LOST.
Examples:
PYTHONPATH=. python scripts/archive_spam_opportunities.py --opportunity-id <uuid>
PYTHONPATH=. python scripts/archive_spam_opportunities.py --scan --apply
"""
from __future__ import annotations
import argparse
from sqlalchemy import text
from app.db import engine
from app.opportunity_service import archive_spam_opportunity_if_safe
def _candidate_rows(limit: int = 200):
with engine.begin() as conn:
return conn.execute(text("""
SELECT DISTINCT
o.id::text,
o.title,
o.customer_name,
o.stage,
o.status,
o.updated_at,
(SELECT COUNT(*) FROM commercial_documents cd WHERE cd.opportunity_id = o.id) AS docs,
(SELECT COUNT(*) FROM reconciliation_items ri WHERE ri.opportunity_id = o.id AND ri.source_system IN ('jasmin','odoo')) AS linked_external,
(SELECT COUNT(*) FROM tasks t WHERE t.opportunity_id = o.id AND (t.action_code = 'IGNORE_SPAM' OR t.route = 'spam')) AS spam_tasks,
(SELECT COUNT(*) FROM communications c WHERE c.opportunity_id = o.id AND c.classification IN ('IGNORE_SPAM','SPAM')) AS spam_comms
FROM opportunities o
LEFT JOIN tasks t ON t.opportunity_id = o.id
LEFT JOIN communications c ON c.opportunity_id = o.id
WHERE COALESCE(o.status, 'open') = 'open'
AND (
t.action_code = 'IGNORE_SPAM'
OR t.route = 'spam'
OR c.classification IN ('IGNORE_SPAM','SPAM')
)
ORDER BY o.updated_at DESC
LIMIT :limit
"""), {"limit": int(limit)}).mappings().all()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--opportunity-id", help="Arquivar uma oportunidade concreta")
parser.add_argument("--scan", action="store_true", help="Procurar oportunidades abertas com evidência de spam")
parser.add_argument("--apply", action="store_true", help="Aplicar alterações. Sem isto é dry-run")
parser.add_argument("--limit", type=int, default=200)
args = parser.parse_args()
if not args.opportunity_id and not args.scan:
parser.error("usa --opportunity-id ou --scan")
rows = []
if args.opportunity_id:
with engine.begin() as conn:
row = conn.execute(text("""
SELECT id::text, title, customer_name, stage, status, updated_at,
0 AS docs, 0 AS linked_external, 1 AS spam_tasks, 0 AS spam_comms
FROM opportunities
WHERE id = CAST(:opportunity_id AS UUID)
"""), {"opportunity_id": args.opportunity_id}).mappings().first()
if row:
rows = [row]
elif args.scan:
rows = list(_candidate_rows(args.limit))
print(f"apply={args.apply} candidates={len(rows)}")
changed = 0
for row in rows:
oid = str(row["id"])
print(f"- opp={oid} status={row.get('status')} stage={row.get('stage')} docs={row.get('docs')} linked_external={row.get('linked_external')} spam_tasks={row.get('spam_tasks')} spam_comms={row.get('spam_comms')} title={row.get('title')!r}")
if not args.apply:
continue
result = archive_spam_opportunity_if_safe(
oid,
reason="spam/falso positivo",
actor="archive_spam_opportunities_script",
)
print(f" result={result}")
if result.get("ok") and result.get("status") in {"archived", "already_archived"}:
changed += 1
print(f"SUMMARY changed={changed} dry_run={not args.apply}")
return 0
if __name__ == "__main__":
raise SystemExit(main())