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/java/org/openmrs/module/emrapi/adt/AdtService.java b/api/src/main/java/org/openmrs/module/emrapi/adt/AdtService.java index cdb9ecf6..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 @@ -344,13 +344,13 @@ 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); /** - * 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< + * 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 getVisitsAwaitingTransfer(Location transferLocation); + List getInpatientRequests(InpatientRequestSearchCriteria criteria); } 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..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 @@ -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; @@ -920,8 +924,90 @@ public List getVisitsAwaitingAdmission(Location location, Collection getVisitsAwaitingTransfer(Location transferLocation) { - // TODO implement! - return Collections.emptyList(); + public List 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(); + if (dispositions != null) { + 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); + 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<>(); + for (Object req : reqs) { + Object[] o = (Object[]) req; + InpatientRequest r = new InpatientRequest(); + 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())); + } + Obs locationObs = (Obs)(o[5] != null ? o[5] : o[6]); + if (locationObs != null) { + r.setDispositionLocation(locationService.getLocation(Integer.parseInt(locationObs.getValueText()))); + } + ret.add(r); + } + return ret; } } 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..11db3d0f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequest.java @@ -0,0 +1,26 @@ +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; +} 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..65c6253f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/emrapi/adt/InpatientRequestSearchCriteria.java @@ -0,0 +1,50 @@ +package org.openmrs.module.emrapi.adt; + +import lombok.Data; +import org.openmrs.Location; +import org.openmrs.module.emrapi.disposition.DispositionType; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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 { + + private Location visitLocation; + private List dispositionLocations; + private List dispositionTypes; + private List patientIds; + private List visitIds; + + 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); + } + + 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 new file mode 100644 index 00000000..0fc152d9 --- /dev/null +++ b/api/src/main/resources/hql/inpatient_request_dispositions.hql @@ -0,0 +1,63 @@ +select + 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 +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 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 adtEncounter + where adtEncounter.visit = visit + and adtEncounter.voided = false + and adtEncounter.encounterType in (:adtEncounterTypes) + and adtEncounter.encounterDatetime >= dispo.obsDatetime + ) = 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 ( + :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/main/resources/hql/visits_awaiting_admission.hql b/api/src/main/resources/hql/visits_awaiting_admission.hql index facfa430..8f7a1350 100644 --- a/api/src/main/resources/hql/visits_awaiting_admission.hql +++ b/api/src/main/resources/hql/visits_awaiting_admission.hql @@ -16,12 +16,24 @@ 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 where admission.visit = visit and admission.voided = false and admission.encounterType = :admissionEncounterType + and admission.encounterDatetime >= dispo.obsDatetime ) = 0 and ( select count(*) 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..28179b90 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/emrapi/adt/AdtServiceImplTest.java @@ -0,0 +1,461 @@ +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.Arrays; +import java.util.Collections; +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.getInternalTransferLocationConcept()).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 shouldGetInpatientRequest() { + 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)); + } + + // Filter based on visit location + + @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); + } + + // 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 shouldGetInpatientRequestsBasedOnDispositionLocation() { + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + 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 shouldGetInpatientRequestsBasedOnPatient() { + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + 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 shouldGetInpatientRequestsBasedOnVisit() { + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + 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 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 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 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); + } + + @Test + 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); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 3)); + assertNumRequests(criteria, 0); + } + + @Test + 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 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 shouldGetAdmissionRequestWithDispositionOfAdmitIfPrecededByAdmissionDenialObs() { + criteria.addDispositionType(DispositionType.ADMIT); + createAdmissionDeniedEncounter(DateUtils.addHours(visitDate, 1)); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + } + + @Test + public void shouldGetAdmissionRequestWithDispositionOfAdmitIfFollowedByAdmissionDenialObsThatIsVoided() { + 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); + } + + // Filter out patients who have died or whose visit is ended + + @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 shouldGetInpatientRequestsForEndedVisits() { + criteria.addDispositionType(DispositionType.ADMIT); + assertNumRequests(criteria, 0); + createAdmissionRequest(DateUtils.addHours(visitDate, 2)); + assertNumRequests(criteria, 1); + testDataManager.getVisitService().endVisit(visit, DateUtils.addHours(visitDate, 4)); + assertNumRequests(criteria, 0); + } + + // Filter out voided data + + @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); + } +} 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/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..76b64e42 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/emrapi/rest/converter/InpatientRequestConverter.java @@ -0,0 +1,36 @@ +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.FullRepresentation; +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()); + + @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); + } + } + } + else if (representation instanceof FullRepresentation) { + for (String property : ret.getProperties().keySet()) { + if (!property.equals("visit")) { + ret.addProperty(property, Representation.DEFAULT); + } + } + } + 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 59dc3bbd..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 @@ -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; 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..42819eb5 --- /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.InpatientRequestConverter; +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/**/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.DEFAULT); + 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 InpatientRequestConverter()); + } +} 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 5b5b333f..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,54 +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) { - 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; - } - } 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")); 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 +