Files
clientflow_backend/scripts/disable_automatic_confirm_delivery.py

142 lines
6.1 KiB
Python
Executable File

#!/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())