From eb5a4ddc8e86b47cf2476078c27bd08d0cc4fb8b Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 28 Jun 2024 16:50:29 -0400 Subject: [PATCH 01/15] In progress --- .../openmrs/module/emrapi/adt/AdtService.java | 4 +- .../module/emrapi/adt/AdtServiceImpl.java | 88 +++++++++++++++++++ .../module/emrapi/adt/InpatientRequest.java | 27 ++++++ .../adt/InpatientRequestSearchCriteria.java | 17 ++++ .../hql/inpatient_request_dispositions.hql | 44 ++++++++++ pom.xml | 7 ++ 6 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java create mode 100644 api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java create mode 100644 api/src/main/resources/hql/inpatient_request_dispositions.hql diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java index cdb9ecf6..5cff6d3f 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java @@ -344,9 +344,11 @@ VisitDomainWrapper createRetrospectiveVisit(Patient patient, Location location, * @param visitIds - if non-null, only returns matches for visits with the given ids * @return List of the matching visits */ - // TODO expand this to take in an admissionLocation parameter and limit to admissions at that location + @Deprecated List getVisitsAwaitingAdmission(Location location, Collection patientIds, Collection visitIds); + List getInpatientRequests(InpatientRequestSearchCriteria criteria); + /** * Returns all patient awaiting transfer * @param transferLocation - if non-null, only return matches for patients awaiting transfer to this location diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java index 69e42265..ba643783 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java @@ -16,6 +16,7 @@ import org.apache.commons.lang.time.DateUtils; import org.joda.time.DateTime; +import org.openmrs.Concept; import org.openmrs.Encounter; import org.openmrs.EncounterRole; import org.openmrs.EncounterType; @@ -45,7 +46,9 @@ import org.openmrs.module.emrapi.concept.EmrConceptService; import org.openmrs.module.emrapi.db.EmrApiDAO; import org.openmrs.module.emrapi.disposition.Disposition; +import org.openmrs.module.emrapi.disposition.DispositionDescriptor; import org.openmrs.module.emrapi.disposition.DispositionService; +import org.openmrs.module.emrapi.disposition.DispositionType; import org.openmrs.module.emrapi.domainwrapper.DomainWrapperFactory; import org.openmrs.module.emrapi.merge.PatientMergeAction; import org.openmrs.module.emrapi.merge.VisitMergeAction; @@ -58,6 +61,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -918,6 +922,90 @@ public List getVisitsAwaitingAdmission(Location location, Collection getInpatientRequests(InpatientRequestSearchCriteria criteria) { + + DispositionDescriptor descriptor = dispositionService.getDispositionDescriptor(); + + // Determine whether to filter visits at a particular location + Location visitLocation = null ; + if (criteria.getVisitLocation() != null ) { + visitLocation = getLocationThatSupportsVisits(criteria.getVisitLocation()); + } + + // Determine what type of dispositions to include. If none specified, default to all + List dispositionTypes = criteria.getDispositionTypes(); + if (dispositionTypes == null) { + dispositionTypes = Arrays.asList(DispositionType.values()); + } + + // Get all disposition concepts based on the given disposition type(s) + Map dispositionValuesToType = new HashMap<>(); + List dispositions = dispositionService.getDispositions(); + for (Disposition d : dispositions) { + if (dispositionTypes.contains(d.getType())) { + dispositionValuesToType.put(emrConceptService.getConcept(d.getConceptCode()), d.getType()); + } + } + + // Get all encounter types that might cause a request to be fulfilled + List adtEncounterTypes = new ArrayList<>(); + if (dispositionTypes.contains(DispositionType.ADMIT)) { + adtEncounterTypes.add(emrApiProperties.getAdmissionEncounterType()); + } + if (dispositionTypes.contains(DispositionType.TRANSFER)) { + adtEncounterTypes.add(emrApiProperties.getTransferWithinHospitalEncounterType()); + } + if (dispositionTypes.contains(DispositionType.DISCHARGE)) { + adtEncounterTypes.add(emrApiProperties.getExitFromInpatientEncounterType()); + } + + // Disposition Locations are stored as Obs where the valueText is the location id. Collect these values + List dispositionLocationIds = null; + if (criteria.getDispositionLocations() != null) { + dispositionLocationIds = new ArrayList<>(); + for (Location l : criteria.getDispositionLocations()) { + dispositionLocationIds.add(l.getLocationId().toString()); + } + } + + Map parameters = new HashMap<>(); + parameters.put("dispositionConcept", descriptor.getDispositionConcept()); + parameters.put("dispositionValues", dispositionValuesToType.keySet()); + parameters.put("visitLocation", visitLocation); + parameters.put("adtEncounterTypes", adtEncounterTypes); + parameters.put("adtDecisionConcept", emrApiProperties.getAdmissionDecisionConcept()); + parameters.put("denyConcept", emrApiProperties.getDenyAdmissionConcept()); + parameters.put("dispositionLocationIds", dispositionLocationIds); + + List dispositionObs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, Obs.class); + List ret = new ArrayList<>(); + for (Obs o : dispositionObs) { + InpatientRequest r = new InpatientRequest(); + r.setVisit(o.getEncounter().getVisit()); + r.setPatient(o.getEncounter().getPatient()); + r.setDispositionEncounter(o.getEncounter()); + r.setDispositionObsGroup(o.getObsGroup()); + r.setDispositionType(dispositionValuesToType.get(o.getValueCoded())); + r.setDisposition(o.getValueCoded()); + + Location dispositionLocation = null; + if (r.getDispositionType() == DispositionType.ADMIT) { + dispositionLocation = descriptor.getAdmissionLocation(o.getObsGroup(), locationService); + } + else if (r.getDispositionType() == DispositionType.TRANSFER) { + dispositionLocation = descriptor.getInternalTransferLocation(o.getObsGroup(), locationService); + } + r.setDispositionLocation(dispositionLocation); + + Date deathDate = descriptor.getDateOfDeath(o.getObsGroup()); + r.setDispositionDate(deathDate == null ? o.getObsDatetime() : deathDate); + ret.add(r); + } + return ret; + } + @Override @Transactional(readOnly = true) public List getVisitsAwaitingTransfer(Location transferLocation) { diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java new file mode 100644 index 00000000..0eb18a9e --- /dev/null +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java @@ -0,0 +1,27 @@ +package org.openmrs.module.emrapi.adt; + +import lombok.Data; +import org.openmrs.Concept; +import org.openmrs.Encounter; +import org.openmrs.Location; +import org.openmrs.Obs; +import org.openmrs.Patient; +import org.openmrs.Visit; +import org.openmrs.module.emrapi.disposition.DispositionType; + +import java.util.Date; + +/** + * Represents and Admission, Discharge, or Transfer request + */ +@Data +public class InpatientRequest { + private Visit visit; + private Patient patient; + private DispositionType dispositionType; + private Encounter dispositionEncounter; + private Obs dispositionObsGroup; + private Concept disposition; + private Location dispositionLocation; + private Date dispositionDate; +} diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java new file mode 100644 index 00000000..289d6dd3 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java @@ -0,0 +1,17 @@ +package org.openmrs.module.emrapi.adt; + +import lombok.Data; +import org.openmrs.Location; +import org.openmrs.module.emrapi.disposition.DispositionType; + +import java.util.List; + +/** + * Represents criteria for searching for AdtRequests + */ +@Data +public class InpatientRequestSearchCriteria { + private Location visitLocation; + private List dispositionLocations; + private List dispositionTypes; +} diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql new file mode 100644 index 00000000..58d20f5a --- /dev/null +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -0,0 +1,44 @@ +select + dispo +from + Obs as dispo +inner join dispo.encounter as dispoEncounter +inner join dispoEncounter.visit as visit +inner join dispo.person as person +where + dispo.voided = false + and dispoEncounter.voided = false + and visit.voided = false + and dispo.concept = :dispositionConcept + and dispo.valueCoded in :dispositionValues + and (:visitLocation is null or visit.location = :visitLocation) + and (:patientIds is null or person.personId in :patientIds) + and (:visitIds is null or visit.visitId in :visitIds) + and person.dead = false + and visit.stopDatetime is null + and ( + select count(*) + from Encounter as adtEncounter + where adtEncounter.visit = visit + and adtEncounter.voided = false + and adtEncounter.encounterType in (:adtEncounterTypes) + ) = 0 + and ( + select count(*) + from Obs as adtDecision + inner join adtDecision.encounter as encounterInVisit + where encounterInVisit.visit = visit + and encounterInVisit.voided = false + and adtDecision.voided = false + and adtDecision.concept = :adtDecisionConcept + and adtDecision.valueCoded = :denyConcept + and encounterInVisit.encounterDatetime > dispoEncounter.encounterDatetime + ) = 0 + and ( + :dispositionLocationIds is null or ( + select count(*) + from Obs as locationObs + where locationObs.obsGroup = dispo.obsGroup + and locationObs.valueText in (:dispositionLocationIds) + ) > 0 + ) diff --git a/pom.xml b/pom.xml index 690105ab..404898cf 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ UTF-8 2.2.1 1.16.0 + 1.18.26 1.8.0-SNAPSHOT 2.5.0 1.6.0 @@ -121,6 +122,12 @@ ${openmrsTestutilsVersion} test + + org.projectlombok + lombok + ${lombokVersion} + provided + From b2606aa63b50f763c17e02c0c0cb0b646b41dc5e Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 28 Jun 2024 19:54:29 -0400 Subject: [PATCH 02/15] Add initial set of unit tests --- .../adt/InpatientRequestSearchCriteria.java | 16 ++ .../hql/inpatient_request_dispositions.hql | 2 - .../module/emrapi/adt/AdtServiceImplTest.java | 269 ++++++++++++++++++ 3 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java index 289d6dd3..7f535c7a 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java @@ -4,6 +4,7 @@ import org.openmrs.Location; import org.openmrs.module.emrapi.disposition.DispositionType; +import java.util.ArrayList; import java.util.List; /** @@ -11,7 +12,22 @@ */ @Data public class InpatientRequestSearchCriteria { + private Location visitLocation; private List dispositionLocations; private List dispositionTypes; + + public void addDispositionLocation(Location location) { + if (dispositionLocations == null) { + dispositionLocations = new ArrayList<>(); + } + dispositionLocations.add(location); + } + + public void addDispositionType(DispositionType dispositionType) { + if (dispositionTypes == null) { + dispositionTypes = new ArrayList<>(); + } + dispositionTypes.add(dispositionType); + } } diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index 58d20f5a..013fa378 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -12,8 +12,6 @@ where and dispo.concept = :dispositionConcept and dispo.valueCoded in :dispositionValues and (:visitLocation is null or visit.location = :visitLocation) - and (:patientIds is null or person.personId in :patientIds) - and (:visitIds is null or visit.visitId in :visitIds) and person.dead = false and visit.stopDatetime is null and ( diff --git a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java new file mode 100644 index 00000000..070abd35 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -0,0 +1,269 @@ +package org.openmrs.module.emrapi.adt; + +import org.apache.commons.lang.time.DateUtils; +import org.junit.Before; +import org.junit.Test; +import org.openmrs.Concept; +import org.openmrs.Encounter; +import org.openmrs.EncounterType; +import org.openmrs.Location; +import org.openmrs.Obs; +import org.openmrs.Patient; +import org.openmrs.Visit; +import org.openmrs.api.ConceptService; +import org.openmrs.contrib.testdata.TestDataManager; +import org.openmrs.contrib.testdata.builder.ObsBuilder; +import org.openmrs.module.emrapi.EmrApiContextSensitiveTest; +import org.openmrs.module.emrapi.EmrApiProperties; +import org.openmrs.module.emrapi.concept.EmrConceptService; +import org.openmrs.module.emrapi.disposition.DispositionDescriptor; +import org.openmrs.module.emrapi.disposition.DispositionService; +import org.openmrs.module.emrapi.disposition.DispositionType; +import org.openmrs.module.emrapi.test.ContextSensitiveMetadataTestUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Date; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +public class AdtServiceImplTest extends EmrApiContextSensitiveTest { + + @Autowired + private ConceptService conceptService; + + @Autowired + private EmrConceptService emrConceptService; + + @Autowired + private DispositionService dispositionService; + + @Autowired + private AdtService adtService; + + @Autowired + private EmrApiProperties emrApiProperties; + + @Autowired + TestDataManager testDataManager; + + private DispositionDescriptor dispositionDescriptor; + + private Patient patient; + private Visit visit; + private Location visitLocation; + private Location preAdmissionLocation; + private Location admissionLocation; + private Location transferLocation; + private Location otherVisitLocation; + private Concept admitDisposition; + private Concept transferDisposition; + private Concept dischargeDisposition; + private Date visitDate; + InpatientRequestSearchCriteria criteria; + List requests; + + @Before + public void setup() throws Exception { + executeDataSet("baseTestDataset.xml"); + dispositionDescriptor = ContextSensitiveMetadataTestUtils.setupDispositionDescriptor(conceptService, dispositionService); + ContextSensitiveMetadataTestUtils.setupAdmissionDecisionConcept(conceptService, emrApiProperties); + visitLocation = testDataManager.location().name("Hospital").tag("Visit Location").save(); + preAdmissionLocation = testDataManager.location().name("Pre-Admission").save(); + preAdmissionLocation.setParentLocation(visitLocation); + testDataManager.getLocationService().saveLocation(preAdmissionLocation); + admissionLocation = testDataManager.location().name("Admission Ward").tag("Admission Location").save(); + admissionLocation.setParentLocation(visitLocation); + testDataManager.getLocationService().saveLocation(admissionLocation); + transferLocation = testDataManager.location().name("Transfer Ward").tag("Admission Location").save(); + transferLocation.setParentLocation(visitLocation); + otherVisitLocation = testDataManager.location().name("Other Hospital").tag("Visit Location").save(); + testDataManager.getLocationService().saveLocation(transferLocation); + admitDisposition = emrConceptService.getConcept("org.openmrs.module.emrapi:Admit to hospital"); + transferDisposition = emrConceptService.getConcept("org.openmrs.module.emrapi:Transfer out of hospital"); + dischargeDisposition = emrConceptService.getConcept("org.openmrs.module.emrapi:Discharged"); + patient = testDataManager.randomPatient().birthdate("2010-01-01").save(); + visit = testDataManager.visit().patient(patient).visitType(emrApiProperties.getAtFacilityVisitType()).location(visitLocation).started("2020-10-30").save(); + visitDate = visit.getStartDatetime(); + criteria = new InpatientRequestSearchCriteria(); + } + + private Encounter createEncounter(EncounterType encounterType, Location location, Date date) { + return testDataManager.encounter().patient(patient).visit(visit).encounterType(encounterType).encounterDatetime(date).location(location).save(); + } + + public Obs createAdmissionRequest(Date encounterDate) { + Encounter e = createEncounter(emrApiProperties.getVisitNoteEncounterType(), preAdmissionLocation, encounterDate); + ObsBuilder groupBuilder = testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionSetConcept()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionConcept()).value(admitDisposition).get()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getAdmissionLocationConcept()).value(admissionLocation.getLocationId().toString()).get()); + return groupBuilder.save(); + } + + public Encounter createAdmissionEncounter(Date encounterDate) { + return createEncounter(emrApiProperties.getAdmissionEncounterType(), admissionLocation, encounterDate); + } + + public Encounter createAdmissionDeniedEncounter(Date encounterDate) { + Encounter e = createEncounter(emrApiProperties.getVisitNoteEncounterType(), preAdmissionLocation, encounterDate); + e.addObs(testDataManager.obs().person(patient).concept(emrApiProperties.getAdmissionDecisionConcept()).value(emrApiProperties.getDenyAdmissionConcept()).get()); + return testDataManager.getEncounterService().saveEncounter(e); + } + + public Obs createTransferRequest(Date encounterDate) { + Encounter e = createEncounter(emrApiProperties.getVisitNoteEncounterType(), admissionLocation, encounterDate); + ObsBuilder groupBuilder = testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionSetConcept()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionConcept()).value(transferDisposition).get()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getAdmissionLocationConcept()).value(transferLocation.getLocationId().toString()).get()); + return groupBuilder.save(); + } + + public Encounter createTransferEncounter(Date encounterDate) { + return createEncounter(emrApiProperties.getTransferWithinHospitalEncounterType(), transferLocation, encounterDate); + } + + public Obs createDischargeRequest(Date encounterDate, Location currentLocation) { + Encounter e = createEncounter(emrApiProperties.getVisitNoteEncounterType(), currentLocation, encounterDate); + ObsBuilder groupBuilder = testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionSetConcept()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionConcept()).value(dischargeDisposition).get()); + return groupBuilder.save(); + } + + public Encounter createDischarge(Date encounterDate, Location currentLocation) { + return createEncounter(emrApiProperties.getExitFromInpatientEncounterType(), currentLocation, encounterDate); + } + + private List assertNumRequests(InpatientRequestSearchCriteria criteria, int expected) { + List requests = adtService.getInpatientRequests(criteria); + assertThat(requests.size(), equalTo(expected)); + return requests; + } + + @Test + public void shouldGetAdmissionRequest() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + requests = adtService.getInpatientRequests(criteria); + assertThat(requests.size(), equalTo(1)); + assertThat(requests.get(0).getDispositionType(), equalTo(DispositionType.ADMIT)); + assertThat(requests.get(0).getDisposition(), equalTo(admitDisposition)); + assertThat(requests.get(0).getDispositionLocation(), equalTo(admissionLocation)); + } + + @Test + public void shouldNotGetAdmissionRequestIfPatientHasBeenAdmitted() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfPatientHasBeenDenied() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfPatientHasDied() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + patient.setDead(true); + patient.setCauseOfDeathNonCoded("Unknown"); + testDataManager.getPatientService().savePatient(patient); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfPatientIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + testDataManager.getPatientService().voidPatient(patient, "Unknown"); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfEncounterIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + requests = assertNumRequests(criteria, 1); + testDataManager.getEncounterService().voidEncounter(o.getEncounter(), "Unknown"); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfObsIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + requests = assertNumRequests(criteria, 1); + testDataManager.getObsService().voidObs(o, "Unknown"); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldGetAdmissionRequestForVisitLocation() { + criteria.addDispositionType(DispositionType.ADMIT); + criteria.setVisitLocation(visitLocation); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + } + + @Test + public void shouldGetAdmissionRequestForParentVisitLocation() { + criteria.addDispositionType(DispositionType.ADMIT); + criteria.setVisitLocation(admissionLocation); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + } + + @Test + public void shouldNotGetAdmissionRequestForDifferentVisitLocation() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + criteria.setVisitLocation(otherVisitLocation); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotConsiderVoidedAdmissionEncounter() throws Exception { + } + + @Test + public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsFollowedByAnotherAdmissionDisposition() throws Exception { + } + + @Test + public void shouldNotFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsFollowedByAnotherAdmissionDispositionFollowedByAnotherAdmissionDenial() throws Exception { + } + + @Test + public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDecisionThatIsNotDeny() throws Exception { + } + + @Test + public void shouldFindVisitWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() throws Exception { + } + + @Test + public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() throws Exception { + } + +} From 7adca3ba34d59e13e530081182e75814ca2e95dc Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Tue, 9 Jul 2024 16:24:16 -0400 Subject: [PATCH 03/15] Additional unit tests --- .../module/emrapi/adt/AdtServiceImplTest.java | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java index 070abd35..48a26476 100644 --- a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -162,6 +162,18 @@ public void shouldNotGetAdmissionRequestIfPatientHasBeenAdmitted() { assertNumRequests(criteria, 0); } + @Test + public void shouldGetAdmissionRequestIfAdmissionEncounterIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + Encounter e = createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + testDataManager.getEncounterService().voidEncounter(e, "Unknown"); + assertNumRequests(criteria, 1); + } + @Test public void shouldNotGetAdmissionRequestIfPatientHasBeenDenied() { criteria.addDispositionType(DispositionType.ADMIT); @@ -243,27 +255,57 @@ public void shouldNotGetAdmissionRequestForDifferentVisitLocation() { } @Test - public void shouldNotConsiderVoidedAdmissionEncounter() throws Exception { - } - - @Test - public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsFollowedByAnotherAdmissionDisposition() throws Exception { - } - - @Test - public void shouldNotFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsFollowedByAnotherAdmissionDispositionFollowedByAnotherAdmissionDenial() throws Exception { + public void shouldGetAdmissionRequestIfPatientHasAnAdmissionRequestAfterADenial() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); } @Test - public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDecisionThatIsNotDeny() throws Exception { + public void shouldGetAdmissionRequestIfPatientHasAnAdmissionDecisionThatIsNotDeny() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + Encounter e = createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + for (Obs o : e.getAllObs()) { + if (o.getConcept().equals(emrApiProperties.getAdmissionDecisionConcept())) { + o.setValueCoded(emrApiProperties.getPatientDiedConcept()); + testDataManager.getObsService().saveObs(o, "Unknown"); + } + } + assertNumRequests(criteria, 1); } @Test - public void shouldFindVisitWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() throws Exception { + public void shouldFindVisitWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() { + criteria.addDispositionType(DispositionType.ADMIT); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 1)); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); } @Test - public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() throws Exception { + public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + Encounter e = createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + for (Obs o : e.getAllObs()) { + if (o.getConcept().equals(emrApiProperties.getAdmissionDecisionConcept())) { + testDataManager.getObsService().voidObs(o, "Unknown"); + } + } + assertNumRequests(criteria, 1); } } From aae9f3eb8c5e7b810cf444ae1b34dd8d4ca2665b Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Wed, 10 Jul 2024 08:29:25 -0400 Subject: [PATCH 04/15] Javadoc --- .../main/java/org/openmrs/module/emrapi/adt/AdtService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java index 5cff6d3f..a5dc0570 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java @@ -347,6 +347,11 @@ VisitDomainWrapper createRetrospectiveVisit(Patient patient, Location location, @Deprecated List getVisitsAwaitingAdmission(Location location, Collection patientIds, Collection visitIds); + /** + * Returns all List of InpatientRequest that match the given search criteria + * @param criteria - represents the criteria by which inpatient requests are searched and returned + * @return List of the matching InpatientRequests that match the criteria + */ List getInpatientRequests(InpatientRequestSearchCriteria criteria); /** From 5727a3d333dd3d6925a1888f5b807843d1667c29 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 12:42:23 -0400 Subject: [PATCH 05/15] Add initial REST controller and add some fixes for performance and to mitigate errors related to infinite recursion when serializing rest data --- .../openmrs/module/emrapi/adt/AdtService.java | 7 --- .../module/emrapi/adt/AdtServiceImpl.java | 49 ++++++++----------- .../module/emrapi/adt/InpatientRequest.java | 1 - .../hql/inpatient_request_dispositions.hql | 9 +++- .../rest/converter/SimpleBeanConverter.java | 4 +- .../InpatientRequestController.java | 48 ++++++++++++++++++ .../controller/InpatientVisitsController.java | 11 +---- 7 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java index a5dc0570..996743ca 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java @@ -353,11 +353,4 @@ VisitDomainWrapper createRetrospectiveVisit(Patient patient, Location location, * @return List of the matching InpatientRequests that match the criteria */ List getInpatientRequests(InpatientRequestSearchCriteria criteria); - - /** - * Returns all patient awaiting transfer - * @param transferLocation - if non-null, only return matches for patients awaiting transfer to this location - * @return List of the matching visits< - */ - List getVisitsAwaitingTransfer(Location transferLocation); } diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java index ba643783..a69cce1d 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java @@ -943,9 +943,11 @@ public List getInpatientRequests(InpatientRequestSearchCriteri // Get all disposition concepts based on the given disposition type(s) Map dispositionValuesToType = new HashMap<>(); List dispositions = dispositionService.getDispositions(); - for (Disposition d : dispositions) { - if (dispositionTypes.contains(d.getType())) { - dispositionValuesToType.put(emrConceptService.getConcept(d.getConceptCode()), d.getType()); + if (dispositions != null) { + for (Disposition d : dispositions) { + if (dispositionTypes.contains(d.getType())) { + dispositionValuesToType.put(emrConceptService.getConcept(d.getConceptCode()), d.getType()); + } } } @@ -978,38 +980,29 @@ public List getInpatientRequests(InpatientRequestSearchCriteri parameters.put("adtDecisionConcept", emrApiProperties.getAdmissionDecisionConcept()); parameters.put("denyConcept", emrApiProperties.getDenyAdmissionConcept()); parameters.put("dispositionLocationIds", dispositionLocationIds); + parameters.put("admitLocationConcept", descriptor.getAdmissionLocationConcept()); + parameters.put("transferLocationConcept", descriptor.getInternalTransferLocationConcept()); - List dispositionObs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, Obs.class); + List reqs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, List.class); List ret = new ArrayList<>(); - for (Obs o : dispositionObs) { + for (Object req : reqs) { + Object[] o = (Object[]) req; InpatientRequest r = new InpatientRequest(); - r.setVisit(o.getEncounter().getVisit()); - r.setPatient(o.getEncounter().getPatient()); - r.setDispositionEncounter(o.getEncounter()); - r.setDispositionObsGroup(o.getObsGroup()); - r.setDispositionType(dispositionValuesToType.get(o.getValueCoded())); - r.setDisposition(o.getValueCoded()); - - Location dispositionLocation = null; - if (r.getDispositionType() == DispositionType.ADMIT) { - dispositionLocation = descriptor.getAdmissionLocation(o.getObsGroup(), locationService); + r.setVisit((Visit)o[0]); + r.setPatient((Patient)o[1]); + r.setDispositionEncounter((Encounter)o[2]); + r.setDispositionObsGroup((Obs)o[3]); + Obs dispositionObs = (Obs)o[4]; + if (dispositionObs != null) { + r.setDisposition(dispositionObs.getValueCoded()); + r.setDispositionType(dispositionValuesToType.get(dispositionObs.getValueCoded())); } - else if (r.getDispositionType() == DispositionType.TRANSFER) { - dispositionLocation = descriptor.getInternalTransferLocation(o.getObsGroup(), locationService); + Obs locationObs = (Obs)(o[5] != null ? o[5] : o[6]); + if (locationObs != null) { + r.setDispositionLocation(locationService.getLocation(Integer.parseInt(locationObs.getValueText()))); } - r.setDispositionLocation(dispositionLocation); - - Date deathDate = descriptor.getDateOfDeath(o.getObsGroup()); - r.setDispositionDate(deathDate == null ? o.getObsDatetime() : deathDate); ret.add(r); } return ret; } - - @Override - @Transactional(readOnly = true) - public List getVisitsAwaitingTransfer(Location transferLocation) { - // TODO implement! - return Collections.emptyList(); - } } diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java index 0eb18a9e..11db3d0f 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java @@ -23,5 +23,4 @@ public class InpatientRequest { private Obs dispositionObsGroup; private Concept disposition; private Location dispositionLocation; - private Date dispositionDate; } diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index 013fa378..7b0de809 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -1,5 +1,11 @@ select - dispo + visit, + dispoEncounter.patient, + dispoEncounter, + dispo.obsGroup, + dispo, + (select o from Obs o where o.obsGroup = dispo.obsGroup and o.voided = 0 and o.concept = :admitLocationConcept) as admitLocation, + (select o from Obs o where o.obsGroup = dispo.obsGroup and o.voided = 0 and o.concept = :transferLocationConcept) as transferLocation from Obs as dispo inner join dispo.encounter as dispoEncounter @@ -40,3 +46,4 @@ where and locationObs.valueText in (:dispositionLocationIds) ) > 0 ) +order by dispo.obsId diff --git a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java index 59dc3bbd..276cf793 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java @@ -5,6 +5,7 @@ import org.apache.commons.logging.LogFactory; import org.openmrs.annotation.Handler; import org.openmrs.module.emrapi.EmrApiProperties; +import org.openmrs.module.emrapi.adt.InpatientRequest; import org.openmrs.module.emrapi.diagnosis.DiagnosisMetadata; import org.openmrs.module.emrapi.disposition.Disposition; import org.openmrs.module.emrapi.disposition.DispositionDescriptor; @@ -26,7 +27,8 @@ DiagnosisMetadata.class, Disposition.class, DispositionObs.class, - DispositionDescriptor.class + DispositionDescriptor.class, + InpatientRequest.class }, order = 0) public class SimpleBeanConverter implements Converter { diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java new file mode 100644 index 00000000..2e745a7b --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java @@ -0,0 +1,48 @@ +package org.openmrs.module.emrapi.web.controller; + +import org.openmrs.Location; +import org.openmrs.module.emrapi.adt.AdtService; +import org.openmrs.module.emrapi.adt.InpatientRequest; +import org.openmrs.module.emrapi.adt.InpatientRequestSearchCriteria; +import org.openmrs.module.emrapi.disposition.DispositionType; +import org.openmrs.module.emrapi.rest.converter.SimpleBeanConverter; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.RestUtil; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.NeedsPaging; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +@Controller +public class InpatientRequestController { + + @Autowired + private AdtService adtService; + + @RequestMapping(method = RequestMethod.GET, value = "/rest/v1/emrapi/inpatient/request") + @ResponseBody + public SimpleObject getInpatientRequests( + HttpServletRequest request, + HttpServletResponse response, + @RequestParam(required = false, value = "visitLocation") Location visitLocation, + @RequestParam(required = false, value = "dispositionLocation") List dispositionLocations, + @RequestParam(required = false, value = "dispositionType") List dispositionTypes + ) { + RequestContext context = RestUtil.getRequestContext(request, response, Representation.REF); + InpatientRequestSearchCriteria criteria = new InpatientRequestSearchCriteria(); + criteria.setVisitLocation(visitLocation); + criteria.setDispositionLocations(dispositionLocations); + criteria.setDispositionTypes(dispositionTypes); + List requests = adtService.getInpatientRequests(criteria); + return new NeedsPaging<>(requests, context).toSimpleObject(new SimpleBeanConverter()); + } +} diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java index ddc1b381..2c4d9188 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java @@ -88,16 +88,7 @@ private List getVisitsAwaitingAdmissionHelper(Location admissionLo } private List getVisitsAwaitingTransferHelper(Location transferLocation) { - List visits = adtService.getVisitsAwaitingTransfer(transferLocation); - List visitObjects = new ArrayList(); - for (Visit visit : visits) { - SimpleObject inpatientVisit = new SimpleObject(); - inpatientVisit.put("visit", ConversionUtil.convertToRepresentation(visit, Representation.DEFAULT)); - inpatientVisit.put("patient", ConversionUtil.convertToRepresentation(visit.getPatient(), Representation.DEFAULT)); - inpatientVisit.put("type", AdtAction.Type.TRANSFER); - visitObjects.add(inpatientVisit); - } - return visitObjects; + return new ArrayList<>(); } } From a0596577dd6d0fb93ee0c69a5ba146c8662ad8bd Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 15:35:35 -0400 Subject: [PATCH 06/15] Add additional constraint to ensure the disposition used is the latest in the visit. --- .../adt/InpatientRequestSearchCriteria.java | 1 + .../hql/inpatient_request_dispositions.hql | 8 +++++++ .../module/emrapi/adt/AdtServiceImplTest.java | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java index 7f535c7a..7a319a1f 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java @@ -9,6 +9,7 @@ /** * Represents criteria for searching for AdtRequests + * Currently the assumption is that all requests returned are active, and this will be the default regardless */ @Data public class InpatientRequestSearchCriteria { diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index 7b0de809..5ffd3187 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -20,6 +20,14 @@ where and (:visitLocation is null or visit.location = :visitLocation) and person.dead = false and visit.stopDatetime is null + and ( + select count(*) + from Obs as laterDispoObs + where laterDispoObs.encounter.visit = visit + and laterDispoObs.voided = false + and laterDispoObs.concept = :dispositionConcept + and laterDispoObs.obsDatetime > dispo.obsDatetime + ) = 0 and ( select count(*) from Encounter as adtEncounter diff --git a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java index 48a26476..45aa9f8a 100644 --- a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -308,4 +308,25 @@ public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsT assertNumRequests(criteria, 1); } + @Test + public void shouldOnlyReturnLatestDispositionRequestWithinAGivenVisit() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createAdmissionRequest(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 1); + } + + @Test + public void shouldOnlyReturnAdmitIfItIsLaterThanDischarge() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createDischargeRequest(DateUtils.addHours(visitDate, 3), preAdmissionLocation); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); + } } From 1e153daffc914adbb4d04f957a12e73bc88fc229 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 16:33:12 -0400 Subject: [PATCH 07/15] Cover edge case where multiple dispositions have the exact same datetime, favoring the one with the later obsId. Add same constraint to existing awaiting admission query. --- .../AwaitingAdmissionVisitQueryEvaluatorTest.java | 6 ------ .../resources/hql/inpatient_request_dispositions.hql | 5 ++++- .../main/resources/hql/visits_awaiting_admission.hql | 11 +++++++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/api-reporting/src/test/java/org/openmrs/module/emrapi/adt/reporting/evaluator/AwaitingAdmissionVisitQueryEvaluatorTest.java b/api-reporting/src/test/java/org/openmrs/module/emrapi/adt/reporting/evaluator/AwaitingAdmissionVisitQueryEvaluatorTest.java index e010f67d..13611dbf 100644 --- a/api-reporting/src/test/java/org/openmrs/module/emrapi/adt/reporting/evaluator/AwaitingAdmissionVisitQueryEvaluatorTest.java +++ b/api-reporting/src/test/java/org/openmrs/module/emrapi/adt/reporting/evaluator/AwaitingAdmissionVisitQueryEvaluatorTest.java @@ -222,12 +222,6 @@ public void shouldFindVisitEvenIfPatientHasMoreRecentVisitNoteWithoutAdmissionDi .encounterType(emrApiProperties.getVisitNoteEncounterType()) .visit(visit) .save(); - testDataManager.obs() - .person(patient) - .encounter(secondVisitNoteEncounter) - .concept(dispositionDescriptor.getDispositionConcept()) - .value(emrConceptService.getConcept("org.openmrs.module.emrapi:Death")) - .save(); VisitQueryResult result = visitQueryService.evaluate(query, null); assertThat(result.getMemberIds().size(), is(1)); diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index 5ffd3187..f40e0b07 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -26,7 +26,10 @@ where where laterDispoObs.encounter.visit = visit and laterDispoObs.voided = false and laterDispoObs.concept = :dispositionConcept - and laterDispoObs.obsDatetime > dispo.obsDatetime + and ( + laterDispoObs.obsDatetime > dispo.obsDatetime or + (laterDispoObs.obsDatetime = dispo.obsDatetime and laterDispoObs.obsId > dispo.obsId) + ) ) = 0 and ( select count(*) diff --git a/api/src/main/resources/hql/visits_awaiting_admission.hql b/api/src/main/resources/hql/visits_awaiting_admission.hql index facfa430..1e65a5c6 100644 --- a/api/src/main/resources/hql/visits_awaiting_admission.hql +++ b/api/src/main/resources/hql/visits_awaiting_admission.hql @@ -16,6 +16,17 @@ where and (:visitIds is null or visit.visitId in :visitIds) and person.dead = false and visit.stopDatetime is null + and ( + select count(*) + from Obs as laterDispoObs + where laterDispoObs.encounter.visit = visit + and laterDispoObs.voided = false + and laterDispoObs.concept = :dispositionConcept + and ( + laterDispoObs.obsDatetime > dispo.obsDatetime or + (laterDispoObs.obsDatetime = dispo.obsDatetime and laterDispoObs.obsId > dispo.obsId) + ) + ) = 0 and ( select count(*) from Encounter as admission From 066a94832309ff9854ed3b2cf94cc4b33e52a477 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 16:43:51 -0400 Subject: [PATCH 08/15] For consistency with other controllers, don't limit to v1 --- .../emrapi/web/controller/InpatientRequestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java index 2e745a7b..d3d89844 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java @@ -28,7 +28,7 @@ public class InpatientRequestController { @Autowired private AdtService adtService; - @RequestMapping(method = RequestMethod.GET, value = "/rest/v1/emrapi/inpatient/request") + @RequestMapping(method = RequestMethod.GET, value = "/rest/**/emrapi/inpatient/request") @ResponseBody public SimpleObject getInpatientRequests( HttpServletRequest request, From 69d555baccb20eb50a9631f4a66d14943a62945a Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 17:27:20 -0400 Subject: [PATCH 09/15] Only consider requests fullfilled if adt encounter occurs after them within the visit. --- .../hql/inpatient_request_dispositions.hql | 1 + .../resources/hql/visits_awaiting_admission.hql | 3 ++- .../module/emrapi/adt/AdtServiceImplTest.java | 13 +++++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index f40e0b07..cf156c17 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -37,6 +37,7 @@ where where adtEncounter.visit = visit and adtEncounter.voided = false and adtEncounter.encounterType in (:adtEncounterTypes) + and adtEncounter.encounterDatetime >= dispo.obsDatetime ) = 0 and ( select count(*) diff --git a/api/src/main/resources/hql/visits_awaiting_admission.hql b/api/src/main/resources/hql/visits_awaiting_admission.hql index 1e65a5c6..1885b02d 100644 --- a/api/src/main/resources/hql/visits_awaiting_admission.hql +++ b/api/src/main/resources/hql/visits_awaiting_admission.hql @@ -33,7 +33,8 @@ where where admission.visit = visit and admission.voided = false and admission.encounterType = :admissionEncounterType - ) = 0 + and admission.encounterDatetime >= dispo.obsDatetime + ) = 0 and ( select count(*) from Obs as admitDecision diff --git a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java index 45aa9f8a..9f150126 100644 --- a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -284,7 +284,7 @@ public void shouldGetAdmissionRequestIfPatientHasAnAdmissionDecisionThatIsNotDen } @Test - public void shouldFindVisitWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() { + public void shouldGetAdmissionRequestWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() { criteria.addDispositionType(DispositionType.ADMIT); createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 1)); assertNumRequests(criteria, 0); @@ -293,7 +293,7 @@ public void shouldFindVisitWithDispositionOfAdmitIfPrecededByAdmissionDenialObs( } @Test - public void shouldFindVisitWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() { + public void shouldGetAdmissionRequestWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); @@ -329,4 +329,13 @@ public void shouldOnlyReturnAdmitIfItIsLaterThanDischarge() { createAdmissionRequest(DateUtils.addHours(visitDate, 4)); assertNumRequests(criteria, 1); } + + @Test + public void shouldGetAdmissionRequestIfAfterAdmissionEncounter() { + criteria.addDispositionType(DispositionType.ADMIT); + createAdmissionEncounter(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); + } } From 2165c368306a9719b896deefee0f44da9f10afb6 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Thu, 11 Jul 2024 17:31:39 -0400 Subject: [PATCH 10/15] Formatting --- api/src/main/resources/hql/visits_awaiting_admission.hql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/resources/hql/visits_awaiting_admission.hql b/api/src/main/resources/hql/visits_awaiting_admission.hql index 1885b02d..8f7a1350 100644 --- a/api/src/main/resources/hql/visits_awaiting_admission.hql +++ b/api/src/main/resources/hql/visits_awaiting_admission.hql @@ -34,7 +34,7 @@ where and admission.voided = false and admission.encounterType = :admissionEncounterType and admission.encounterDatetime >= dispo.obsDatetime - ) = 0 + ) = 0 and ( select count(*) from Obs as admitDecision From bea208d5dbfe6e221f2384e9e02f31c02d0e34b3 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 12 Jul 2024 06:43:39 -0400 Subject: [PATCH 11/15] Set the default representation to DEFAULT --- .../emrapi/web/controller/InpatientRequestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java index d3d89844..a753e998 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java @@ -37,7 +37,7 @@ public SimpleObject getInpatientRequests( @RequestParam(required = false, value = "dispositionLocation") List dispositionLocations, @RequestParam(required = false, value = "dispositionType") List dispositionTypes ) { - RequestContext context = RestUtil.getRequestContext(request, response, Representation.REF); + RequestContext context = RestUtil.getRequestContext(request, response, Representation.DEFAULT); InpatientRequestSearchCriteria criteria = new InpatientRequestSearchCriteria(); criteria.setVisitLocation(visitLocation); criteria.setDispositionLocations(dispositionLocations); From d58090bf5cac37ebc9f9a64c3f180c254907a154 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 12 Jul 2024 08:41:34 -0400 Subject: [PATCH 12/15] Re-organize and add additional unit tests, add ability to search for requests based on patient(s) and visit(s) for compatibility with previous deprecated method. --- .../module/emrapi/adt/AdtServiceImpl.java | 5 + .../adt/InpatientRequestSearchCriteria.java | 16 ++ .../hql/inpatient_request_dispositions.hql | 4 +- .../module/emrapi/adt/AdtServiceImplTest.java | 206 ++++++++++++++---- .../disposition/DispositionServiceTest.java | 2 +- api/src/test/resources/dispositionConfig.json | 8 + .../EmrApiConfigurationControllerTest.java | 7 +- 7 files changed, 202 insertions(+), 46 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java index a69cce1d..16853108 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtServiceImpl.java @@ -980,8 +980,13 @@ public List getInpatientRequests(InpatientRequestSearchCriteri parameters.put("adtDecisionConcept", emrApiProperties.getAdmissionDecisionConcept()); parameters.put("denyConcept", emrApiProperties.getDenyAdmissionConcept()); parameters.put("dispositionLocationIds", dispositionLocationIds); + parameters.put("limitByDispositionLocation", dispositionLocationIds != null); parameters.put("admitLocationConcept", descriptor.getAdmissionLocationConcept()); parameters.put("transferLocationConcept", descriptor.getInternalTransferLocationConcept()); + parameters.put("patientIds", criteria.getPatientIds()); + parameters.put("limitByPatient", criteria.getPatientIds() != null); + parameters.put("visitIds", criteria.getVisitIds()); + parameters.put("limitByVisit", criteria.getVisitIds() != null); List reqs = emrApiDAO.executeHqlFromResource("hql/inpatient_request_dispositions.hql", parameters, List.class); List ret = new ArrayList<>(); diff --git a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java index 7a319a1f..65c6253f 100644 --- a/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java @@ -17,6 +17,8 @@ public class InpatientRequestSearchCriteria { private Location visitLocation; private List dispositionLocations; private List dispositionTypes; + private List patientIds; + private List visitIds; public void addDispositionLocation(Location location) { if (dispositionLocations == null) { @@ -31,4 +33,18 @@ public void addDispositionType(DispositionType dispositionType) { } dispositionTypes.add(dispositionType); } + + public void addPatientId(Integer patientId) { + if (patientIds == null) { + patientIds = new ArrayList<>(); + } + patientIds.add(patientId); + } + + public void addVisitId(Integer visitId) { + if (visitIds == null) { + visitIds = new ArrayList<>(); + } + visitIds.add(visitId); + } } diff --git a/api/src/main/resources/hql/inpatient_request_dispositions.hql b/api/src/main/resources/hql/inpatient_request_dispositions.hql index cf156c17..0fc152d9 100644 --- a/api/src/main/resources/hql/inpatient_request_dispositions.hql +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -51,11 +51,13 @@ where and encounterInVisit.encounterDatetime > dispoEncounter.encounterDatetime ) = 0 and ( - :dispositionLocationIds is null or ( + :limitByDispositionLocation = false or ( select count(*) from Obs as locationObs where locationObs.obsGroup = dispo.obsGroup and locationObs.valueText in (:dispositionLocationIds) ) > 0 ) + and (:limitByPatient is false or dispoEncounter.patient.patientId in (:patientIds)) + and (:limitByVisit is false or visit.visitId in (:visitIds)) order by dispo.obsId diff --git a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java index 9f150126..28179b90 100644 --- a/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -22,6 +22,8 @@ import org.openmrs.module.emrapi.test.ContextSensitiveMetadataTestUtils; import org.springframework.beans.factory.annotation.Autowired; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -115,7 +117,7 @@ public Obs createTransferRequest(Date encounterDate) { Encounter e = createEncounter(emrApiProperties.getVisitNoteEncounterType(), admissionLocation, encounterDate); ObsBuilder groupBuilder = testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionSetConcept()); groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getDispositionConcept()).value(transferDisposition).get()); - groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getAdmissionLocationConcept()).value(transferLocation.getLocationId().toString()).get()); + groupBuilder.member(testDataManager.obs().encounter(e).concept(dispositionDescriptor.getInternalTransferLocationConcept()).value(transferLocation.getLocationId().toString()).get()); return groupBuilder.save(); } @@ -141,8 +143,7 @@ private List assertNumRequests(InpatientRequestSearchCriteria } @Test - public void shouldGetAdmissionRequest() { - criteria.addDispositionType(DispositionType.ADMIT); + public void shouldGetInpatientRequest() { assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); requests = adtService.getInpatientRequests(criteria); @@ -152,105 +153,199 @@ public void shouldGetAdmissionRequest() { assertThat(requests.get(0).getDispositionLocation(), equalTo(admissionLocation)); } + // Filter based on visit location + @Test - public void shouldNotGetAdmissionRequestIfPatientHasBeenAdmitted() { + public void shouldGetAdmissionRequestForVisitLocation() { criteria.addDispositionType(DispositionType.ADMIT); + criteria.setVisitLocation(visitLocation); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); + } + + @Test + public void shouldGetAdmissionRequestForParentVisitLocation() { + criteria.addDispositionType(DispositionType.ADMIT); + criteria.setVisitLocation(admissionLocation); assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); } @Test - public void shouldGetAdmissionRequestIfAdmissionEncounterIsVoided() { + public void shouldNotGetAdmissionRequestForDifferentVisitLocation() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - Encounter e = createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); + criteria.setVisitLocation(otherVisitLocation); assertNumRequests(criteria, 0); - testDataManager.getEncounterService().voidEncounter(e, "Unknown"); + } + + // Filter based on Disposition Type + + @Test + public void shouldGetInpatientRequestsBasedOnDispositionType() { + assertNumRequests(criteria, 0); + + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.ADMIT)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.TRANSFER)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.DISCHARGE)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.TRANSFER)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.DISCHARGE)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Arrays.asList(DispositionType.TRANSFER, DispositionType.DISCHARGE)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(null); + + createTransferRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.ADMIT)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.TRANSFER)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.DISCHARGE)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.TRANSFER)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.DISCHARGE)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Arrays.asList(DispositionType.TRANSFER, DispositionType.DISCHARGE)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(null); + + createDischargeRequest(DateUtils.addHours(visitDate, 6), preAdmissionLocation); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.ADMIT)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.TRANSFER)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Collections.singletonList(DispositionType.DISCHARGE)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.TRANSFER)); + assertNumRequests(criteria, 0); + criteria.setDispositionTypes(Arrays.asList(DispositionType.ADMIT, DispositionType.DISCHARGE)); + assertNumRequests(criteria, 1); + criteria.setDispositionTypes(Arrays.asList(DispositionType.TRANSFER, DispositionType.DISCHARGE)); assertNumRequests(criteria, 1); } + // Filter based on disposition location + @Test - public void shouldNotGetAdmissionRequestIfPatientHasBeenDenied() { - criteria.addDispositionType(DispositionType.ADMIT); + public void shouldGetInpatientRequestsBasedOnDispositionLocation() { assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + criteria.addDispositionLocation(preAdmissionLocation); assertNumRequests(criteria, 0); + criteria.addDispositionLocation(transferLocation); + assertNumRequests(criteria, 0); + criteria.addDispositionLocation(admissionLocation); + assertNumRequests(criteria, 1); } + // Filter based on patient ids + @Test - public void shouldNotGetAdmissionRequestIfPatientHasDied() { - criteria.addDispositionType(DispositionType.ADMIT); + public void shouldGetInpatientRequestsBasedOnPatient() { assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - patient.setDead(true); - patient.setCauseOfDeathNonCoded("Unknown"); - testDataManager.getPatientService().savePatient(patient); + criteria.addPatientId(patient.getPatientId() + 1); + assertNumRequests(criteria, 0); + criteria.addPatientId(patient.getPatientId() + 2); assertNumRequests(criteria, 0); + criteria.addPatientId(patient.getPatientId()); + assertNumRequests(criteria, 1); } + // Filter based on visit ids + @Test - public void shouldNotGetAdmissionRequestIfPatientIsVoided() { - criteria.addDispositionType(DispositionType.ADMIT); + public void shouldGetInpatientRequestsBasedOnVisit() { assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - testDataManager.getPatientService().voidPatient(patient, "Unknown"); + criteria.addVisitId(visit.getVisitId() + 1); assertNumRequests(criteria, 0); + criteria.addVisitId(visit.getVisitId() + 2); + assertNumRequests(criteria, 0); + criteria.addVisitId(visit.getVisitId()); + assertNumRequests(criteria, 1); } + // Filter based on timeline of disposition obs and encounters within visit + @Test - public void shouldNotGetAdmissionRequestIfEncounterIsVoided() { + public void shouldNotGetAdmissionRequestIfPatientHasBeenAdmitted() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); - Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); - requests = assertNumRequests(criteria, 1); - testDataManager.getEncounterService().voidEncounter(o.getEncounter(), "Unknown"); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); assertNumRequests(criteria, 0); } @Test - public void shouldNotGetAdmissionRequestIfObsIsVoided() { + public void shouldGetAdmissionRequestIfAdmissionEncounterIsVoided() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); - Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); - requests = assertNumRequests(criteria, 1); - testDataManager.getObsService().voidObs(o, "Unknown"); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + Encounter e = createAdmissionEncounter(DateUtils.addHours(visitDate, 3)); assertNumRequests(criteria, 0); + testDataManager.getEncounterService().voidEncounter(e, "Unknown"); + assertNumRequests(criteria, 1); } @Test - public void shouldGetAdmissionRequestForVisitLocation() { + public void shouldOnlyReturnLatestDispositionRequestWithinAGivenVisit() { criteria.addDispositionType(DispositionType.ADMIT); - criteria.setVisitLocation(visitLocation); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); + createAdmissionRequest(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 1); } @Test - public void shouldGetAdmissionRequestForParentVisitLocation() { + public void shouldOnlyReturnAdmitIfItIsLaterThanDischarge() { criteria.addDispositionType(DispositionType.ADMIT); - criteria.setVisitLocation(admissionLocation); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); + createDischargeRequest(DateUtils.addHours(visitDate, 3), preAdmissionLocation); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); } @Test - public void shouldNotGetAdmissionRequestForDifferentVisitLocation() { + public void shouldGetAdmissionRequestIfAfterAdmissionEncounter() { + criteria.addDispositionType(DispositionType.ADMIT); + createAdmissionEncounter(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 1); + } + + // Filter based on timeline of disposition obs and denial obs within visit + + @Test + public void shouldNotGetAdmissionRequestIfPatientHasBeenDenied() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - criteria.setVisitLocation(otherVisitLocation); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); assertNumRequests(criteria, 0); } @@ -308,34 +403,59 @@ public void shouldGetAdmissionRequestWithDispositionOfAdmitIfFollowedByAdmission assertNumRequests(criteria, 1); } + // Filter out patients who have died or whose visit is ended + @Test - public void shouldOnlyReturnLatestDispositionRequestWithinAGivenVisit() { + public void shouldNotGetAdmissionRequestIfPatientHasDied() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - createAdmissionRequest(DateUtils.addHours(visitDate, 3)); - assertNumRequests(criteria, 1); + patient.setDead(true); + patient.setCauseOfDeathNonCoded("Unknown"); + testDataManager.getPatientService().savePatient(patient); + assertNumRequests(criteria, 0); } @Test - public void shouldOnlyReturnAdmitIfItIsLaterThanDischarge() { + public void shouldGetInpatientRequestsForEndedVisits() { criteria.addDispositionType(DispositionType.ADMIT); assertNumRequests(criteria, 0); createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); - createDischargeRequest(DateUtils.addHours(visitDate, 3), preAdmissionLocation); + testDataManager.getVisitService().endVisit(visit, DateUtils.addHours(visitDate, 4)); assertNumRequests(criteria, 0); - createAdmissionRequest(DateUtils.addHours(visitDate, 4)); - assertNumRequests(criteria, 1); } + // Filter out voided data + @Test - public void shouldGetAdmissionRequestIfAfterAdmissionEncounter() { + public void shouldNotGetAdmissionRequestIfPatientIsVoided() { criteria.addDispositionType(DispositionType.ADMIT); - createAdmissionEncounter(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 0); - createAdmissionRequest(DateUtils.addHours(visitDate, 4)); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); assertNumRequests(criteria, 1); + testDataManager.getPatientService().voidPatient(patient, "Unknown"); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfEncounterIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + requests = assertNumRequests(criteria, 1); + testDataManager.getEncounterService().voidEncounter(o.getEncounter(), "Unknown"); + assertNumRequests(criteria, 0); + } + + @Test + public void shouldNotGetAdmissionRequestIfObsIsVoided() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + Obs o = createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + requests = assertNumRequests(criteria, 1); + testDataManager.getObsService().voidObs(o, "Unknown"); + assertNumRequests(criteria, 0); } } diff --git a/api/src/test/java/org/openmrs/module/emrapi/disposition/DispositionServiceTest.java b/api/src/test/java/org/openmrs/module/emrapi/disposition/DispositionServiceTest.java index b38f1c60..4c7fa1b4 100644 --- a/api/src/test/java/org/openmrs/module/emrapi/disposition/DispositionServiceTest.java +++ b/api/src/test/java/org/openmrs/module/emrapi/disposition/DispositionServiceTest.java @@ -66,7 +66,7 @@ public void shouldParseDispositionJsonFromDefaultConfig() throws IOException { List dispositions = dispositionService.getDispositions(); - assertEquals(4, dispositions.size()); + assertEquals(5, dispositions.size()); assertEquals(deathDisposition, dispositions.get(0)); assertEquals(homeDisposition, dispositions.get(1)); diff --git a/api/src/test/resources/dispositionConfig.json b/api/src/test/resources/dispositionConfig.json index e7e50cc0..ed6699c3 100644 --- a/api/src/test/resources/dispositionConfig.json +++ b/api/src/test/resources/dispositionConfig.json @@ -21,6 +21,14 @@ "actions": [], "additionalObs": [] }, + { + "uuid": "8297651b-4046-11ef-ba6a-0242ac120002", + "name": "disposition.transfer", + "type": "TRANSFER", + "conceptCode": "org.openmrs.module.emrapi:Transfer out of hospital", + "actions": [], + "additionalObs": [] + }, { "uuid": "687d966bb-9c91-4886-b8b0-e63361f495f0", "name": "disposition.observation", diff --git a/omod/src/test/java/org/openmrs/module/emrapi/web/controller/EmrApiConfigurationControllerTest.java b/omod/src/test/java/org/openmrs/module/emrapi/web/controller/EmrApiConfigurationControllerTest.java index 1b24b108..0a916ca2 100644 --- a/omod/src/test/java/org/openmrs/module/emrapi/web/controller/EmrApiConfigurationControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/emrapi/web/controller/EmrApiConfigurationControllerTest.java @@ -108,7 +108,7 @@ public void shouldGetDispositions() { request.addParameter("v", "custom:(dispositions)"); SimpleObject config = emrApiConfigurationController.getEmrApiConfiguration(request, response); List> dispositions = listNode(config, "dispositions"); - assertThat(dispositions.size(), equalTo(4)); + assertThat(dispositions.size(), equalTo(5)); for (Map d : dispositions) { if (d.get("uuid").equals("d2d89630-b698-11e2-9e96-0800200c9a66")) { assertThat(d.get("name"), equalTo("disposition.death")); @@ -121,6 +121,11 @@ else if (d.get("uuid").equals("66de7f60-b73a-11e2-9e96-0800200c9a66")) { assertThat(d.get("conceptCode"), equalTo("org.openmrs.module.emrapi:Admit to hospital")); assertThat(listNode(d, "additionalObs").size(), equalTo(0)); } + else if (d.get("uuid").equals("8297651b-4046-11ef-ba6a-0242ac120002")) { + assertThat(d.get("name"), equalTo("disposition.transfer")); + assertThat(d.get("conceptCode"), equalTo("org.openmrs.module.emrapi:Transfer out of hospital")); + assertThat(listNode(d, "additionalObs").size(), equalTo(0)); + } else if (d.get("uuid").equals("687d966bb-9c91-4886-b8b0-e63361f495f0")) { assertThat(d.get("name"), equalTo("disposition.observation")); assertThat(d.get("conceptCode"), equalTo("org.openmrs.module.emrapi:ED Observation")); From fd9edf8a5b7776e11c91d4e64f227b7bbca95928 Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 12 Jul 2024 08:47:33 -0400 Subject: [PATCH 13/15] Remove specific inpatient request methods from inpatient visits controller in favor of inpatient requests controller. --- .../controller/InpatientVisitsController.java | 47 ++----------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java index 7a85e440..d4ddd4ce 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientVisitsController.java @@ -1,11 +1,6 @@ package org.openmrs.module.emrapi.web.controller; -import java.util.ArrayList; -import java.util.List; - import org.openmrs.Location; -import org.openmrs.Visit; -import org.openmrs.module.emrapi.adt.AdtAction; import org.openmrs.module.emrapi.adt.AdtService; import org.openmrs.module.emrapi.visit.VisitDomainWrapper; import org.openmrs.module.webservices.rest.SimpleObject; @@ -18,6 +13,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import java.util.ArrayList; +import java.util.List; + @Controller @RequestMapping(value = "/rest/**/emrapi/inpatient") public class InpatientVisitsController { @@ -52,45 +50,6 @@ public List getInpatientVisits(@RequestParam(value = "currentLocat return response; } - - @RequestMapping(method = RequestMethod.GET, value = "/admissionRequests") - @ResponseBody - public List getVisitsAwaitingAdmission(@RequestParam("admissionLocation") Location admissionLocation) { - return getVisitsAwaitingAdmissionHelper(admissionLocation); - } - - @RequestMapping(method = RequestMethod.GET, value = "/transferRequests") - @ResponseBody - public List getVisitsAwaitingTransfer(@RequestParam("transferLocation") Location transferLocation) { - return getVisitsAwaitingTransferHelper(transferLocation); - } - - @RequestMapping(method = RequestMethod.GET, value = "/admissionAndTransferRequests") - @ResponseBody - public List getVisitsAwaitingAdminstOrTransfer(@RequestParam("location") Location location) { - List response = getVisitsAwaitingAdmissionHelper(location); - response.addAll(getVisitsAwaitingTransferHelper(location)); - return response; - } - - private List getVisitsAwaitingAdmissionHelper(Location admissionLocation) { - // TODO note also that this service method does *not* actually limit by admission location; we will need to expand the underlying service method/hql query to do this, see: https://openmrs.atlassian.net/browse/O3-3464 - List visits = adtService.getVisitsAwaitingAdmission(admissionLocation, null, null); - List visitObjects = new ArrayList(); - for (Visit visit : visits) { - SimpleObject inpatientVisit = new SimpleObject(); - inpatientVisit.put("visit", ConversionUtil.convertToRepresentation(visit, Representation.DEFAULT)); - inpatientVisit.put("patient", ConversionUtil.convertToRepresentation(visit.getPatient(), Representation.DEFAULT)); - inpatientVisit.put("type", AdtAction.Type.ADMISSION); - visitObjects.add(inpatientVisit); - } - return visitObjects; - } - - private List getVisitsAwaitingTransferHelper(Location transferLocation) { - return new ArrayList<>(); - } - } From 6dae3e252a96aba8df3793abd268b8a629716fac Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 12 Jul 2024 10:20:34 -0400 Subject: [PATCH 14/15] Change default representation of inpatient request to ref for all properties except visit, which is default --- .../converter/InpatientRequestConverter.java | 32 +++++++++++++++++++ .../rest/converter/SimpleBeanConverter.java | 3 +- .../InpatientRequestController.java | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java diff --git a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java new file mode 100644 index 00000000..7df0051b --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java @@ -0,0 +1,32 @@ +package org.openmrs.module.emrapi.rest.converter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.annotation.Handler; +import org.openmrs.module.emrapi.adt.InpatientRequest; +import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; + +@Handler(supports = InpatientRequest.class, order = 0) +public class InpatientRequestConverter extends SimpleBeanConverter { + + private final Log log = LogFactory.getLog(getClass()); + + /** + * @return a representation of each property that matches the given representation, except when + * requesting the default representation, in which case all properties are ref except for visit which is default + */ + @Override + public DelegatingResourceDescription getResourceDescription(InpatientRequest req, Representation representation) { + DelegatingResourceDescription ret = super.getResourceDescription(req, representation); + if (representation instanceof DefaultRepresentation) { + for (String property : ret.getProperties().keySet()) { + if (!property.equals("visit")) { + ret.addProperty(property, Representation.REF); + } + } + } + return ret; + } +} diff --git a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java index 276cf793..cd7e8b38 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/SimpleBeanConverter.java @@ -27,8 +27,7 @@ DiagnosisMetadata.class, Disposition.class, DispositionObs.class, - DispositionDescriptor.class, - InpatientRequest.class + DispositionDescriptor.class }, order = 0) public class SimpleBeanConverter implements Converter { diff --git a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java index a753e998..42819eb5 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/web/controller/InpatientRequestController.java @@ -5,7 +5,7 @@ import org.openmrs.module.emrapi.adt.InpatientRequest; import org.openmrs.module.emrapi.adt.InpatientRequestSearchCriteria; import org.openmrs.module.emrapi.disposition.DispositionType; -import org.openmrs.module.emrapi.rest.converter.SimpleBeanConverter; +import org.openmrs.module.emrapi.rest.converter.InpatientRequestConverter; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.module.webservices.rest.web.RequestContext; import org.openmrs.module.webservices.rest.web.RestUtil; @@ -43,6 +43,6 @@ public SimpleObject getInpatientRequests( criteria.setDispositionLocations(dispositionLocations); criteria.setDispositionTypes(dispositionTypes); List requests = adtService.getInpatientRequests(criteria); - return new NeedsPaging<>(requests, context).toSimpleObject(new SimpleBeanConverter()); + return new NeedsPaging<>(requests, context).toSimpleObject(new InpatientRequestConverter()); } } From e2d012396fd6ef3a92d17a0a0cd5b34e008ee33f Mon Sep 17 00:00:00 2001 From: Michael Seaton Date: Fri, 12 Jul 2024 10:45:06 -0400 Subject: [PATCH 15/15] Change default representation of inpatient request to ref for all properties except visit, which is default --- .../rest/converter/InpatientRequestConverter.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java index 7df0051b..76b64e42 100644 --- a/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java +++ b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java @@ -5,6 +5,7 @@ import org.openmrs.annotation.Handler; import org.openmrs.module.emrapi.adt.InpatientRequest; import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; import org.openmrs.module.webservices.rest.web.representation.Representation; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; @@ -13,10 +14,6 @@ public class InpatientRequestConverter extends SimpleBeanConverter