Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public void validate(Reporter reporter, TrackerBundle bundle, Event event) {
}
validateExpiryPeriodType(reporter, event, program, bundle.getUser());
validateCompletedDateIsSetOnlyForSupportedStatus(reporter, event);
validateCompletionExpiryDays(reporter, event, program, bundle.getUser());
validateCompletionExpiryDays(reporter, preheat, event, program, bundle.getUser());
}

private void validateCompletedDateIsSetOnlyForSupportedStatus(Reporter reporter, Event event) {
Expand All @@ -87,19 +87,58 @@ private void validateCompletedDateIsSetOnlyForSupportedStatus(Reporter reporter,
}

private void validateCompletionExpiryDays(
Reporter reporter, Event event, Program program, UserDetails user) {
if (event.getCompletedAt() == null || user.isAuthorized(Authorities.F_EDIT_EXPIRED.name())) {
Reporter reporter, TrackerPreheat preheat, Event event, Program program, UserDetails user) {
if (program.getCompleteEventsExpiryDays() == 0
|| user.isAuthorized(Authorities.F_EDIT_EXPIRED.name())) {
return;
}

if (program.getCompleteEventsExpiryDays() > 0
&& EventStatus.COMPLETED == event.getStatus()
&& now()
.isAfter(event.getCompletedAt().plus(ofDays(program.getCompleteEventsExpiryDays())))) {
Instant completedAt = getCompletedDate(preheat, event);

if (completedAt != null
&& now().isAfter(completedAt.plus(ofDays(program.getCompleteEventsExpiryDays())))) {
reporter.addError(event, E1043, event);
}
}

/**
* Returns the completion date the expiry check should be anchored to, or {@code null} if the
* event is not completed.
*
* <p>When the event is already completed in the database, the persisted completion date is used.
* This makes the expiry check work even when the update payload does not include {@code
* completedAt}, and prevents the payload from resetting the expiry clock on an already completed
* event. Otherwise, when the payload itself completes the event, its completion date is used.
*/
private Instant getCompletedDate(TrackerPreheat preheat, Event event) {
Date persistedCompletedDate = getPersistedCompletedDate(preheat, event);
if (persistedCompletedDate != null) {
return persistedCompletedDate.toInstant();
}

if (EventStatus.COMPLETED == event.getStatus()) {
return event.getCompletedAt();
}

return null;
}

private Date getPersistedCompletedDate(TrackerPreheat preheat, Event event) {
if (event instanceof TrackerEvent) {
org.hisp.dhis.tracker.model.TrackerEvent persisted =
preheat.getTrackerEvent(event.getEvent());
return persisted != null && EventStatus.COMPLETED == persisted.getStatus()
? persisted.getCompletedDate()
: null;
} else if (event instanceof SingleEvent) {
org.hisp.dhis.tracker.model.SingleEvent persisted = preheat.getSingleEvent(event.getEvent());
return persisted != null && EventStatus.COMPLETED == persisted.getStatus()
? persisted.getCompletedDate()
: null;
}
return null;
}

private void validateExpiryPeriodType(
Reporter reporter, Event event, Program program, UserDetails user) {
PeriodType periodType = program.getExpiryPeriodType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.stream.Stream;
import org.hisp.dhis.common.UID;
import org.hisp.dhis.event.EventStatus;
Expand Down Expand Up @@ -196,6 +197,118 @@ void shouldFailWhenTrackerEventCompletedAtIsTooSoonAndEventIsCompleted() {
assertHasError(reporter, event, E1043);
}

@Test
void shouldFailWhenTrackerEventCompletionHasExpiredEvenIfPayloadHasNoCompletedAt() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
when(preheat.getTrackerEvent(uid)).thenReturn(completedTrackerEvent(sevenDaysAgo()));
Event event =
TrackerEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
.status(EventStatus.COMPLETED)
.build();

validator.validate(reporter, bundle, event);

assertHasError(reporter, event, E1043);
}

@Test
void shouldFailWhenTrackerEventCompletionHasExpiredEvenIfPayloadHasNoStatusNorCompletedAt() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
when(preheat.getTrackerEvent(uid)).thenReturn(completedTrackerEvent(sevenDaysAgo()));
Event event =
TrackerEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
.build();

validator.validate(reporter, bundle, event);

assertHasError(reporter, event, E1043);
}

@Test
void shouldFailWhenSingleEventCompletionHasExpiredEvenIfPayloadHasNoCompletedAt() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
when(preheat.getSingleEvent(uid)).thenReturn(completedSingleEvent(sevenDaysAgo()));
Event event =
SingleEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
.status(EventStatus.COMPLETED)
.build();

validator.validate(reporter, bundle, event);

assertHasError(reporter, event, E1043);
}

@Test
void shouldUsePersistedCompletionDateAndIgnorePayloadCompletedAtWhenAlreadyCompleted() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
// event was completed in the database long ago (expired)...
when(preheat.getTrackerEvent(uid)).thenReturn(completedTrackerEvent(sevenDaysAgo()));
Event event =
TrackerEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
// ...so a fresh completedAt in the payload must not reset the expiry clock
.completedAt(now())
.status(EventStatus.COMPLETED)
.build();

validator.validate(reporter, bundle, event);

assertHasError(reporter, event, E1043);
}

@Test
void shouldPassWhenTrackerEventPersistedCompletionIsWithinExpiryDays() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
when(preheat.getTrackerEvent(uid)).thenReturn(completedTrackerEvent(twoDaysAgo()));
Event event =
TrackerEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
.status(EventStatus.COMPLETED)
.build();

validator.validate(reporter, bundle, event);

assertIsEmpty(reporter.getErrors());
}

@Test
void shouldPassWhenPersistedCompletionHasExpiredButUserIsAuthorized() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
UID uid = UID.generate();
UserDetails user = mock(UserDetails.class);
when(user.isAuthorized(Authorities.F_EDIT_EXPIRED.name())).thenReturn(true);
bundle.setUser(user);
Event event =
TrackerEvent.builder()
.event(uid)
.program(MetadataIdentifier.ofUid(PROGRAM_ID))
.occurredAt(now())
.status(EventStatus.COMPLETED)
.build();

validator.validate(reporter, bundle, event);

assertIsEmpty(reporter.getErrors());
}

@Test
void shouldFailWhenTrackerEventOccurredAtAndScheduledAtAreNotPresent() {
when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_ID))).thenReturn(getProgram(5));
Expand Down Expand Up @@ -292,6 +405,22 @@ private static Stream<Arguments> getEvents() {
return Stream.of(Arguments.of(trackerEvent), Arguments.of(singleEvent));
}

private static org.hisp.dhis.tracker.model.TrackerEvent completedTrackerEvent(
Instant completedDate) {
org.hisp.dhis.tracker.model.TrackerEvent event = new org.hisp.dhis.tracker.model.TrackerEvent();
event.setStatus(EventStatus.COMPLETED);
event.setCompletedDate(Date.from(completedDate));
return event;
}

private static org.hisp.dhis.tracker.model.SingleEvent completedSingleEvent(
Instant completedDate) {
org.hisp.dhis.tracker.model.SingleEvent event = new org.hisp.dhis.tracker.model.SingleEvent();
event.setStatus(EventStatus.COMPLETED);
event.setCompletedDate(Date.from(completedDate));
return event;
}

private Program getProgram(int expiryDays) {
Program program = createProgram('A');
program.setUid(PROGRAM_ID);
Expand All @@ -309,6 +438,10 @@ private static Instant sevenDaysAgo() {
return LocalDateTime.now().minusDays(7).toInstant(ZoneOffset.UTC);
}

private static Instant twoDaysAgo() {
return LocalDateTime.now().minusDays(2).toInstant(ZoneOffset.UTC);
}

private Instant sevenDaysLater() {
return LocalDateTime.now().plusDays(7).toInstant(ZoneOffset.UTC);
}
Expand Down
Loading