diff --git a/src/eligibility_signposting_api/model/eligibility.py b/src/eligibility_signposting_api/model/eligibility.py index dcf81dec..e33b2b43 100644 --- a/src/eligibility_signposting_api/model/eligibility.py +++ b/src/eligibility_signposting_api/model/eligibility.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import date -from enum import Enum, auto +from enum import Enum, StrEnum, auto from functools import total_ordering from typing import NewType, Self @@ -13,7 +13,7 @@ RuleResult = NewType("RuleResult", str) -class RuleType(str, Enum): +class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" diff --git a/src/eligibility_signposting_api/model/rules.py b/src/eligibility_signposting_api/model/rules.py index 26a75ee0..b524b6e4 100644 --- a/src/eligibility_signposting_api/model/rules.py +++ b/src/eligibility_signposting_api/model/rules.py @@ -2,7 +2,7 @@ import typing from datetime import UTC, date, datetime -from enum import Enum +from enum import StrEnum from functools import cached_property from operator import attrgetter from typing import Literal, NewType @@ -29,13 +29,13 @@ CohortLabel = NewType("CohortLabel", str) -class RuleType(str, Enum): +class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" -class RuleOperator(str, Enum): +class RuleOperator(StrEnum): equals = "=" gt = ">" lt = "<" @@ -73,7 +73,7 @@ class RuleOperator(str, Enum): year_gt = "Y>" -class RuleAttributeLevel(str, Enum): +class RuleAttributeLevel(StrEnum): PERSON = "PERSON" TARGET = "TARGET" COHORT = "COHORT" diff --git a/src/eligibility_signposting_api/views/response_model/eligibility.py b/src/eligibility_signposting_api/views/response_model/eligibility.py index 06f23354..ae4ecfc8 100644 --- a/src/eligibility_signposting_api/views/response_model/eligibility.py +++ b/src/eligibility_signposting_api/views/response_model/eligibility.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import StrEnum from typing import NewType from pydantic import UUID4, BaseModel, Field, HttpUrl, field_serializer @@ -17,13 +17,13 @@ CohortText = NewType("CohortText", str) -class Status(str, Enum): +class Status(StrEnum): not_eligible = "NotEligible" not_actionable = "NotActionable" actionable = "Actionable" -class RuleType(str, Enum): +class RuleType(StrEnum): filter = "F" suppression = "S" redirect = "R" diff --git a/tests/fixtures/builders/model/rule.py b/tests/fixtures/builders/model/rule.py index 04b8bd3d..4b72e26d 100644 --- a/tests/fixtures/builders/model/rule.py +++ b/tests/fixtures/builders/model/rule.py @@ -5,6 +5,7 @@ from polyfactory.factories.pydantic_factory import ModelFactory from eligibility_signposting_api.model import rules +from eligibility_signposting_api.model.rules import RulePriority def past_date(days_behind: int = 365) -> date: @@ -38,6 +39,7 @@ class PersonAgeSuppressionRuleFactory(IterationRuleFactory): type = rules.RuleType.suppression name = "Exclude too young less than 75" description = "Exclude too young less than 75" + priority = RulePriority(10) operator = rules.RuleOperator.year_gt attribute_level = rules.RuleAttributeLevel.PERSON attribute_name = "DATE_OF_BIRTH" diff --git a/tests/unit/services/test_eligibility_services.py b/tests/unit/services/test_eligibility_services.py index 56cb200c..613080c8 100644 --- a/tests/unit/services/test_eligibility_services.py +++ b/tests/unit/services/test_eligibility_services.py @@ -7,7 +7,7 @@ from hamcrest import assert_that, empty, has_item from eligibility_signposting_api.model.eligibility import ConditionName, DateOfBirth, NHSNumber, Status -from eligibility_signposting_api.model.rules import IterationDate +from eligibility_signposting_api.model.rules import IterationDate, RuleType from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError, RulesRepo from eligibility_signposting_api.services import EligibilityService, UnknownPersonError from tests.fixtures.builders.model import rule as rule_builder @@ -25,10 +25,10 @@ def test_eligibility_service_returns_from_repo(): eligibility_repo = MagicMock(spec=EligibilityRepo) rules_repo = MagicMock(spec=RulesRepo) eligibility_repo.get_eligibility = MagicMock(return_value=[]) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber("1234567890")) + actual = service.get_eligibility_status(NHSNumber("1234567890")) # Then assert_that(actual, is_eligibility_status().with_conditions(empty())) @@ -39,11 +39,11 @@ def test_eligibility_service_for_nonexistent_nhs_number(): eligibility_repo = MagicMock(spec=EligibilityRepo) rules_repo = MagicMock(spec=RulesRepo) eligibility_repo.get_eligibility_data = MagicMock(side_effect=NotFoundError) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When with pytest.raises(UnknownPersonError): - ps.get_eligibility_status(NHSNumber("1234567890")) + service.get_eligibility_status(NHSNumber("1234567890")) def test_not_base_eligible(faker: Faker): @@ -69,10 +69,10 @@ def test_not_base_eligible(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that( @@ -123,10 +123,10 @@ def test_only_live_campaigns_considered(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that( @@ -161,10 +161,10 @@ def test_base_eligible_and_simple_rule_includes(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that( @@ -178,7 +178,7 @@ def test_base_eligible_and_simple_rule_includes(faker: Faker): def test_base_eligible_but_simple_rule_excludes(faker: Faker): # Given nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") - date_of_birth = DateOfBirth(faker.date_of_birth(maximum_age=74)) + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) eligibility_repo = MagicMock(spec=EligibilityRepo) rules_repo = MagicMock(spec=RulesRepo) @@ -199,10 +199,10 @@ def test_base_eligible_but_simple_rule_excludes(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that( @@ -252,10 +252,10 @@ def test_simple_rule_only_excludes_from_live_iteration(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that( @@ -287,10 +287,98 @@ def test_campaign_with_no_active_iteration_not_considered(faker: Faker): ] ) - ps = EligibilityService(eligibility_repo, rules_repo) + service = EligibilityService(eligibility_repo, rules_repo) # When - actual = ps.get_eligibility_status(NHSNumber(nhs_number)) + actual = service.get_eligibility_status(NHSNumber(nhs_number)) # Then assert_that(actual, is_eligibility_status().with_conditions(empty())) + + +@pytest.mark.parametrize( + ("rule_type", "expected_status"), + [ + (RuleType.suppression, Status.not_actionable), + (RuleType.filter, Status.not_eligible), + (RuleType.redirect, Status.actionable), + ], +) +def test_rule_types_cause_correct_statuses(rule_type: RuleType, expected_status: Status, faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) + + eligibility_repo = MagicMock(spec=EligibilityRepo) + rules_repo = MagicMock(spec=RulesRepo) + eligibility_repo.get_eligibility_data = MagicMock( + return_value=eligibility_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + ) + rules_repo.get_campaign_configs = MagicMock( + return_value=[ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[rule_builder.PersonAgeSuppressionRuleFactory.build(type=rule_type)], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + ) + + service = EligibilityService(eligibility_repo, rules_repo) + + # When + actual = service.get_eligibility_status(NHSNumber(nhs_number)) + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(expected_status)) + ), + ) + + +def test_multiple_rule_types_cause_correct_status(faker: Faker): + # Given + nhs_number = NHSNumber(f"5{faker.random_int(max=999999999):09d}") + date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74)) + + eligibility_repo = MagicMock(spec=EligibilityRepo) + rules_repo = MagicMock(spec=RulesRepo) + eligibility_repo.get_eligibility_data = MagicMock( + return_value=eligibility_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"]) + ) + rules_repo.get_campaign_configs = MagicMock( + return_value=[ + rule_builder.CampaignConfigFactory.build( + target="RSV", + iterations=[ + rule_builder.IterationFactory.build( + iteration_rules=[ + rule_builder.PersonAgeSuppressionRuleFactory.build(type=RuleType.suppression), + rule_builder.PersonAgeSuppressionRuleFactory.build(type=RuleType.filter), + rule_builder.PersonAgeSuppressionRuleFactory.build(type=RuleType.suppression), + ], + iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")], + ) + ], + ) + ] + ) + + service = EligibilityService(eligibility_repo, rules_repo) + + # When + actual = service.get_eligibility_status(NHSNumber(nhs_number)) + + # Then + assert_that( + actual, + is_eligibility_status().with_conditions( + has_item(is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_eligible)) + ), + )