Skip to content

Commit

Permalink
O3-2624, prevent creating overlapping queue entries
Browse files Browse the repository at this point in the history
  • Loading branch information
cioan committed Dec 6, 2023
1 parent 32119d2 commit 7454f58
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.openmrs.module.queue.api;

import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;

public class DuplicateQueueEntryException extends APIException {

Expand All @@ -23,7 +22,7 @@ public DuplicateQueueEntryException() {
* @param message
*/
public DuplicateQueueEntryException(String message) {
super(Context.getMessageSourceService().getMessage(message));
super(message);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
import org.openmrs.api.VisitService;
import org.openmrs.api.context.Context;
import org.openmrs.api.impl.BaseOpenmrsService;
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.queue.api.DuplicateQueueEntryException;
import org.openmrs.module.queue.api.QueueEntryService;
import org.openmrs.module.queue.api.dao.QueueEntryDao;
import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria;
import org.openmrs.module.queue.model.Queue;
import org.openmrs.module.queue.model.QueueEntry;
import org.openmrs.module.queue.utils.QueueUtils;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
Expand All @@ -48,6 +50,8 @@ public class QueueEntryServiceImpl extends BaseOpenmrsService implements QueueEn

private VisitService visitService;

private MessageSourceService messageSourceService;

public void setDao(QueueEntryDao<QueueEntry> dao) {
this.dao = dao;
}
Expand All @@ -56,6 +60,10 @@ public void setVisitService(VisitService visitService) {
this.visitService = visitService;
}

public void setMessageSourceService(MessageSourceService messageSourceService) {
this.messageSourceService = messageSourceService;
}

/**
* @see QueueEntryService#getQueueEntryByUuid(String)
*/
Expand Down Expand Up @@ -89,12 +97,9 @@ public QueueEntry saveQueueEntry(QueueEntry queueEntry) {
searchCriteria.setQueues(Collections.singletonList(queueEntry.getQueue()));
List<QueueEntry> queueEntries = getQueueEntries(searchCriteria);
for (QueueEntry entry : queueEntries) {
if (!((queueEntry.getStartedAt().before(entry.getStartedAt())
&& (queueEntry.getEndedAt() != null && queueEntry.getEndedAt().before(entry.getStartedAt())))
|| ((entry.getEndedAt() != null) && (queueEntry.getStartedAt().after(entry.getEndedAt()))))) {
//if the new queueEntry endDate is NOT before the entry startDate OR the new queueEntry startDate is NOT after the entry endDate,
//then the new queueEntry overlaps with the existing entry
throw new DuplicateQueueEntryException("queue.entry.duplicate.patient");
if (QueueUtils.datesOverlap(entry.getStartedAt(), entry.getEndedAt(), queueEntry.getStartedAt(),
queueEntry.getEndedAt())) {
throw new DuplicateQueueEntryException(messageSourceService.getMessage("queue.entry.duplicate.patient"));
}
}
return dao.createOrUpdate(queueEntry);
Expand Down
17 changes: 17 additions & 0 deletions api/src/main/java/org/openmrs/module/queue/utils/QueueUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,21 @@ public static double computeAverageWaitTimeInMinutes(List<QueueEntry> queueEntri
}
return averageWaitTime;
}

/**
* @param date startDate1, endDate1 - the start and end date of one timeframe
* @param date startDate2, endDate2 - the start and end date of second timeframe
* @return boolean - indicating whether or not the timeframes overlap
*/
public static boolean datesOverlap(Date startDate1, Date endDate1, Date startDate2, Date endDate2) {
if (startDate1 != null && startDate2 != null) {
if (!((startDate2.before(startDate1) && (endDate2 != null && endDate2.compareTo(startDate1) <= 0))
|| ((endDate1 != null) && (startDate2.compareTo(endDate1) >= 0)))) {
//if the endDate2 is NOT before the startDate1 OR the startDate2 is NOT after the entry endDate1,
//then the time intervals overlap
return true;
}
}
return false;
}
}
2 changes: 1 addition & 1 deletion api/src/main/resources/messages_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
# graphic logo is a trademark of OpenMRS Inc.
#

queue.entry.duplicate.patient=Patient already in the queue
queue.entry.duplicate.patient=Paciente ya en la cola
2 changes: 1 addition & 1 deletion api/src/main/resources/messages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
# graphic logo is a trademark of OpenMRS Inc.
#

queue.entry.duplicate.patient=Patient already in the queue
queue.entry.duplicate.patient=Patient déjà dans la queue
2 changes: 1 addition & 1 deletion api/src/main/resources/messages_ht.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
# graphic logo is a trademark of OpenMRS Inc.
#

queue.entry.duplicate.patient=Patient already in the queue
queue.entry.duplicate.patient=Pasyan deja nan keu a
1 change: 1 addition & 0 deletions api/src/main/resources/moduleApplicationContext.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<bean class="org.openmrs.module.queue.api.impl.QueueEntryServiceImpl">
<property name="dao" ref="queueEntryDao"/>
<property name="visitService" ref="visitService"/>
<property name="messageSourceService" ref="messageSourceService"/>
</bean>
</property>
<property name="preInterceptors" ref="serviceInterceptors"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.openmrs.api.VisitService;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.UserContext;
import org.openmrs.messagesource.MessageSourceService;
import org.openmrs.module.queue.api.dao.QueueEntryDao;
import org.openmrs.module.queue.api.impl.QueueEntryServiceImpl;
import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria;
Expand All @@ -53,7 +54,6 @@ public class QueueEntryServiceTest {
private static final String QUEUE_ENTRY_UUID = "j8f0bb90-86f4-4d9c-8b6c-3713d748ef74";

private static final Integer QUEUE_ENTRY_ID = 14;
private static final Integer SECOND_QUEUE_ENTRY_ID = 15;

private QueueEntryServiceImpl queueEntryService;

Expand All @@ -63,6 +63,9 @@ public class QueueEntryServiceTest {
@Mock
private VisitService visitService;

@Mock
private MessageSourceService messageSourceService;

@Captor
ArgumentCaptor<QueueEntrySearchCriteria> queueEntrySearchCriteriaArgumentCaptor;

Expand All @@ -72,6 +75,7 @@ public void setupMocks() {
queueEntryService = new QueueEntryServiceImpl();
queueEntryService.setDao(dao);
queueEntryService.setVisitService(visitService);
queueEntryService.setMessageSourceService(messageSourceService);
}

@Test
Expand Down Expand Up @@ -141,17 +145,15 @@ public void shouldNotCreateDuplicateOverlappingQueueEntryRecords() {
assertThat(result.getPriority(), is(conceptPriority));
assertThat(result.getPatient(), is(patient));
assertThat(result.getStartedAt(), is(queueStartDate));

//attempt to add a second queue entry for the same patient to the same queue
Date secondQueueStartDate = new Date();
QueueEntry secondQueueEntry = mock(QueueEntry.class);
when(secondQueueEntry.getQueueEntryId()).thenReturn(SECOND_QUEUE_ENTRY_ID);
when(secondQueueEntry.getQueue()).thenReturn(queue);
when(secondQueueEntry.getStatus()).thenReturn(conceptStatus);
when(secondQueueEntry.getPriority()).thenReturn(conceptPriority);
when(secondQueueEntry.getPatient()).thenReturn(patient);
when(secondQueueEntry.getStartedAt()).thenReturn(secondQueueStartDate);

when(messageSourceService.getMessage("queue.entry.duplicate.patient")).thenReturn("Patient already in the queue");

QueueEntrySearchCriteria searchCriteria = new QueueEntrySearchCriteria();
searchCriteria.setPatient(secondQueueEntry.getPatient());
searchCriteria.setQueues(Collections.singletonList(queue));
Expand All @@ -165,7 +167,7 @@ public void shouldNotCreateDuplicateOverlappingQueueEntryRecords() {
fail("Expected DuplicateQueueEntryException");
}
catch (DuplicateQueueEntryException e) {
assertThat(e.getMessage(), is(Context.getMessageSourceService().getMessage("queue.entry.duplicate.patient")));
assertThat(e.getMessage(), is(messageSourceService.getMessage("queue.entry.duplicate.patient")));
}

}
Expand Down
113 changes: 113 additions & 0 deletions api/src/test/java/org/openmrs/module/queue/utils/QueueUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.module.queue.utils;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import java.util.Calendar;
import java.util.Date;

import org.junit.Test;

public class QueueUtilsTest {

@Test
public void shouldReturnFalseWhenDatesAreNull() {
assertThat(QueueUtils.datesOverlap(null, null, null, null), is(false));
}

@Test
public void shouldReturnFalseWhenSecondTimeIntervalIsBeforeFirstTimeInterval() {
// time interval startDate2-endDate2(t-12hr, t-10hr) is to the left of time interval startDate1-endDate1
Calendar calendar = Calendar.getInstance();
Date startDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, -12);
Date startDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, 2);
Date endDate2 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, null, startDate2, endDate2), is(false));
}

@Test
public void shouldReturnFalseWhenSecondTimeIntervalEndsWhenFirstTimeIntervalStarts() {
// time interval startDate2-endDate2(t-12hr, t) ends when time interval startDate1-endDate1(t) starts
Calendar calendar = Calendar.getInstance();
Date startDate1 = calendar.getTime();
Date endDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, -12);
Date startDate2 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, null, startDate2, endDate2), is(false));
}

@Test
public void shouldReturnTrueWhenDatesOverlap() {
Calendar calendar = Calendar.getInstance();
Date startDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, 1);
Date startDate2 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, null, startDate2, null), is(true));
}

@Test
public void shouldReturnTrueWhenDateIntervalsAreBoundedAndInclusive() {
// Dt1 = t, t+12
// Dt2 = t+1,t+8
Calendar calendar = Calendar.getInstance();
Date startDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, 1);
Date startDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, 7);
Date endDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, 5);
Date endDate1 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, endDate1, startDate2, endDate2), is(true));
}

@Test
public void shouldReturnTrueWhenDateIntervalsAreBoundedAndOverlap() {
// DT1 = t, t+4
// DT2 = t+1, t+8
Calendar calendar = Calendar.getInstance();
Date startDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, 1);
Date startDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, 3);
Date endDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, 4);
Date endDate2 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, endDate1, startDate2, endDate2), is(true));
}

@Test
public void shouldReturnFalseWhenSecondTimeIntervalStartsAfterFirstTimeIntervalEndsl() {
// time interval startDate2-endDate2(t) is to the right of time interval startDate1-endDate1(t-12hr, t-10hr)
Calendar calendar = Calendar.getInstance();
Date startDate2 = calendar.getTime();
calendar.add(Calendar.HOUR, -10);
Date endDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, -2);
Date startDate1 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, endDate1, startDate2, null), is(false));
}

@Test
public void shouldReturnFalseWhenSecondTimeIntervalStartsWhenFirstTimeIntervalEndsl() {
// time interval startDate2(t) starts when first time interval startDate1-endDate1(t-12hr, t) ends
// DT1 = t-12, t
// DT2 = t
Calendar calendar = Calendar.getInstance();
Date startDate2 = calendar.getTime();
Date endDate1 = calendar.getTime();
calendar.add(Calendar.HOUR, -12);
Date startDate1 = calendar.getTime();
assertThat(QueueUtils.datesOverlap(startDate1, endDate1, startDate2, null), is(false));
}
}

0 comments on commit 7454f58

Please sign in to comment.