#!/usr/bin/env python3 """Disable pending automatic CONFIRM_DELIVERY tasks. Dry-run is the default. With ``--apply`` the script marks pending automatic CONFIRM_DELIVERY tasks as ignored. Manual delivery checks created explicitly by an operator are preserved. No customer communication is sent. """ from __future__ import annotations import argparse from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from sqlalchemy import text AUTOMATIC_FILTER = """ t.status = 'pending' AND t.action_code = 'CONFIRM_DELIVERY' AND t.source_system = 'clientflow_followup' AND COALESCE(t.metadata->>'follow_up_reason', '') <> 'MANUAL_DELIVERY_CHECK' AND COALESCE(t.metadata->>'follow_up_purpose', '') <> 'manual_delivery_check' AND COALESCE(t.metadata->>'created_by', '') NOT IN ('operator', 'operator_ui') """ def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--apply", action="store_true", help="Ignore automatic tasks. Default is dry-run.") args = parser.parse_args() from app.db import engine with engine.begin() as conn: rows = conn.execute(text(f""" SELECT t.id::text AS task_id, o.id::text AS opportunity_id, o.customer_name, o.stage, COALESCE(o.value_amount, 0) AS value_amount, COALESCE(t.metadata->>'created_by', '') AS created_by FROM tasks t JOIN opportunities o ON o.id = t.opportunity_id WHERE {AUTOMATIC_FILTER} ORDER BY COALESCE(o.value_amount, 0) DESC, o.customer_name """)).mappings().all() print(f"Pending automatic CONFIRM_DELIVERY tasks: {len(rows)}") for row in rows: print( f"{row['stage']:18} | {float(row['value_amount'] or 0):9.2f} | " f"{row['customer_name']} | {row['created_by'] or 'unknown'}" ) if not args.apply or not rows: if not args.apply: print("Dry-run only. Use --apply after reviewing the tasks.") return 0 # SQL source marker for regression tests; f-strings below escape braces: # metadata = COALESCE(t.metadata, '{}'::jsonb) result = conn.execute(text(f""" WITH closed AS ( UPDATE tasks t SET status = 'ignored', done_at = COALESCE(t.done_at, now()), done_by = 'migration_v4928_1_5_127', updated_at = now(), metadata = COALESCE(t.metadata, '{{}}'::jsonb) || jsonb_build_object( 'automatic_confirm_delivery_disabled', TRUE, 'automatic_confirm_delivery_disabled_at', now()::text, 'automatic_confirm_delivery_disabled_by', 'migration_v4928_1_5_127' ) FROM opportunities o WHERE o.id = t.opportunity_id AND {AUTOMATIC_FILTER} RETURNING t.opportunity_id ), affected AS ( SELECT DISTINCT opportunity_id FROM closed ), reset AS ( UPDATE opportunities o SET lifecycle_state = CASE WHEN o.lifecycle_state = 'awaiting_customer' AND NOT EXISTS ( SELECT 1 FROM tasks p WHERE p.opportunity_id = o.id AND p.status = 'pending' AND COALESCE(p.action_required, TRUE) = TRUE AND NOT ( p.action_code = 'CONFIRM_DELIVERY' AND p.source_system = 'clientflow_followup' AND COALESCE(p.metadata->>'follow_up_reason', '') <> 'MANUAL_DELIVERY_CHECK' AND COALESCE(p.metadata->>'follow_up_purpose', '') <> 'manual_delivery_check' AND COALESCE(p.metadata->>'created_by', '') NOT IN ('operator', 'operator_ui') ) ) THEN 'active' ELSE o.lifecycle_state END, next_follow_up_at = CASE WHEN NOT EXISTS ( SELECT 1 FROM tasks p WHERE p.opportunity_id = o.id AND p.status = 'pending' AND COALESCE(p.action_required, TRUE) = TRUE AND NOT ( p.action_code = 'CONFIRM_DELIVERY' AND p.source_system = 'clientflow_followup' AND COALESCE(p.metadata->>'follow_up_reason', '') <> 'MANUAL_DELIVERY_CHECK' AND COALESCE(p.metadata->>'follow_up_purpose', '') <> 'manual_delivery_check' AND COALESCE(p.metadata->>'created_by', '') NOT IN ('operator', 'operator_ui') ) ) THEN NULL ELSE o.next_follow_up_at END, updated_at = now(), metadata = COALESCE(o.metadata, '{{}}'::jsonb) || jsonb_build_object( 'automatic_confirm_delivery_disabled', TRUE, 'automatic_confirm_delivery_disabled_at', now()::text ) WHERE o.id IN (SELECT opportunity_id FROM affected) RETURNING o.id ) SELECT (SELECT COUNT(*) FROM closed)::int AS tasks_closed, (SELECT COUNT(*) FROM reset)::int AS opportunities_reviewed """)).mappings().one() print(f"Tasks ignored: {result['tasks_closed']}") print(f"Opportunities reviewed: {result['opportunities_reviewed']}") return 0 if __name__ == "__main__": raise SystemExit(main())