Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/eligibility_signposting_api/model/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ class IterationRule(BaseModel):
rule_stop: RuleStop | None = Field(None, alias="RuleStop")

@field_validator("rule_stop", mode="before")
def parse_yn_to_bool(cls, v: str) -> bool: # noqa: N805
def parse_yn_to_bool(cls, v: str | bool) -> bool: # noqa: N805
if isinstance(v, str):
return v.upper() == "Y"
return False
return v

model_config = {"populate_by_name": True, "extra": "ignore"}

Expand Down
33 changes: 33 additions & 0 deletions tests/unit/model/test_rules.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import json

import pytest
from dateutil.relativedelta import relativedelta
from faker import Faker
from hamcrest import assert_that

from eligibility_signposting_api.model.rules import IterationRule
from tests.fixtures.builders.model.rule import IterationFactory, RawCampaignConfigFactory
from tests.fixtures.matchers.rules import is_iteration_rule


def test_campaign_must_have_at_least_one_iteration():
Expand Down Expand Up @@ -59,3 +64,31 @@ def test_iteration_must_have_active_iteration_from_its_start(faker: Faker):
r".*1st iteration starts later",
):
RawCampaignConfigFactory.build(start_date=start_date, iterations=[iteration])


@pytest.mark.parametrize(
("rule_stop", "expected"),
[
("Y", True),
("N", False),
("", False),
(None, False),
],
)
def test_iteration_rule_deserialisation(rule_stop: str, expected):
# Given
rule_json = f"""{{"Type": "F",
"Name": "Exclude TOO YOUNG",
"Description": "Exclude too Young less than 75 on the day of run",
"Priority": 110,
"AttributeLevel": "PERSON",
"AttributeName": "DATE_OF_BIRTH",
"Operator": "Y>",
"Comparator": "-75",
"RuleStop": "{rule_stop if rule_stop is not None else "null"}"}}"""

# When
actual = IterationRule.model_validate(json.loads(rule_json))

# Then
assert_that(actual, is_iteration_rule().with_rule_stop(expected))
36 changes: 8 additions & 28 deletions tests/unit/services/calculators/test_eligibility_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from eligibility_signposting_api.model import rules
from eligibility_signposting_api.model import rules as rules_model
from eligibility_signposting_api.model.eligibility import ConditionName, DateOfBirth, NHSNumber, Postcode, Status
from eligibility_signposting_api.model.rules import IterationRule
from eligibility_signposting_api.services.calculators.eligibility_calculator import EligibilityCalculator
from tests.fixtures.builders.model import rule as rule_builder
from tests.fixtures.builders.repos.person import person_rows_builder
Expand Down Expand Up @@ -817,49 +816,30 @@ def test_status_if_iteration_rules_contains_cohort_label_field(
@pytest.mark.parametrize(
("rule_stop", "expected_status", "test_comment"),
[
("Y", Status.not_actionable, "Stops at the first rule"),
("N", Status.not_eligible, "Both the rules are executed"),
("", Status.not_eligible, "Both the rules are executed"),
(None, Status.not_eligible, "Both the rules are executed"),
(True, Status.not_actionable, "Stops at the first rule"),
(False, Status.not_eligible, "Both the rules are executed"),
],
)
def test_rules_stop_behavior(rule_stop: str | None, expected_status: Status, test_comment: str, faker: Faker) -> None:
def test_rules_stop_behavior(rule_stop: bool, expected_status: Status, test_comment: str, faker: Faker) -> None: # noqa: FBT001
# Given
nhs_number = NHSNumber(faker.nhs_number())
date_of_birth = DateOfBirth(faker.date_of_birth(minimum_age=18, maximum_age=74))
person_rows = person_rows_builder(nhs_number, date_of_birth=date_of_birth, cohorts=["cohort1"])

# Base rule template
# Not using model factory to create Iteration rules since it sets boolean values for "Y"/"N"
simple_age_data = {
"Name": "Exclude too young less than 75",
"Description": "Exclude too young less than 75",
"AttributeLevel": "PERSON",
"AttributeName": "DATE_OF_BIRTH",
"Operator": "Y>",
"Comparator": "-75",
}

# Build rule variations
rule_variants = [
{"Type": "S", "Priority": 10, "RuleStop": rule_stop},
{"Type": "S", "Priority": 10},
{"Type": "F", "Priority": 15},
]

iteration_rules = [IterationRule.model_validate({**simple_age_data, **variant}) for variant in rule_variants]

# Build campaign configuration
campaign_config = rule_builder.CampaignConfigFactory.build(
target="RSV",
iterations=[
rule_builder.IterationFactory.build(
iteration_rules=[],
iteration_rules=[
rule_builder.PersonAgeSuppressionRuleFactory.build(priority=10, rule_stop=rule_stop),
rule_builder.PersonAgeSuppressionRuleFactory.build(priority=10),
rule_builder.PersonAgeSuppressionRuleFactory.build(type=rules.RuleType.filter, priority=15),
],
iteration_cohorts=[rule_builder.IterationCohortFactory.build(cohort_label="cohort1")],
)
],
)
campaign_config.iterations[0].iteration_rules.extend(iteration_rules)

calculator = EligibilityCalculator(person_rows, [campaign_config])

Expand Down
Loading