diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/tracker/imports/validation/ValidationCode.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/tracker/imports/validation/ValidationCode.java index 7e1270969f1a..4f8dd0e49343 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/tracker/imports/validation/ValidationCode.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/tracker/imports/validation/ValidationCode.java @@ -58,9 +58,8 @@ public enum ValidationCode { E1020("Enrollment date: `{0}`, cannot be a future date."), E1021("Incident date: `{0}`, cannot be a future date."), E1022("TrackedEntity: `{0}`, must have same TrackedEntityType as Program `{1}`."), - E1023( - "DisplayIncidentDate is true but property occurredAt is null or has an invalid format: `{0}`."), - E1025("Property enrolledAt is null or has an invalid format: `{0}`."), + E1023("DisplayIncidentDate is true but property occurredAt is null."), + E1025("Property enrolledAt is null."), E1029("Event OrganisationUnit: `{0}`, and Program: `{1}`, don't match."), E1030("Event: `{0}`, already exists."), E1031("Event occurredAt date is missing."), @@ -69,7 +68,6 @@ public enum ValidationCode { E1035("Event: `{0}`, ProgramStage value is null."), E1039("ProgramStage: `{0}`, is not repeatable and an event already exists."), E1041("Enrollment OrganisationUnit: `{0}`, and Program: `{1}`, don't match."), - E1042("Event: `{0}`, needs to have completed date."), E1043("Event: `{0}`, completeness date has expired. Not possible to make changes to this event."), E1044("Event: `{0}`, needs to have event date."), E1045( @@ -80,6 +78,8 @@ public enum ValidationCode { E1048("Object: `{0}`, uid: `{1}`, has an invalid uid format."), E1049("Could not find OrganisationUnit: `{0}`, linked to Tracked Entity."), E1050("Event ScheduledAt date is missing."), + E1051("Event: `{0}`, completedAt must be null when status is `{1}`"), + E1052("Enrollment: `{0}`, completedAt must be null when status is `{1}`"), E1054("AttributeOptionCombo `{0}` is not in the event programs category combo `{1}`."), E1055("Default AttributeOptionCombo is not allowed since program has non-default CategoryCombo."), E1056("Event date: `{0}`, is before start date: `{1}`, for AttributeOption: `{2}`."), diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java index f15f756a7a0d..b446ed92da7e 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/bundle/TrackerObjectsMapper.java @@ -141,17 +141,21 @@ private TrackerObjectsMapper() { if (enrollment.getStatus() != dbEnrollment.getStatus()) { dbEnrollment.setStatus(enrollment.getStatus()); - switch (dbEnrollment.getStatus()) { + Date completedDate = + enrollment.getCompletedAt() == null + ? now + : DateUtils.fromInstant(enrollment.getCompletedAt()); + switch (enrollment.getStatus()) { case ACTIVE -> { dbEnrollment.setCompletedDate(null); dbEnrollment.setCompletedBy(null); } case COMPLETED -> { - dbEnrollment.setCompletedDate(now); + dbEnrollment.setCompletedDate(completedDate); dbEnrollment.setCompletedBy(user.getUsername()); } case CANCELLED -> { - dbEnrollment.setCompletedDate(now); + dbEnrollment.setCompletedDate(completedDate); dbEnrollment.setCompletedBy(null); } } @@ -208,7 +212,8 @@ private TrackerObjectsMapper() { EventStatus currentStatus = event.getStatus(); EventStatus previousStatus = dbEvent.getStatus(); if (currentStatus != previousStatus && currentStatus == EventStatus.COMPLETED) { - dbEvent.setCompletedDate(now); + dbEvent.setCompletedDate( + event.getCompletedAt() == null ? now : DateUtils.fromInstant(event.getCompletedAt())); dbEvent.setCompletedBy(user.getUsername()); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Enrollment.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Enrollment.java index 9fc015c798d8..8af77cdcec24 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Enrollment.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Enrollment.java @@ -70,6 +70,8 @@ public class Enrollment implements TrackerDto, Serializable { @JsonProperty private String storedBy; + @JsonProperty private Instant completedAt; + @JsonProperty private Geometry geometry; @JsonProperty @Builder.Default private List attributes = new ArrayList<>(); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Event.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Event.java index d5232846f0cc..ca526d080edf 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Event.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/domain/Event.java @@ -78,8 +78,6 @@ public class Event implements TrackerDto, Serializable { @JsonProperty @Builder.Default private Set attributeCategoryOptions = new HashSet<>(); - @JsonProperty private String completedBy; - @JsonProperty private Instant completedAt; @JsonProperty private Geometry geometry; diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java index 3018ef48020e..f541434cdc55 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java @@ -27,10 +27,13 @@ */ package org.hisp.dhis.tracker.imports.validation.validator.enrollment; +import static org.hisp.dhis.program.EnrollmentStatus.CANCELLED; +import static org.hisp.dhis.program.EnrollmentStatus.COMPLETED; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1020; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1021; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1023; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1025; +import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1052; import java.time.LocalDate; import java.time.ZoneOffset; @@ -55,13 +58,24 @@ public void validate(Reporter reporter, TrackerBundle bundle, Enrollment enrollm if (Boolean.TRUE.equals(program.getDisplayIncidentDate()) && Objects.isNull(enrollment.getOccurredAt())) { - reporter.addError(enrollment, E1023, enrollment.getOccurredAt()); + reporter.addError(enrollment, E1023); + } + + validateCompletedDateIsSetOnlyForSupportedStatus(reporter, enrollment); + } + + private void validateCompletedDateIsSetOnlyForSupportedStatus( + Reporter reporter, Enrollment enrollment) { + if (enrollment.getCompletedAt() != null + && enrollment.getStatus() != COMPLETED + && enrollment.getStatus() != CANCELLED) { + reporter.addError(enrollment, E1052, enrollment, enrollment.getStatus()); } } private void validateMandatoryDates(Reporter reporter, Enrollment enrollment) { if (Objects.isNull(enrollment.getEnrolledAt())) { - reporter.addError(enrollment, E1025, enrollment.getEnrolledAt()); + reporter.addError(enrollment, E1025); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidator.java index 38756479b321..935ca01f35fc 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidator.java @@ -30,11 +30,11 @@ import static java.time.Duration.ofDays; import static java.time.Instant.now; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1031; -import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1042; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1043; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1046; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1047; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1050; +import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1051; import java.time.Instant; import java.util.Date; @@ -70,25 +70,28 @@ public void validate(Reporter reporter, TrackerBundle bundle, Event event) { return; } + validateCompletedDateIsSetOnlyForSupportedStatus(reporter, event); validateExpiryDays(reporter, event, program, bundle.getUser()); validatePeriodType(reporter, event, program); } + private void validateCompletedDateIsSetOnlyForSupportedStatus(Reporter reporter, Event event) { + if (event.getCompletedAt() != null && EventStatus.COMPLETED != event.getStatus()) { + reporter.addError(event, E1051, event, event.getStatus()); + } + } + private void validateExpiryDays( Reporter reporter, Event event, Program program, UserDetails user) { - if (user.isAuthorized(Authorities.F_EDIT_EXPIRED.name())) { + if (event.getCompletedAt() == null || user.isAuthorized(Authorities.F_EDIT_EXPIRED.name())) { return; } - if ((program.getCompleteEventsExpiryDays() > 0 && EventStatus.COMPLETED == event.getStatus())) { - if (event.getCompletedAt() == null) { - reporter.addError(event, E1042, event); - } else { - if (now() + if (program.getCompleteEventsExpiryDays() > 0 + && EventStatus.COMPLETED == event.getStatus() + && now() .isAfter(event.getCompletedAt().plus(ofDays(program.getCompleteEventsExpiryDays())))) { - reporter.addError(event, E1043, event); - } - } + reporter.addError(event, E1043, event); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java index 3b89470f8558..b85f1a618769 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java @@ -27,18 +27,22 @@ */ package org.hisp.dhis.tracker.imports.validation.validator.enrollment; +import static java.time.Instant.now; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1020; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1021; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1023; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1025; +import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1052; import static org.hisp.dhis.tracker.imports.validation.validator.AssertValidations.assertHasError; +import static org.hisp.dhis.tracker.imports.validation.validator.AssertValidations.assertHasNoError; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.Mockito.when; import java.time.Duration; import java.time.Instant; import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.program.EnrollmentStatus; import org.hisp.dhis.program.Program; import org.hisp.dhis.tracker.imports.TrackerIdSchemeParams; import org.hisp.dhis.tracker.imports.bundle.TrackerBundle; @@ -49,6 +53,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -82,7 +88,7 @@ void testMandatoryDatesMustBePresent() { Enrollment.builder() .enrollment(CodeGenerator.generateUid()) .program(MetadataIdentifier.ofUid(CodeGenerator.generateUid())) - .occurredAt(Instant.now()) + .occurredAt(now()) .build(); when(preheat.getProgram(enrollment.getProgram())).thenReturn(new Program()); @@ -94,7 +100,7 @@ void testMandatoryDatesMustBePresent() { @Test void testDatesMustNotBeInTheFuture() { - final Instant dateInTheFuture = Instant.now().plus(Duration.ofDays(2)); + final Instant dateInTheFuture = now().plus(Duration.ofDays(2)); Enrollment enrollment = Enrollment.builder() .enrollment(CodeGenerator.generateUid()) @@ -114,7 +120,7 @@ void testDatesMustNotBeInTheFuture() { @Test void testDatesShouldBeAllowedOnSameDayIfFutureDatesAreNotAllowed() { - final Instant today = Instant.now().plus(Duration.ofMinutes(1)); + final Instant today = now().plus(Duration.ofMinutes(1)); Enrollment enrollment = Enrollment.builder() .enrollment(CodeGenerator.generateUid()) @@ -132,7 +138,7 @@ void testDatesShouldBeAllowedOnSameDayIfFutureDatesAreNotAllowed() { @Test void testDatesCanBeInTheFuture() { - final Instant dateInTheFuture = Instant.now().plus(Duration.ofDays(2)); + final Instant dateInTheFuture = now().plus(Duration.ofDays(2)); Enrollment enrollment = Enrollment.builder() .enrollment(CodeGenerator.generateUid()) @@ -157,7 +163,7 @@ void testFailOnMissingOccurredAtDate() { Enrollment.builder() .enrollment(CodeGenerator.generateUid()) .program(MetadataIdentifier.ofUid(CodeGenerator.generateUid())) - .enrolledAt(Instant.now()) + .enrolledAt(now()) .build(); Program program = new Program(); @@ -168,4 +174,39 @@ void testFailOnMissingOccurredAtDate() { assertHasError(reporter, enrollment, E1023); } + + @Test + void shouldFailWhenCompletedAtIsPresentAndStatusIsNotCompleted() { + Enrollment enrollment = new Enrollment(); + enrollment.setEnrollment(CodeGenerator.generateUid()); + enrollment.setProgram(MetadataIdentifier.ofUid(CodeGenerator.generateUid())); + enrollment.setOccurredAt(now()); + enrollment.setCompletedAt(now()); + enrollment.setStatus(EnrollmentStatus.ACTIVE); + + when(preheat.getProgram(enrollment.getProgram())).thenReturn(new Program()); + + validator.validate(reporter, bundle, enrollment); + + assertHasError(reporter, enrollment, E1052); + } + + @ParameterizedTest + @EnumSource( + value = EnrollmentStatus.class, + names = {"COMPLETED", "CANCELLED"}) + void shouldValidateWhenCompletedAtIsPresentAndStatusAcceptCompletedAt(EnrollmentStatus status) { + Enrollment enrollment = new Enrollment(); + enrollment.setEnrollment(CodeGenerator.generateUid()); + enrollment.setProgram(MetadataIdentifier.ofUid(CodeGenerator.generateUid())); + enrollment.setOccurredAt(now()); + enrollment.setCompletedAt(now()); + enrollment.setStatus(status); + + when(preheat.getProgram(enrollment.getProgram())).thenReturn(new Program()); + + validator.validate(reporter, bundle, enrollment); + + assertHasNoError(reporter, enrollment, E1052); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidatorTest.java index 9445f3203162..52eca791b667 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DateValidatorTest.java @@ -29,11 +29,11 @@ import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1031; -import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1042; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1043; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1046; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1047; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1050; +import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1051; import static org.hisp.dhis.tracker.imports.validation.validator.AssertValidations.assertHasError; import static org.mockito.Mockito.when; @@ -174,7 +174,7 @@ void testEventIsNotValidWhenScheduledDateIsNotPresentAndEventIsSchedule() { } @Test - void testEventIsNotValidWhenCompletedAtIsNotPresentAndEventIsCompleted() { + void shouldFailWhenCompletedAtIsPresentAndStatusIsNotCompleted() { // given when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_WITH_REGISTRATION_ID))) .thenReturn(getProgramWithRegistration()); @@ -182,13 +182,12 @@ void testEventIsNotValidWhenCompletedAtIsNotPresentAndEventIsCompleted() { event.setEvent(CodeGenerator.generateUid()); event.setProgram(MetadataIdentifier.ofUid(PROGRAM_WITH_REGISTRATION_ID)); event.setOccurredAt(now()); - event.setStatus(EventStatus.COMPLETED); + event.setCompletedAt(now()); + event.setStatus(EventStatus.ACTIVE); - // when validator.validate(reporter, bundle, event); - // then - assertHasError(reporter, event, E1042); + assertHasError(reporter, event, E1051); } @Test diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/bundle/TrackerNotificationHandlerServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/bundle/TrackerNotificationHandlerServiceTest.java index 13b10de7c07c..aa46c5c8564f 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/bundle/TrackerNotificationHandlerServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/bundle/TrackerNotificationHandlerServiceTest.java @@ -210,7 +210,6 @@ void shouldSendTrackerNotificationAtEnrollmentCompletionAndThenEventCompletion() .programStage(MetadataIdentifier.ofUid(programStageA.getUid())) .status(EventStatus.ACTIVE) .attributeOptionCombo(MetadataIdentifier.EMPTY_UID) - .completedAt(Instant.now()) .occurredAt(Instant.now()) .build();