import pytest from ai_platform.lead_scoring.domain.scoring import ( Rule, ScoringConfiguration, derive_metrics, score_rules, ) CAPTURED = "2025-09-25T10:00:00Z" def _msg(sender, sent_at): return {"sender_type": sender, "sent_at": sent_at} def _conv(*messages): return {"messages": list(messages)} def _metrics(*conversations): return derive_metrics({"captured_at": CAPTURED, "conversations": list(conversations)}) def test_counts_and_turns_use_timestamps_not_array_order(): conversations = [ _conv( _msg("agent", "2025-09-25T11:00:00Z"), _msg("contact", "2025-09-25T09:00:00Z"), _msg("agent", "2025-09-25T10:00:00Z"), ), _conv(_msg("contact", "2025-09-25T11:00:00Z"), _msg("agent", "2025-09-25T10:00:00Z")), ] metrics = _metrics(*conversations) for conversation in conversations: conversation["messages"].reverse() assert metrics["conversation_turn_count"] == 4 assert metrics["contact_started_conversation_count"] == 1 assert metrics["contact_message_count"] == 2 assert _metrics(*conversations) == metrics @pytest.mark.parametrize( ("sent_at", "hours", "days"), [ ("2025-09-25T08:46:00Z", 1.23, 0.05), ("2025-09-25T08:00:00+02:00", 4.0, 0.17), ("2025-09-25T11:00:00Z", 0.0, 0.0), ], ) def test_contact_recency_is_rounded_clamped_and_timezone_aware(sent_at, hours, days): metrics = _metrics(_conv(_msg("contact", "not-a-timestamp"), _msg("contact", sent_at))) assert metrics["contact_message_recency_hours"] == hours assert metrics["contact_message_recency_days"] == days def test_no_contact_messages_means_no_recency_and_no_return(): metrics = _metrics(_conv(), _conv(_msg("agent", "2025-09-25T09:00:00Z"))) assert metrics["conversation_count"] == 2 assert metrics["contact_message_count"] == 0 assert metrics["contact_message_recency_hours"] is None assert metrics["customer_initiated_return"] is False def test_customer_initiated_return_uses_chronology_and_breaks_ties_by_position(): agent = _conv(_msg("agent", "2025-09-25T08:00:00Z")) contact = _conv(_msg("contact", "2025-09-25T09:00:00Z")) later = _conv(_msg("contact", "2025-09-25T10:00:00Z")) tie_agent = _conv(_msg("agent", "2025-09-25T07:00:00Z")) tie_contact = _conv(_msg("contact", "2025-09-25T07:00:00Z")) assert _metrics(later, _conv(), agent, contact)["customer_initiated_return"] is True assert _metrics(contact, later, agent)["contact_started_conversation_count"] == 2 assert _metrics(tie_agent, tie_contact)["customer_initiated_return"] is True assert _metrics(tie_contact, tie_agent)["customer_initiated_return"] is False def test_caller_rules_see_the_derived_metrics_and_booleans_never_match_numbers(): snapshot = { "captured_at": CAPTURED, "lead_profile": {"name": "Example", "email": "a@example.invalid", "custom_fields": {}}, "conversations": [ _conv(_msg("contact", "2025-09-24T10:00:00Z"), _msg("agent", "2025-09-25T09:00:00Z")), _conv(_msg("bot", "2025-09-25T09:30:00Z")), ], } rules = ( Rule("profile", "metrics.profile_field_count", "eq", 2, 1), Rule("conversations", "metrics.conversation_count", "eq", 2, 1), Rule("messages", "metrics.message_count", "eq", 3, 1), Rule("contacts", "metrics.contact_message_count", "eq", 1, 1), Rule("turns", "metrics.conversation_turn_count", "eq", 3, 1), Rule("started", "metrics.contact_started_conversation_count", "eq", 1, 1), Rule("recency", "metrics.contact_message_recency_days", "eq", 1.0, 1), Rule("bool-as-number", "metrics.customer_initiated_return", "eq", 1, 1), ) score, breakdown = score_rules(snapshot, ScoringConfiguration(rules, 1.0, 0.0)) assert score == 7 assert [item["matched"] for item in breakdown] == [True] * 7 + [False]