Skip to content

Commit 03fcfd9

Browse files
committed
work in progress
1 parent 014d138 commit 03fcfd9

4 files changed

Lines changed: 140 additions & 1 deletion

File tree

src/eligibility_signposting_api/services/processors/rule_processor.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,22 @@ def is_actionable(
7878
priority_getter = attrgetter("priority")
7979
suppression_reasons = []
8080

81-
sorted_rules_by_priority = sorted(self.get_exclusion_rules(cohort, suppression_rules), key=priority_getter)
81+
sorted_rules_by_priority = sorted(suppression_rules, key=priority_getter)
8282

8383
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
84+
group_rules = list(rule_group)
85+
cohort_specific_rules = [rule for rule in group_rules if rule.cohort_label is not None]
86+
matching_specific_rules = [
87+
rule for rule in cohort_specific_rules if rule.cohort_label == cohort.cohort_label
88+
]
89+
if cohort_specific_rules and not matching_specific_rules:
90+
continue
91+
92+
applicable_rules = list(self.get_exclusion_rules(cohort, group_rules))
93+
94+
if not applicable_rules:
95+
continue
96+
8497
status, group_exclusion_reasons, rule_stop = self.evaluate_rules_priority_group(person, rule_group)
8598
if status.is_exclusion:
8699
is_actionable = False

tests/integration/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,40 @@ def campaign_config(s3_client: BaseClient, rules_bucket: BucketName) -> Generato
480480
yield campaign
481481
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
482482

483+
@pytest.fixture(scope="class")
484+
def campaign_config_with_and_rule(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[CampaignConfig]:
485+
campaign: CampaignConfig = rule.CampaignConfigFactory.build(
486+
target="RSV",
487+
iterations=[
488+
rule.IterationFactory.build(
489+
iteration_rules=[
490+
rule.PostcodeSuppressionRuleFactory.build(cohort_label="cohort2",),
491+
rule.PersonAgeSuppressionRuleFactory.build(),
492+
],
493+
iteration_cohorts=[
494+
rule.IterationCohortFactory.build(
495+
cohort_label="cohort1",
496+
cohort_group="cohort_group1",
497+
positive_description="positive_description",
498+
negative_description="negative_description",
499+
),
500+
rule.IterationCohortFactory.build(
501+
cohort_label="cohort2",
502+
cohort_group="cohort_group2",
503+
positive_description="positive_description",
504+
negative_description="negative_description",
505+
)
506+
],
507+
)
508+
],
509+
)
510+
campaign_data = {"CampaignConfig": campaign.model_dump(by_alias=True)}
511+
s3_client.put_object(
512+
Bucket=rules_bucket, Key=f"{campaign.name}.json", Body=json.dumps(campaign_data), ContentType="application/json"
513+
)
514+
yield campaign
515+
s3_client.delete_object(Bucket=rules_bucket, Key=f"{campaign.name}.json")
516+
483517

484518
@pytest.fixture(scope="class")
485519
def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[list[CampaignConfig]]:

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,57 @@ def test_actionable(
238238
)
239239

240240

241+
def test_actionable_with_and_rule(
242+
self,
243+
client: FlaskClient,
244+
persisted_person: NHSNumber,
245+
campaign_config_with_and_rule: CampaignConfig, # noqa: ARG002
246+
):
247+
# Given
248+
249+
# When
250+
response = client.get(f"/patient-check/{persisted_person}?includeActions=Y")
251+
252+
# Then
253+
assert_that(
254+
response,
255+
is_response()
256+
.with_status_code(HTTPStatus.OK)
257+
.and_text(
258+
is_json_that(
259+
has_entry(
260+
"processedSuggestions",
261+
equal_to(
262+
[
263+
{
264+
"condition": "RSV",
265+
"status": "Actionable",
266+
"eligibilityCohorts": [
267+
{
268+
"cohortCode": "cohort_group1",
269+
"cohortStatus": "Actionable",
270+
"cohortText": "positive_description",
271+
}
272+
],
273+
"actions": [
274+
{
275+
"actionCode": "action_code",
276+
"actionType": "defaultcomms",
277+
"description": "",
278+
"urlLabel": "",
279+
"urlLink": "",
280+
}
281+
],
282+
"suitabilityRules": [],
283+
"statusText": "You should have the RSV vaccine",
284+
}
285+
]
286+
),
287+
)
288+
)
289+
),
290+
)
291+
241292
class TestMagicCohortResponse:
242293
def test_not_eligible_by_rule_when_only_magic_cohort_is_present(
243294
self,

tests/unit/services/processors/test_rule_processor.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,47 @@ def test_evaluate_rules_priority_group_with_rule_stop(mock_rule_calculator_class
144144
assert_that(is_rule_stop, is_(True))
145145

146146

147+
@patch.object(RuleProcessor, "evaluate_rules_priority_group")
148+
def test_general_rule_should_not_evaluate_in_isolation_without_matching_specific_rule(
149+
mock_evaluate_rules_priority_group,
150+
rule_processor,
151+
):
152+
# Person is in COHORT_B
153+
cohort = rule_builder.IterationCohortFactory.build(
154+
cohort_label="COHORT_B",
155+
positive_description="Eligible"
156+
)
157+
cohort_results = {}
158+
159+
# Rule 1: cohort-specific to COHORT_A — should be filtered out
160+
rule_specific = rule_builder.IterationRuleFactory.build(
161+
priority=510,
162+
type=RuleType.suppression,
163+
cohort_label="COHORT_A",
164+
name="SPECIFIC_RULE"
165+
)
166+
167+
# Rule 2: General rule
168+
rule_general = rule_builder.IterationRuleFactory.build(
169+
priority=510,
170+
type=RuleType.suppression,
171+
cohort_label=None,
172+
name="GENERAL_RULE"
173+
)
174+
175+
suppression_rules = [rule_specific, rule_general]
176+
177+
# Act
178+
rule_processor.is_actionable(MOCK_PERSON_DATA, cohort, cohort_results, suppression_rules)
179+
180+
# ❌ BUG: General rule should not be evaluated in isolation.
181+
mock_evaluate_rules_priority_group.assert_not_called()
182+
183+
# Cohort remains actionable
184+
assert_that(cohort_results["COHORT_B"].status, is_(Status.actionable))
185+
186+
187+
147188
@patch.object(RuleProcessor, "evaluate_rules_priority_group")
148189
@patch.object(RuleProcessor, "get_exclusion_rules", side_effect=lambda cohort, rules_to_filter: rules_to_filter) # noqa: ARG005
149190
def test_is_eligible_by_filter_rules_eligible(

0 commit comments

Comments
 (0)