diff --git a/block.py b/block.py index 1892530..c5aace1 100644 --- a/block.py +++ b/block.py @@ -15,7 +15,8 @@ def __main__( hd_score_s2: float, hd_score_s3: float, hd_score_iso_m2: float, - hd_score_g2: float + hd_score_g2: float, + hd_score_s4: float = 0 ) -> dict: data = { @@ -25,7 +26,8 @@ def __main__( "hd_score_s2": hd_score_s2, "hd_score_s3": hd_score_s3, "hd_score_iso_m2": hd_score_iso_m2, - "hd_score_g2": hd_score_g2 + "hd_score_g2": hd_score_g2, + "hd_score_s4": hd_score_s4 } final = processing(data) diff --git a/request_schema.json b/request_schema.json index 6db4557..7b086bc 100644 --- a/request_schema.json +++ b/request_schema.json @@ -26,6 +26,10 @@ "type": ["number", "null"], "description": "HD Fraud Score S3" }, + "hd_score_s4": { + "type": ["number", "null"], + "description": "HD Fraud Score S4" + }, "hd_score_iso_m2": { "type": ["number", "null"], "description": "HD Fraud Score M2" diff --git a/score_processing.py b/score_processing.py index eceab98..38d5b9f 100644 --- a/score_processing.py +++ b/score_processing.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) def processing(data: dict) -> dict: score_threshold = 1200 score_threshold2 = 1225 + review_threshold = 1191 # ---- Compute hd_score based on hierarchy ---- try: @@ -49,7 +50,14 @@ def processing(data: dict) -> dict: data.get("hd_score_iso_m2", 0), data.get("hd_score_g2", 0), ]) - winning_reason = "Pass" + hd_score_s4 = data.get("hd_score_s4", 0) or 0 + if hd_score_s4 >= review_threshold: + winning_score = hd_score_s4 + winning_reason = "S4 Velocity Triggered" + else: + if review_threshold <= winning_score <= score_threshold: + winning_score = winning_score - 10 + winning_reason = "Pass" hd_score = winning_score @@ -59,7 +67,9 @@ def processing(data: dict) -> dict: # ---- recommended_action ---- try: - if winning_reason != "Pass": + if winning_reason == "S4 Velocity Triggered": + recommended_action = "Review Application" + elif winning_reason != "Pass": recommended_action = "Decline Application" else: recommended_action = "Pass Application" @@ -72,7 +82,12 @@ def processing(data: dict) -> dict: # ---- description ---- try: - if winning_reason != "Pass": + if winning_reason == "S4 Velocity Triggered": + description = ( + "HD Fraud Score is in the review range, " + "Recommended action: Review Application" + ) + elif winning_reason != "Pass": threshold_used = winning_threshold if winning_threshold is not None else score_threshold description = ( f"HD Fraud Score is above the risk threshold {threshold_used}, " diff --git a/test_block.py b/test_block.py index 8cd54a8..2e52a4b 100644 --- a/test_block.py +++ b/test_block.py @@ -1,16 +1,88 @@ +import json import unittest +from pathlib import Path + from block import __main__ -data = {'hd_score_m1': 1173.0, 'hd_score_g1': 1203.0, 'hd_score_s1': 1240.0, 'hd_score_s2': 0, 'hd_score_s3': 0, 'hd_score_iso_m2': 1001.0, 'hd_score_g2': 0.0} + +BASE_DIR = Path(__file__).resolve().parent + +data = { + "hd_score_m1": 1173.0, + "hd_score_g1": 1203.0, + "hd_score_s1": 1240.0, + "hd_score_s2": 0, + "hd_score_s3": 0, + "hd_score_iso_m2": 1001.0, + "hd_score_g2": 0.0, +} + + +def score_data(**overrides): + values = { + "hd_score_m1": 1100.0, + "hd_score_g1": 0.0, + "hd_score_s1": 0.0, + "hd_score_s2": 0.0, + "hd_score_s3": 0.0, + "hd_score_iso_m2": 1001.0, + "hd_score_g2": 0.0, + "hd_score_s4": 0.0, + } + values.update(overrides) + return values + class TestBlock(unittest.TestCase): def test_main_success(self): - # blockResult = main(data) - blockResult = __main__(**data) + block_result = __main__(**data) - # breakpoint() - self.assertIsInstance(blockResult, dict, "Result should be a dictionary.") - self.assertIn("hd_score", blockResult, "Result dictionary should contain 'hd_score' if success.") + self.assertIsInstance(block_result, dict, "Result should be a dictionary.") + self.assertIn("hd_score", block_result, "Result dictionary should contain 'hd_score' if success.") + + def test_s4_sets_review_application_when_no_decline(self): + block_result = __main__(**score_data(hd_score_s4=1191)) + + self.assertEqual(block_result["hd_score"], 1191) + self.assertEqual(block_result["recommended_action"], "Review Application") + self.assertEqual(block_result["hd_action_reasoncode"], "S4 Velocity Triggered") + self.assertIn("Review Application", block_result["description"]) + + def test_s4_review_can_use_top_of_reserved_band(self): + block_result = __main__(**score_data(hd_score_s4=1200)) + + self.assertEqual(block_result["hd_score"], 1200) + self.assertEqual(block_result["recommended_action"], "Review Application") + + def test_decline_precedence_beats_s4_review(self): + block_result = __main__(**score_data(hd_score_s1=1225, hd_score_s4=1200)) + + self.assertEqual(block_result["hd_score"], 1225) + self.assertEqual(block_result["recommended_action"], "Decline Application") + self.assertEqual(block_result["hd_action_reasoncode"], "S1 Triggered") + + def test_non_s4_pass_score_in_reserved_band_shifts_down(self): + block_result = __main__(**score_data(hd_score_m1=1195, hd_score_s4=0)) + + self.assertEqual(block_result["hd_score"], 1185) + self.assertEqual(block_result["recommended_action"], "Pass Application") + self.assertEqual(block_result["hd_action_reasoncode"], "Pass") + + def test_non_s4_pass_score_below_reserved_band_stays_same(self): + block_result = __main__(**score_data(hd_score_m1=1190, hd_score_s4=0)) + + self.assertEqual(block_result["hd_score"], 1190) + self.assertEqual(block_result["recommended_action"], "Pass Application") + + def test_request_schema_adds_s4_and_response_schema_is_unchanged(self): + request_schema = json.loads((BASE_DIR / "request_schema.json").read_text()) + response_schema = json.loads((BASE_DIR / "response_schema.json").read_text()) + + self.assertEqual(request_schema["properties"]["hd_score_s4"]["type"], ["number", "null"]) + self.assertEqual( + set(response_schema["properties"]), + {"hd_score", "recommended_action", "description", "hd_action_reasoncode"}, + ) if __name__ == "__main__":