Skip to content

Commit f441269

Browse files
Return empty FHIR bundle from the enterprise view rather than the custom model.
1 parent d6dbb08 commit f441269

File tree

11 files changed

+111
-58
lines changed

11 files changed

+111
-58
lines changed

src/eligibility_signposting_api/model/eligibility.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
Postcode = NewType("Postcode", str)
99

1010

11-
class Eligibility(BaseModel):
12-
processed_suggestions: list[dict]
11+
class EligibilityStatus(BaseModel):
12+
eligible: bool
13+
reasons: list[dict]
14+
actions: list[dict]

src/eligibility_signposting_api/services/eligibility_services.py

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

33
from wireup import service
44

5-
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
5+
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
66
from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError, RulesRepo
77

88
logger = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ def __init__(self, eligibility_repo: EligibilityRepo, rules_repo: RulesRepo) ->
1919
self.eligibility_repo = eligibility_repo
2020
self.rules_repo = rules_repo
2121

22-
def get_eligibility(self, nhs_number: NHSNumber | None = None) -> Eligibility:
22+
def get_eligibility_status(self, nhs_number: NHSNumber | None = None) -> EligibilityStatus:
2323
if nhs_number:
2424
try:
2525
person_data = self.eligibility_repo.get_eligibility_data(nhs_number)
@@ -47,6 +47,6 @@ def get_eligibility(self, nhs_number: NHSNumber | None = None) -> Eligibility:
4747
"nhs_number": nhs_number,
4848
},
4949
)
50-
return Eligibility(processed_suggestions=[])
50+
return EligibilityStatus(eligible=True, reasons=[], actions=[])
5151

5252
raise UnknownPersonError
Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import logging
22
from http import HTTPStatus
33

4-
from fhir.resources.operationoutcome import OperationOutcome, OperationOutcomeIssue
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
7+
from fhir.resources.R4B.operationoutcome import OperationOutcome, OperationOutcomeIssue
8+
from fhir.resources.R4B.requestgroup import RequestGroup
9+
from fhir.resources.R4B.task import Task
510
from flask import Blueprint, make_response, request
611
from flask.typing import ResponseReturnValue
712
from wireup import Injected
813

9-
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
14+
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
1015
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
11-
from eligibility_signposting_api.views.response_models import EligibilityResponse
1216

1317
logger = logging.getLogger(__name__)
1418

@@ -20,7 +24,7 @@ def check_eligibility(eligibility_service: Injected[EligibilityService]) -> Resp
2024
nhs_number = NHSNumber(request.args.get("nhs_number", ""))
2125
logger.debug("checking nhs_number %r in %r", nhs_number, eligibility_service, extra={"nhs_number": nhs_number})
2226
try:
23-
eligibility = eligibility_service.get_eligibility(nhs_number)
27+
eligibility_status = eligibility_service.get_eligibility_status(nhs_number)
2428
except UnknownPersonError:
2529
logger.debug("nhs_number %r not found", nhs_number, extra={"nhs_number": nhs_number})
2630
problem = OperationOutcome(
@@ -34,9 +38,20 @@ def check_eligibility(eligibility_service: Injected[EligibilityService]) -> Resp
3438
)
3539
return make_response(problem.model_dump(by_alias=True), HTTPStatus.NOT_FOUND)
3640
else:
37-
eligibility_response = build_eligibility_response(eligibility)
38-
return make_response(eligibility_response.model_dump(by_alias=True), HTTPStatus.OK)
39-
40-
41-
def build_eligibility_response(eligibility: Eligibility) -> EligibilityResponse:
42-
return EligibilityResponse(processed_suggestions=eligibility.processed_suggestions)
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+
],
57+
)

src/eligibility_signposting_api/views/response_models.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/integration/in_process/test_eligibility_endpoint.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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(processed_suggestions=[]))),
26+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_entries(resourceType="Bundle"))),
2727
)
2828

2929

tests/integration/lambda/test_app_running_as_lambda.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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(processed_suggestions=[]))),
60+
has_entries(statusCode=HTTPStatus.OK, body=is_json_that(has_entries(resourceType="Bundle"))),
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(processed_suggestions=[]))),
81+
is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_entries(resourceType="Bundle"))),
8282
)
8383

8484

tests/unit/services/test_eligibility_services.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from unittest.mock import MagicMock
22

33
import pytest
4+
from brunns.matchers.object import true
45
from faker import Faker
6+
from hamcrest import assert_that
57

6-
from eligibility_signposting_api.model.eligibility import DateOfBirth, Eligibility, NHSNumber, Postcode
8+
from eligibility_signposting_api.model.eligibility import DateOfBirth, NHSNumber, Postcode
79
from eligibility_signposting_api.model.rules import RuleOperator, RuleType
810
from eligibility_signposting_api.repos import EligibilityRepo, NotFoundError, RulesRepo
911
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
1012
from tests.utils.builders import CampaignConfigFactory, IterationFactory, IterationRuleFactory
13+
from tests.utils.matchers.eligibility import is_eligibility_status
1114

1215

1316
@pytest.fixture(scope="session")
@@ -23,10 +26,10 @@ def test_eligibility_service_returns_from_repo():
2326
ps = EligibilityService(eligibility_repo, rules_repo)
2427

2528
# When
26-
actual = ps.get_eligibility(NHSNumber("1234567890"))
29+
actual = ps.get_eligibility_status(NHSNumber("1234567890"))
2730

2831
# Then
29-
assert actual == Eligibility(processed_suggestions=[])
32+
assert_that(actual, is_eligibility_status().with_eligible(true()))
3033

3134

3235
def test_eligibility_service_for_nonexistent_nhs_number():
@@ -38,7 +41,7 @@ def test_eligibility_service_for_nonexistent_nhs_number():
3841

3942
# When
4043
with pytest.raises(UnknownPersonError):
41-
ps.get_eligibility(NHSNumber("1234567890"))
44+
ps.get_eligibility_status(NHSNumber("1234567890"))
4245

4346

4447
def test_simple_rule(faker: Faker):
@@ -72,7 +75,7 @@ def test_simple_rule(faker: Faker):
7275
ps = EligibilityService(eligibility_repo, rules_repo)
7376

7477
# When
75-
actual = ps.get_eligibility(NHSNumber(nhs_number))
78+
actual = ps.get_eligibility_status(NHSNumber(nhs_number))
7679

7780
# Then
78-
assert actual == Eligibility(processed_suggestions=[])
81+
assert_that(actual, is_eligibility_status().with_eligible(true()))

tests/unit/views/test_eligibility.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from hamcrest import assert_that, contains_exactly, has_entries
99
from wireup.integration.flask import get_app_container
1010

11-
from eligibility_signposting_api.model.eligibility import Eligibility, NHSNumber
11+
from eligibility_signposting_api.model.eligibility import EligibilityStatus, NHSNumber
1212
from eligibility_signposting_api.services import EligibilityService, UnknownPersonError
1313

1414
logger = logging.getLogger(__name__)
@@ -18,23 +18,23 @@ class FakeEligibilityService(EligibilityService):
1818
def __init__(self):
1919
pass
2020

21-
def get_eligibility(self, _: NHSNumber | None = None) -> Eligibility:
22-
return Eligibility(processed_suggestions=[])
21+
def get_eligibility_status(self, _: NHSNumber | None = None) -> EligibilityStatus:
22+
return EligibilityStatus(eligible=True, reasons=[], actions=[])
2323

2424

2525
class FakeUnknownPersonEligibilityService(EligibilityService):
2626
def __init__(self):
2727
pass
2828

29-
def get_eligibility(self, _: NHSNumber | None = None) -> Eligibility:
29+
def get_eligibility_status(self, _: NHSNumber | None = None) -> EligibilityStatus:
3030
raise UnknownPersonError
3131

3232

3333
class FakeUnexpectedErrorEligibilityService(EligibilityService):
3434
def __init__(self):
3535
pass
3636

37-
def get_eligibility(self, _: NHSNumber | None = None) -> Eligibility:
37+
def get_eligibility_status(self, _: NHSNumber | None = None) -> EligibilityStatus:
3838
raise ValueError
3939

4040

@@ -47,7 +47,7 @@ def test_nhs_number_given(app: Flask, client: FlaskClient):
4747
# Then
4848
assert_that(
4949
response,
50-
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_entries(processed_suggestions=[]))),
50+
is_response().with_status_code(HTTPStatus.OK).and_text(is_json_that(has_entries(resourceType="Bundle"))),
5151
)
5252

5353

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from hamcrest.core.matcher import Matcher
2+
3+
from eligibility_signposting_api.model.eligibility import EligibilityStatus
4+
5+
from .meta import BaseAutoMatcher
6+
7+
8+
class EligibilityStatusMatcher(BaseAutoMatcher[EligibilityStatus]): ...
9+
10+
11+
def is_eligibility_status() -> Matcher[EligibilityStatus]:
12+
return EligibilityStatusMatcher()

tests/utils/matchers/meta.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, get_args, get_origin
22

33
from hamcrest import anything
44
from hamcrest.core.base_matcher import BaseMatcher
@@ -14,8 +14,21 @@ def __new__(cls, name, bases, namespace, **_kwargs):
1414
return super().__new__(cls, name, bases, namespace)
1515

1616
domain_class = namespace.get("__domain_class__")
17+
1718
if domain_class is None:
18-
msg = f"{name} must define __domain_class__"
19+
orig_bases = namespace.get("__orig_bases__", [])
20+
for orig in orig_bases:
21+
origin = get_origin(orig)
22+
args = get_args(orig)
23+
if origin is BaseAutoMatcher and args:
24+
inferred_type = args[0]
25+
if hasattr(inferred_type, "__annotations__"):
26+
domain_class = inferred_type
27+
namespace["__domain_class__"] = domain_class
28+
break
29+
30+
if domain_class is None or not hasattr(domain_class, "__annotations__"):
31+
msg = f"{name} must define or infer __domain_class__ with annotations"
1932
raise TypeError(msg)
2033

2134
for field_name in domain_class.__annotations__:
@@ -25,8 +38,27 @@ def __new__(cls, name, bases, namespace, **_kwargs):
2538
return super().__new__(cls, name, bases, namespace)
2639

2740

28-
class BaseAutoMatcher(BaseMatcher, metaclass=AutoMatcherMeta):
29-
__domain_class__ = None # must be overridden
41+
class BaseAutoMatcher[T](BaseMatcher, metaclass=AutoMatcherMeta):
42+
"""Create matchers for classes. Use like so:
43+
44+
```python
45+
from hamcrest import assert_that, equal_to
46+
47+
class EligibilityStatus(BaseModel):
48+
status: str
49+
reason: str | None = None
50+
51+
class EligibilityStatusMatcher(BaseAutoMatcher[EligibilityStatus]): ...
52+
def is_eligibility_status() -> Matcher[EligibilityStatus]: return EligibilityStatusMatcher()
53+
54+
assert_that(EligibilityStatus(status="ACTIVE"), is_eligibility_status().with_status("ACTIVE").and_reason(None))
55+
```
56+
57+
Works only for classes with `__annotations__`; manually annotated classes, dataclasses.dataclass and
58+
pydantic.BaseModel instances.
59+
"""
60+
61+
__domain_class__ = None # Will be inferred when subclassed generically
3062

3163
def __init__(self):
3264
super().__init__()
@@ -37,24 +69,24 @@ def describe_to(self, description: Description) -> None:
3769
attr_name = f"{field_name}_" if field_name in {"id", "type"} else field_name
3870
self.append_matcher_description(getattr(self, attr_name), field_name, description)
3971

40-
def _matches(self, item) -> bool:
72+
def _matches(self, item: T) -> bool:
4173
return all(
4274
getattr(self, f"{field}_" if field in {"id", "type"} else field).matches(getattr(item, field))
4375
for field in self.__domain_class__.__annotations__
4476
)
4577

46-
def describe_mismatch(self, item, mismatch_description: Description) -> None:
78+
def describe_mismatch(self, item: T, mismatch_description: Description) -> None:
4779
mismatch_description.append_text(f"was {self.__domain_class__.__name__} with")
4880
for field_name in self.__domain_class__.__annotations__:
49-
value = getattr(item, field_name)
5081
matcher = getattr(self, f"{field_name}_" if field_name in {"id", "type"} else field_name)
82+
value = getattr(item, field_name)
5183
self.describe_field_mismatch(matcher, field_name, value, mismatch_description)
5284

53-
def describe_match(self, item, match_description: Description) -> None:
85+
def describe_match(self, item: T, match_description: Description) -> None:
5486
match_description.append_text(f"was {self.__domain_class__.__name__} with")
5587
for field_name in self.__domain_class__.__annotations__:
56-
value = getattr(item, field_name)
5788
matcher = getattr(self, f"{field_name}_" if field_name in {"id", "type"} else field_name)
89+
value = getattr(item, field_name)
5890
self.describe_field_match(matcher, field_name, value, match_description)
5991

6092
def __getattr__(self, name: str):
@@ -74,8 +106,8 @@ def setter(value):
74106
def __dir__(self):
75107
dynamic_methods = []
76108
for field_name in self.__domain_class__.__annotations__:
77-
method_base = field_name.rstrip("_") if field_name in {"id", "type"} else field_name
78-
dynamic_methods.extend([f"with_{method_base}", f"and_{method_base}"])
109+
base = field_name.rstrip("_") if field_name in {"id", "type"} else field_name
110+
dynamic_methods.extend([f"with_{base}", f"and_{base}"])
79111
return list(super().__dir__()) + dynamic_methods
80112

81113
@staticmethod

0 commit comments

Comments
 (0)