Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
be13818
test action mapper doesn't accept invalid actions
Karthikeyannhs Aug 7, 2025
4511b17
Attribute level and name relations when it is cohort
Karthikeyannhs Aug 7, 2025
3d4b26e
added iteration_cohorts_validation
Karthikeyannhs Aug 7, 2025
d02cb00
chainging the validations
Karthikeyannhs Aug 7, 2025
b97650d
fix
Karthikeyannhs Aug 7, 2025
758235c
fix
Karthikeyannhs Aug 7, 2025
826f3b2
fix lint
Karthikeyannhs Aug 7, 2025
fc1869e
fix lint
Karthikeyannhs Aug 7, 2025
161374a
Merge branch 'main' into feature/eli-328-cohort-validations
Karthikeyannhs Aug 8, 2025
c8bb5a5
lint fixes
Karthikeyannhs Aug 8, 2025
ad10cb7
lint fixes
Karthikeyannhs Aug 8, 2025
3b58fd5
test fixes
Karthikeyannhs Aug 8, 2025
4f302a3
lint fixes
Karthikeyannhs Aug 8, 2025
afa0a48
Removed defaultcomms from iteration level of test config.
ayeshalshukri1-nhs Aug 8, 2025
679bb65
Reorder feilds in config.
ayeshalshukri1-nhs Aug 8, 2025
1a9a0dd
Update to config.
ayeshalshukri1-nhs Aug 8, 2025
1ab0de5
default comm routing validation
Karthikeyannhs Aug 8, 2025
8d46e26
unit tests default comm routing validation
Karthikeyannhs Aug 8, 2025
ae6730b
default comm routing validation in rules
Karthikeyannhs Aug 8, 2025
e762fa7
lint fixes
Karthikeyannhs Aug 8, 2025
8be8c83
test data fixed
Karthikeyannhs Aug 8, 2025
4f6727d
grouped model validators
Karthikeyannhs Aug 8, 2025
f5bd2fb
Merge branch 'main' into feature/eli-328-cohort-validations
Karthikeyannhs Aug 8, 2025
3a1140a
Merge branch 'main' into feature/eli-328-cohort-validations
Karthikeyannhs Aug 11, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from eligibility_signposting_api.model.campaign_config import ActionsMapper


class ActionsMapperValidator(ActionsMapper):
class ActionsMapperValidation(ActionsMapper):
@model_validator(mode="after")
def validate_keys(self) -> "ActionsMapperValidator":
def validate_keys(self) -> "ActionsMapperValidation":
invalid_keys = [key for key in self.root if key is None or key == ""]
if invalid_keys:
msg = f"Invalid keys found in ActionsMapper: {invalid_keys}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@


class CampaignConfigValidation(CampaignConfig):
@classmethod
@field_validator("iterations")
@classmethod
def validate_iterations(cls, iterations: list[Iteration]) -> list[IterationValidation]:
return [IterationValidation(**i.model_dump()) for i in iterations]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from eligibility_signposting_api.model.campaign_config import IterationCohort


class IterationCohortValidation(IterationCohort):
pass
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
from eligibility_signposting_api.model.campaign_config import IterationRule
from typing import Self

from pydantic import model_validator

from eligibility_signposting_api.model.campaign_config import IterationRule, RuleAttributeLevel, RuleAttributeName


class IterationRuleValidation(IterationRule):
pass
@model_validator(mode="after")
def check_cohort_attribute_name(self) -> Self:
if (
self.attribute_level == RuleAttributeLevel.COHORT
and self.attribute_name
and self.attribute_name != RuleAttributeName("COHORT_LABEL")
):
msg = "When attribute_level is COHORT, attribute_name must be COHORT_LABEL or None (default:COHORT_LABEL)"
raise ValueError(msg)
return self
149 changes: 132 additions & 17 deletions src/rules_validation_api/validators/iteration_validator.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,152 @@
import typing

from pydantic import ValidationError, field_validator, model_validator
from pydantic import Field, ValidationError, field_validator, model_validator
from pydantic_core import InitErrorDetails

from eligibility_signposting_api.model.campaign_config import ActionsMapper, Iteration, IterationRule
from rules_validation_api.validators.actions_mapper_validator import ActionsMapperValidator
from eligibility_signposting_api.model.campaign_config import (
ActionsMapper,
Iteration,
IterationCohort,
IterationRule,
RuleType,
)
from rules_validation_api.validators.actions_mapper_validator import ActionsMapperValidation
from rules_validation_api.validators.iteration_cohort_validator import IterationCohortValidation
from rules_validation_api.validators.iteration_rules_validator import IterationRuleValidation


class IterationValidation(Iteration):
@classmethod
iteration_cohorts: list[IterationCohort] = Field(..., alias="IterationCohorts")
iteration_rules: list[IterationRule] = Field(..., alias="IterationRules")
actions_mapper: ActionsMapper = Field(..., alias="ActionsMapper")

@field_validator("iteration_rules")
def validate_iterations(cls, iteration_rules: list[IterationRule]) -> list[IterationRuleValidation]:
@classmethod
def validate_iteration_rules(cls, iteration_rules: list[IterationRule]) -> list[IterationRuleValidation]:
return [IterationRuleValidation(**i.model_dump()) for i in iteration_rules]

@field_validator("iteration_cohorts")
@classmethod
def validate_iteration_cohorts(cls, iteration_cohorts: list[IterationCohort]) -> list[IterationCohortValidation]:
return [IterationCohortValidation(**i.model_dump()) for i in iteration_cohorts]

@field_validator("actions_mapper", mode="after")
@classmethod
def transform_actions_mapper(cls, action_mapper: ActionsMapper) -> ActionsMapper:
ActionsMapperValidator.model_validate(action_mapper.model_dump())
ActionsMapperValidation.model_validate(action_mapper.model_dump())
return action_mapper

@model_validator(mode="after")
def action_mapper_validation(self) -> typing.Self:
all_errors = []

for validator in [
self.validate_default_comms_routing_in_actions_mapper,
self.validate_default_not_eligible_routing_in_actions_mapper,
self.validate_default_not_actionable_routing_in_actions_mapper,
self.validate_iteration_rules_against_actions_mapper,
]:
try:
validator()
except ValidationError as ve:
all_errors.extend(ve.errors(include_input=False))

if all_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=all_errors)

return self

def validate_default_comms_routing_in_actions_mapper(self) -> typing.Self:
default_routing = self.default_comms_routing
actions_mapper = self.actions_mapper.root.keys()

if default_routing and (not actions_mapper or default_routing not in actions_mapper):
error = InitErrorDetails(
type="value_error",
loc=("actions_mapper",),
input=actions_mapper,
ctx={"error": f"Missing entry for DefaultCommsRouting '{default_routing}' in ActionsMapper"},
)
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=[error])
default_routes = self.default_comms_routing
actions_keys = list(self.actions_mapper.root.keys())
line_errors = []

for routing in default_routes.split("|"):
cleaned_routing = routing.strip()
if cleaned_routing and (not actions_keys or cleaned_routing not in actions_keys):
error = InitErrorDetails(
type="value_error",
loc=("actions_mapper",),
input=actions_keys,
ctx={"error": f"Missing entry for DefaultCommsRouting '{cleaned_routing}' in ActionsMapper"},
)
line_errors.append(error)

if line_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=line_errors)

return self

def validate_default_not_eligible_routing_in_actions_mapper(self) -> typing.Self:
default_not_eligibile_routes = self.default_not_eligible_routing
actions_keys = list(self.actions_mapper.root.keys())
line_errors = []

for routing in default_not_eligibile_routes.split("|"):
cleaned_routing = routing.strip()
if cleaned_routing and (not actions_keys or cleaned_routing not in actions_keys):
error = InitErrorDetails(
type="value_error",
loc=("actions_mapper",),
input=actions_keys,
ctx={"error": f"Missing entry for DefaultNotEligibleRouting '{cleaned_routing}' in ActionsMapper"},
)
line_errors.append(error)

if line_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=line_errors)

return self

def validate_default_not_actionable_routing_in_actions_mapper(self) -> typing.Self:
default_not_actionable_routes = self.default_not_actionable_routing
actions_keys = list(self.actions_mapper.root.keys())
line_errors = []

for routing in default_not_actionable_routes.split("|"):
cleaned_routing = routing.strip()
if cleaned_routing and (not actions_keys or cleaned_routing not in actions_keys):
error = InitErrorDetails(
type="value_error",
loc=("actions_mapper",),
input=actions_keys,
ctx={
"error": f"Missing entry for DefaultNotActionableRouting '{cleaned_routing}' in ActionsMapper"
},
)
line_errors.append(error)

if line_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=line_errors)

return self

def validate_iteration_rules_against_actions_mapper(self) -> typing.Self:
actions_keys = list(self.actions_mapper.root.keys())
line_errors = []

for rule in self.iteration_rules:
if (
rule.type
in [
RuleType.redirect,
RuleType.not_actionable_actions,
RuleType.not_eligible_actions,
]
and rule.comms_routing
):
for routing in rule.comms_routing.split("|"):
cleaned_routing = routing.strip()
if cleaned_routing and (not actions_keys or cleaned_routing not in actions_keys):
error = InitErrorDetails(
type="value_error",
loc=("iteration_rules",),
input=actions_keys,
ctx={"error": f"Missing entry for CommsRouting '{cleaned_routing}' in ActionsMapper"},
)
line_errors.append(error)

if line_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=line_errors)

return self
2 changes: 1 addition & 1 deletion src/rules_validation_api/validators/rules_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class RulesValidation(Rules):
@classmethod
@field_validator("campaign_config")
@classmethod
def validate_campaign_config(cls, campaign_config: CampaignConfig) -> CampaignConfig:
return CampaignConfigValidation(**campaign_config.model_dump())
29 changes: 14 additions & 15 deletions tests/test_data/test_config/test_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,24 @@
"Manager": ["person@test.com"],
"Approver": ["person@test.com"],
"Reviewer": ["person@test.com"],
"StartDate": "20250101",
"EndDate": "20260101",
"ApprovalMinimum": 1,
"ApprovalMaximum": 5000000,
"IterationFrequency": "X",
"IterationType": "M",
"IterationTime": "07:00:00",
"DefaultCommsRouting": "Default_Comms_1",
"Iterations": [
{
"ID": "id_100",
"Version": "1",
"Name": "Test Config",
"Type": "M",
"IterationDate": "20250101",
"IterationNumber": 1,
"CommsType": "R",
"ApprovalMinimum": 1,
"ApprovalMaximum": 5000000,
"DefaultCommsRouting": "INTERNALCONTACTGP1",
"DefaultNotActionableRouting": "INTERNALCONTACTGP1",
"DefaultNotEligibleRouting": "INTERNALCONTACTGP1",
Expand Down Expand Up @@ -119,20 +130,8 @@
"Comparator": "19000101",
"CommsRouting": "YRULEID1|INTERNALTESCO"
}
],
"Version": "1",
"Name": "Test Config",
"Type": "M",
"IterationDate": "20250101",
"IterationNumber": 1,
"CommsType": "R",
"ApprovalMinimum": 1,
"ApprovalMaximum": 5000000
]
}
],
"StartDate": "20250101",
"EndDate": "20260101",
"ApprovalMinimum": 1,
"ApprovalMaximum": 5000000
]
}
}
16 changes: 4 additions & 12 deletions tests/unit/validation/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,12 @@ def valid_campaign_config_with_only_mandatory_fields():
"ApprovalMinimum": 10,
"ApprovalMaximum": 100,
"Type": "A",
"DefaultCommsRouting": "BOOK_NBS",
"DefaultNotEligibleRouting": "RouteB",
"DefaultNotActionableRouting": "RouteC",
"DefaultCommsRouting": "",
"DefaultNotEligibleRouting": "",
"DefaultNotActionableRouting": "",
"IterationCohorts": [],
"IterationRules": [],
"ActionsMapper": {
"BOOK_NBS": {
"ExternalRoutingCode": "BookNBS",
"ActionDescription": "",
"ActionType": "ButtonWithAuthLink",
"UrlLink": "http://www.nhs.uk/book-rsv",
"UrlLabel": "Continue to booking",
}
},
"ActionsMapper": {},
}
],
}
Expand Down
25 changes: 20 additions & 5 deletions tests/unit/validation/test_actions_mapper_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydantic import ValidationError

from eligibility_signposting_api.model.campaign_config import AvailableAction
from rules_validation_api.validators.actions_mapper_validator import ActionsMapperValidator
from rules_validation_api.validators.actions_mapper_validator import ActionsMapperValidation


@pytest.fixture
Expand All @@ -25,16 +25,31 @@ def test_valid_actions_mapper(self, valid_available_action):
"action1": self.make_action(valid_available_action),
"action2": self.make_action({**valid_available_action, "ExternalRoutingCode": "AltCode"}),
}
mapper = ActionsMapperValidator(root=data)
mapper = ActionsMapperValidation(root=data)

expected_action_count = 2
assert isinstance(mapper, ActionsMapperValidator)
assert isinstance(mapper, ActionsMapperValidation)
assert len(mapper.root) == expected_action_count

@pytest.mark.parametrize(
"invalid_action",
[
{"action1": ""},
{"action1": "invalid_action"},
{"action3": None},
{"action1": "", "action3": None},
{"action1": "invalid_action", "action2": ""},
],
)
def test_if_exception_raised_when_adding_invalid_actions_to_action_mapper(self, invalid_action):
data = {"": invalid_action}
with pytest.raises(ValidationError):
ActionsMapperValidation(root=data)

def test_invalid_actions_mapper_empty_key(self, valid_available_action):
data = {"": self.make_action(valid_available_action), "action2": self.make_action(valid_available_action)}
with pytest.raises(ValidationError) as exc_info:
ActionsMapperValidator(root=data)
ActionsMapperValidation(root=data)
assert "Invalid keys found in ActionsMapper" in str(exc_info.value)
assert "['']" in str(exc_info.value)

Expand All @@ -45,5 +60,5 @@ def test_invalid_keys_parametrized(self, bad_key, valid_available_action):
"valid_key": self.make_action(valid_available_action),
}
with pytest.raises(ValidationError) as exc_info:
ActionsMapperValidator(root=data)
ActionsMapperValidation(root=data)
assert "Invalid keys found in ActionsMapper" in str(exc_info.value)
Loading