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"