from copy import deepcopy from ai_platform.lead_scoring.domain.scoring import ( Rule, ScoringConfiguration, derive_metrics, score_rules, ) def test_profile_and_conversation_metrics_are_available_to_caller_rules(): snapshot = { "captured_at": "2025-09-25T10:00:00Z", "lead_profile": { "name": "Example", "email": "example@example.invalid", "custom_fields": {"region": "north"}, }, "conversations": [ { "messages": [ {"sender_type": "contact", "sent_at": "2025-09-24T10:00:00Z"}, {"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"}, ] }, {"messages": [{"sender_type": "bot", "sent_at": "2025-09-25T09:30:00Z"}]}, ], } before = deepcopy(snapshot) policy = ScoringConfiguration( version="omni-8", rules=( Rule("region", "custom_fields.region", "eq", "north", 10), Rule("profile-fields", "metrics.profile_field_count", "eq", 2, 1), Rule("conversations", "metrics.conversation_count", "eq", 2, 20), Rule("messages", "metrics.message_count", "eq", 3, 20), Rule("contacts", "metrics.contact_message_count", "eq", 1, 20), Rule("legacy-customer-messages", "metrics.customer_message_count", "eq", 1, 1), Rule("turns", "metrics.conversation_turn_count", "eq", 3, 1), Rule("contact-started", "metrics.contact_started_conversation_count", "eq", 1, 1), Rule("recency", "metrics.contact_message_recency_days", "eq", 1.0, 20), ), rule_weight=1.0, ai_weight=0.0, ) score, breakdown = score_rules(snapshot, policy) assert score == 94 assert all(item["matched"] for item in breakdown) assert snapshot == before assert derive_metrics(snapshot)["customer_initiated_return"] is False def test_turn_and_contact_start_metrics_use_message_timestamps_not_array_order(): conversations = [ { "messages": [ {"sender_type": "agent", "sent_at": "2025-09-25T11:00:00Z"}, {"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"}, {"sender_type": "agent", "sent_at": "2025-09-25T10:00:00Z"}, ] }, { "messages": [ {"sender_type": "contact", "sent_at": "2025-09-25T11:00:00Z"}, {"sender_type": "agent", "sent_at": "2025-09-25T10:00:00Z"}, ] }, ] snapshot = {"conversations": conversations} metrics = derive_metrics(snapshot) for conversation in conversations: conversation["messages"].reverse() assert metrics["conversation_turn_count"] == 4 assert metrics["contact_started_conversation_count"] == 1 assert metrics["customer_message_count"] == metrics["contact_message_count"] == 2 assert derive_metrics(snapshot) == metrics def test_contact_recency_hours_uses_newest_valid_message_and_rounds_to_two_decimals(): metrics = derive_metrics( { "captured_at": "2025-09-25T10:00:00Z", "conversations": [ { "messages": [ {"sender_type": "contact", "sent_at": "not-a-timestamp"}, {"sender_type": "contact", "sent_at": "2025-09-25T08:46:00Z"}, ] } ], } ) assert metrics["contact_message_recency_hours"] == 1.23 assert metrics["contact_message_recency_days"] == 0.05 assert metrics["contact_message_count"] == 2 def test_contact_recency_hours_clamps_future_contact_timestamp_to_zero(): metrics = derive_metrics( { "captured_at": "2025-09-25T10:00:00Z", "conversations": [ {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T11:00:00Z"}]} ], } ) assert metrics["contact_message_recency_hours"] == 0.0 assert metrics["contact_message_recency_days"] == 0.0 def test_empty_conversations_and_no_contact_messages_have_no_recency_or_return(): metrics = derive_metrics( { "captured_at": "2025-09-25T10:00:00Z", "conversations": [ {"messages": []}, {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"}]}, ], } ) assert metrics["conversation_count"] == 2 assert metrics["contact_started_conversation_count"] == 0 assert metrics["contact_message_count"] == 0 assert metrics["contact_message_recency_hours"] is None assert metrics["contact_message_recency_days"] is None assert metrics["customer_initiated_return"] is False def test_customer_initiated_return_uses_chronology_not_array_order_and_skips_empty_conversations(): agent_start = {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]} contact_middle = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"}]} contact_latest = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T10:00:00Z"}]} empty = {"messages": []} first = derive_metrics({"conversations": [contact_latest, empty, agent_start, contact_middle]}) shuffled = derive_metrics( {"conversations": [contact_middle, contact_latest, agent_start, empty]} ) assert first["customer_initiated_return"] is True assert shuffled["customer_initiated_return"] is True assert first["contact_started_conversation_count"] == 2 assert shuffled["contact_started_conversation_count"] == 2 def test_customer_initiated_return_breaks_equal_start_times_by_snapshot_position(): agent_start = {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]} contact_start = {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T08:00:00Z"}]} assert ( derive_metrics({"conversations": [agent_start, contact_start]})["customer_initiated_return"] is True ) assert ( derive_metrics({"conversations": [contact_start, agent_start]})["customer_initiated_return"] is False ) def test_boolean_metric_does_not_match_integer_rule_value(): config = ScoringConfiguration( version="omni-15", rules=(Rule("boolean-as-number", "metrics.customer_initiated_return", "eq", 1, 10),), rule_weight=1.0, ai_weight=0.0, ) snapshot = { "conversations": [ {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T08:00:00Z"}]}, {"messages": [{"sender_type": "contact", "sent_at": "2025-09-25T09:00:00Z"}]}, ] } score, breakdown = score_rules(snapshot, config) assert derive_metrics(snapshot)["customer_initiated_return"] is True assert score == 0 assert breakdown[0]["matched"] is False