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( 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_missing_customer_fields_do_not_match_neq_or_null_and_unknown_operators_fail(): snapshot = {"lead_profile": {"custom_fields": {}}} policy = ScoringConfiguration( 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( 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( 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( 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 def _matched(operator, value, current): snapshot = {"lead_profile": {"custom_fields": {"x": current}}} policy = ScoringConfiguration( rules=(Rule("r", "custom_fields.x", operator, value, 10),), rule_weight=1.0, ai_weight=0.0 ) return score_rules(snapshot, policy)[1][0]["matched"] def test_between_is_inclusive_and_numeric_only(): assert [_matched("between", [3, 10], v) for v in (3, 5.5, 10)] == [True] * 3 assert [_matched("between", [3, 10], v) for v in (2.99, 10.01, "5", True, None, [5])] == [ False ] * 6 def test_in_matches_whole_values_only(): assert _matched("in", ["hot", "warm"], "hot") assert not _matched("in", ["hot"], "not_hot") assert not _matched("in", ["hot"], ["hot"]) assert _matched("in", [1, 2], 2.0) assert not _matched("in", [1], True)