Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
119
scripts/sync_closed_opportunity_followups.py
Executable file
119
scripts/sync_closed_opportunity_followups.py
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Close pending follow-up tasks on terminal opportunities.
|
||||
|
||||
Read-only by default. With --apply, marks pending FOLLOW_UP_* tasks as skipped
|
||||
when the linked opportunity is already closed/concluded/lost/no-interest.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
|
||||
FOLLOW_UP_ACTIONS = (
|
||||
"FOLLOW_UP_QUOTE",
|
||||
"FOLLOW_UP_PROFORMA",
|
||||
"FOLLOW_UP_PAYMENT",
|
||||
"FOLLOW_UP_CUSTOMER_REVIEW",
|
||||
"FOLLOW_UP_GENERIC",
|
||||
)
|
||||
|
||||
TERMINAL_STAGES = ("WON", "LOST", "NO_INTEREST", "DELIVERED")
|
||||
|
||||
|
||||
def _json(value: Any) -> str:
|
||||
return json.dumps(value or {}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def find_candidates(limit: int) -> list[dict[str, Any]]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(text("""
|
||||
SELECT
|
||||
t.id::text AS task_id,
|
||||
t.created_at,
|
||||
t.due_at,
|
||||
t.route,
|
||||
t.action_code,
|
||||
t.action,
|
||||
t.note,
|
||||
t.status AS task_status,
|
||||
t.priority,
|
||||
t.opportunity_id::text,
|
||||
o.title AS opportunity_title,
|
||||
o.customer_name,
|
||||
o.stage AS opportunity_stage,
|
||||
o.status AS opportunity_status,
|
||||
o.closed_at
|
||||
FROM tasks t
|
||||
JOIN opportunities o ON o.id = t.opportunity_id
|
||||
WHERE t.status = 'pending'
|
||||
AND t.action_code IN (
|
||||
'FOLLOW_UP_QUOTE',
|
||||
'FOLLOW_UP_PROFORMA',
|
||||
'FOLLOW_UP_PAYMENT',
|
||||
'FOLLOW_UP_CUSTOMER_REVIEW',
|
||||
'FOLLOW_UP_GENERIC'
|
||||
)
|
||||
AND (
|
||||
o.status = 'closed'
|
||||
OR o.stage IN ('WON', 'LOST', 'NO_INTEREST', 'DELIVERED')
|
||||
)
|
||||
ORDER BY t.due_at NULLS LAST, t.created_at DESC
|
||||
LIMIT :limit
|
||||
"""), {"limit": int(limit)}).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def apply_candidate(task_id: str, *, actor: str = "sync_closed_opportunity_followups") -> int:
|
||||
patch = {
|
||||
"auto_closed_follow_up": True,
|
||||
"auto_closed_reason": "opportunity_already_terminal",
|
||||
"auto_closed_by": actor,
|
||||
}
|
||||
with engine.begin() as conn:
|
||||
result = conn.execute(text("""
|
||||
UPDATE tasks
|
||||
SET status = 'skipped',
|
||||
done_at = COALESCE(done_at, now()),
|
||||
done_by = CAST(:actor AS TEXT),
|
||||
updated_at = now(),
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata_patch AS JSONB)
|
||||
WHERE id = CAST(:task_id AS UUID)
|
||||
AND status = 'pending'
|
||||
AND action_code IN (
|
||||
'FOLLOW_UP_QUOTE',
|
||||
'FOLLOW_UP_PROFORMA',
|
||||
'FOLLOW_UP_PAYMENT',
|
||||
'FOLLOW_UP_CUSTOMER_REVIEW',
|
||||
'FOLLOW_UP_GENERIC'
|
||||
)
|
||||
"""), {"task_id": task_id, "actor": actor, "metadata_patch": _json(patch)})
|
||||
return int(getattr(result, "rowcount", 0) or 0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
parser.add_argument("--limit", type=int, default=100)
|
||||
args = parser.parse_args()
|
||||
|
||||
rows = find_candidates(args.limit)
|
||||
print(f"Follow-ups pendentes em oportunidades terminais: {len(rows)}")
|
||||
updated = 0
|
||||
for row in rows:
|
||||
print("-", row["task_id"], row["action_code"], row["opportunity_stage"], row["opportunity_status"], row["opportunity_title"])
|
||||
if args.apply:
|
||||
updated += apply_candidate(row["task_id"])
|
||||
if args.apply:
|
||||
print(f"updated: {updated}")
|
||||
else:
|
||||
print("dry-run: usa --apply para fechar estes follow-ups.")
|
||||
return 1 if rows and not args.apply else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user