Skip to content

Commit 229b969

Browse files
ayeshalshukri1-nhsKarthikeyannhsshweta-nhs
authored
Eli 440 magic cohort redefined with virtual (#344)
* eli 440 : virtual flag for magic cohort * added more cases for virtual cohorts * cohort labels with virtual cohorts * validation for virtual with more test * fix virtual - normalize_virtual * updated TODOS * eli-440 small invalid test addition * eli-440 updated tests, lint, format * ELI-440: Fixing format --------- Co-authored-by: karthikeyannhs <[email protected]> Co-authored-by: Shweta <[email protected]>
1 parent 010e860 commit 229b969

File tree

10 files changed

+311
-44
lines changed

10 files changed

+311
-44
lines changed
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Literal
22

3-
MAGIC_COHORT_LABEL = "elid_all_people"
43
RULE_STOP_DEFAULT = False
54
NHS_NUMBER_HEADER = "nhs-login-nhs-number"
65
ALLOWED_CONDITIONS = Literal["COVID", "FLU", "MMR", "RSV"]

src/eligibility_signposting_api/model/campaign_config.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from pydantic import BaseModel, Field, HttpUrl, RootModel, field_serializer, field_validator, model_validator
1313

14-
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS, MAGIC_COHORT_LABEL, RULE_STOP_DEFAULT
14+
from eligibility_signposting_api.config.contants import ALLOWED_CONDITIONS, RULE_STOP_DEFAULT
1515

1616
if typing.TYPE_CHECKING: # pragma: no cover
1717
from pydantic import SerializationInfo
@@ -90,18 +90,38 @@ class RuleAttributeLevel(StrEnum):
9090
COHORT = "COHORT"
9191

9292

93+
class Virtual(StrEnum):
94+
YES = "Y"
95+
NO = "N"
96+
97+
9398
class IterationCohort(BaseModel):
9499
cohort_label: CohortLabel = Field(alias="CohortLabel")
95100
cohort_group: CohortGroup = Field(alias="CohortGroup")
96101
positive_description: Description | None = Field(None, alias="PositiveDescription")
97102
negative_description: Description | None = Field(None, alias="NegativeDescription")
98103
priority: int | None = Field(None, alias="Priority")
104+
virtual: Virtual = Field(default=Virtual.NO, alias="Virtual")
99105

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

102108
@cached_property
103109
def is_magic_cohort(self) -> bool:
104-
return self.cohort_label.upper() == MAGIC_COHORT_LABEL.upper()
110+
return self.virtual == Virtual.YES
111+
112+
@field_validator("virtual", mode="before")
113+
@classmethod
114+
def normalize_virtual(cls, value: str) -> Virtual:
115+
if value is None:
116+
return Virtual.NO
117+
if isinstance(value, str):
118+
value = value.strip().upper()
119+
if value == "Y":
120+
return Virtual.YES
121+
if value == "N":
122+
return Virtual.NO
123+
msg = f"Invalid value for Virtual: {value!r}"
124+
raise ValueError(msg)
105125

106126

107127
class IterationRule(BaseModel):

src/rules_validation_api/validators/iteration_validator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ def validate_iteration_rules(cls, iteration_rules: list[IterationRule]) -> list[
2828
@field_validator("iteration_cohorts")
2929
@classmethod
3030
def validate_iteration_cohorts(cls, iteration_cohorts: list[IterationCohort]) -> list[IterationCohortValidation]:
31+
seen_labels = set()
32+
errors = []
33+
for cohort in iteration_cohorts:
34+
label = cohort.cohort_label
35+
if label in seen_labels:
36+
error = InitErrorDetails(
37+
type="value_error",
38+
loc=("iteration_cohort",),
39+
input=label,
40+
ctx={"error": f"Duplicate iteration_cohort: {label}"},
41+
)
42+
errors.append(error)
43+
seen_labels.add(label)
44+
if errors:
45+
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=errors)
3146
return [IterationCohortValidation(**i.model_dump()) for i in iteration_cohorts]
3247

3348
@field_validator("actions_mapper", mode="after")

tests/fixtures/builders/model/rule.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
RulePriority,
2626
RuleType,
2727
StatusText,
28+
Virtual,
2829
)
2930

3031

@@ -38,6 +39,7 @@ def future_date(days_ahead: int = 365) -> date:
3839

3940
class IterationCohortFactory(ModelFactory[IterationCohort]):
4041
priority = RulePriority(0)
42+
virtual = Virtual.NO
4143

4244

4345
class IterationRuleFactory(ModelFactory[IterationRule]):
@@ -114,11 +116,12 @@ def fix_iteration_date_invariants(iterations: list[Iteration], start_date: date)
114116

115117

116118
# Iteration cohort factories
117-
class MagicCohortFactory(IterationCohortFactory):
118-
cohort_label = CohortLabel("elid_all_people")
119-
cohort_group = CohortGroup("magic cohort group")
120-
positive_description = Description("magic positive description")
121-
negative_description = Description("magic negative description")
119+
class VirtualCohortFactory(IterationCohortFactory):
120+
cohort_label = CohortLabel("virtual cohort label")
121+
cohort_group = CohortGroup("virtual cohort group")
122+
positive_description = Description("virtual positive description")
123+
negative_description = Description("virtual negative description")
124+
virtual = Virtual.YES
122125
priority = 1
123126

124127

@@ -127,6 +130,7 @@ class Rsv75RollingCohortFactory(IterationCohortFactory):
127130
cohort_group = CohortGroup("rsv_age_range")
128131
positive_description = Description("rsv_age_range positive description")
129132
negative_description = Description("rsv_age_range negative description")
133+
virtual = Virtual.NO
130134
priority = 2
131135

132136

@@ -135,6 +139,7 @@ class Rsv75to79CohortFactory(IterationCohortFactory):
135139
cohort_group = CohortGroup("rsv_age_range")
136140
positive_description = Description("rsv_age_range positive description")
137141
negative_description = Description("rsv_age_range negative description")
142+
virtual = Virtual.NO
138143
priority = 3
139144

140145

@@ -143,6 +148,7 @@ class RsvPretendClinicalCohortFactory(IterationCohortFactory):
143148
cohort_group = CohortGroup("rsv_clinical_cohort")
144149
positive_description = Description("rsv_clinical_cohort positive description")
145150
negative_description = Description("rsv_clinical_cohort negative description")
151+
virtual = Virtual.NO
146152
priority = 4
147153

148154

tests/integration/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ def multiple_campaign_configs(s3_client: BaseClient, rules_bucket: BucketName) -
788788

789789

790790
@pytest.fixture(scope="class")
791-
def campaign_config_with_magic_cohort(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[CampaignConfig]:
791+
def campaign_config_with_virtual_cohort(s3_client: BaseClient, rules_bucket: BucketName) -> Generator[CampaignConfig]:
792792
campaign: CampaignConfig = rule.CampaignConfigFactory.build(
793793
target="COVID",
794794
iterations=[
@@ -797,7 +797,7 @@ def campaign_config_with_magic_cohort(s3_client: BaseClient, rules_bucket: Bucke
797797
rule.PostcodeSuppressionRuleFactory.build(type=RuleType.filter),
798798
rule.PersonAgeSuppressionRuleFactory.build(),
799799
],
800-
iteration_cohorts=[rule.MagicCohortFactory.build(cohort_label="elid_all_people")],
800+
iteration_cohorts=[rule.VirtualCohortFactory.build(cohort_label="virtual_cohort")],
801801
status_text=None,
802802
)
803803
],

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,12 @@ def test_actionable_with_and_rule(
289289
)
290290

291291

292-
class TestMagicCohortResponse:
293-
def test_not_eligible_by_rule_when_only_magic_cohort_is_present(
292+
class TestVirtualCohortResponse:
293+
def test_not_eligible_by_rule_when_only_virtual_cohort_is_present(
294294
self,
295295
client: FlaskClient,
296296
persisted_person_pc_sw19: NHSNumber,
297-
campaign_config_with_magic_cohort: CampaignConfig, # noqa: ARG002
297+
campaign_config_with_virtual_cohort: CampaignConfig, # noqa: ARG002
298298
):
299299
# Given
300300

@@ -317,9 +317,9 @@ def test_not_eligible_by_rule_when_only_magic_cohort_is_present(
317317
"status": "NotEligible",
318318
"eligibilityCohorts": [
319319
{
320-
"cohortCode": "magic cohort group",
320+
"cohortCode": "virtual cohort group",
321321
"cohortStatus": "NotEligible",
322-
"cohortText": "magic negative description",
322+
"cohortText": "virtual negative description",
323323
}
324324
],
325325
"actions": [],
@@ -333,11 +333,11 @@ def test_not_eligible_by_rule_when_only_magic_cohort_is_present(
333333
),
334334
)
335335

336-
def test_not_actionable_when_only_magic_cohort_is_present(
336+
def test_not_actionable_when_only_virtual_cohort_is_present(
337337
self,
338338
client: FlaskClient,
339339
persisted_person: NHSNumber,
340-
campaign_config_with_magic_cohort: CampaignConfig, # noqa: ARG002
340+
campaign_config_with_virtual_cohort: CampaignConfig, # noqa: ARG002
341341
):
342342
# Given
343343

@@ -360,9 +360,9 @@ def test_not_actionable_when_only_magic_cohort_is_present(
360360
"status": "NotActionable",
361361
"eligibilityCohorts": [
362362
{
363-
"cohortCode": "magic cohort group",
363+
"cohortCode": "virtual cohort group",
364364
"cohortStatus": "NotActionable",
365-
"cohortText": "magic positive description",
365+
"cohortText": "virtual positive description",
366366
}
367367
],
368368
"actions": [],
@@ -382,11 +382,11 @@ def test_not_actionable_when_only_magic_cohort_is_present(
382382
),
383383
)
384384

385-
def test_actionable_when_only_magic_cohort_is_present(
385+
def test_actionable_when_only_virtual_cohort_is_present(
386386
self,
387387
client: FlaskClient,
388388
persisted_77yo_person: NHSNumber,
389-
campaign_config_with_magic_cohort: CampaignConfig, # noqa: ARG002
389+
campaign_config_with_virtual_cohort: CampaignConfig, # noqa: ARG002
390390
):
391391
# Given
392392

@@ -409,9 +409,9 @@ def test_actionable_when_only_magic_cohort_is_present(
409409
"status": "Actionable",
410410
"eligibilityCohorts": [
411411
{
412-
"cohortCode": "magic cohort group",
412+
"cohortCode": "virtual cohort group",
413413
"cohortStatus": "Actionable",
414-
"cohortText": "magic positive description",
414+
"cohortText": "virtual positive description",
415415
}
416416
],
417417
"actions": [

0 commit comments

Comments
 (0)