90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from inspect import getsource
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import app.opportunity_next_action_service as service
|
|
from app.admin_ui.pages.opportunities import _opportunity_lifecycle_state
|
|
from app.domain.opportunity_flow.v2 import EffectiveOperationalDecision, suppress_duplicate_representation
|
|
|
|
|
|
class Decision:
|
|
def to_dict(self):
|
|
return {"action_code": "V1_ACTION", "label": "V1", "description": "legacy"}
|
|
|
|
|
|
def test_compare_mode_returns_v1_behavior(monkeypatch, caplog):
|
|
caplog.set_level("INFO")
|
|
monkeypatch.setattr(service.settings, "blif_flow_v2_mode", "compare")
|
|
monkeypatch.setattr(service, "_build_db_evidence", lambda *args, **kwargs: SimpleNamespace(company_profile="blif"))
|
|
monkeypatch.setattr(service, "load_company_profile", lambda *_: object())
|
|
monkeypatch.setattr(service, "decide_opportunity_next_action", lambda *_: Decision())
|
|
monkeypatch.setattr(service, "_load_v2_comparison_rows", lambda ids: {
|
|
"opp": {"business_state": "COMPLETED", "business_next_action": None,
|
|
"reason_code": "BUSINESS_TRANSITION"}
|
|
})
|
|
assert service.get_opportunity_next_action("opp", preloaded={})["action_code"] == "V1_ACTION"
|
|
assert "blif_flow_v2_compare" in caplog.text
|
|
|
|
|
|
def test_compare_mode_has_no_business_mutation_sql():
|
|
source = getsource(service._load_v2_comparison_rows).upper() + getsource(service._observe_flow_v2).upper()
|
|
assert "UPDATE " not in source
|
|
assert "INSERT " not in source
|
|
assert "DELETE " not in source
|
|
|
|
|
|
def test_stale_legacy_stage_cannot_override_v2_factual_state():
|
|
rows = service.compare_v1_v2_decisions(
|
|
{"opp": {"action_code": "CREATE_JASMIN_QUOTE"}},
|
|
{"opp": {"business_state": "PROFORMA_REQUIRED", "business_next_action": "CREATE_PROFORMA",
|
|
"reason_code": "BUSINESS_TRANSITION"}},
|
|
)
|
|
assert rows[0]["v2_business_state"] == "PROFORMA_REQUIRED"
|
|
assert rows[0]["returned_source"] == "v1"
|
|
|
|
|
|
def test_historical_next_followup_without_active_task_creates_no_work():
|
|
assert _opportunity_lifecycle_state({
|
|
"lifecycle_state": "active", "next_follow_up_at": "2020-01-01T00:00:00+00:00",
|
|
"pending_follow_up_action_code": None,
|
|
}) == "active"
|
|
|
|
|
|
def test_due_active_followup_still_creates_work():
|
|
assert _opportunity_lifecycle_state({
|
|
"lifecycle_state": "awaiting_customer", "next_follow_up_at": "2020-01-01T00:00:00+00:00",
|
|
"pending_follow_up_action_code": "FOLLOW_UP_CUSTOMER_REVIEW",
|
|
"pending_follow_up_due_at": datetime.now(timezone.utc) - timedelta(days=1),
|
|
}) == "follow_up_due"
|
|
|
|
|
|
def test_future_active_followup_remains_scheduled():
|
|
assert _opportunity_lifecycle_state({
|
|
"lifecycle_state": "awaiting_customer",
|
|
"pending_follow_up_action_code": "FOLLOW_UP_CUSTOMER_REVIEW",
|
|
"pending_follow_up_due_at": datetime.now(timezone.utc) + timedelta(days=1),
|
|
}) == "scheduled_follow_up"
|
|
|
|
|
|
def test_duplicate_representation_stays_suppressed():
|
|
raw = EffectiveOperationalDecision("REVIEW_REQUIRED", "REVIEW_REQUIRED", "REVIEW_REQUIRED",
|
|
"review", "review", "medium", "business_transition")
|
|
suppressed = suppress_duplicate_representation(raw, canonical_process_id="canonical")
|
|
assert suppressed.effective_operational_action is None
|
|
assert suppressed.effective_operational_queue == "not_current"
|
|
|
|
|
|
def test_authoritative_mode_is_fail_closed(monkeypatch):
|
|
monkeypatch.setattr(service.settings, "blif_flow_v2_mode", "authoritative")
|
|
with pytest.raises(RuntimeError, match="disabled"):
|
|
service.get_opportunity_next_actions([])
|
|
|
|
|
|
def test_shadow_mode_has_no_decision_side_effect(monkeypatch):
|
|
monkeypatch.setattr(service.settings, "blif_flow_v2_mode", "shadow")
|
|
monkeypatch.setattr(service, "_load_v2_comparison_rows",
|
|
lambda ids: pytest.fail("shadow must not read comparison projection"))
|
|
service._observe_flow_v2({"opp": {"action_code": "V1"}})
|