Skip to content

Commit 1964f7f

Browse files
Refactor CodeableConceptCdMapper (#1136)
* Refactor `CodeableConceptCdMapper` * Refactor `mapCodeableConceptForMedication` and `mapCodeableConceptToCd` to reduce code duplication. * Refactor `mapCodeableConceptToCdForAllergy` for readability * * Add a test case when mapping to nullFlavor codeable concept for allergy when a snomed code is not present. * * Add unit test to handle the case where an allergy intolerance is neither ACTIVE nor RESOLVED. * Remove unnecessary nesting in `findOriginalTextForAllergy` as the equals already handles case where it is not present. * * Remove check for `resolved` clinical status as call to method `getOriginalTextForActiveAllergy` yields as the fallback method contains the exact same functionality. * * Removed unnecessary `isEmpty` check as fallback was to return an Optional.empty() which so this is already implied. * Refactor method to functional style for code consistency
1 parent d60623c commit 1964f7f

File tree

2 files changed

+106
-113
lines changed

2 files changed

+106
-113
lines changed

service/src/main/java/uk/nhs/adaptors/gp2gp/ehr/mapper/CodeableConceptCdMapper.java

Lines changed: 64 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Collections;
44
import java.util.List;
55
import java.util.Optional;
6+
import java.util.function.BiFunction;
67

78
import org.apache.commons.lang3.StringUtils;
89
import org.hl7.fhir.dstu3.model.AllergyIntolerance;
@@ -37,7 +38,6 @@ public class CodeableConceptCdMapper {
3738
private static final String FIXED_ACTUAL_PROBLEM_CODE = "55607006";
3839
private static final String PROBLEM_DISPLAY_NAME = "Problem";
3940
private static final String ACTIVE_CLINICAL_STATUS = "active";
40-
private static final String RESOLVED_CLINICAL_STATUS = "resolved";
4141
private static final String PRESCRIBING_AGENCY_GP_PRACTICE_CODE = "prescribed-at-gp-practice";
4242
private static final String PRESCRIBING_AGENCY_PREVIOUS_PRACTICE_CODE = "prescribed-by-previous-practice";
4343
private static final String PRESCRIBING_AGENCY_ANOTHER_ORGANISATION_CODE = "prescribed-by-another-organisation";
@@ -51,10 +51,23 @@ public class CodeableConceptCdMapper {
5151
private static final String OTHER_CATEGORY_DESCRIPTION = "Other category";
5252

5353
public String mapCodeableConceptToCd(CodeableConcept codeableConcept) {
54+
return mapCodeableConcept(codeableConcept, this::getMainCode);
55+
}
56+
57+
public String mapCodeableConceptForMedication(CodeableConcept codeableConcept) {
58+
return mapCodeableConcept(
59+
codeableConcept,
60+
(descriptionExtensions, snomedCodeCoding) -> Optional.ofNullable(snomedCodeCoding.getCode()));
61+
}
62+
63+
private String mapCodeableConcept(
64+
CodeableConcept codeableConcept,
65+
BiFunction<Optional<List<Extension>>, Coding, Optional<String>> getMainCodeFunction
66+
) {
5467
Optional<Coding> snomedCodeCoding = getSnomedCodeCoding(codeableConcept);
5568

5669
if (snomedCodeCoding.isEmpty()) {
57-
return buildNullFlavourCodeableConceptCd(codeableConcept, snomedCodeCoding);
70+
return mapToNullFlavorCodeableConcept(codeableConcept);
5871
}
5972

6073
var builder = CodeableConceptCdTemplateParameters.builder();
@@ -64,87 +77,60 @@ public String mapCodeableConceptToCd(CodeableConcept codeableConcept) {
6477

6578
builder.mainCodeSystem(SNOMED_SYSTEM_CODE);
6679

67-
var mainCode = getMainCode(descriptionExtensions, snomedCodeCoding.get());
68-
mainCode.ifPresent(builder::mainCode);
69-
7080
var mainDisplayName = getMainDisplayName(descriptionExtensions, snomedCodeCoding.get());
7181
mainDisplayName.ifPresent(builder::mainDisplayName);
7282

7383
builder.mainOriginalText(codeableConcept.getText());
7484
builder.translations(getNonSnomedCodeCodings(codeableConcept));
7585

86+
var mainCode = getMainCodeFunction.apply(descriptionExtensions, snomedCodeCoding.get());
87+
mainCode.ifPresent(builder::mainCode);
88+
7689
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
7790
}
7891

79-
// Medications are currently using D&T Codes rather than snomed codes but are being passed through as SNOMED codes which is
80-
// creating a degradation on the receiving side. Until the types are configured correctly and agreed to a specification
81-
// we have agreed to use the Concept ID rather than Description Id for medications which will avoided the degradation.
82-
public String mapCodeableConceptForMedication(CodeableConcept codeableConcept) {
92+
public String mapCodeableConceptToCdForAllergy(
93+
CodeableConcept codeableConcept,
94+
AllergyIntolerance.AllergyIntoleranceClinicalStatus allergyIntoleranceClinicalStatus
95+
) {
8396
var builder = CodeableConceptCdTemplateParameters.builder();
84-
var snomedCodeCoding = getSnomedCodeCoding(codeableConcept);
97+
Optional<Coding> snomedCodeCoding = getSnomedCodeCoding(codeableConcept);
8598

8699
if (snomedCodeCoding.isEmpty()) {
87-
return buildNullFlavourCodeableConceptCd(codeableConcept, snomedCodeCoding);
100+
builder.nullFlavor(true);
101+
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
88102
}
89103

90-
var extension = retrieveDescriptionExtension(snomedCodeCoding.get())
91-
.map(Extension::getExtension)
92-
.orElse(Collections.emptyList());
93-
94-
builder.mainCodeSystem(SNOMED_SYSTEM_CODE);
95-
96-
Optional<String> code = Optional.ofNullable(snomedCodeCoding.get().getCode());
97-
code.ifPresent(builder::mainCode);
104+
if (ACTIVE_CLINICAL_STATUS.equals(allergyIntoleranceClinicalStatus.toCode())) {
105+
builder.mainCodeSystem(SNOMED_SYSTEM_CODE);
106+
} else {
107+
builder.nullFlavor(true);
108+
}
98109

99-
Optional<String> displayName = extension.stream()
100-
.filter(displayExtension -> DESCRIPTION_DISPLAY.equals(displayExtension.getUrl()))
101-
.map(description -> description.getValue().toString())
102-
.findFirst()
103-
.or(() -> Optional.ofNullable(snomedCodeCoding.get().getDisplay()));
104-
displayName.ifPresent(builder::mainDisplayName);
110+
getAllergyMainCode(snomedCodeCoding.get()).ifPresent(builder::mainCode);
111+
getCodingDisplayName(snomedCodeCoding.get()).ifPresent(builder::mainDisplayName);
105112

106-
builder.mainOriginalText(codeableConcept.getText());
113+
if (codeableConcept.hasText()) {
114+
builder.mainOriginalText(codeableConcept.getText());
115+
} else {
116+
var originalText = findOriginalTextForAllergy(codeableConcept, snomedCodeCoding, allergyIntoleranceClinicalStatus);
117+
originalText.ifPresent(builder::mainOriginalText);
118+
}
107119

108-
builder.translations(getNonSnomedCodeCodings(codeableConcept));
109120
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
110121
}
111122

112-
public String mapCodeableConceptToCdForAllergy(CodeableConcept codeableConcept, AllergyIntolerance.AllergyIntoleranceClinicalStatus
113-
allergyIntoleranceClinicalStatus) {
114-
var builder = CodeableConceptCdTemplateParameters.builder();
115-
var mainCode = getSnomedCodeCoding(codeableConcept);
116-
117-
builder.nullFlavor(mainCode.isEmpty());
118-
119-
if (mainCode.isPresent()) {
120-
var extension = retrieveDescriptionExtension(mainCode.get())
121-
.map(Extension::getExtension)
122-
.orElse(Collections.emptyList());
123-
124-
if (ACTIVE_CLINICAL_STATUS.equals(allergyIntoleranceClinicalStatus.toCode())) {
125-
builder.mainCodeSystem(SNOMED_SYSTEM_CODE);
126-
} else {
127-
builder.nullFlavor(true);
128-
}
123+
private static Optional<String> getCodingDisplayName(Coding snomedCodeCoding) {
124+
return Optional.ofNullable(snomedCodeCoding.getDisplay());
125+
}
129126

130-
Optional<String> code = extension.stream()
127+
private static Optional<String> getAllergyMainCode(Coding snomedCodeCoding) {
128+
return retrieveDescriptionExtension(snomedCodeCoding)
129+
.flatMap(extension -> extension.getExtension().stream()
131130
.filter(descriptionExt -> DESCRIPTION_ID.equals(descriptionExt.getUrl()))
132-
.map(description -> description.getValue().toString())
133131
.findFirst()
134-
.or(() -> Optional.ofNullable(mainCode.get().getCode()));
135-
code.ifPresent(builder::mainCode);
136-
137-
Optional<String> displayName = Optional.ofNullable(mainCode.get().getDisplay());
138-
displayName.ifPresent(builder::mainDisplayName);
139-
140-
if (codeableConcept.hasText()) {
141-
builder.mainOriginalText(codeableConcept.getText());
142-
} else {
143-
var originalText = findOriginalTextForAllergy(codeableConcept, mainCode, allergyIntoleranceClinicalStatus);
144-
originalText.ifPresent(builder::mainOriginalText);
145-
}
146-
}
147-
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
132+
.map(description -> description.getValue().toString()))
133+
.or(() -> Optional.ofNullable(snomedCodeCoding.getCode()));
148134
}
149135

150136
public String mapCodeableConceptToCdForTransformedActualProblemHeader(CodeableConcept codeableConcept) {
@@ -343,7 +329,7 @@ private Optional<String> findOriginalText(CodeableConcept codeableConcept, Optio
343329
return Optional.ofNullable(codeableConcept.getText());
344330
} else {
345331
if (coding.get().hasDisplay()) {
346-
return Optional.ofNullable(coding.get().getDisplay());
332+
return getCodingDisplayName(coding.get());
347333
} else {
348334
var extension = retrieveDescriptionExtension(coding.get());
349335
return extension.stream()
@@ -362,52 +348,27 @@ private Optional<String> findOriginalTextForAllergy(
362348
Optional<Coding> coding,
363349
AllergyIntolerance.AllergyIntoleranceClinicalStatus allergyIntoleranceClinicalStatus
364350
) {
365-
if (!allergyIntoleranceClinicalStatus.toCode().isEmpty()) {
366-
if (RESOLVED_CLINICAL_STATUS.equals(allergyIntoleranceClinicalStatus.toCode())) {
367-
if (coding.isPresent()) {
368-
if (codeableConcept.hasText()) {
369-
return Optional.ofNullable(codeableConcept.getText());
370-
} else {
371-
var extension = retrieveDescriptionExtension(coding.get());
372-
if (extension.isPresent()) {
373-
Optional<String> originalText = extension
374-
.get()
375-
.getExtension().stream()
376-
.filter(displayExtension -> DESCRIPTION_DISPLAY.equals(displayExtension.getUrl()))
377-
.map(extension1 -> extension1.getValue().toString())
378-
.findFirst();
379-
380-
if (originalText.isPresent()) {
381-
return originalText;
382-
} else if (coding.get().hasDisplay()) {
383-
return Optional.ofNullable(coding.get().getDisplay());
384-
}
385-
} else if (coding.get().hasDisplay()) {
386-
return Optional.ofNullable(coding.get().getDisplay());
387-
}
388-
}
389-
}
390-
} else if (ACTIVE_CLINICAL_STATUS.equals(allergyIntoleranceClinicalStatus.toCode())) {
391-
Optional<Extension> extension = retrieveDescriptionExtension(coding.get());
392-
if (extension.isPresent()) {
393-
Optional<String> originalText = extension
394-
.get()
395-
.getExtension().stream()
396-
.filter(displayExtension -> DESCRIPTION_DISPLAY.equals(displayExtension.getUrl()))
397-
.map(extension1 -> extension1.getValue().toString())
398-
.findFirst();
399-
if (originalText.isPresent() && StringUtils.isNotBlank(originalText.get())) {
400-
return originalText;
401-
}
402-
}
351+
if (coding.isEmpty()) {
352+
return Optional.empty();
353+
}
403354

404-
return Optional.empty();
405-
}
355+
if (ACTIVE_CLINICAL_STATUS.equals(allergyIntoleranceClinicalStatus.toCode())) {
356+
return getOriginalTextForActiveAllergy(coding.get());
406357
}
407358

408359
return CodeableConceptMappingUtils.extractTextOrCoding(codeableConcept);
409360
}
410361

362+
private Optional<String> getOriginalTextForActiveAllergy(Coding coding) {
363+
return retrieveDescriptionExtension(coding)
364+
.flatMap(value -> value
365+
.getExtension().stream()
366+
.filter(displayExtension -> DESCRIPTION_DISPLAY.equals(displayExtension.getUrl()))
367+
.map(extension1 -> extension1.getValue().toString())
368+
.findFirst()
369+
);
370+
}
371+
411372
private Optional<String> findDisplayText(Coding coding) {
412373
return Optional.ofNullable(coding.getDisplay());
413374
}
@@ -420,7 +381,7 @@ private boolean isPrescribingAgency(Coding coding) {
420381
return coding.hasSystem() && coding.getSystem().equals(CARE_CONNECT_PRESCRIBING_AGENCY_SYSTEM);
421382
}
422383

423-
private Optional<Extension> retrieveDescriptionExtension(Coding coding) {
384+
private static Optional<Extension> retrieveDescriptionExtension(Coding coding) {
424385
return coding
425386
.getExtension()
426387
.stream()
@@ -435,7 +396,6 @@ public String getDisplayFromCodeableConcept(CodeableConcept codeableConcept) {
435396
}
436397

437398
public String mapToNullFlavorCodeableConcept(CodeableConcept codeableConcept) {
438-
439399
var builder = CodeableConceptCdTemplateParameters.builder().nullFlavor(true);
440400
var mainCode = getSnomedCodeCoding(codeableConcept);
441401

@@ -456,15 +416,6 @@ public String mapToNullFlavorCodeableConceptForAllergy(
456416
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
457417
}
458418

459-
private String buildNullFlavourCodeableConceptCd(CodeableConcept codeableConcept, Optional<Coding> snomedCode) {
460-
var builder = CodeableConceptCdTemplateParameters.builder();
461-
builder.nullFlavor(true);
462-
var originalText = findOriginalText(codeableConcept, snomedCode);
463-
originalText.ifPresent(builder::mainOriginalText);
464-
465-
return TemplateUtils.fillTemplate(CODEABLE_CONCEPT_CD_TEMPLATE, builder.build());
466-
}
467-
468419
private Optional<String> getMainCode(Optional<List<Extension>> descriptionExtensions, Coding snomedCodeCoding) {
469420
if (descriptionExtensions.isPresent()) {
470421
var descriptionCode = descriptionExtensions.get().stream()
@@ -494,4 +445,4 @@ private Optional<String> getMainDisplayName(Optional<List<Extension>> descriptio
494445

495446
return Optional.ofNullable(snomedCodeCoding.getDisplay());
496447
}
497-
}
448+
}

service/src/test/java/uk/nhs/adaptors/gp2gp/ehr/mapper/CodeableConceptCdMapperTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,48 @@ void When_MappingCodeableConceptWithNonSnomedCodeSystems_Expect_ManifestedXmlCon
127127
assertThat(outputMessageXml).isEqualToIgnoringWhitespace(expectedOutputXml);
128128
}
129129

130+
@Test
131+
void When_MapToNullFlavorCodeableConceptForAllergyWithoutSnomedCode_Expect_OriginalTextIsNotPresent() {
132+
var inputJson = """
133+
{
134+
"resourceType": "AllergyIntolerance",
135+
"id": "0C1232CF-D34B-4C16-A5F4-0F6461C51A41",
136+
"meta": {
137+
"profile": [
138+
"https://fhir.nhs.uk/STU3/StructureDefinition/CareConnect-GPC-AllergyIntolerance-1"
139+
]
140+
},
141+
"identifier": [
142+
{
143+
"system": "https://EMISWeb/A82038",
144+
"value": "55D2363D57A248F49A745B2E03F5E93D0C1232CFD34B4C16A5F40F6461C51A41"
145+
}
146+
],
147+
"code": {
148+
"coding": [
149+
{
150+
"system": "http://read.info/readv2",
151+
"code": "TJ00800",
152+
"display": "Adverse reaction to pivampicillin rt"
153+
}
154+
],
155+
"text": "Adverse reaction to pivampicillin"
156+
}
157+
}""";
158+
var expectedOutputXML = """
159+
<code nullFlavor="UNK">
160+
</code>
161+
""";
162+
var codeableConcept = fhirParseService.parseResource(inputJson, AllergyIntolerance.class).getCode();
163+
164+
var outputXml = codeableConceptCdMapper.mapToNullFlavorCodeableConceptForAllergy(
165+
codeableConcept,
166+
AllergyIntolerance.AllergyIntoleranceClinicalStatus.ACTIVE
167+
);
168+
169+
assertThat(outputXml).isEqualToIgnoringWhitespace(expectedOutputXML);
170+
}
171+
130172
@ParameterizedTest
131173
@MethodSource("getTestArgumentsActualProblem")
132174
public void When_MappingStubbedCodeableConceptForActualProblemHeader_Expect_HL7CdObjectXml(String inputJson, String outputXml)

0 commit comments

Comments
 (0)