From 800265a57c317ad45ce093705cd391ac3f288e44 Mon Sep 17 00:00:00 2001 From: cbrinson-rise8 <127439654+cbrinson-rise8@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:45:53 -0400 Subject: [PATCH] Create New Parsers Framework (#102) ## Description Abstract the parsing of FHIR out into its own module, as the linking algorithm should be input format agnostic. This approach will let us expand our supported formats for ELR and eCR ahead. ## Related Issues closes #80 ## Additional Notes - creates `recordlinker.parsers.fhir` - add new `link/fhir` endpoint that accepts `fhir` bundles but response similar to the `/link` endpoint - add tests for new endpoint and new test file for `fhir` parser Co-authored-by: Eric Buckley --- docs/process_for_adding_feature.md | 2 +- src/recordlinker/hl7/__init__.py | 0 src/recordlinker/hl7/fhir.py | 87 ++++++++++++++++ src/recordlinker/linking/link.py | 80 -------------- src/recordlinker/routes/link_router.py | 62 ++++++++++- tests/unit/hl7/test_fhir.py | 138 +++++++++++++++++++++++++ tests/unit/linking/test_link.py | 130 +---------------------- tests/unit/routes/test_link_router.py | 129 ++++++++++++++++++++++- 8 files changed, 415 insertions(+), 213 deletions(-) create mode 100644 src/recordlinker/hl7/__init__.py create mode 100644 src/recordlinker/hl7/fhir.py create mode 100644 tests/unit/hl7/test_fhir.py diff --git a/docs/process_for_adding_feature.md b/docs/process_for_adding_feature.md index 736b2f4b..fedb6dd3 100644 --- a/docs/process_for_adding_feature.md +++ b/docs/process_for_adding_feature.md @@ -11,7 +11,7 @@ - Update the [PIIRecord.feature_iter](https://github.com/CDCgov/RecordLinker/blob/a672d2b6409cbd1a08f729d94fba5692f57f6fc6/src/recordlinker/schemas/pii.py#L246) method to return the value of the new feature when it's used for comparison. ### Extract the FHIR Field in `fhir_record_to_pii_record` -- In [src/recordlinker/linking/link.py](https://github.com/CDCgov/RecordLinker/blob/a672d2b6409cbd1a08f729d94fba5692f57f6fc6/src/recordlinker/linking/link.py), update the [fhir_record_to_pii_record](https://github.com/CDCgov/RecordLinker/blob/a672d2b6409cbd1a08f729d94fba5692f57f6fc6/src/recordlinker/linking/link.py#L26) function to map the relevant FHIR field to the new feature in [PIIRecord](https://github.com/CDCgov/RecordLinker/blob/c85f555e5da91d54eb8c51e3bdf0789d1e204b2f/src/recordlinker/schemas/pii.py#L97). +- In [src/recordlinker/linking/link.py](https://github.com/CDCgov/RecordLinker/blob/e8a64407b6e8564595cad6380d5291e9f5c959e3/src/recordlinker/parsers/fhir.py), update the [fhir_record_to_pii_record](https://github.com/CDCgov/RecordLinker/blob/e8a64407b6e8564595cad6380d5291e9f5c959e3/src/recordlinker/parsers/fhir.py#L12) function to map the relevant FHIR field to the new feature in [PIIRecord](https://github.com/CDCgov/RecordLinker/blob/e8a64407b6e8564595cad6380d5291e9f5c959e3/src/recordlinker/schemas/pii.py#L141). ### Update the Tests - Add or modify unit tests to verify that the new feature is properly extracted, mapped, and compared. \ No newline at end of file diff --git a/src/recordlinker/hl7/__init__.py b/src/recordlinker/hl7/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/recordlinker/hl7/fhir.py b/src/recordlinker/hl7/fhir.py new file mode 100644 index 00000000..1a1a61c9 --- /dev/null +++ b/src/recordlinker/hl7/fhir.py @@ -0,0 +1,87 @@ +""" +recordlinker.hl7.fhir +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is used to handle fhir parsing +""" + +import typing + +import pydantic + +from recordlinker import schemas + + +def fhir_record_to_pii_record(fhir_record: dict) -> schemas.PIIRecord: + """ + Parse the FHIR record into a PIIRecord object + """ + val = { + "external_id": fhir_record.get("id"), + "name": fhir_record.get("name", []), + "birthDate": fhir_record.get("birthDate"), + "sex": fhir_record.get("gender"), + "address": fhir_record.get("address", []), + "mrn": None, + "ssn": None, + "race": None, + "gender": None, + "telecom": fhir_record.get("telecom", []), + } + for identifier in fhir_record.get("identifier", []): + for coding in identifier.get("type", {}).get("coding", []): + if coding.get("code") == "MR": + val["mrn"] = identifier.get("value") + elif coding.get("code") == "SS": + val["ssn"] = identifier.get("value") + for address in val["address"]: + address["county"] = address.get("district", "") + for extension in address.get("extension", []): + if extension.get("url") == "http://hl7.org/fhir/StructureDefinition/geolocation": + for coord in extension.get("extension", []): + if coord.get("url") == "latitude": + address["latitude"] = coord.get("valueDecimal") + elif coord.get("url") == "longitude": + address["longitude"] = coord.get("valueDecimal") + for extension in fhir_record.get("extension", []): + if extension.get("url") == "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race": + for ext in extension.get("extension", []): + if ext.get("url") == "ombCategory": + val["race"] = ext.get("valueCoding", {}).get("display") + if extension.get("url") == "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity": + for ext in extension.get("extension", []): + if ext.get("url") == "value": + for coding in ext.get("valueCodeableConcept", {}).get("coding", []): + val["gender"] = coding.get("display") + + return schemas.PIIRecord(**val) + +def add_person_resource( + person_id: str, + patient_id: typing.Optional[str] = "", + bundle: dict = pydantic.Field(description="A FHIR bundle"), +) -> dict: + """ + Adds a simplified person resource to a bundle if the patient resource in the bundle + matches an existing record in the Master Patient Index. Returns the bundle with + the newly added person resource. + + :param person_id: _description_ + :param patient_id: _description_ + :param bundle: _description_, defaults to Field(description="A FHIR bundle") + :return: _description_ + """ + person_resource = { + "fullUrl": f"urn:uuid:{person_id}", + "resource": { + "resourceType": "Person", + "id": f"{person_id}", + "link": [{"target": {"reference": f"Patient/{patient_id}"}}], + }, + "request": { + "method": "PUT", + "url": f"Person/{person_id}", + }, + } + bundle.get("entry", []).append(person_resource) + return bundle \ No newline at end of file diff --git a/src/recordlinker/linking/link.py b/src/recordlinker/linking/link.py index 1462d38d..c0a36b1a 100644 --- a/src/recordlinker/linking/link.py +++ b/src/recordlinker/linking/link.py @@ -9,7 +9,6 @@ import typing import uuid -import pydantic from sqlalchemy import orm from recordlinker import models @@ -29,85 +28,6 @@ TRACER = MockTracer() - -# TODO: This is a FHIR specific function, should be moved to a FHIR module -def fhir_record_to_pii_record(fhir_record: dict) -> schemas.PIIRecord: - """ - Parse the FHIR record into a PIIRecord object - """ - val = { - "external_id": fhir_record.get("id"), - "name": fhir_record.get("name", []), - "birthDate": fhir_record.get("birthDate"), - "sex": fhir_record.get("gender"), - "address": fhir_record.get("address", []), - "mrn": None, - "ssn": None, - "race": None, - "gender": None, - "telecom": fhir_record.get("telecom", []), - } - for identifier in fhir_record.get("identifier", []): - for coding in identifier.get("type", {}).get("coding", []): - if coding.get("code") == "MR": - val["mrn"] = identifier.get("value") - elif coding.get("code") == "SS": - val["ssn"] = identifier.get("value") - for address in val["address"]: - address["county"] = address.get("district", "") - for extension in address.get("extension", []): - if extension.get("url") == "http://hl7.org/fhir/StructureDefinition/geolocation": - for coord in extension.get("extension", []): - if coord.get("url") == "latitude": - address["latitude"] = coord.get("valueDecimal") - elif coord.get("url") == "longitude": - address["longitude"] = coord.get("valueDecimal") - for extension in fhir_record.get("extension", []): - if extension.get("url") == "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race": - for ext in extension.get("extension", []): - if ext.get("url") == "ombCategory": - val["race"] = ext.get("valueCoding", {}).get("display") - if extension.get("url") == "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity": - for ext in extension.get("extension", []): - if ext.get("url") == "value": - for coding in ext.get("valueCodeableConcept", {}).get("coding", []): - val["gender"] = coding.get("display") - - return schemas.PIIRecord(**val) - - -# TODO: This is a FHIR specific function, should be moved to a FHIR module -def add_person_resource( - person_id: str, - patient_id: typing.Optional[str] = "", - bundle: dict = pydantic.Field(description="A FHIR bundle"), -) -> dict: - """ - Adds a simplified person resource to a bundle if the patient resource in the bundle - matches an existing record in the Master Patient Index. Returns the bundle with - the newly added person resource. - - :param person_id: _description_ - :param patient_id: _description_ - :param bundle: _description_, defaults to Field(description="A FHIR bundle") - :return: _description_ - """ - person_resource = { - "fullUrl": f"urn:uuid:{person_id}", - "resource": { - "resourceType": "Person", - "id": f"{person_id}", - "link": [{"target": {"reference": f"Patient/{patient_id}"}}], - }, - "request": { - "method": "PUT", - "url": f"Person/{person_id}", - }, - } - bundle.get("entry", []).append(person_resource) - return bundle - - def compare( record: schemas.PIIRecord, patient: models.Patient, algorithm_pass: models.AlgorithmPass ) -> bool: diff --git a/src/recordlinker/routes/link_router.py b/src/recordlinker/routes/link_router.py index a42b5626..7db4e27c 100644 --- a/src/recordlinker/routes/link_router.py +++ b/src/recordlinker/routes/link_router.py @@ -13,6 +13,7 @@ from recordlinker import schemas from recordlinker.database import get_session +from recordlinker.hl7 import fhir from recordlinker.linking import algorithm_service from recordlinker.linking import link @@ -108,7 +109,7 @@ async def link_dibbs( ) # convert record to PII - pii_record: schemas.PIIRecord = link.fhir_record_to_pii_record(record_to_link) + pii_record: schemas.PIIRecord = fhir.fhir_record_to_pii_record(record_to_link) # Now link the record try: @@ -118,7 +119,7 @@ async def link_dibbs( algorithm=algorithm, external_person_id=external_id, ) - updated_bundle = link.add_person_resource( + updated_bundle = fhir.add_person_resource( str(new_person_id), pii_record.external_id, input_bundle ) return schemas.LinkFhirResponse(found_match=found_match, updated_bundle=updated_bundle) @@ -130,3 +131,60 @@ async def link_dibbs( updated_bundle=input_bundle, message=f"Could not connect to database: {err}", ) + +@router.post("/fhir", summary="Link FHIR") +async def link_fhir( + request: fastapi.Request, + input: typing.Annotated[schemas.LinkFhirInput, fastapi.Body()], + response: fastapi.Response, + db_session: orm.Session = fastapi.Depends(get_session), +) -> schemas.LinkResponse: + """ + Compare a FHIR bundle with records in the Master Patient Index (MPI) to + check for matches with existing patient records If matches are found, + returns the patient and person reference id's + """ + input_bundle = input.bundle + external_id = input.external_person_id + + if input.algorithm: + algorithm = algorithm_service.get_algorithm(db_session, input.algorithm) + else: + algorithm = algorithm_service.default_algorithm(db_session) + + if not algorithm: + response.status_code = fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY + raise fastapi.HTTPException(status_code=422, detail="Error: Invalid algorithm specified") + + # Now extract the patient record we want to link + try: + record_to_link = [ + entry.get("resource") + for entry in input_bundle.get("entry", []) + if entry.get("resource", {}).get("resourceType", "") == "Patient" + ][0] + except IndexError: + response.status_code = fastapi.status.HTTP_400_BAD_REQUEST + raise fastapi.HTTPException(status_code=400, detail="Error: Supplied bundle contains no Patient resource to link on.") + + # convert record to PII + pii_record: schemas.PIIRecord = fhir.fhir_record_to_pii_record(record_to_link) + + # link the record + try: + # Make a copy of pii_record so we don't modify the original + (found_match, new_person_id, patient_reference_id) = link.link_record_against_mpi( + record=pii_record, + session=db_session, + algorithm=algorithm, + external_person_id=external_id, + ) + return schemas.LinkResponse( + is_match=found_match, + patient_reference_id=patient_reference_id, + person_reference_id=new_person_id, + ) + + except ValueError: + response.status_code = fastapi.status.HTTP_400_BAD_REQUEST + raise fastapi.HTTPException(status_code=400, detail="Error: Bad request") \ No newline at end of file diff --git a/tests/unit/hl7/test_fhir.py b/tests/unit/hl7/test_fhir.py new file mode 100644 index 00000000..a5622ed6 --- /dev/null +++ b/tests/unit/hl7/test_fhir.py @@ -0,0 +1,138 @@ +""" +unit.parsers.test_fhir.py +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains the unit tests for the recordlinker.parsers.fhir module +""" + +import copy + +from conftest import load_json_asset + +from recordlinker.hl7 import fhir + + +def test_fhir_record_to_pii_record(): + fhir_record = { + "resourceType": "Patient", + "id": "f6a16ff7-4a31-11eb-be7b-8344edc8f36b", + "identifier": [ + { + "value": "1234567890", + "type": { + "coding": [{ + "code": "MR", + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "display": "Medical record number" + }] + }, + }, + { + "system" : "http://hl7.org/fhir/sid/us-ssn", + "value" : "111223333", + "type" : { + "coding" : [{ + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "SS" + }] + }, + } + ], + "name": [ + { + "family": "Shepard", + "given": [ + "John" + ], + "use": "official" + } + ], + "birthDate": "2053-11-07", + "gender": "male", + "address": [ + { + "line": [ + "1234 Silversun Strip" + ], + "buildingNumber": "1234", + "city": "Boston", + "state": "Massachusetts", + "postalCode": "99999", + "district": "county", + "use": "home" + } + ], + "telecom": [ + { + "use": "home", + "system": "phone", + "value": "123-456-7890" + } + ], + "extension" : [ + { + "url" : "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity", + "extension" : [{ + "url" : "value", + "valueCodeableConcept" : { + "coding" : [{ + "system" : "http://snomed.info/sct", + "code" : "446141000124107", + "display" : "Identifies as female gender (finding)" + }] + } + }] + }, + { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url" : "ombCategory", + "valueCoding" : { + "system" : "urn:oid:2.16.840.1.113883.6.238", + "code" : "2106-3", + "display" : "White" + } + } + ] + } + ] + } + + pii_record = fhir.fhir_record_to_pii_record(fhir_record) + + assert pii_record.external_id == "f6a16ff7-4a31-11eb-be7b-8344edc8f36b" + assert pii_record.name[0].family == "Shepard" + assert pii_record.name[0].given == ["John"] + assert str(pii_record.birth_date) == "2053-11-07" + assert str(pii_record.sex) == "M" + assert pii_record.address[0].line == ["1234 Silversun Strip"] + assert pii_record.address[0].city == "Boston" + assert pii_record.address[0].state == "Massachusetts" + assert pii_record.address[0].postal_code == "99999" + assert pii_record.address[0].county == "county" + assert pii_record.mrn == "1234567890" + assert pii_record.ssn == "111-22-3333" + assert pii_record.telecom[0].value == "123-456-7890" + assert pii_record.telecom[0].system == "phone" + assert str(pii_record.race) == "WHITE" + assert str(pii_record.gender) == "FEMALE" + + +def test_add_person_resource(): + bundle = load_json_asset("patient_bundle.json") + raw_bundle = copy.deepcopy(bundle) + patient_id = "TEST_PATIENT_ID" + person_id = "TEST_PERSON_ID" + + returned_bundle = fhir.add_person_resource( + person_id=person_id, patient_id=patient_id, bundle=raw_bundle + ) + + # Assert returned_bundle has added element in "entry" + assert len(returned_bundle.get("entry")) == len(bundle.get("entry")) + 1 + + # Assert the added element is the person_resource bundle + assert returned_bundle.get("entry")[-1].get("resource").get("resourceType") == "Person" + assert returned_bundle.get("entry")[-1].get("request").get("url") == "Person/TEST_PERSON_ID" + diff --git a/tests/unit/linking/test_link.py b/tests/unit/linking/test_link.py index dd923c59..ad7e72da 100644 --- a/tests/unit/linking/test_link.py +++ b/tests/unit/linking/test_link.py @@ -14,28 +14,10 @@ from recordlinker import models from recordlinker import schemas +from recordlinker.hl7 import fhir from recordlinker.linking import link -class TestAddPersonResource: - def test_add_person_resource(self): - bundle = load_json_asset("patient_bundle.json") - raw_bundle = copy.deepcopy(bundle) - patient_id = "TEST_PATIENT_ID" - person_id = "TEST_PERSON_ID" - - returned_bundle = link.add_person_resource( - person_id=person_id, patient_id=patient_id, bundle=raw_bundle - ) - - # Assert returned_bundle has added element in "entry" - assert len(returned_bundle.get("entry")) == len(bundle.get("entry")) + 1 - - # Assert the added element is the person_resource bundle - assert returned_bundle.get("entry")[-1].get("resource").get("resourceType") == "Person" - assert returned_bundle.get("entry")[-1].get("request").get("url") == "Person/TEST_PERSON_ID" - - class TestCompare: def test_compare_match(self): rec = schemas.PIIRecord( @@ -128,7 +110,7 @@ def patients(self): patients: list[schemas.PIIRecord] = [] for entry in bundle["entry"]: if entry.get("resource", {}).get("resourceType", {}) == "Patient": - patients.append(link.fhir_record_to_pii_record(entry["resource"])) + patients.append(fhir.fhir_record_to_pii_record(entry["resource"])) return patients def test_basic_match_one(self, session, basic_algorithm, patients): @@ -188,110 +170,4 @@ def test_enhanced_match_three(self, session, enhanced_algorithm, patients: list[ # in second pass name blocks on different cluster and address matches it, # finds greatest strength match and correctly assigns to larger cluster assert matches == [False, True, False, True, False, False, True] - assert sorted(list(mapped_patients.values())) == [1, 1, 1, 4] - -def test_fhir_record_to_pii_record(): - fhir_record = { - "resourceType": "Patient", - "id": "f6a16ff7-4a31-11eb-be7b-8344edc8f36b", - "identifier": [ - { - "value": "1234567890", - "type": { - "coding": [{ - "code": "MR", - "system": "http://terminology.hl7.org/CodeSystem/v2-0203", - "display": "Medical record number" - }] - }, - }, - { - "system" : "http://hl7.org/fhir/sid/us-ssn", - "value" : "111223333", - "type" : { - "coding" : [{ - "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", - "code" : "SS" - }] - }, - } - ], - "name": [ - { - "family": "Shepard", - "given": [ - "John" - ], - "use": "official" - } - ], - "birthDate": "2053-11-07", - "gender": "male", - "address": [ - { - "line": [ - "1234 Silversun Strip" - ], - "buildingNumber": "1234", - "city": "Boston", - "state": "Massachusetts", - "postalCode": "99999", - "district": "county", - "use": "home" - } - ], - "telecom": [ - { - "use": "home", - "system": "phone", - "value": "123-456-7890" - } - ], - "extension" : [ - { - "url" : "http://hl7.org/fhir/StructureDefinition/individual-genderIdentity", - "extension" : [{ - "url" : "value", - "valueCodeableConcept" : { - "coding" : [{ - "system" : "http://snomed.info/sct", - "code" : "446141000124107", - "display" : "Identifies as female gender (finding)" - }] - } - }] - }, - { - "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", - "extension": [ - { - "url" : "ombCategory", - "valueCoding" : { - "system" : "urn:oid:2.16.840.1.113883.6.238", - "code" : "2106-3", - "display" : "White" - } - } - ] - } - ] - } - - pii_record = link.fhir_record_to_pii_record(fhir_record) - - assert pii_record.external_id == "f6a16ff7-4a31-11eb-be7b-8344edc8f36b" - assert pii_record.name[0].family == "Shepard" - assert pii_record.name[0].given == ["John"] - assert str(pii_record.birth_date) == "2053-11-07" - assert str(pii_record.sex) == "M" - assert pii_record.address[0].line == ["1234 Silversun Strip"] - assert pii_record.address[0].city == "Boston" - assert pii_record.address[0].state == "Massachusetts" - assert pii_record.address[0].postal_code == "99999" - assert pii_record.address[0].county == "county" - assert pii_record.mrn == "1234567890" - assert pii_record.ssn == "111-22-3333" - assert pii_record.telecom[0].value == "123-456-7890" - assert pii_record.telecom[0].system == "phone" - assert str(pii_record.race) == "WHITE" - assert str(pii_record.gender) == "FEMALE" + assert sorted(list(mapped_patients.values())) == [1, 1, 1, 4] \ No newline at end of file diff --git a/tests/unit/routes/test_link_router.py b/tests/unit/routes/test_link_router.py index 837b0f80..366c31be 100644 --- a/tests/unit/routes/test_link_router.py +++ b/tests/unit/routes/test_link_router.py @@ -14,10 +14,10 @@ from fastapi import status from recordlinker import schemas -from recordlinker.linking import link +from recordlinker.hl7 import fhir -class TestLinkFHIR: +class TestLinkDIBBS: @mock.patch("recordlinker.linking.algorithm_service.default_algorithm") def test_bundle_with_no_patient(self, patched_subprocess, basic_algorithm, client): patched_subprocess.return_value = basic_algorithm @@ -174,7 +174,7 @@ def patients(self): patients: list[schemas.PIIRecord] = [] for entry in bundle["entry"]: if entry.get("resource", {}).get("resourceType", {}) == "Patient": - patients.append(link.fhir_record_to_pii_record(entry["resource"])) + patients.append(fhir.fhir_record_to_pii_record(entry["resource"])) return patients @mock.patch("recordlinker.linking.algorithm_service.default_algorithm") @@ -296,3 +296,126 @@ def test_link_invalid_algorithm_param(self, patched_subprocess, patients, client assert actual_response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert actual_response.json()["detail"] == "Error: Invalid algorithm specified" + +class TestLinkFHIR: + @mock.patch("recordlinker.linking.algorithm_service.default_algorithm") + def test_linkrecord_bundle_with_no_patient(self, patched_subprocess, basic_algorithm, client): + patched_subprocess.return_value = basic_algorithm + bad_bundle = {"entry": []} + actual_response = client.post( + "/link/fhir", + json={"bundle": bad_bundle}, + ) + + assert actual_response.status_code == status.HTTP_400_BAD_REQUEST + assert actual_response.json()["detail"] == "Error: Supplied bundle contains no Patient resource to link on." + + @mock.patch("recordlinker.linking.algorithm_service.default_algorithm") + def test_link_success(self, patched_subprocess, basic_algorithm, client): + patched_subprocess.return_value = basic_algorithm + test_bundle = load_json_asset("patient_bundle_to_link_with_mpi.json") + entry_list = copy.deepcopy(test_bundle["entry"]) + + bundle_1 = test_bundle + bundle_1["entry"] = [entry_list[0]] + response_1 = client.post("/link/fhir", json={"bundle": bundle_1}) + person_1 = response_1.json()["person_reference_id"] + assert not response_1.json()["is_match"] + + bundle_2 = test_bundle + bundle_2["entry"] = [entry_list[1]] + response_2 = client.post("/link/fhir", json={"bundle": bundle_2}) + person_2 = response_2.json()["person_reference_id"] + assert response_2.json()["is_match"] + assert person_2 == person_1 + + bundle_3 = test_bundle + bundle_3["entry"] = [entry_list[2]] + response_3 = client.post("/link/fhir", json={"bundle": bundle_3}) + assert not response_3.json()["is_match"] + + # Cluster membership success--justified match + bundle_4 = test_bundle + bundle_4["entry"] = [entry_list[3]] + response_4 = client.post("/link/fhir", json={"bundle": bundle_4}) + person_4 = response_4.json()["person_reference_id"] + assert response_4.json()["is_match"] + assert person_4 == person_1 + + bundle_5 = test_bundle + bundle_5["entry"] = [entry_list[4]] + response_5 = client.post("/link/fhir", json={"bundle": bundle_5}) + assert not response_5.json()["is_match"] + + bundle_6 = test_bundle + bundle_6["entry"] = [entry_list[5]] + response_6 = client.post("/link/fhir", json={"bundle": bundle_6}) + assert not response_6.json()["is_match"] + + @mock.patch("recordlinker.linking.algorithm_service.get_algorithm") + def test_link_enhanced_algorithm( + self, patched_subprocess, enhanced_algorithm, client + ): + patched_subprocess.return_value = enhanced_algorithm + test_bundle = load_json_asset("patient_bundle_to_link_with_mpi.json") + entry_list = copy.deepcopy(test_bundle["entry"]) + + bundle_1 = test_bundle + bundle_1["entry"] = [entry_list[0]] + response_1 = client.post( + "/link/fhir", json={"bundle": bundle_1, "algorithm": "dibbs-enhanced"} + ) + person_1 = response_1.json()["person_reference_id"] + assert not response_1.json()["is_match"] + + bundle_2 = test_bundle + bundle_2["entry"] = [entry_list[1]] + response_2 = client.post( + "/link/fhir", json={"bundle": bundle_2, "algorithm": "dibbs-enhanced"} + ) + person_2 = response_2.json()["person_reference_id"] + assert response_2.json()["is_match"] + assert person_2 == person_1 + + bundle_3 = test_bundle + bundle_3["entry"] = [entry_list[2]] + response_3 = client.post( + "/link/fhir", json={"bundle": bundle_3, "algorithm": "dibbs-enhanced"} + ) + assert not response_3.json()["is_match"] + + # Cluster membership success--justified match + bundle_4 = test_bundle + bundle_4["entry"] = [entry_list[3]] + response_4 = client.post( + "/link/fhir", json={"bundle": bundle_4, "algorithm": "dibbs-enhanced"} + ) + person_4 = response_4.json()["person_reference_id"] + assert response_4.json()["is_match"] + assert person_4 == person_1 + + bundle_5 = test_bundle + bundle_5["entry"] = [entry_list[4]] + response_5 = client.post( + "/link/fhir", json={"bundle": bundle_5, "algorithm": "dibbs-enhanced"} + ) + assert not response_5.json()["is_match"] + + bundle_6 = test_bundle + bundle_6["entry"] = [entry_list[5]] + response_6 = client.post( + "/link/fhir", json={"bundle": bundle_6, "algorithm": "dibbs-enhanced"} + ) + assert not response_6.json()["is_match"] + + @mock.patch("recordlinker.linking.algorithm_service.get_algorithm") + def test_linkrecord_invalid_algorithm_param(self, patched_subprocess, client): + patched_subprocess.return_value = None + test_bundle = load_json_asset("patient_bundle_to_link_with_mpi.json") + + actual_response = client.post( + "/link/fhir", json={"bundle": test_bundle, "algorithm": "INVALID"} + ) + + assert actual_response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + assert actual_response.json()["detail"] == "Error: Invalid algorithm specified" \ No newline at end of file