Skip to content

Commit 4c3faf2

Browse files
committed
Merge branch 'main' into feature/eli-138-audit-record-within-lambda
2 parents b9961b5 + c2f9ae7 commit 4c3faf2

11 files changed

Lines changed: 266 additions & 80 deletions

File tree

src/eligibility_signposting_api/model/eligibility.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from functools import total_ordering
77
from typing import NewType, Self
88

9+
from pydantic import HttpUrl
10+
911
NHSNumber = NewType("NHSNumber", str)
1012
DateOfBirth = NewType("DateOfBirth", date)
1113
Postcode = NewType("Postcode", str)
@@ -18,7 +20,7 @@
1820
ActionType = NewType("ActionType", str)
1921
ActionCode = NewType("ActionCode", str)
2022
ActionDescription = NewType("ActionDescription", str)
21-
UrlLink = NewType("UrlLink", str)
23+
UrlLink = NewType("UrlLink", HttpUrl)
2224
UrlLabel = NewType("UrlLabel", str)
2325

2426

@@ -81,17 +83,12 @@ class SuggestedAction:
8183
url_label: UrlLabel | None
8284

8385

84-
@dataclass
85-
class SuggestedActions:
86-
actions: list[SuggestedAction]
87-
88-
8986
@dataclass
9087
class Condition:
9188
condition_name: ConditionName
9289
status: Status
9390
cohort_results: list[CohortGroupResult]
94-
actions: SuggestedActions | None = None
91+
actions: list[SuggestedAction] | None = None
9592

9693

9794
@dataclass
@@ -107,7 +104,7 @@ class CohortGroupResult:
107104
class IterationResult:
108105
status: Status
109106
cohort_results: list[CohortGroupResult]
110-
actions: SuggestedActions | None
107+
actions: list[SuggestedAction] | None
111108

112109

113110
@dataclass

src/eligibility_signposting_api/model/rules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from operator import attrgetter
1010
from typing import Literal, NewType
1111

12-
from pydantic import BaseModel, Field, RootModel, field_serializer, field_validator, model_validator
12+
from pydantic import BaseModel, Field, HttpUrl, RootModel, field_serializer, field_validator, model_validator
1313

1414
from eligibility_signposting_api.config.contants import MAGIC_COHORT_LABEL, RULE_STOP_DEFAULT
1515

@@ -132,7 +132,7 @@ class AvailableAction(BaseModel):
132132
action_type: str = Field(..., alias="ActionType")
133133
action_code: str = Field(..., alias="ExternalRoutingCode")
134134
action_description: str | None = Field(None, alias="ActionDescription")
135-
url_link: str | None = Field(None, alias="UrlLink")
135+
url_link: HttpUrl | None = Field(None, alias="UrlLink")
136136
url_label: str | None = Field(None, alias="UrlLabel")
137137

138138
model_config = {"populate_by_name": True}

src/eligibility_signposting_api/services/calculators/eligibility_calculator.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
IterationResult,
3434
Status,
3535
SuggestedAction,
36-
SuggestedActions,
3736
UrlLabel,
3837
UrlLink,
3938
)
@@ -140,7 +139,7 @@ def get_redirect_rules(
140139
def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibility.EligibilityStatus:
141140
"""Iterates over campaign groups, evaluates eligibility, and returns a consolidated status."""
142141
condition_results: dict[ConditionName, IterationResult] = {}
143-
actions: SuggestedActions | None = SuggestedActions([])
142+
actions: list[SuggestedAction] | None = []
144143
redirect_rule_priority, redirect_rule_name = None, None
145144

146145
for condition_name, campaign_group in self.campaigns_grouped_by_condition_name:
@@ -206,7 +205,7 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil
206205
# add actions to condition results
207206
condition_results[condition_name].actions = actions
208207
# reset actions for the next condition
209-
actions: SuggestedActions | None = SuggestedActions([])
208+
actions: list[SuggestedAction] | None = []
210209

211210
# add audit data
212211
# TODO: Do we need to use deduplicated cohort results from build_condition_results instead of here?
@@ -222,14 +221,12 @@ def evaluate_eligibility(self, *, include_actions_flag: bool = True) -> eligibil
222221
final_result = self.build_condition_results(condition_results)
223222
return eligibility.EligibilityStatus(conditions=final_result)
224223

225-
def handle_redirect_rules(
226-
self, best_active_iteration: Iteration
227-
) -> tuple[SuggestedActions | None, RulePriority | None, RuleName | None]:
224+
def handle_redirect_rules(self, best_active_iteration: Iteration) -> tuple[list[SuggestedAction] | None, RulePriority | None, RuleName | None]:
228225
redirect_rules, action_mapper, default_comms = self.get_redirect_rules(best_active_iteration)
229226
priority_getter = attrgetter("priority")
230227
sorted_rules_by_priority = sorted(redirect_rules, key=priority_getter)
231228

232-
actions: SuggestedActions | None = self.get_actions_from_comms(action_mapper, default_comms)
229+
actions: list[SuggestedAction] | None = self.get_actions_from_comms(action_mapper, default_comms)
233230
matched_redirect_rule_priority, matched_redirect_rule_name = None, None
234231
for _, rule_group in groupby(sorted_rules_by_priority, key=priority_getter):
235232
rule_group_list = list(rule_group)
@@ -241,7 +238,7 @@ def handle_redirect_rules(
241238
comms_routing = rule_group_list[0].comms_routing
242239
if comms_routing and all(matcher_matched_list):
243240
rule_actions = self.get_actions_from_comms(action_mapper, comms_routing)
244-
if rule_actions and len(rule_actions.actions) > 0:
241+
if rule_actions and len(rule_actions) > 0:
245242
actions = rule_actions
246243
matched_redirect_rule_priority = rule_group_list[0].priority
247244
matched_redirect_rule_name = rule_group_list[0].name
@@ -393,12 +390,12 @@ def evaluate_rules_priority_group(
393390
return best_status, inclusion_reasons, exclusion_reasons, is_rule_stop
394391

395392
@staticmethod
396-
def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> SuggestedActions | None:
397-
suggested_actions: SuggestedActions = SuggestedActions([])
393+
def get_actions_from_comms(action_mapper: ActionsMapper, comms: str) -> list[SuggestedAction] | None:
394+
suggested_actions: list[SuggestedAction] = []
398395
for comm in comms.split("|"):
399396
action = action_mapper.get(comm)
400397
if action is not None:
401-
suggested_actions.actions.append(
398+
suggested_actions.append(
402399
SuggestedAction(
403400
action_type=ActionType(action.action_type),
404401
action_code=ActionCode(action.action_code),

src/eligibility_signposting_api/views/eligibility.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def check_eligibility(
4848
except UnknownPersonError:
4949
return handle_unknown_person_error(nhs_number)
5050
else:
51-
eligibility_response = build_eligibility_response(eligibility_status)
51+
eligibility_response: eligibility.EligibilityResponse = build_eligibility_response(eligibility_status)
5252
AuditContext.write_to_firehose(audit_service)
5353
return make_response(
5454
eligibility_response.model_dump(by_alias=True, mode="json", exclude_none=True), HTTPStatus.OK
@@ -113,11 +113,7 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi
113113
statusText=eligibility.StatusText(f"{condition.status}"), # pyright: ignore[reportCallIssue]
114114
eligibilityCohorts=build_eligibility_cohorts(condition), # pyright: ignore[reportCallIssue]
115115
suitabilityRules=build_suitability_results(condition), # pyright: ignore[reportCallIssue]
116-
actions=(
117-
condition.actions.actions
118-
if condition.actions is not None and condition.actions.actions is not None
119-
else None
120-
),
116+
actions=build_actions(condition),
121117
)
122118

123119
processed_suggestions.append(suggestions)
@@ -135,6 +131,24 @@ def build_eligibility_response(eligibility_status: EligibilityStatus) -> eligibi
135131
)
136132

137133

134+
def build_actions(condition: Condition) -> list[eligibility.Action] | None:
135+
if condition.actions is not None:
136+
return [
137+
eligibility.Action(
138+
actionType=eligibility.ActionType(action.action_type),
139+
actionCode=eligibility.ActionCode(action.action_code),
140+
description=eligibility.Description(action.action_description)
141+
if action.action_description is not None
142+
else None,
143+
urlLink=eligibility.HttpUrl(action.url_link) if action.url_link is not None else None,
144+
urlLabel=eligibility.UrlLabel(action.url_label) if action.url_label is not None else None,
145+
)
146+
for action in condition.actions
147+
]
148+
149+
return None
150+
151+
138152
def build_eligibility_cohorts(condition: Condition) -> list[eligibility.EligibilityCohort]:
139153
"""Group Iteration cohorts and make only one entry per cohort group"""
140154

src/eligibility_signposting_api/views/response_model/eligibility.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
from pydantic import UUID4, BaseModel, Field, HttpUrl, field_serializer
66
from pydantic_core.core_schema import SerializationInfo
77

8-
from eligibility_signposting_api.model.eligibility import SuggestedAction
9-
108
LastUpdated = NewType("LastUpdated", datetime)
119
ConditionName = NewType("ConditionName", str)
1210
StatusText = NewType("StatusText", str)
@@ -17,6 +15,7 @@
1715
RuleText = NewType("RuleText", str)
1816
CohortCode = NewType("CohortCode", str)
1917
CohortText = NewType("CohortText", str)
18+
UrlLabel = NewType("UrlLabel", str)
2019

2120

2221
class Status(StrEnum):
@@ -50,8 +49,9 @@ class SuitabilityRule(BaseModel):
5049
class Action(BaseModel):
5150
action_type: ActionType = Field(..., alias="actionType")
5251
action_code: ActionCode = Field(..., alias="actionCode")
53-
description: Description
54-
url_link: HttpUrl = Field(..., alias="urlLink")
52+
description: Description | None
53+
url_link: HttpUrl | None = Field(..., alias="urlLink")
54+
url_label: UrlLabel | None = Field(..., alias="urlLabel")
5555

5656
model_config = {"populate_by_name": True}
5757

@@ -62,7 +62,7 @@ class ProcessedSuggestion(BaseModel):
6262
status_text: StatusText = Field(..., alias="statusText")
6363
eligibility_cohorts: list[EligibilityCohort] = Field(..., alias="eligibilityCohorts")
6464
suitability_rules: list[SuitabilityRule] = Field(..., alias="suitabilityRules")
65-
actions: list[SuggestedAction] | None
65+
actions: list[Action] | None
6666

6767
model_config = {"populate_by_name": True}
6868

tests/fixtures/builders/model/eligibility.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@
33

44
from polyfactory import Use
55
from polyfactory.factories import DataclassFactory
6+
from pydantic import HttpUrl
67

7-
from eligibility_signposting_api.model.eligibility import CohortGroupResult, Condition, EligibilityStatus
8+
from eligibility_signposting_api.model import eligibility
9+
from eligibility_signposting_api.model.eligibility import UrlLink
810

911

10-
class ConditionFactory(DataclassFactory[Condition]): ...
12+
class SuggestedActionFactory(DataclassFactory[eligibility.SuggestedAction]):
13+
url_link = UrlLink(HttpUrl("https://test-example.com"))
1114

1215

13-
class EligibilityStatusFactory(DataclassFactory[EligibilityStatus]):
14-
condition = Use(ConditionFactory.batch, size=2)
16+
class ConditionFactory(DataclassFactory[eligibility.Condition]):
17+
actions = Use(SuggestedActionFactory.batch, size=2)
1518

1619

17-
class CohortResultFactory(DataclassFactory[CohortGroupResult]): ...
20+
class EligibilityStatusFactory(DataclassFactory[eligibility.EligibilityStatus]):
21+
conditions = Use(ConditionFactory.batch, size=2)
22+
23+
24+
class CohortResultFactory(DataclassFactory[eligibility.CohortGroupResult]): ...
1825

1926

2027
def random_str(length: int) -> str:

tests/fixtures/matchers/eligibility.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from hamcrest.core.matcher import Matcher
22

33
from eligibility_signposting_api.model.eligibility import CohortGroupResult, Condition, EligibilityStatus, Reason
4-
from eligibility_signposting_api.views.response_model.eligibility import EligibilityCohort, SuitabilityRule
4+
from eligibility_signposting_api.views.response_model.eligibility import Action, EligibilityCohort, SuitabilityRule
55

66
from .meta import BaseAutoMatcher
77

@@ -24,6 +24,9 @@ class EligibilityCohortMatcher(BaseAutoMatcher[EligibilityCohort]): ...
2424
class SuitabilityRuleMatcher(BaseAutoMatcher[SuitabilityRule]): ...
2525

2626

27+
class ActionMatcher(BaseAutoMatcher[Action]): ...
28+
29+
2730
def is_eligibility_status() -> Matcher[EligibilityStatus]:
2831
return EligibilityStatusMatcher()
2932

@@ -46,3 +49,7 @@ def is_eligibility_cohort() -> Matcher[EligibilityCohort]:
4649

4750
def is_suitability_rule() -> Matcher[SuitabilityRule]:
4851
return SuitabilityRuleMatcher()
52+
53+
54+
def is_action() -> Matcher[Action]:
55+
return ActionMatcher()

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def test_actionable(
218218
"cohortText": "positive_description",
219219
}
220220
],
221-
"actions": [{"action_type": "defaultcomms", "action_code": "action_code"}],
221+
"actions": [{"actionType": "defaultcomms", "actionCode": "action_code"}],
222222
"suitabilityRules": [],
223223
"statusText": "Status.actionable",
224224
}
@@ -355,7 +355,7 @@ def test_actionable_when_only_magic_cohort_is_present(
355355
"cohortText": "magic positive description",
356356
}
357357
],
358-
"actions": [{"action_type": "defaultcomms", "action_code": "action_code"}],
358+
"actions": [{"actionType": "defaultcomms", "actionCode": "action_code"}],
359359
"suitabilityRules": [],
360360
"statusText": "Status.actionable",
361361
}
@@ -511,7 +511,7 @@ def test_actionable(
511511
"condition": "FLU",
512512
"status": "Actionable",
513513
"eligibilityCohorts": [],
514-
"actions": [{"action_code": "action_code", "action_type": "defaultcomms"}],
514+
"actions": [{"actionCode": "action_code", "actionType": "defaultcomms"}],
515515
"suitabilityRules": [],
516516
"statusText": "Status.actionable",
517517
}

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def get_log_messages(flask_function: str, logs_client: BaseClient) -> list[str]:
163163
return [e["message"] for e in log_events["events"]]
164164

165165

166-
def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_return_response( # noqa: PLR0913
166+
def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_if_audited( # noqa: PLR0913
167167
lambda_client: BaseClient, # noqa:ARG001
168168
persisted_person: NHSNumber,
169169
campaign_config: CampaignConfig,
@@ -193,6 +193,7 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_return_
193193
is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_key("processedSuggestions"))),
194194
)
195195

196+
# Then - check if audited
196197
objects = s3_client.list_objects_v2(Bucket=audit_bucket).get("Contents", [])
197198
object_keys = [obj["Key"] for obj in objects]
198199
latest_key = sorted(object_keys)[-1]

0 commit comments

Comments
 (0)