Skip to content

Commit e94f77a

Browse files
Use John's response schema rather than FHIR.
1 parent 2563730 commit e94f77a

File tree

10 files changed

+182
-55
lines changed

10 files changed

+182
-55
lines changed
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
from dataclasses import dataclass
12
from datetime import date
3+
from enum import Enum, auto
24
from typing import NewType
35

4-
from pydantic import BaseModel
5-
66
NHSNumber = NewType("NHSNumber", str)
77
DateOfBirth = NewType("DateOfBirth", date)
88
Postcode = NewType("Postcode", str)
99

1010

11-
class EligibilityStatus(BaseModel):
12-
eligible: bool
13-
reasons: list[dict]
14-
actions: list[dict]
11+
class Status(Enum):
12+
not_eligible = auto()
13+
not_actionable = auto()
14+
actionable = auto()
15+
16+
17+
@dataclass
18+
class Condition:
19+
condition: str
20+
status: Status
21+
22+
23+
@dataclass
24+
class EligibilityStatus:
25+
conditions: list[Condition]

src/eligibility_signposting_api/services/eligibility_services.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dateutil.relativedelta import relativedelta
66
from wireup import service
77

8-
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
8+
from eligibility_signposting_api.model.eligibility import Condition, EligibilityStatus, NHSNumber, Status
99
from eligibility_signposting_api.model.rules import CampaignConfig, IterationRule, RuleAttributeLevel, RuleOperator
1010
from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError, RulesRepo
1111

@@ -49,17 +49,20 @@ def get_eligibility_status(self, nhs_number: NHSNumber | None = None) -> Eligibi
4949
def evaluate_eligibility(
5050
campaign_configs: list[CampaignConfig], person_data: list[dict[str, Any]]
5151
) -> EligibilityStatus:
52-
eligible, reasons, actions = True, [], []
53-
for iteration_rule in [
54-
iteration_rule
55-
for campaign_config in campaign_configs
56-
for iteration in campaign_config.iterations
57-
for iteration_rule in iteration.iteration_rules
58-
]:
59-
if EligibilityService.evaluate_exclusion(iteration_rule, person_data):
60-
eligible = False
52+
conditions: dict[str, Condition] = {}
53+
for campaign_config in campaign_configs:
54+
for iteration_rule in [
55+
iteration_rule
56+
for iteration in campaign_config.iterations
57+
for iteration_rule in iteration.iteration_rules
58+
]:
59+
condition = conditions.setdefault(
60+
campaign_config.target, Condition(condition=campaign_config.target, status=Status.actionable)
61+
)
62+
if EligibilityService.evaluate_exclusion(iteration_rule, person_data):
63+
condition.status = Status.not_actionable
6164

62-
return EligibilityStatus(eligible=eligible, reasons=reasons, actions=actions)
65+
return EligibilityStatus(conditions=list(conditions.values()))
6366

6467
@staticmethod
6568
def evaluate_exclusion(iteration_rule: IterationRule, person_data: list[dict[str, Any]]) -> bool:
Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import logging
22
from http import HTTPStatus
33

4-
from fhir.resources.R4B.bundle import Bundle, BundleEntry
5-
from fhir.resources.R4B.guidanceresponse import GuidanceResponse
6-
from fhir.resources.R4B.location import Location
74
from fhir.resources.R4B.operationoutcome import OperationOutcome, OperationOutcomeIssue
8-
from fhir.resources.R4B.requestgroup import RequestGroup
9-
from fhir.resources.R4B.task import Task
105
from flask import Blueprint, make_response, request
116
from flask.typing import ResponseReturnValue
127
from wireup import Injected
138

14-
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
9+
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber, Status
1510
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
11+
from eligibility_signposting_api.views.response_models import EligibilityResponse, ProcessedSuggestion
12+
from eligibility_signposting_api.views.response_models import Status as ResponseStatus
13+
14+
STATUS_MAPPING = {
15+
Status.actionable: ResponseStatus.actionable,
16+
Status.not_actionable: ResponseStatus.not_actionable,
17+
Status.not_eligible: ResponseStatus.not_eligible,
18+
}
1619

1720
logger = logging.getLogger(__name__)
1821

@@ -38,20 +41,21 @@ def check_eligibility(eligibility_service: Injected[EligibilityService]) -> Resp
3841
)
3942
return make_response(problem.model_dump(by_alias=True), HTTPStatus.NOT_FOUND)
4043
else:
41-
bundle = build_bundle(eligibility_status)
42-
return make_response(bundle.model_dump(by_alias=True), HTTPStatus.OK)
43-
44-
45-
def build_bundle(_eligibility_status: EligibilityStatus) -> Bundle:
46-
return Bundle( # pyright: ignore[reportCallIssue]
47-
id="dummy-bundle",
48-
type="collection",
49-
entry=[
50-
BundleEntry( # pyright: ignore[reportCallIssue]
51-
resource=GuidanceResponse(id="dummy-guidance-response", status="requested", moduleCodeableConcept={}) # pyright: ignore[reportCallIssue]
52-
),
53-
BundleEntry(resource=RequestGroup(id="dummy-request-group", intent="proposal", status="requested")), # pyright: ignore[reportCallIssue]
54-
BundleEntry(resource=Task(id="dummy-task", intent="proposal", status="requested")), # pyright: ignore[reportCallIssue]
55-
BundleEntry(resource=Location(id="dummy-location")), # pyright: ignore[reportCallIssue]
56-
],
44+
eligibility_response = build_eligibility_response(eligibility_status)
45+
return make_response(eligibility_response.model_dump(by_alias=True), HTTPStatus.OK)
46+
47+
48+
def build_eligibility_response(eligibility_status: EligibilityStatus) -> EligibilityResponse:
49+
return EligibilityResponse( # pyright: ignore[reportCallIssue]
50+
processed_suggestions=[ # pyright: ignore[reportCallIssue]
51+
ProcessedSuggestion( # pyright: ignore[reportCallIssue]
52+
condition=condition.condition,
53+
status=STATUS_MAPPING[condition.status],
54+
status_text=f"{condition.status}", # pyright: ignore[reportCallIssue]
55+
eligibility_cohorts=[], # pyright: ignore[reportCallIssue]
56+
suitability_rules=[], # pyright: ignore[reportCallIssue]
57+
actions=[],
58+
)
59+
for condition in eligibility_status.conditions
60+
]
5761
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from enum import Enum
2+
3+
from pydantic import BaseModel, Field
4+
5+
6+
class Status(str, Enum):
7+
not_eligible = "NotEligible"
8+
not_actionable = "NotActionable"
9+
actionable = "Actionable"
10+
11+
12+
class RuleType(str, Enum):
13+
filter = "F"
14+
suppression = "S"
15+
redirect = "R"
16+
17+
18+
class EligibilityCohort(BaseModel):
19+
cohort_code: str = Field(..., alias="cohortCode")
20+
cohort_text: str = Field(..., alias="cohortText")
21+
cohort_status: Status = Field(..., alias="cohortStatus")
22+
23+
model_config = {"populate_by_name": True}
24+
25+
26+
class SuitabilityRule(BaseModel):
27+
type: RuleType = Field(..., alias="ruleType")
28+
rule_code: str = Field(..., alias="ruleCode")
29+
rule_text: str = Field(..., alias="ruleText")
30+
31+
model_config = {"populate_by_name": True}
32+
33+
34+
class Action(BaseModel):
35+
action_type: str = Field(..., alias="actionType")
36+
action_code: str = Field(..., alias="actionCode")
37+
description: str
38+
url_link: str = Field(..., alias="urlLink")
39+
40+
model_config = {"populate_by_name": True}
41+
42+
43+
class ProcessedSuggestion(BaseModel):
44+
condition: str
45+
status: Status
46+
status_text: str = Field(..., alias="statusText")
47+
eligibility_cohorts: list[EligibilityCohort] = Field(..., alias="eligibilityCohorts")
48+
suitability_rules: list[SuitabilityRule] = Field(..., alias="suitabilityRules")
49+
actions: list[Action]
50+
51+
model_config = {"populate_by_name": True}
52+
53+
54+
class EligibilityResponse(BaseModel):
55+
processed_suggestions: list[ProcessedSuggestion] = Field(..., alias="processedSuggestions")
56+
57+
model_config = {"populate_by_name": True}

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from brunns.matchers.data import json_matching as is_json_that
44
from brunns.matchers.werkzeug import is_werkzeug_response as is_response
55
from flask.testing import FlaskClient
6-
from hamcrest import assert_that, has_entries
6+
from hamcrest import assert_that, has_entries, has_key
77

88
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode
99
from eligibility_signposting_api.model.rules import CampaignConfig
@@ -23,7 +23,7 @@ def test_nhs_number_given(
2323
# Then
2424
assert_that(
2525
response,
26-
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_entries(resourceType="Bundle"))),
26+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_key("processedSuggestions"))),
2727
)
2828

2929

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from brunns.matchers.data import json_matching as is_json_that
1111
from brunns.matchers.response import is_response
1212
from faker import Faker
13-
from hamcrest import assert_that, contains_exactly, contains_string, has_entries, has_item
13+
from hamcrest import assert_that, contains_exactly, contains_string, has_entries, has_item, has_key
1414
from yarl import URL
1515

1616
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode
@@ -57,7 +57,7 @@ def test_install_and_call_lambda_flask(
5757
logger.info(response_payload)
5858
assert_that(
5959
response_payload,
60-
has_entries(statusCode=HTTPStatus.OK, body=is_json_that(has_entries(resourceType="Bundle"))),
60+
has_entries(statusCode=HTTPStatus.OK, body=is_json_that(has_key("processedSuggestions"))),
6161
)
6262

6363
assert_that(log_output, contains_string("person_data"))
@@ -78,7 +78,7 @@ def test_install_and_call_flask_lambda_over_http(
7878
# Then
7979
assert_that(
8080
response,
81-
is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_entries(resourceType="Bundle"))),
81+
is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_key("processedSuggestions"))),
8282
)
8383

8484

tests/unit/services/test_eligibility_services.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22
from unittest.mock import MagicMock
33

44
import pytest
5-
from brunns.matchers.object import false, true
65
from dateutil.relativedelta import relativedelta
76
from faker import Faker
8-
from hamcrest import assert_that
7+
from hamcrest import assert_that, empty, has_item
98

10-
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode
9+
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode, Status
1110
from eligibility_signposting_api.model.rules import RuleAttributeLevel, RuleOperator, RuleType
1211
from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError, RulesRepo
1312
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
1413
from tests.utils.builders import CampaignConfigFactory, IterationFactory, IterationRuleFactory
15-
from tests.utils.matchers.eligibility import is_eligibility_status
14+
from tests.utils.matchers.eligibility import is_condition, is_eligibility_status
1615

1716

1817
@pytest.fixture(scope="session")
@@ -31,7 +30,7 @@ def test_eligibility_service_returns_from_repo():
3130
actual = ps.get_eligibility_status(NHSNumber("1234567890"))
3231

3332
# Then
34-
assert_that(actual, is_eligibility_status().with_eligible(true()))
33+
assert_that(actual, is_eligibility_status().with_conditions(empty()))
3534

3635

3736
def test_eligibility_service_for_nonexistent_nhs_number():
@@ -99,7 +98,12 @@ def test_simple_rule_eligible(faker: Faker):
9998
actual = ps.get_eligibility_status(NHSNumber(nhs_number))
10099

101100
# Then
102-
assert_that(actual, is_eligibility_status().with_eligible(true()))
101+
assert_that(
102+
actual,
103+
is_eligibility_status().with_conditions(
104+
has_item(is_condition().with_condition("RSV").and_status(Status.actionable))
105+
),
106+
)
103107

104108

105109
def test_simple_rule_ineligible(faker: Faker):
@@ -155,7 +159,12 @@ def test_simple_rule_ineligible(faker: Faker):
155159
actual = ps.get_eligibility_status(NHSNumber(nhs_number))
156160

157161
# Then
158-
assert_that(actual, is_eligibility_status().with_eligible(false()))
162+
assert_that(
163+
actual,
164+
is_eligibility_status().with_conditions(
165+
has_item(is_condition().with_condition("RSV").and_status(Status.not_actionable))
166+
),
167+
)
159168

160169

161170
def test_equals_rule():

tests/unit/views/test_eligibility.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from brunns.matchers.werkzeug import is_werkzeug_response as is_response
66
from flask import Flask
77
from flask.testing import FlaskClient
8-
from hamcrest import assert_that, contains_exactly, has_entries
8+
from hamcrest import assert_that, contains_exactly, has_entries, has_key
99
from wireup.integration.flask import get_app_container
1010

1111
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
1212
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
13+
from tests.utils.builders import EligibilityStatusFactory
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -19,7 +20,7 @@ def __init__(self):
1920
pass
2021

2122
def get_eligibility_status(self, _: NHSNumber | None = None) -> EligibilityStatus:
22-
return EligibilityStatus(eligible=True, reasons=[], actions=[])
23+
return EligibilityStatusFactory.build()
2324

2425

2526
class FakeUnknownPersonEligibilityService(EligibilityService):
@@ -47,7 +48,7 @@ def test_nhs_number_given(app: Flask, client: FlaskClient):
4748
# Then
4849
assert_that(
4950
response,
50-
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_entries(resourceType="Bundle"))),
51+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_key("processedSuggestions"))),
5152
)
5253

5354

tests/utils/builders.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@
22
import string
33

44
from polyfactory import Use
5+
from polyfactory.factories import DataclassFactory
56
from polyfactory.factories.pydantic_factory import ModelFactory
67

8+
from eligibility_signposting_api.model.eligibility import Condition, EligibilityStatus
79
from eligibility_signposting_api.model.rules import CampaignConfig, Iteration, IterationCohort, IterationRule
10+
from eligibility_signposting_api.views.response_models import (
11+
Action,
12+
EligibilityCohort,
13+
EligibilityResponse,
14+
ProcessedSuggestion,
15+
SuitabilityRule,
16+
)
817

918

1019
class IterationCohortFactory(ModelFactory[IterationCohort]): ...
@@ -22,5 +31,31 @@ class CampaignConfigFactory(ModelFactory[CampaignConfig]):
2231
iterations = Use(IterationFactory.batch, size=2)
2332

2433

34+
class EligibilityCohortFactory(ModelFactory[EligibilityCohort]): ...
35+
36+
37+
class SuitabilityRuleFactory(ModelFactory[SuitabilityRule]): ...
38+
39+
40+
class ActionFactory(ModelFactory[Action]): ...
41+
42+
43+
class ProcessedSuggestionFactory(ModelFactory[ProcessedSuggestion]):
44+
eligibility_cohorts = Use(EligibilityCohortFactory.batch, size=2)
45+
suitability_rules = Use(SuitabilityRuleFactory.batch, size=2)
46+
actions = Use(ActionFactory.batch, size=2)
47+
48+
49+
class EligibilityResponseFactory(ModelFactory[EligibilityResponse]):
50+
processed_suggestions = Use(ProcessedSuggestionFactory.batch, size=2)
51+
52+
53+
class ConditionFactory(DataclassFactory[Condition]): ...
54+
55+
56+
class EligibilityStatusFactory(DataclassFactory[EligibilityStatus]):
57+
condition = Use(ConditionFactory.batch, size=2)
58+
59+
2560
def random_str(length: int) -> str:
2661
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)) # noqa: S311

0 commit comments

Comments
 (0)