Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ class RuleProcessor:

def is_base_eligible(self, person: Person, cohort: IterationCohort) -> bool:
if cohort.is_virtual_cohort:
for row in person.data:
if row.get("ATTRIBUTE_TYPE", "") == "COHORTS":
row["COHORT_MEMBERSHIPS"].append({"COHORT_LABEL": cohort.cohort_label})
cohorts_data = next((row for row in person.data if row.get("ATTRIBUTE_TYPE") == "COHORTS"), None)

if cohorts_data is None:
cohorts_data = {"ATTRIBUTE_TYPE": "COHORTS", "COHORT_MEMBERSHIPS": []}
person.data.append(cohorts_data)

cohorts_data.setdefault("COHORT_MEMBERSHIPS", []).append({"COHORT_LABEL": cohort.cohort_label})

person_cohorts = self.person_data_reader.get_person_cohorts(person)

Expand Down
44 changes: 22 additions & 22 deletions tests/fixtures/builders/repos/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def person_rows_builder( # noqa:PLR0913
gender: Gender | None = ...,
postcode: str | None = ...,
cohorts: Sequence[str] | None = ...,
vaccines: Sequence[tuple[str, date]] | None = ...,
vaccines: dict[str, dict[str, str | None]] | None = ...,
icb: str | None = ...,
gp_practice: str | None = ...,
pcn: str | None = ...,
Expand All @@ -36,8 +36,6 @@ def person_rows_builder( # noqa:PLR0913
date_of_birth = date_of_birth if date_of_birth is not ... else faker.date_of_birth(minimum_age=18, maximum_age=99)
gender = gender if gender is not ... else choice(get_args(Gender))
postcode = postcode if postcode is not ... else faker.postcode()
cohorts = cohorts if cohorts is not ... else ["cohort-a", "cohort-b"]
vaccines = vaccines if vaccines is not ... else [("RSV", faker.past_date("-5y")), ("COVID", faker.past_date("-5y"))]
icb = icb if icb is not ... else faker.icb()
gp_practice = gp_practice if gp_practice is not ... else faker.gp_practice()
pcn = pcn if pcn is not ... else faker.pcn()
Expand All @@ -64,26 +62,28 @@ def person_rows_builder( # noqa:PLR0913
"MSOA": msoa,
"LSOA": lsoa,
},
{
"NHS_NUMBER": key,
"ATTRIBUTE_TYPE": "COHORTS",
"COHORT_MEMBERSHIPS": [
{"COHORT_LABEL": cohort, "DATE_JOINED": faker.past_date().strftime("%Y%m%d")} for cohort in cohorts
],
},
]
rows.extend(
{
"NHS_NUMBER": key,
"ATTRIBUTE_TYPE": vaccine,
"LAST_SUCCESSFUL_DATE": (
last_successful_date.strftime("%Y%m%d") if last_successful_date else last_successful_date
),
"OPTOUT": choice(["Y", "N"]),
"LAST_INVITE_DATE": faker.past_date("-5y").strftime("%Y%m%d"),
}
for vaccine, last_successful_date in vaccines
)

if cohorts is not ... and cohorts:
rows.append(
{
"NHS_NUMBER": key,
"ATTRIBUTE_TYPE": "COHORTS",
"COHORT_MEMBERSHIPS": [
{"COHORT_LABEL": cohort, "DATE_JOINED": faker.past_date().strftime("%Y%m%d")} for cohort in cohorts
],
}
)

if vaccines is not ... and vaccines:
rows.extend(
{
"NHS_NUMBER": key,
"ATTRIBUTE_TYPE": vaccine_name,
**details,
}
for vaccine_name, details in vaccines.items()
)

shuffle(rows)

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def person_with_all_data(person_table: Any, faker: Faker) -> Generator[eligibili
gender="0",
postcode="SW18",
cohorts=["cohort_label1", "cohort_label2"],
vaccines=[("RSV", None)],
vaccines={"RSV": {"LAST_SUCCESSFUL_DATE": None}},
icb="QE1",
gp_practice="C81002",
pcn="U78207",
Expand Down
2 changes: 0 additions & 2 deletions tests/integration/repo/test_person_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ def test_person_found(person_table: Any, persisted_person: NHSNumber):
contains_inanyorder(
has_entries({"NHS_NUMBER": persisted_person, "ATTRIBUTE_TYPE": "PERSON"}),
has_entries({"NHS_NUMBER": persisted_person, "ATTRIBUTE_TYPE": "COHORTS"}),
has_entries({"NHS_NUMBER": persisted_person, "ATTRIBUTE_TYPE": "COVID"}),
has_entries({"NHS_NUMBER": persisted_person, "ATTRIBUTE_TYPE": "RSV"}),
),
)

Expand Down
132 changes: 116 additions & 16 deletions tests/unit/services/calculators/test_eligibility_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,16 +329,7 @@ def test_status_on_target_based_on_last_successful_date(
nhs_number = NHSNumber(faker.nhs_number())

target_rows = person_rows_builder(
nhs_number,
cohorts=["cohort1"],
vaccines=[
(
vaccine,
datetime.datetime.strptime(last_successful_date, "%Y%m%d").replace(tzinfo=datetime.UTC)
if last_successful_date
else None,
)
],
nhs_number, cohorts=["cohort1"], vaccines={vaccine: {"LAST_SUCCESSFUL_DATE": last_successful_date}}
)

campaign_configs = [
Expand Down Expand Up @@ -728,7 +719,7 @@ def test_cohort_group_descriptions_are_selected_based_on_priority_when_cohorts_h
@freeze_time("2025-04-25")
def test_no_active_iteration_returns_empty_conditions_with_single_active_campaign(faker: Faker):
# Given
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()))
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()), cohorts=[])
campaign_configs = [
rule_builder.CampaignConfigFactory.build(
target="RSV",
Expand Down Expand Up @@ -757,7 +748,7 @@ def test_no_active_iteration_returns_empty_conditions_with_single_active_campaig
@freeze_time("2025-04-25")
def test_returns_no_condition_data_for_campaign_without_active_iteration(faker: Faker, caplog):
# Given
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()))
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()), cohorts=[])
campaign_configs = [
rule_builder.CampaignConfigFactory.build(
target="RSV",
Expand Down Expand Up @@ -801,7 +792,7 @@ def test_returns_no_condition_data_for_campaign_without_active_iteration(faker:
@freeze_time("2025-04-25")
def test_no_active_campaign(faker: Faker):
# Given
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()))
person_rows = person_rows_builder(NHSNumber(faker.nhs_number()), cohorts=[])
campaign_configs = [rule_builder.CampaignConfigFactory.build()]
# Need to set the campaign dates to override CampaignConfigFactory.fix_iteration_date_invariants behavior
campaign_configs[0].start_date = datetime.date(2025, 5, 10)
Expand All @@ -824,7 +815,7 @@ def test_eligibility_status_replaces_tokens_with_attribute_data(faker: Faker):
nhs_number,
date_of_birth=date_of_birth,
cohorts=["cohort_1", "cohort_2", "cohort_3"],
vaccines=[("RSV", datetime.date(2024, 1, 3))],
vaccines={"RSV": {"LAST_SUCCESSFUL_DATE": datetime.date(2024, 1, 3).strftime("%Y%m%d")}},
icb="QE1",
gp_practice=None,
)
Expand Down Expand Up @@ -1161,6 +1152,109 @@ def test_multiple_virtual_cohorts(faker: Faker):
)


@freeze_time("2025-10-02")
def test_virtual_cohorts_when_person_has_no_existing_cohorts(faker: Faker):
# Given
nhs_number = NHSNumber(faker.nhs_number())
date_of_birth = DateOfBirth(datetime.date(1980, 10, 2))
person_rows = person_rows_builder(
nhs_number,
date_of_birth=date_of_birth,
cohorts=[],
vaccines={
"RSV": {
"LAST_SUCCESSFUL_DATE": datetime.date(2025, 9, 25).strftime("%Y%m%d"),
"BOOKED_APPOINTMENT_DATE": datetime.date(2025, 10, 9).strftime("%Y%m%d"),
},
},
)
campaign_configs = [
rule_builder.CampaignConfigFactory.build(
target="RSV",
iterations=[
rule_builder.IterationFactory.build(
iteration_cohorts=[
rule_builder.IterationCohortFactory.build(
cohort_label="rsv_75to79",
cohort_group="rsv_age",
positive_description="In rsv_75to79",
negative_description="Out rsv_75to79",
priority=0,
),
rule_builder.IterationCohortFactory.build(
cohort_label="rsv_80_since_02_Sept_2024",
cohort_group="rsv_age_catchup",
positive_description="In rsv_80_since_02_Sept_2024",
negative_description="Out rsv_80_since_02_Sept_2024",
priority=10,
),
rule_builder.IterationCohortFactory.build(
cohort_label="elid_all_people",
cohort_group="magic_cohort",
positive_description="In elid_all_people",
negative_description="Out elid_all_people",
priority=20,
virtual="Y",
),
],
iteration_rules=[
rule_builder.PersonAgeSuppressionRuleFactory.build(
attribute_level=RuleAttributeLevel.TARGET,
attribute_name="LAST_SUCCESSFUL_DATE",
attribute_target="RSV",
cohort_label="elid_all_people",
comparator="-25[[NVL:18000101]]",
description="Remove anyone NOT already vaccinated within the last 25 years",
name="Remove from magic cohort unless already vaccinated or have future booking",
operator=RuleOperator.year_lte,
priority=100,
type=RuleType.filter,
),
rule_builder.PersonAgeSuppressionRuleFactory.build(
attribute_level=RuleAttributeLevel.TARGET,
attribute_name="BOOKED_APPOINTMENT_DATE",
attribute_target="RSV",
cohort_label="elid_all_people",
comparator="0[[NVL:18000101]]",
description="Remove anyone without a future booking from magic cohort",
name="Remove from magic cohort unless already vaccinated or have future booking",
operator=RuleOperator.day_lt,
priority=110,
type=RuleType.filter,
),
rule_builder.PersonAgeSuppressionRuleFactory.build(
attribute_level=RuleAttributeLevel.TARGET,
attribute_name="LAST_SUCCESSFUL_DATE",
attribute_target="RSV",
comparator="-25[[NVL:18000101]]",
description="## You've had your RSV vaccination\n\nWe believe you had your vaccination.",
name="Already Vaccinated",
operator=RuleOperator.year_gte,
priority=200,
rule_stop=True,
type=RuleType.suppression,
),
],
)
],
)
]

calculator = EligibilityCalculator(person_rows, campaign_configs)

# When
actual = calculator.get_eligibility_status("Y", ["ALL"], "ALL")

assert_that(
actual,
is_eligibility_status().with_conditions(
has_items(
is_condition().with_condition_name(ConditionName("RSV")).and_status(Status.not_actionable),
)
),
)


def test_regardless_of_final_status_audit_all_types_of_cohort_status_rules(faker: Faker):
# Given
nhs_number = NHSNumber(faker.nhs_number())
Expand Down Expand Up @@ -1277,7 +1371,10 @@ def test_eligibility_status_with_invalid_tokens_raises_attribute_error(faker: Fa
date_of_birth = DateOfBirth(datetime.date(2025, 5, 10))

person_rows = person_rows_builder(
nhs_number, date_of_birth=date_of_birth, cohorts=["cohort_1"], vaccines=[("RSV", datetime.date(2024, 1, 3))]
nhs_number,
date_of_birth=date_of_birth,
cohorts=["cohort_1"],
vaccines={"RSV": {"LAST_SUCCESSFUL_DATE": datetime.date(2024, 1, 3).strftime("%Y%m%d")}},
)

target_attribute_token = "LAST_SUCCESSFUL_DATE: [[TARGET.RSV.LAST_SUCCESSFUL_DATE:INVALID_DATE_FORMAT(%d %B %Y)]]" # noqa: S105
Expand Down Expand Up @@ -1309,7 +1406,10 @@ def test_eligibility_status_with_invalid_person_attribute_name_raises_value_erro
date_of_birth = DateOfBirth(datetime.date(2025, 5, 10))

person_rows = person_rows_builder(
nhs_number, date_of_birth=date_of_birth, cohorts=["cohort_1"], vaccines=[("RSV", datetime.date(2024, 1, 3))]
nhs_number,
date_of_birth=date_of_birth,
cohorts=["cohort_1"],
vaccines={"RSV": {"LAST_SUCCESSFUL_DATE": datetime.date(2024, 1, 3).strftime("%Y%m%d")}},
)

target_attribute_token = "LAST_SUCCESSFUL_DATE: [[TARGET.RSV.ICECREAM]]" # noqa: S105
Expand Down