Skip to content

Commit e72c9c3

Browse files
ELI-615 | wip
1 parent 10c66a4 commit e72c9c3

3 files changed

Lines changed: 53 additions & 113 deletions

File tree

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ def get_the_best_cohort_memberships(
8181

8282
return best_status, best_cohorts
8383

84-
def get_eligibility_status(self, include_actions: str, conditions: list[str], category: str) -> EligibilityStatus:
84+
def get_eligibility_status(self, include_actions: str, conditions: list[str], requested_category: str) -> EligibilityStatus:
8585
include_actions_flag = include_actions.upper() == "Y"
8686
condition_results: dict[ConditionName, IterationResult] = {}
8787
final_result = []
8888

89-
requested_grouped_campaigns = self.campaign_evaluator.get_requested_grouped_campaigns(
90-
self.campaign_configs, conditions, category
89+
requested_grouped_campaigns = self.campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
90+
self.campaign_configs, conditions, requested_category
9191
)
9292
for condition_name, campaign_group in requested_grouped_campaigns:
9393
best_iteration_result = self.get_best_iteration_result(campaign_group)
Lines changed: 18 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
from collections.abc import Collection, Iterator
32
from itertools import groupby
43
from operator import attrgetter
@@ -8,8 +7,6 @@
87
from eligibility_signposting_api.model import eligibility_status
98
from eligibility_signposting_api.model.campaign_config import CampaignConfig
109

11-
logger = logging.getLogger(__name__)
12-
1310

1411
@service
1512
class CampaignEvaluator:
@@ -18,43 +15,22 @@ class CampaignEvaluator:
1815
def get_active_campaigns(self, campaign_configs: Collection[CampaignConfig]) -> list[CampaignConfig]:
1916
return [cc for cc in campaign_configs if cc.campaign_live]
2017

21-
def get_campaign_with_latest_iteration(self, active_campaigns: list[CampaignConfig]) -> CampaignConfig | None:
22-
"""
23-
Returns the campaign with the latest active iteration date.
24-
25-
1. Collect all campaigns with an active iteration.
26-
2. Sort by iteration date (descending).
27-
3. Extract the lead campaign, throwing an error if a tie for the latest date exists.
28-
"""
29-
30-
valid_items = []
31-
32-
for cc in active_campaigns:
33-
try:
34-
valid_items.append((cc.current_iteration.iteration_date, cc))
35-
except StopIteration:
36-
logger.info(
37-
"Skipping campaign ID %s as no active iteration was found.",
38-
cc.id,
39-
)
40-
41-
if not valid_items:
42-
latest_campaign = None
43-
else:
44-
max_date = max(item[0] for item in valid_items)
45-
cc_with_max_iteration_date: list[CampaignConfig] = [item[1] for item in valid_items if item[0] == max_date]
46-
if len(cc_with_max_iteration_date) > 1:
47-
err_msg = (
48-
f"Ambiguous result: '{len(cc_with_max_iteration_date)}' active iterations "
49-
f"for target {cc_with_max_iteration_date[0].target} "
50-
f"found for date '{max_date}' "
51-
f"across campaign(s) {[cc.id for cc in cc_with_max_iteration_date]}"
52-
)
53-
raise ValueError(err_msg)
54-
55-
latest_campaign = cc_with_max_iteration_date[0]
56-
57-
return latest_campaign
18+
def get_latest_campaign(self, campaign_group: list[CampaignConfig]):
19+
if not campaign_group:
20+
return None
21+
22+
latest_date = max(c.start_date for c in campaign_group)
23+
24+
latest = [c for c in campaign_group if c.start_date == latest_date]
25+
26+
if len(latest) == 1:
27+
return latest[0]
28+
29+
if len(latest) > 1:
30+
raise ValueError(
31+
f"Multiple campaigns share the latest start_date: {latest_date}") # TODO handle it in FHIR format
32+
33+
return None
5834

5935
def get_campaign_with_latest_active_iteration_per_target(
6036
self, campaign_configs: Collection[CampaignConfig], conditions: list[str], requested_category: str
@@ -76,10 +52,6 @@ def get_campaign_with_latest_active_iteration_per_target(
7652
sorted(active_campaigns, key=attrgetter("target")),
7753
key=attrgetter("target"),
7854
):
79-
filtered_campaigns = [
80-
c for c in campaign_group if filter_all_conditions or str(condition_name) in conditions
81-
]
55+
campaigns = [c for c in allowed_campaigns if filter_all_conditions or str(condition_name) in conditions]
8256

83-
campaign = self.get_campaign_with_latest_iteration(filtered_campaigns)
84-
if campaign is not None:
85-
yield (condition_name, campaign)
57+
yield condition_name, self.get_latest_campaign(campaigns)

tests/unit/services/processors/test_campaign_evaluator.py

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,25 @@ def campaign_evaluator():
1616
@pytest.mark.parametrize(
1717
("campaign_target", "campaign_type", "conditions_filter", "category_filter", "expected_result"),
1818
[
19-
("RSV", "V", ["RSV"], "VACCINATIONS", ("RSV", "V")),
20-
("RSV", "V", ["COVID"], "VACCINATIONS", None),
21-
("RSV", "S", ["RSV"], "ALL", ("RSV", "S")),
22-
("RSV", "S", ["ALL"], "ALL", ("RSV", "S")),
23-
("RSV", "S", ["RSV"], "VACCINATIONS", None),
24-
("RSV", "V", ["RSV"], "ALL", ("RSV", "V")),
25-
("FLU", "V", ["COVID", "RSV"], "ALL", None),
26-
("FLU", "S", ["ALL"], "ALL", ("FLU", "S")),
27-
("COVID", "V", ["UNKNOWN"], "VACCINATIONS", None),
28-
("FLU", "V", ["COVID", "FLU"], "VACCINATIONS", ("FLU", "V")),
19+
("RSV", "V", ["RSV"], "VACCINATIONS", [("RSV", "V")]),
20+
("RSV", "V", ["COVID"], "VACCINATIONS", []),
21+
("RSV", "S", ["RSV"], "ALL", [("RSV", "S")]),
22+
("RSV", "S", ["ALL"], "ALL", [("RSV", "S")]),
23+
("RSV", "S", ["RSV"], "VACCINATIONS", []),
24+
("RSV", "V", ["RSV"], "ALL", [("RSV", "V")]),
25+
("FLU", "V", ["COVID", "RSV"], "ALL", []),
26+
("FLU", "S", ["ALL"], "ALL", [("FLU", "S")]),
27+
("COVID", "V", ["UNKNOWN"], "VACCINATIONS", []),
28+
("FLU", "V", ["COVID", "FLU"], "VACCINATIONS", [("FLU", "V")]),
2929
],
3030
)
3131
def test_campaigns_grouped_by_condition_name_filters_correctly( # noqa: PLR0913
3232
campaign_evaluator, campaign_target, campaign_type, conditions_filter, category_filter, expected_result
3333
):
3434
campaign = rule.CampaignConfigFactory.build(target=campaign_target, type=campaign_type)
3535

36-
result = campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
37-
[campaign], conditions_filter, category_filter
38-
)
39-
40-
actual = next(((str(name), camp.type) for name, camp in result if camp is not None), None)
41-
assert actual == expected_result
36+
result = campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], conditions_filter, category_filter)
37+
assert_that([(str(name), group[0].type) for name, group in result], is_(expected_result))
4238

4339

4440
def test_campaigns_grouped_by_condition_name_with_no_campaigns(campaign_evaluator):
@@ -51,9 +47,7 @@ def test_campaigns_grouped_by_condition_name_with_no_active_campaigns(campaign_e
5147
target="RSV", type="V", start_date=datetime.date(2025, 4, 20), end_date=datetime.date(2025, 4, 21)
5248
)
5349

54-
result = campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
55-
[campaign], ["RSV"], "VACCINATIONS"
56-
)
50+
result = campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], ["RSV"], "VACCINATIONS")
5751
assert_that(list(result), is_([]))
5852

5953

@@ -69,81 +63,55 @@ def test_campaigns_grouped_by_condition_name_with_various_categories(
6963
campaign_evaluator, category_filter, campaign_type, expected_count
7064
):
7165
campaign = rule.CampaignConfigFactory.build(target="COVID", type=campaign_type)
72-
result = list(
73-
campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], ["COVID"], category_filter)
74-
)
66+
result = list(campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], ["COVID"], category_filter))
7567
assert_that(len(result), is_(expected_count))
7668
if expected_count > 0:
7769
assert_that(str(result[0][0]), is_("COVID"))
7870

7971

8072
def test_campaigns_grouped_by_condition_name_with_empty_conditions_filter(campaign_evaluator):
8173
campaign = rule.CampaignConfigFactory.build(target="RSV", type="V")
82-
result = list(
83-
campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], [], "VACCINATIONS")
84-
)
85-
86-
assert_that(result, is_([]))
74+
result = campaign_evaluator.get_campaign_with_latest_active_iteration_per_target([campaign], [], "VACCINATIONS")
75+
assert_that(list(result), is_([]))
8776

8877

8978
def test_campaigns_grouped_by_condition_name_groups_multiple_campaigns_for_same_target(campaign_evaluator):
90-
# providing the start_date here, because CampaignConfigFactory used it for iteration_date
91-
campaign1 = rule.CampaignConfigFactory.build(
92-
target="COVID",
93-
type="V",
94-
id="C1",
95-
start_date=datetime.datetime.now(datetime.UTC).date() - datetime.timedelta(days=1),
96-
iterations=[rule.IterationFactory.build()],
97-
)
98-
campaign2 = rule.CampaignConfigFactory.build(
99-
target="COVID",
100-
type="V",
101-
id="C2",
102-
start_date=datetime.datetime.now(datetime.UTC).date(),
103-
iterations=[rule.IterationFactory.build()],
104-
)
79+
campaign1 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C1")
80+
campaign2 = rule.CampaignConfigFactory.build(target="COVID", type="V", id="C2")
10581
campaign3 = rule.CampaignConfigFactory.build(target="FLU", type="V", id="F1")
10682
inactive_campaign = rule.CampaignConfigFactory.build(
10783
target="COVID", type="V", id="C3", start_date=datetime.date(2025, 4, 20), end_date=datetime.date(2025, 4, 21)
10884
)
10985

11086
all_campaigns = [campaign1, campaign2, campaign3, inactive_campaign]
111-
result = list(
112-
campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
113-
all_campaigns, ["COVID", "FLU"], "VACCINATIONS"
114-
)
115-
)
87+
result = list(campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(all_campaigns, ["COVID", "FLU"], "VACCINATIONS"))
11688

11789
assert_that(len(result), is_(2))
11890

119-
result_dict = {str(name): campaign for name, campaign in result}
120-
91+
result_dict = {str(name): campaigns for name, campaigns in result}
12192
assert_that("COVID" in result_dict)
12293
assert_that("FLU" in result_dict)
12394

124-
assert_that(result_dict["COVID"].id, is_(CampaignID("C2")))
125-
assert_that(result_dict["FLU"].id, is_(CampaignID("F1")))
95+
assert_that(len(result_dict["COVID"]), is_(2))
96+
assert_that({c.id for c in result_dict["COVID"]}, is_({CampaignID("C1"), CampaignID("C2")}))
97+
98+
assert_that(len(result_dict["FLU"]), is_(1))
99+
assert_that(result_dict["FLU"][0].id, is_(CampaignID("F1")))
126100

127101

128-
def test_campaign_grouping_is_not_affected_by_order_for_mixed_types(campaign_evaluator):
102+
def test_campaign_grouping_is_affected_by_order_for_mixed_types(campaign_evaluator):
129103
campaign_v = rule.CampaignConfigFactory.build(target="RSV", type="V")
130104
campaign_s = rule.CampaignConfigFactory.build(target="RSV", type="S")
131105

132-
# Order: S then V
106+
evaluator_s_first = campaign_evaluator
133107
result_s_first = list(
134-
campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
135-
[campaign_s, campaign_v], ["RSV"], "VACCINATIONS"
136-
)
108+
evaluator_s_first.get_campaign_with_latest_active_iteration_per_target([campaign_s, campaign_v], ["RSV"], "VACCINATIONS")
137109
)
138-
# Even if S is first, it is filtered out by 'allowed_types'
139-
assert_that(len(result_s_first), is_(1))
140-
assert_that(result_s_first[0][1].type, is_("V"))
110+
assert_that(result_s_first, is_([]))
141111

142-
# Order: V then S
112+
evaluator_v_first = campaign_evaluator
143113
result_v_first = list(
144-
campaign_evaluator.get_campaign_with_latest_active_iteration_per_target(
145-
[campaign_v, campaign_s], ["RSV"], "VACCINATIONS"
146-
)
114+
evaluator_v_first.get_campaign_with_latest_active_iteration_per_target([campaign_v, campaign_s], ["RSV"], "VACCINATIONS")
147115
)
148116
assert_that(len(result_v_first), is_(1))
149-
assert_that(result_v_first[0][1].type, is_("V"))
117+
assert_that(len(result_v_first[0][1]), is_(2))

0 commit comments

Comments
 (0)