Release v4928.1.4.2 stable
This commit is contained in:
105
scripts/cleanup_non_commercial_opportunities.py
Executable file
105
scripts/cleanup_non_commercial_opportunities.py
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Identify or close opportunities that were probably created from system messages.
|
||||
|
||||
Conservative by default: use --dry-run to list candidates. Use --apply to mark
|
||||
safe candidates as LOST with a metadata reason. It only targets opportunities
|
||||
with value 0, no commercial documents and no shipments.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
from app.opportunity_service import ensure_opportunity_schema
|
||||
|
||||
SYSTEM_TERMS = (
|
||||
"mail delivery subsystem",
|
||||
"mailer-daemon",
|
||||
"postmaster",
|
||||
"returned mail",
|
||||
"undelivered mail",
|
||||
"delivery status notification",
|
||||
"failure notice",
|
||||
"mail delivery failed",
|
||||
)
|
||||
|
||||
|
||||
def _json(value: object) -> str:
|
||||
return json.dumps(value or {}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def find_candidates(limit: int = 100) -> list[dict]:
|
||||
ensure_opportunity_schema()
|
||||
like_sql = " OR ".join(
|
||||
["lower(coalesce(o.title,'') || ' ' || coalesce(o.customer_name,'') || ' ' || coalesce(o.customer_email,'') || ' ' || coalesce(o.metadata::text,'')) LIKE :term_{}".format(i) for i, _ in enumerate(SYSTEM_TERMS)]
|
||||
)
|
||||
params = {f"term_{i}": f"%{term}%" for i, term in enumerate(SYSTEM_TERMS)}
|
||||
params["limit"] = int(limit)
|
||||
sql = text(f"""
|
||||
SELECT o.id::text, o.title, o.customer_name, o.customer_email, o.value_amount, o.stage, o.status, o.created_at
|
||||
FROM opportunities o
|
||||
WHERE o.status = 'open'
|
||||
AND COALESCE(o.value_amount, 0) = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM commercial_documents cd WHERE cd.opportunity_id = o.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM shipments s WHERE s.opportunity_id = o.id)
|
||||
AND ({like_sql})
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(sql, params).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def close_candidates(candidates: list[dict]) -> int:
|
||||
if not candidates:
|
||||
return 0
|
||||
ids = [row["id"] for row in candidates]
|
||||
with engine.begin() as conn:
|
||||
for opportunity_id in ids:
|
||||
conn.execute(text("""
|
||||
UPDATE opportunities
|
||||
SET status = 'closed',
|
||||
stage = 'LOST',
|
||||
closed_at = COALESCE(closed_at, now()),
|
||||
updated_at = now(),
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB)
|
||||
WHERE id = CAST(:opportunity_id AS UUID)
|
||||
"""), {
|
||||
"opportunity_id": opportunity_id,
|
||||
"metadata": _json({"closed_reason": "system_or_bounce_created_by_mistake", "closed_by": "cleanup_non_commercial_opportunities"}),
|
||||
})
|
||||
conn.execute(text("""
|
||||
INSERT INTO opportunity_events (id, opportunity_id, event_type, from_stage, to_stage, note, payload, created_by)
|
||||
VALUES (CAST(:id AS UUID), CAST(:opportunity_id AS UUID), 'cleanup_closed_non_commercial', NULL, 'LOST', :note, CAST(:payload AS JSONB), 'system')
|
||||
"""), {
|
||||
"id": str(uuid.uuid4()),
|
||||
"opportunity_id": opportunity_id,
|
||||
"note": "Oportunidade fechada por parecer mensagem automática/bounce sem atividade comercial.",
|
||||
"payload": _json({"reason": "system_or_bounce_created_by_mistake"}),
|
||||
})
|
||||
return len(ids)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--limit", type=int, default=100)
|
||||
parser.add_argument("--apply", action="store_true", help="Apply changes. Without this, only prints candidates.")
|
||||
args = parser.parse_args()
|
||||
candidates = find_candidates(limit=args.limit)
|
||||
print(f"Found {len(candidates)} candidate(s).")
|
||||
for row in candidates:
|
||||
print(f"- {row.get('id')} | {row.get('title')} | {row.get('customer_name')} | {row.get('created_at')}")
|
||||
if args.apply:
|
||||
total = close_candidates(candidates)
|
||||
print(f"Closed {total} candidate(s).")
|
||||
else:
|
||||
print("Dry-run only. Re-run with --apply to close candidates.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user