Skip to content

Commit

Permalink
BS 290 | Dosage Validation (IHTSDO#12)
Browse files Browse the repository at this point in the history
* added route and dose unti check

* updated roa dosage flow

* updated message template and other feedback comments

* Added dosage route and dose unit cdss card flow and misconfigured medications flow

Updated test cases for the same

* BS-290 | Mani | Updated test method names suggested in review
  • Loading branch information
manimaarans committed Nov 16, 2023
1 parent 304c361 commit 098ea49
Show file tree
Hide file tree
Showing 16 changed files with 414 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.annotation.PostConstruct;
import net.steppschuh.markdowngenerator.list.UnorderedList;
import net.steppschuh.markdowngenerator.list.UnorderedListItem;
import net.steppschuh.markdowngenerator.text.Text;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Coding;
Expand Down Expand Up @@ -38,7 +39,6 @@
import java.io.FileReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -72,6 +72,9 @@ public class SnomedMedicationDefinedDailyDoseService {
public static final String INFO = "info";
private static final String NEW_LINE = "\n";
private static final String HIGH_DOSAGE_ALERT_TYPE = "High Dosage";
private static final String INVALID_DOSAGE_ALERT_TYPE = "Validation Error";
private static final String DOSAGE_EXCEPTION_DOSE_INPUT = "dose unit";
private static final String DOSAGE_EXCEPTION_ROUTE_INPUT = "dose route";

@Autowired
private FHIRTerminologyServerClient tsClient;
Expand Down Expand Up @@ -147,8 +150,18 @@ public List<CDSCard> checkMedications(List<MedicationRequest> medicationRequests
if (snomedMedication.isPresent()) {
String snomedMedicationCode = snomedMedication.get().getCode();
String snomedMedicationLabel = snomedMedication.get().getDisplay();

ConceptParameters conceptParameters = tsClient.lookup(SNOMEDCT_SYSTEM, snomedMedicationCode);
if ("NA".equals(doseQuantity.getUnit())) {
logger.debug("Prescribed dosage could not be validated for {}. Reason: Dose unit unknown to CDSS.", snomedMedicationLabel);
continue;
}
ConceptParameters conceptParameters = null;
try {
conceptParameters = tsClient.lookup(SNOMEDCT_SYSTEM, snomedMedicationCode);
} catch (Exception e) {
String errorMessage = String.format("Bahmni->SNOMED mapping is misconfigured for medication %s.", snomedMedicationLabel);
logger.error(errorMessage);
throw new ResponseStatusException(HttpStatus.PRECONDITION_FAILED, errorMessage, null);
}
if (conceptParameters == null) {
logger.debug("No SNOMED concept found for code {}, ignoring.", snomedMedicationCode);
continue;
Expand Down Expand Up @@ -176,7 +189,13 @@ public List<CDSCard> checkMedications(List<MedicationRequest> medicationRequests
logger.info("SNOMED dose form {} is not covered by the route of administration dynamic map, skipping", manufacturedDoseForm);
continue;
}
aggregateMedicationsBySubstance(aggregatedMedicationsBySubstanceMap, prescribedDailyDose, codingList, snomedMedicationLabel, atcRouteOfAdministrationCode, routeOfAdministrationLabel, normalForm);
if (!(dosage.getRoute() != null && atcRouteOfAdministrationCode.equals(dosage.getRoute().getText()))) {
String expectedUnit = atcRouteOfAdministrationCode + (routeOfAdministrationLabel.equals(atcRouteOfAdministrationCode) ? "" : " (" + routeOfAdministrationLabel.trim() + ")" );
logger.info("Expected route {} for medication {}", expectedUnit, snomedMedicationLabel);
composeCdssCardForDosageMismatch(cards, snomedMedicationLabel, codingList, expectedUnit, false);
continue;
}
aggregateMedicationsBySubstance(aggregatedMedicationsBySubstanceMap, prescribedDailyDose, codingList, snomedMedicationLabel, atcRouteOfAdministrationCode, routeOfAdministrationLabel, normalForm, cards);
}
}
composeDosageAlerts(aggregatedMedicationsBySubstanceMap, cards);
Expand Down Expand Up @@ -241,7 +260,7 @@ private void updateTotalPrescribedDailyDose(AggregatedMedicationsBySubstance agg
}
}

private void aggregateMedicationsBySubstance(Map<String, AggregatedMedicationsBySubstance> aggregatedMedicationsBySubstanceMap, PrescribedDailyDose prescribedDailyDose, List<Coding> codingList, String snomedMedicationLabel, String atcRouteOfAdministrationCode, String routeOfAdministrationLabel, SnomedConceptNormalForm normalForm) {
private void aggregateMedicationsBySubstance(Map<String, AggregatedMedicationsBySubstance> aggregatedMedicationsBySubstanceMap, PrescribedDailyDose prescribedDailyDose, List<Coding> codingList, String snomedMedicationLabel, String atcRouteOfAdministrationCode, String routeOfAdministrationLabel, SnomedConceptNormalForm normalForm, List<CDSCard> cards) {
// The substances within the clinical drug concepts are contained within attribute groups
for (Map<String, String> attributeGroup : normalForm.getAttributeGroups()) {
String substance = attributeGroup.get(ATTRIBUTE_HAS_BASIS_OF_STRENGTH_SUBSTANCE);
Expand Down Expand Up @@ -287,10 +306,11 @@ private void aggregateMedicationsBySubstance(Map<String, AggregatedMedicationsBy
PrescribedDailyDose prescribedDailyDoseInUnitOfSubstanceStrength;
PrescribedDailyDose prescribedDailyDoseInUnitOfDDD;
try {
prescribedDailyDoseInUnitOfSubstanceStrength = getPrescribedDailyDoseInUnitOfSubstanceStrength(prescribedDailyDose, strengthValue, strengthUnit, denominatorValue, denominatorUnit);
prescribedDailyDoseInUnitOfDDD = getPrescribedDailyDoseInUnitOfDDD(prescribedDailyDoseInUnitOfSubstanceStrength.getQuantity(), prescribedDailyDoseInUnitOfSubstanceStrength.getUnit(), substanceDefinedDailyDose.unit());
prescribedDailyDoseInUnitOfSubstanceStrength = getPrescribedDailyDoseInUnitOfSubstanceStrength(prescribedDailyDose, strengthValue, strengthUnit, denominatorValue, denominatorUnit, cards, snomedMedicationLabel, codingList);
prescribedDailyDoseInUnitOfDDD = getPrescribedDailyDoseInUnitOfDDD(prescribedDailyDoseInUnitOfSubstanceStrength.getQuantity(), prescribedDailyDoseInUnitOfSubstanceStrength.getUnit(), substanceDefinedDailyDose.unit(), cards, snomedMedicationLabel, codingList);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.PRECONDITION_FAILED, String.format("Prescribed dosage could not be validated for %s. Reason: Invalid dose unit.", snomedMedicationLabel), null);
logger.debug(String.format("Prescribed dosage could not be validated for %s. Reason: Invalid dose unit.", snomedMedicationLabel));
continue;
}
AggregatedMedicationsBySubstance aggregatedMedicationsBySubstance = aggregatedMedicationsBySubstanceMap.get(substance);
if (aggregatedMedicationsBySubstance == null) {
Expand All @@ -305,6 +325,25 @@ private void aggregateMedicationsBySubstance(Map<String, AggregatedMedicationsBy
}
}

private void composeCdssCardForDosageMismatch(List<CDSCard> cards, String medicationLabel, List<Coding> codingList, String expectedUnit, boolean isDoseUnit) {
UUID randomUuid = UUID.fromString(UUID.nameUUIDFromBytes(medicationLabel.getBytes()).toString());
String invalidMessage = isDoseUnit ? DOSAGE_EXCEPTION_DOSE_INPUT : DOSAGE_EXCEPTION_ROUTE_INPUT;
Optional<CDSCard> optionalCard = cards.stream().filter(cdsCard -> cdsCard.getUuid().equals(randomUuid.toString())).findFirst();
String cardDetailMsg = "Expected %s for %s";
cardDetailMsg = new UnorderedListItem(String.format(cardDetailMsg, expectedUnit, invalidMessage)).toString();
String cardSummaryMessage = String.format("One or more dose Inputs are invalid for %s", medicationLabel);
if(optionalCard.isEmpty()) {
CDSCard cdsCard = new CDSCard(randomUuid.toString(), cardSummaryMessage, NEW_LINE + cardDetailMsg, CDSIndicator.valueOf(WARNING), new CDSSource("DummyService", null), Collections.singletonList(new CDSReference(getCodings(codingList))), null, INVALID_DOSAGE_ALERT_TYPE);
cards.add(cdsCard);
return;
}
CDSCard card = optionalCard.get();
if(!card.getDetail().contains(invalidMessage)) {
card.setDetail(card.getDetail() + NEW_LINE + cardDetailMsg);
}

}

void composeDosageAlerts(Map<String, AggregatedMedicationsBySubstance> aggregatedMedicationsBySubstanceMap, List<CDSCard> cards) {
for (var aggregatedMedicationsBySubstanceEntry : aggregatedMedicationsBySubstanceMap.entrySet()) {
String substanceName = aggregatedMedicationsBySubstanceEntry.getValue().getSubstanceShortName();
Expand Down Expand Up @@ -360,24 +399,34 @@ private void addMedications(AggregatedMedicationsBySubstance aggregatedMedicatio
}
}

private PrescribedDailyDose getPrescribedDailyDoseInUnitOfSubstanceStrength(PrescribedDailyDose prescribedDailyDose, String strengthValue, String strengthUnit, String denominatorValue, String denominatorUnit) {
private PrescribedDailyDose getPrescribedDailyDoseInUnitOfSubstanceStrength(PrescribedDailyDose prescribedDailyDose, String strengthValue, String strengthUnit, String denominatorValue, String denominatorUnit, List<CDSCard> cards, String medicationLabel, List<Coding> codingList) {
BigDecimal prescribedDoseQuantity = prescribedDailyDose.getQuantity();
String prescribedDisplayUnit = prescribedDailyDose.getUnit();

String strengthDisplayUnit = getSnomedParameterValue(strengthUnit, "display");
String denominatorDisplayUnit = getSnomedParameterValue(denominatorUnit, "display");
BigDecimal prescribedDoseQuantityInUnitOfSubstanceStrength = prescribedDoseQuantity.multiply((new BigDecimal(strengthValue)).divide(new BigDecimal(denominatorValue))).multiply(new BigDecimal(UnitConversion.factorOfConversion(prescribedDisplayUnit, denominatorDisplayUnit)));
return new PrescribedDailyDose(prescribedDoseQuantityInUnitOfSubstanceStrength, strengthDisplayUnit);
try {
BigDecimal prescribedDoseQuantityInUnitOfSubstanceStrength = prescribedDoseQuantity.multiply((new BigDecimal(strengthValue)).divide(new BigDecimal(denominatorValue))).multiply(new BigDecimal(UnitConversion.factorOfConversion(prescribedDisplayUnit, denominatorDisplayUnit)));
return new PrescribedDailyDose(prescribedDoseQuantityInUnitOfSubstanceStrength, strengthDisplayUnit);
} catch (Exception e) {
composeCdssCardForDosageMismatch(cards, medicationLabel, codingList, denominatorDisplayUnit, true);
throw new RuntimeException(e);
}
}

private String getSnomedParameterValue(String snomedCode, String parameterName) {
ConceptParameters conceptParameters = tsClient.lookup(SNOMEDCT_SYSTEM, snomedCode);
return conceptParameters.getParameter(parameterName).getValue().toString();
}

private PrescribedDailyDose getPrescribedDailyDoseInUnitOfDDD(BigDecimal inputStrengthValue, String inputStrengthUnit, String targetStrengthUnit) {
BigDecimal prescribedDailyDoseQuantity = inputStrengthValue.multiply(new BigDecimal(UnitConversion.factorOfConversion(inputStrengthUnit, targetStrengthUnit)));
return new PrescribedDailyDose(prescribedDailyDoseQuantity, targetStrengthUnit);
private PrescribedDailyDose getPrescribedDailyDoseInUnitOfDDD(BigDecimal inputStrengthValue, String inputStrengthUnit, String targetStrengthUnit, List<CDSCard> cards, String medicationLabel, List<Coding> codingList) {
try {
BigDecimal prescribedDailyDoseQuantity = inputStrengthValue.multiply(new BigDecimal(UnitConversion.factorOfConversion(inputStrengthUnit, targetStrengthUnit)));
return new PrescribedDailyDose(prescribedDailyDoseQuantity, targetStrengthUnit);
} catch (Exception e) {
composeCdssCardForDosageMismatch(cards, medicationLabel, codingList, targetStrengthUnit, true);
throw new RuntimeException(e);
}
}

private String getCardSummaryTemplate() {
Expand Down Expand Up @@ -409,15 +458,6 @@ private List<CDSCoding> getCodings(List<Coding> codings) {
return codings.stream().map(coding -> new CDSCoding(coding.getSystem(), coding.getCode(), coding.getDisplay())).collect(Collectors.toList());
}

private String getDecimalPlace(Double value) {
return String.format("%.2f", value);
}

private String getDynamicDecimalPlace(Double value) {
DecimalFormat decimalFormat = new DecimalFormat("#.##");
return decimalFormat.format(value);
}

private BigDecimal formatToTwoDecimalPlaces(BigDecimal value){
return value.setScale(2, RoundingMode.HALF_UP);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ class MedicationOrderSelectCDSServiceTest {
public static final String SNOMEDCT_SYSTEM = "http://snomed.info/sct";
private static final String CONTRAINDICATION_ALERT_TYPE = "Contraindication";
private static final String HIGH_DOSAGE_ALERT_TYPE = "High Dosage";
@Autowired
private static final String INVALID_DOSAGE_ALERT_TYPE = "Validation Error";

@Autowired
SnomedMedicationDefinedDailyDoseService snomedMedicationDefinedDailyDoseService;
@MockBean
private MedicationConditionRuleLoaderService ruleLoaderService;
Expand Down Expand Up @@ -96,6 +98,7 @@ void setMockOutput() throws ServiceException {
when(mockTsClient.lookup(eq(SNOMEDCT_SYSTEM), eq("387365004"))).thenReturn(getConceptParamsForSubstanceProbenecid());
when(mockTsClient.lookup(eq(SNOMEDCT_SYSTEM), eq("258685003"))).thenReturn(getConceptParamsForDoseUnitMcg());
when(mockTsClient.lookup(eq(SNOMEDCT_SYSTEM), eq("387413002"))).thenReturn(getConceptParamsForSubstanceColchicine());
when(mockTsClient.lookup(eq(SNOMEDCT_SYSTEM), eq("dummyCode"))).thenThrow(new RuntimeException("dummy exception"));
when(mockDoseFormsLoaderService.loadDoseFormMap()).thenReturn(getMockMapList());
snomedMedicationDefinedDailyDoseService.setDoseFormsManySnomedToOneAtcCodeMap(getMockMapList());
}
Expand Down Expand Up @@ -387,12 +390,31 @@ public void shouldReturnOverDoseAlert_WhenPrescribedDailyDoseExceedsThresholdFac
}

@Test
public void shouldThrowException_WhenRequestBundleContainsMismatchedDoseUnits() throws IOException {
public void shouldCreateInvalidDosageCdssAlerts_WhenRequestBundleContainsMismatchedDoseUnitsAndRoutes() throws IOException {
CDSRequest cdsRequest = new CDSRequest();
cdsRequest.setPrefetchStrings(Map.of(
"patient", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/PatientResource.json"), StandardCharsets.UTF_8),
"conditions", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/ConditionBundle.json"), StandardCharsets.UTF_8),
"draftMedicationRequests", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/MedicationRequestBundleWithMismatchedDoseUnitsAndRoutes.json"), StandardCharsets.UTF_8)
));
List<CDSCard> cards = service.call(cdsRequest);
CDSCard cdsCard1 = cards.get(0);
CDSCard cdsCard2 = cards.get(1);
CDSCard cdsCard3 = cards.get(2);
assertEquals(3, cards.size());
assertEquals(CONTRAINDICATION_ALERT_TYPE, cdsCard1.getAlertType());
assertEquals(INVALID_DOSAGE_ALERT_TYPE, cdsCard2.getAlertType());
assertEquals(INVALID_DOSAGE_ALERT_TYPE, cdsCard3.getAlertType());
assertTrue(cdsCard2.getDetail().contains("dose unit"));
assertTrue(cdsCard3.getDetail().contains("dose route"));
}
@Test
public void shouldThrowException_WhenRequestBundleContainsInvalidMedicationCode() throws IOException {
CDSRequest cdsRequest = new CDSRequest();
cdsRequest.setPrefetchStrings(Map.of(
"patient", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/PatientResource.json"), StandardCharsets.UTF_8),
"conditions", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/ConditionBundle.json"), StandardCharsets.UTF_8),
"draftMedicationRequests", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/MedicationRequestBundleWithMismatchedDoseUnits"), StandardCharsets.UTF_8)
"draftMedicationRequests", StreamUtils.copyToString(getClass().getResourceAsStream("/medication-order-select/MedicationRequestBundleWithInvalidMedicationCode.json"), StandardCharsets.UTF_8)
));
assertThrows(ResponseStatusException.class, () ->service.call(cdsRequest) );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down Expand Up @@ -132,7 +141,16 @@
"unit": "mL"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "P"
}
],
"text": "P"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down Expand Up @@ -132,7 +141,16 @@
"unit": "mL"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "P"
}
],
"text": "P"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@
"unit": "Tablet"
}
}
]
],
"route": {
"coding": [
{
"code": "",
"display": "O"
}
],
"text": "O"
}
}
]
},
Expand Down
Loading

0 comments on commit 098ea49

Please sign in to comment.