diff --git a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java index b0acb34..491336f 100644 --- a/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/queue/api/impl/QueueEntryServiceImpl.java @@ -116,7 +116,12 @@ public QueueEntry transitionQueueEntry(QueueEntryTransition queueEntryTransition if (currentState.getEndedAt() != null) { throw new IllegalStateException("Cannot transition a queue entry that has already ended"); } - + // Validate transition date is not in the future (with 1-minute tolerance for clock drift) + Date maxAllowedDate = new Date(System.currentTimeMillis() + 60000L); + if (queueEntryTransition.getTransitionDate() != null + && queueEntryTransition.getTransitionDate().after(maxAllowedDate)) { + throw new APIException("Transition date cannot be in the future"); + } // Capture the dateChanged for optimistic locking Date expectedDateChanged = currentState.getDateChanged(); diff --git a/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java b/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java index 28b4d6f..ae7a614 100644 --- a/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java +++ b/api/src/test/java/org/openmrs/module/queue/api/QueueEntryServiceTest.java @@ -40,6 +40,7 @@ import org.openmrs.User; import org.openmrs.Visit; import org.openmrs.VisitAttributeType; +import org.openmrs.api.APIException; import org.openmrs.api.VisitService; import org.openmrs.api.context.Context; import org.openmrs.api.context.UserContext; @@ -470,4 +471,47 @@ public void shouldGenerateVisitQueueNumber() { assertThat(queueNumber, notNullValue()); assertThat(queueNumber, equalTo("CON-053")); } + + @Test(expected = APIException.class) + public void shouldThrowWhenTransitionDateIsInTheFuture() { + QueueEntry queueEntry = new QueueEntry(); + queueEntry.setQueueEntryId(1); + when(dao.get(1)).thenReturn(Optional.of(queueEntry)); + + QueueEntryTransition transition = new QueueEntryTransition(); + transition.setQueueEntryToTransition(queueEntry); + // Set transition date 2 minutes in the future — well beyond the 1-minute tolerance + transition.setTransitionDate(DateUtils.addMinutes(new Date(), 2)); + + queueEntryService.transitionQueueEntry(transition); + } + + @Test + public void shouldAllowTransitionDateWithinOneMinuteClockDriftTolerance() { + QueueEntry queueEntry = new QueueEntry(); + queueEntry.setQueueEntryId(1); + queueEntry.setQueue(new Queue()); + queueEntry.setPatient(new Patient()); + queueEntry.setStatus(new Concept()); + queueEntry.setPriority(new Concept()); + queueEntry.setStartedAt(DateUtils.addHours(new Date(), -1)); + when(dao.get(1)).thenReturn(Optional.of(queueEntry)); + when(dao.updateIfUnmodified(any(), any())).thenReturn(true); + when(dao.createOrUpdate(any())).thenAnswer(invocation -> { + QueueEntry entry = invocation.getArgument(0); + if (entry.getId() == null) { + entry.setQueueEntryId(2); + } + return entry; + }); + + QueueEntryTransition transition = new QueueEntryTransition(); + transition.setQueueEntryToTransition(queueEntry); + // Set transition date 30 seconds in future — within the 1-minute tolerance + transition.setTransitionDate(DateUtils.addMinutes(new Date(), 1)); + + // Should NOT throw — 30 seconds is within the allowed clock drift window + QueueEntry result = queueEntryService.transitionQueueEntry(transition); + assertThat(result, notNullValue()); + } }