from ai_platform.lead_scoring.domain.scoring import Rule, ScoringConfiguration, score_rules def test_caller_supplied_rules_are_deterministic_and_bounded(): snapshot = {"lead_profile": {"custom_fields": {"region": "north", "age": 22}}} policy = ScoringConfiguration( version="omni-7", rules=( Rule("region", "custom_fields.region", "eq", "north", 65), Rule("age", "custom_fields.age", "gte", 18, 20), Rule("miss", "custom_fields.age", "gt", 30, 40), ), rule_weight=1.0, ai_weight=0.0, ) first = score_rules(snapshot, policy) assert first == score_rules(snapshot, policy) assert first[0] == 85 assert [item["rule_id"] for item in first[1]] == ["region", "age", "miss"] def test_contact_recency_uses_latest_instant_across_timezones_and_message_order(): base = { "captured_at": "2025-09-25T10:00:00Z", "lead_profile": {"custom_fields": {}}, "conversations": [ { "messages": [ {"sender_type": "contact", "sent_at": "2025-09-25T08:00:00+02:00"}, {"sender_type": "contact", "sent_at": "2025-09-24T09:00:00Z"}, {"sender_type": "agent", "sent_at": "2025-09-25T09:59:00Z"}, ] } ], } policy = ScoringConfiguration( version="omni-9", rules=(Rule("recency", "metrics.contact_message_recency_days", "eq", 0.17, 1),), rule_weight=1.0, ai_weight=0.0, ) assert score_rules(base, policy)[1][0]["matched"] base["conversations"][0]["messages"].reverse() assert score_rules(base, policy)[1][0]["matched"] def test_contact_recency_is_none_without_contact_messages(): snapshot = { "captured_at": "2025-09-25T10:00:00Z", "lead_profile": {"custom_fields": {}}, "conversations": [ {"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"}]} ], } policy = ScoringConfiguration( version="omni-10", rules=(Rule("missing", "metrics.contact_message_recency_days", "exists", None, 1),), rule_weight=1.0, ai_weight=0.0, ) assert score_rules(snapshot, policy)[1][0]["matched"] is False def test_missing_customer_fields_do_not_match_neq_or_null_and_unknown_operators_fail(): snapshot = {"lead_profile": {"custom_fields": {}}} policy = ScoringConfiguration( version="omni-11", rules=( Rule("neq-missing", "custom_fields.region", "ne", "north", 10), Rule("null-missing", "custom_fields.region", "eq", None, 10), Rule("unknown", "custom_fields.region", "unsupported", "north", 10), ), rule_weight=1.0, ai_weight=0.0, ) score, breakdown = score_rules(snapshot, policy) assert score == 0 assert all(not item["matched"] and item["points"] == 0 for item in breakdown) def test_rule_comparisons_do_not_coerce_boolean_or_string_numbers(): snapshot = { "lead_profile": { "custom_fields": { "flag": True, "age_text": "18", "tags": [True], "nested": {"region": "north"}, } } } policy = ScoringConfiguration( version="omni-12", rules=( Rule("bool-eq-int", "custom_fields.flag", "eq", 1, 20), Rule("bool-ne-int", "custom_fields.flag", "ne", 1, 20), Rule("bool-gte", "custom_fields.flag", "gte", 1, 20), Rule("string-gte-number", "custom_fields.age_text", "gte", 18, 20), Rule("contains-int-in-bool-list", "custom_fields.tags", "contains", 1, 20), Rule("nested-field", "custom_fields.nested.region", "eq", "north", 20), Rule("int-eq-float", "metrics.conversation_count", "eq", 0.0, 20), ), rule_weight=1.0, ai_weight=0.0, ) score, breakdown = score_rules(snapshot, policy) assert score == 40 assert [item["matched"] for item in breakdown] == [ False, False, False, False, False, True, True, ] def test_rule_points_are_bounded_after_all_customer_rules(): snapshot = {"lead_profile": {"custom_fields": {"match": True}}} positive = ScoringConfiguration( version="omni-13", rules=( Rule("a", "custom_fields.match", "eq", True, 100), Rule("b", "custom_fields.match", "eq", True, 100), ), rule_weight=1.0, ai_weight=0.0, ) negative = ScoringConfiguration( version="omni-14", rules=(Rule("a", "custom_fields.match", "eq", True, -100),), rule_weight=1.0, ai_weight=0.0, ) assert score_rules(snapshot, positive)[0] == 100 assert score_rules(snapshot, negative)[0] == 0