Files
clientflow_backend/scripts/repair_stale_followups.py

86 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Audit/repair pending follow-ups that contradict the current opportunity stage.
Dry-run by default. Use --apply to update rows. The repair is idempotent and
never sends email or Chatwoot messages.
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from dotenv import load_dotenv
from sqlalchemy import create_engine, text
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
load_dotenv(dotenv_path=Path(".env"), override=False)
db_url = os.environ.get("DATABASE_URL")
if not db_url:
raise SystemExit("DATABASE_URL em falta")
engine = create_engine(db_url)
select_sql = text("""
SELECT t.id::text AS task_id,
t.opportunity_id::text,
o.customer_name,
o.stage,
t.action_code,
t.action,
t.route,
t.status,
EXISTS (
SELECT 1 FROM tasks tx
WHERE tx.opportunity_id = o.id
AND tx.status = 'pending'
AND tx.action_code IN ('SEND_INVOICE','CONFIRM_PAYMENT','PREPARE_ORDER','CREATE_SHIPMENT')
) AS has_superseding_task
FROM tasks t
JOIN opportunities o ON o.id = t.opportunity_id
WHERE o.status = 'open'
AND t.status = 'pending'
AND t.action_code IN ('FOLLOW_UP_QUOTE','FOLLOW_UP_PROFORMA')
AND (
upper(o.stage) IN (
'INVOICE_REQUESTED','INVOICE_SENT','WAITING_PAYMENT','PAYMENT_CONFIRMED',
'ODOO_ORDER_CREATED','IN_PRODUCTION','ORDER_PREPARATION','READY_TO_SHIP',
'INVOICED','SHIPMENT_CREATED','SHIPPED','TRACKING_SENT','DELIVERED','WON'
)
OR EXISTS (
SELECT 1 FROM tasks tx
WHERE tx.opportunity_id = o.id
AND tx.status = 'pending'
AND tx.action_code IN ('SEND_INVOICE','CONFIRM_PAYMENT','PREPARE_ORDER','CREATE_SHIPMENT')
)
)
ORDER BY o.updated_at DESC
""")
with engine.begin() as conn:
rows = [dict(r) for r in conn.execute(select_sql).mappings().all()]
print(f"Candidatos: {len(rows)}")
for row in rows:
decision = "close" if row["has_superseding_task"] or row["stage"] != "WAITING_PAYMENT" else "convert_to_payment"
print(f"{decision:20} {row['task_id']} | {row['customer_name']} | {row['stage']} | {row['action_code']}")
if not args.apply:
print("Dry-run: use --apply para corrigir.")
return 0
from app.followup_service import align_pending_followups_for_opportunity
opportunity_ids = sorted({r["opportunity_id"] for r in rows})
updated = closed = 0
for opportunity_id in opportunity_ids:
result = align_pending_followups_for_opportunity(opportunity_id=opportunity_id, created_by="repair_stale_followups")
updated += int(result.get("updated") or 0)
closed += int(result.get("closed") or 0)
print(opportunity_id, result)
print(f"Aplicado: convertidas={updated}; encerradas={closed}")
return 0
if __name__ == "__main__":
raise SystemExit(main())