Files
clientflow_backend/tests/test_operational_eligibility_p1b.py

348 lines
14 KiB
Python

from datetime import datetime, timedelta, timezone
from app.operational_eligibility import evaluate_operational_eligibility
from app.canonical_operations import canonicalize_operations, partition_canonical_items
from app.timezone_utils import format_operator_datetime, parse_operator_local_datetime
NOW = datetime(2026, 8, 15, 12, tzinfo=timezone.utc)
def test_lisbon_summer_and_winter_schedule_conversion_and_display():
summer = parse_operator_local_datetime("2026-08-21T10:00")
winter = parse_operator_local_datetime("2026-12-21T10:00")
assert summer == datetime(2026, 8, 21, 9, tzinfo=timezone.utc)
assert winter == datetime(2026, 12, 21, 10, tzinfo=timezone.utc)
assert format_operator_datetime(summer) == "21/08/2026 10:00"
assert format_operator_datetime(winter) == "21/12/2026 10:00"
def test_lisbon_scheduled_due_comparison_uses_same_utc_instant():
due = parse_operator_local_datetime("2026-08-21T10:00")
before = evaluate_operational_eligibility(item("CALL_CUSTOMER", refs=[task_ref("CALL_CUSTOMER")], due_at=due),
now=datetime(2026, 8, 21, 8, 59, tzinfo=timezone.utc))
at_due = evaluate_operational_eligibility(item("CALL_CUSTOMER", refs=[task_ref("CALL_CUSTOMER")], due_at=due),
now=datetime(2026, 8, 21, 9, 0, tzinfo=timezone.utc))
assert before.reason_code == "FOLLOW_UP_NOT_DUE"
assert at_due.reason_code == "FOLLOW_UP_DUE"
def item(action, *, refs=(), due_at=None, **extra):
value = {
"current_action_code": action,
"action_code": action,
"source_refs": list(refs),
"due_at": due_at,
"operational_queue": "do_now",
}
value.update(extra)
return value
def task_ref(action):
return {"source": "task", "id": f"task-{action}", "status": "pending", "action_code": action}
def test_future_and_due_customer_call_are_waiting_then_do_now():
future = evaluate_operational_eligibility(
item("CALL_CUSTOMER", refs=[task_ref("CALL_CUSTOMER")], due_at=NOW + timedelta(days=1)), now=NOW,
)
assert (future.queue, future.reason_code, future.eligible) == ("waiting", "FOLLOW_UP_NOT_DUE", True)
due = evaluate_operational_eligibility(
item("CALL_CUSTOMER", refs=[task_ref("CALL_CUSTOMER")], due_at=NOW), now=NOW,
)
assert (due.queue, due.reason_code) == ("do_now", "FOLLOW_UP_DUE")
def test_answered_standalone_info_and_support_are_not_current():
for action in ("SEND_INFO", "SUPPORT"):
result = evaluate_operational_eligibility(item(
action, latest_public_inbound=NOW - timedelta(hours=2),
latest_public_outbound=NOW - timedelta(hours=1),
), now=NOW)
assert (result.queue, result.reason_code, result.eligible) == ("not_current", "ALREADY_ANSWERED", False)
def test_newer_inbound_keeps_response_work_current():
result = evaluate_operational_eligibility(item(
"SEND_INFO", latest_public_inbound=NOW, latest_public_outbound=NOW - timedelta(hours=1),
), now=NOW)
assert result.queue == "do_now"
def test_review_and_no_interest_are_not_resolved_by_outbound():
for action in ("REVIEW_MANUALLY", "MARK_NO_INTEREST"):
result = evaluate_operational_eligibility(item(
action, latest_public_inbound=NOW - timedelta(hours=2),
latest_public_outbound=NOW - timedelta(hours=1),
), now=NOW)
assert result.queue == "review"
def test_explicit_response_task_is_not_hidden_by_timeline():
result = evaluate_operational_eligibility(item(
"SEND_INFO", refs=[task_ref("SEND_INFO")],
latest_public_inbound=NOW - timedelta(hours=2), latest_public_outbound=NOW,
), now=NOW)
assert (result.queue, result.reason_code) == ("do_now", "EXPLICIT_PENDING_TASK")
def test_fiscal_prerequisite_is_backlog_unless_structurally_blocking():
hygiene = evaluate_operational_eligibility(item(
"VALIDATE_FISCAL_CUSTOMER", decision={"financial_state": "no_document"},
), now=NOW)
assert (hygiene.queue, hygiene.reason_code) == ("backlog", "FISCAL_DATA_NOT_CURRENTLY_BLOCKING")
blocked = evaluate_operational_eligibility(item(
"VALIDATE_FISCAL_CUSTOMER", refs=[task_ref("SEND_INVOICE")],
decision={"financial_state": "payment_confirmed"},
), now=NOW)
assert (blocked.queue, blocked.reason_code) == ("do_now", "CURRENT_ACTION_BLOCKED_BY_FISCAL_IDENTITY")
def test_reconciliation_hygiene_vs_current_blocker():
hygiene = evaluate_operational_eligibility(item(
"RECONCILE_DOCUMENTS",
refs=[{"source": "reconciliation", "id": "r1", "status": "open", "action_code": "RECONCILE_DOCUMENTS"}],
), now=NOW)
assert hygiene.queue == "backlog"
blocker = evaluate_operational_eligibility(item(
"RECONCILE_DOCUMENTS", refs=[task_ref("SEND_INVOICE")],
), now=NOW)
assert blocker.queue == "review"
def reconciliation_ref(status="open", action="SEND_INVOICE"):
return {"source": "reconciliation", "id": f"rec-{action}", "status": status, "action_code": action}
def test_reconciliation_only_open_invoice_and_proforma_are_backlog():
for action in ("SEND_INVOICE", "SEND_PROFORMA"):
result = evaluate_operational_eligibility(item(
action, refs=[reconciliation_ref("open", action)], source="reconciliation",
), now=NOW)
assert (result.queue, result.reason_code, result.eligible) == (
"backlog", "DOCUMENT_RECONCILIATION_BACKLOG", False,
)
def test_reconciliation_only_review_invoice_and_payment_are_review():
for action in ("SEND_INVOICE", "CONFIRM_PAYMENT"):
result = evaluate_operational_eligibility(item(
action, refs=[reconciliation_ref("needs_review", action)], source="reconciliation",
), now=NOW)
assert (result.queue, result.reason_code) == (
"review", "RECONCILIATION_ASSOCIATION_REQUIRED",
)
def test_explicit_task_or_opportunity_document_action_wins_over_reconciliation_backlog():
for action in ("SEND_INVOICE", "SEND_PROFORMA"):
task_backed = evaluate_operational_eligibility(item(
action, refs=[reconciliation_ref("open", action), task_ref(action)],
source="task", opportunity_id="opp-1",
), now=NOW)
assert (task_backed.queue, task_backed.reason_code) == ("do_now", "EXPLICIT_PENDING_TASK")
opportunity_backed = evaluate_operational_eligibility(item(
action, refs=[reconciliation_ref("open", action)], source="opportunity",
opportunity_id="opp-1", current_downstream_obligation=True,
), now=NOW)
assert opportunity_backed.queue == "do_now"
def test_reconciliation_backlog_keeps_canonical_identity_unique():
source = {
"source": "reconciliation", "source_system": "jasmin", "id": "rec-1",
"status": "open", "action_code": "SEND_PROFORMA", "created_at": NOW,
"priority": "normal", "queue": "rever",
}
projected = canonicalize_operations([source, dict(source)], {})
assert len(projected["all_items"]) == 1
work = projected["all_items"][0]
assert work["work_item_key"] == "reconciliation:rec-1:action:SEND_PROFORMA"
assert (work["operational_queue"], work["eligibility_reason_code"]) == (
"backlog", "DOCUMENT_RECONCILIATION_BACKLOG",
)
def test_waiting_and_backlog_never_become_immediate_by_age():
from app.admin_ui.view_models.operations import is_high_priority, is_overdue
old = item("CALL_CUSTOMER", due_at=NOW + timedelta(days=1), operational_queue="waiting",
created_at=NOW - timedelta(days=100), priority="alta")
assert is_overdue(old) is False
assert is_high_priority(old) is False
def test_outbox_exception_remains_visible():
result = evaluate_operational_eligibility(item("JASMIN_SYNC", source="outbox", status="failed"), now=NOW)
assert (result.queue, result.reason_code, result.eligible) == ("exception", "INTEGRATION_FAILURE", True)
def test_waiting_customer_without_explicit_task_is_not_immediate_work():
result = evaluate_operational_eligibility(item(
"CREATE_JASMIN_QUOTE", opportunity_lifecycle_state="awaiting_customer",
), now=NOW)
assert (result.queue, result.reason_code) == ("waiting", "WAITING_CUSTOMER")
def test_scheduled_call_overrides_theoretical_presentation_without_changing_process_identity():
task = {
"source": "task", "id": "task-call", "status": "pending",
"action_code": "CALL_CUSTOMER", "opportunity_id": "opp-1",
"due_at": datetime(2099, 1, 1, tzinfo=timezone.utc), "created_at": NOW - timedelta(days=2),
"priority": "normal", "queue": "vendas", "href": "/tasks/task-call",
"title": "Ligar ao cliente", "detail": "Conforme combinado",
}
projected = canonicalize_operations([task], {"opp-1": {
"action_code": "CREATE_JASMIN_QUOTE", "label": "Criar orçamento",
"reason": "Passo teórico", "priority": "normal",
}})
assert projected["canonical_count"] == 1
work = projected["items"][0]
assert work["process_key"] == "opportunity:opp-1"
assert work["work_item_key"] == "opportunity:opp-1:action:CALL_CUSTOMER"
assert work["operational_queue"] == "waiting"
partition = partition_canonical_items(projected["all_items"], display_limit=20)
assert partition["work_queue_total"] == 0
assert partition["waiting_total"] == 1
def test_due_scheduled_call_appears_once_and_preserves_quote_stage_evidence():
task = {
"source": "task", "id": "task-call", "status": "pending",
"action_code": "CALL_CUSTOMER", "opportunity_id": "opp-1", "opportunity_stage": "QUOTE_SENT",
"due_at": datetime(2020, 1, 1, tzinfo=timezone.utc), "created_at": NOW - timedelta(days=2),
"priority": "normal", "queue": "vendas", "href": "/tasks/task-call",
}
projected = canonicalize_operations([task, dict(task)], {"opp-1": {"action_code": "CONFIRM_PAYMENT"}})
assert len(projected["all_items"]) == 1
assert projected["items"][0]["operational_queue"] == "do_now"
assert projected["items"][0]["opportunity_stage"] == "QUOTE_SENT"
class _Result:
def __init__(self, rows=()):
self.rows = list(rows)
def mappings(self):
return self
def first(self):
return self.rows[0] if self.rows else None
def all(self):
return self.rows
class _FollowupConnection:
def __init__(self, existing=True):
self.existing = existing
self.sql = []
def execute(self, statement, params=None):
sql = str(statement)
self.sql.append((sql, params or {}))
if "SELECT id::text FROM tasks" in sql:
return _Result([{"id": "00000000-0000-0000-0000-000000000010"}] if self.existing else [])
if "UPDATE tasks SET status='skipped'" in sql:
return _Result([{"id": "00000000-0000-0000-0000-000000000010"}])
return _Result()
class _Context:
def __init__(self, conn):
self.conn = conn
def __enter__(self):
return self.conn
def __exit__(self, *_args):
return False
class _Engine:
def __init__(self, conn):
self.conn = conn
def begin(self):
return _Context(self.conn)
def test_reschedule_customer_call_updates_existing_without_duplicate(monkeypatch):
import app.followup_service as followups
conn = _FollowupConnection(existing=True)
monkeypatch.setattr(followups, "engine", _Engine(conn))
monkeypatch.setattr(followups, "_load_opportunity_context", lambda _id: {"id": _id, "stage": "QUOTE_SENT"})
monkeypatch.setattr(followups, "create_follow_up_task", lambda **kw: (_ for _ in ()).throw(AssertionError("duplicate created")))
result = followups.schedule_customer_call(
opportunity_id="00000000-0000-0000-0000-000000000001",
due_at=NOW + timedelta(days=2), note="Ligar de manhã",
)
assert result["status"] == "rescheduled"
assert sum("UPDATE tasks SET due_at" in sql for sql, _ in conn.sql) == 1
assert any("lifecycle_state='scheduled_follow_up'" in sql for sql, _ in conn.sql)
def test_cancel_customer_call_removes_scheduled_condition(monkeypatch):
import app.followup_service as followups
conn = _FollowupConnection(existing=True)
monkeypatch.setattr(followups, "engine", _Engine(conn))
result = followups.cancel_scheduled_customer_call(
opportunity_id="00000000-0000-0000-0000-000000000001",
)
assert result["cancelled"] == 1
assert any("status='skipped'" in sql for sql, _ in conn.sql)
assert any("next_follow_up_at=NULL" in sql for sql, _ in conn.sql)
def test_stale_scheduled_lifecycle_without_active_task_is_ignored():
from app.admin_ui.pages.opportunities import _opportunity_lifecycle_state
assert _opportunity_lifecycle_state({
"lifecycle_state": "scheduled_follow_up",
"next_follow_up_at": "2099-01-01T10:00:00+00:00",
"pending_follow_up_action_code": None,
}) == "active"
def test_missing_cancelled_or_completed_call_never_produces_scheduled_work():
cases = [
[],
[{"source": "task", "id": "cancelled", "status": "cancelled", "action_code": "CALL_CUSTOMER"}],
[{"source": "task", "id": "done", "status": "done", "action_code": "CALL_CUSTOMER"}],
]
for refs in cases:
result = evaluate_operational_eligibility(item(
"CALL_CUSTOMER", refs=refs, due_at=datetime(2099, 1, 1, tzinfo=timezone.utc),
opportunity_lifecycle_state="scheduled_follow_up",
), now=NOW)
assert (result.eligible, result.queue, result.reason_code) == (
False, "not_current", "NO_ACTIVE_SCHEDULED_CALL",
)
def test_scheduled_call_read_helpers_contain_no_repair_write():
from inspect import getsource
from app.admin_ui.pages.opportunities import _opportunity_lifecycle_state
source = getsource(_opportunity_lifecycle_state).upper()
assert "UPDATE " not in source and "INSERT " not in source and "DELETE " not in source
def test_active_call_task_wins_when_lifecycle_state_is_stale():
from app.admin_ui.pages.opportunities import _opportunity_lifecycle_state
assert _opportunity_lifecycle_state({
"lifecycle_state": "active",
"pending_follow_up_action_code": "CALL_CUSTOMER",
"pending_follow_up_due_at": "2099-01-01T10:00:00+00:00",
}) == "scheduled_follow_up"
assert _opportunity_lifecycle_state({
"lifecycle_state": "active",
"pending_follow_up_action_code": "CALL_CUSTOMER",
"pending_follow_up_due_at": "2020-01-01T10:00:00+00:00",
}) == "follow_up_due"