diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml
index b68a211e7850..4754e022f602 100644
--- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml
+++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/note/hibernate/Note.hbm.xml
@@ -11,7 +11,13 @@
- &identifiableProperties;
+
+
+
+
+
+
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java
index 33e787ce3228..09c6e1c9a542 100644
--- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java
+++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java
@@ -132,7 +132,6 @@ class JdbcEventStore {
n.created as note_created,\
n.creator as note_creator,\
n.uid as note_uid,\
- n.lastupdated as note_lastupdated,\
userinfo.userinfoid as note_user_id,\
userinfo.code as note_user_code,\
userinfo.uid as note_user_uid,\
@@ -467,8 +466,6 @@ private List fetchEvents(EventQueryParams queryParams, PageParams pagePar
note.setLastUpdatedBy(noteLastUpdatedBy);
}
- note.setLastUpdated(resultSet.getTimestamp("note_lastupdated"));
-
event.getNotes().add(note);
notes.add(resultSet.getString("note_id"));
}
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 36eb83558601..56848247f2f6 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
@@ -33,6 +33,7 @@
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.hisp.dhis.category.CategoryOptionCombo;
import org.hisp.dhis.common.UID;
import org.hisp.dhis.event.EventStatus;
@@ -302,13 +303,18 @@ private TrackerObjectsMapper() {
@Nonnull TrackerPreheat preheat,
@Nonnull org.hisp.dhis.tracker.imports.domain.Note note,
@Nonnull UserDetails user) {
+
+ return map(note, preheat.getUserByUid(user.getUid()).orElse(null));
+ }
+
+ public static @Nonnull Note map(
+ @Nonnull org.hisp.dhis.tracker.imports.domain.Note note, @Nullable User user) {
Date now = new Date();
Note dbNote = new Note();
dbNote.setUid(note.getNote().getValue());
dbNote.setCreated(now);
- dbNote.setLastUpdated(now);
- dbNote.setLastUpdatedBy(preheat.getUserByUid(user.getUid()).orElse(null));
+ dbNote.setLastUpdatedBy(user);
dbNote.setCreator(note.getStoredBy());
dbNote.setNoteText(note.getValue());
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java
new file mode 100644
index 000000000000..8d6dac0d820e
--- /dev/null
+++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/DefaultNoteService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2004-2024, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.hisp.dhis.tracker.imports.note;
+
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+
+import lombok.RequiredArgsConstructor;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.feedback.BadRequestException;
+import org.hisp.dhis.feedback.ForbiddenException;
+import org.hisp.dhis.feedback.NotFoundException;
+import org.hisp.dhis.tracker.export.enrollment.EnrollmentService;
+import org.hisp.dhis.tracker.export.event.EventService;
+import org.hisp.dhis.tracker.imports.domain.Note;
+import org.hisp.dhis.user.CurrentUserUtil;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class DefaultNoteService implements NoteService {
+ private final EnrollmentService enrollmentService;
+
+ private final EventService eventService;
+
+ private final JdbcNoteStore noteStore;
+
+ @Transactional
+ public void addNoteForEnrollment(Note note, UID enrollment)
+ throws ForbiddenException, NotFoundException, BadRequestException {
+ // Check enrollment existence and access
+ enrollmentService.getEnrollment(enrollment);
+ validateNote(note);
+
+ noteStore.saveEnrollmentNote(enrollment, note, CurrentUserUtil.getCurrentUserDetails());
+ }
+
+ @Transactional
+ public void addNoteForEvent(Note note, UID event)
+ throws ForbiddenException, NotFoundException, BadRequestException {
+ // Check event existence and access
+ eventService.getEvent(event);
+ validateNote(note);
+
+ noteStore.saveEventNote(event, note, CurrentUserUtil.getCurrentUserDetails());
+ }
+
+ private void validateNote(Note note) throws BadRequestException {
+ if (isEmpty(note.getValue())) {
+ throw new BadRequestException("Value cannot be empty");
+ }
+
+ if (noteStore.exists(note.getNote())) {
+ throw new BadRequestException(String.format("Note `%s` already exists.", note.getNote()));
+ }
+ }
+}
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java
new file mode 100644
index 000000000000..41e198a25c29
--- /dev/null
+++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/JdbcNoteStore.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2004-2024, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.hisp.dhis.tracker.imports.note;
+
+import java.util.Date;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import lombok.RequiredArgsConstructor;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.tracker.imports.bundle.persister.PersistenceException;
+import org.hisp.dhis.tracker.imports.domain.Note;
+import org.hisp.dhis.user.UserDetails;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class JdbcNoteStore {
+ private final NamedParameterJdbcTemplate jdbcTemplate;
+
+ public void saveEnrollmentNote(
+ @Nonnull UID enrollment, @Nonnull Note note, @Nonnull UserDetails user) {
+ long noteId = saveNote(note, user);
+ String sql =
+ """
+ INSERT INTO enrollment_notes(enrollmentid, noteid, sort_order)
+ VALUES ((select enrollmentid from enrollment where uid = :enrollment), :noteId, coalesce((select max(sort_order) + 1 from enrollment_notes where enrollmentid = (select enrollmentid from enrollment where uid = :enrollment)),1))
+ """;
+ jdbcTemplate.update(sql, Map.of("enrollment", enrollment.getValue(), "noteId", noteId));
+ }
+
+ public void saveEventNote(@Nonnull UID event, @Nonnull Note note, @Nonnull UserDetails user) {
+ long noteId = saveNote(note, user);
+ String sql =
+ """
+ INSERT INTO event_notes(eventid, noteid, sort_order)
+ VALUES ((select eventid from event where uid = :event), :noteId, coalesce((select max(sort_order) + 1 from event_notes where eventid = (select eventid from event where uid = :event)),1))
+ """;
+ jdbcTemplate.update(sql, Map.of("event", event.getValue(), "noteId", noteId));
+ }
+
+ boolean exists(@Nonnull UID note) {
+ Integer count =
+ jdbcTemplate.queryForObject(
+ "select count(1) from note where uid = :uid",
+ Map.of("uid", note.getValue()),
+ Integer.class);
+ return count == null || count > 0;
+ }
+
+ private long saveNote(@Nonnull Note note, @Nonnull UserDetails user) {
+ String sql =
+ "INSERT INTO public.note(noteid, notetext, creator, lastupdatedby, uid, created) "
+ + "VALUES (nextVal('note_id_sequence'), :text, :creator, (select userinfoid from userinfo where uid = :lastUpdatedBy), :uid, :created) RETURNING noteid";
+
+ MapSqlParameterSource params = new MapSqlParameterSource();
+ params.addValue("text", note.getValue());
+ params.addValue("creator", note.getStoredBy());
+ params.addValue("lastUpdatedBy", user.getUid());
+ params.addValue("uid", note.getNote().getValue());
+ params.addValue("created", new Date());
+
+ Long noteId = jdbcTemplate.queryForObject(sql, params, Long.class);
+
+ if (noteId == null) {
+ throw new PersistenceException("Note could not be saved");
+ }
+
+ return noteId;
+ }
+}
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java
new file mode 100644
index 000000000000..a297b9a0f32d
--- /dev/null
+++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/note/NoteService.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2004-2024, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.hisp.dhis.tracker.imports.note;
+
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.feedback.BadRequestException;
+import org.hisp.dhis.feedback.ForbiddenException;
+import org.hisp.dhis.feedback.NotFoundException;
+import org.hisp.dhis.tracker.imports.domain.Note;
+
+public interface NoteService {
+ void addNoteForEnrollment(Note note, UID enrollment)
+ throws ForbiddenException, NotFoundException, BadRequestException;
+
+ void addNoteForEvent(Note note, UID event)
+ throws ForbiddenException, NotFoundException, BadRequestException;
+}
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java
index 0cf9b1067088..5ad3e87297df 100644
--- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java
+++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/SupplementaryDataProvider.java
@@ -29,8 +29,10 @@
import com.google.common.collect.Maps;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -58,7 +60,7 @@ public class SupplementaryDataProvider {
public Map> getSupplementaryData(
List programRules, UserDetails user) {
- List orgUnitGroups = new ArrayList<>();
+ Set orgUnitGroups = new HashSet<>();
for (ProgramRule programRule : programRules) {
Matcher matcher = PATTERN.matcher(StringUtils.defaultIfBlank(programRule.getCondition(), ""));
diff --git a/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql
new file mode 100644
index 000000000000..4b89b4e8ccf2
--- /dev/null
+++ b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_34__Create_sequence_for_note_table.sql
@@ -0,0 +1,5 @@
+create sequence if not exists note_id_sequence;
+select setval('note_id_sequence', coalesce((select max(noteid) from note), 1)) FROM note;
+
+alter table if exists note drop column code;
+alter table if exists note drop column lastupdated;
\ No newline at end of file
diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java
new file mode 100644
index 000000000000..07b703927e8f
--- /dev/null
+++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/note/NoteServiceTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2004-2024, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.hisp.dhis.tracker.imports.note;
+
+import static org.hisp.dhis.tracker.Assertions.assertNoErrors;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.util.List;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.feedback.BadRequestException;
+import org.hisp.dhis.feedback.ForbiddenException;
+import org.hisp.dhis.feedback.NotFoundException;
+import org.hisp.dhis.program.Enrollment;
+import org.hisp.dhis.program.Event;
+import org.hisp.dhis.tracker.TrackerTest;
+import org.hisp.dhis.tracker.export.enrollment.EnrollmentService;
+import org.hisp.dhis.tracker.export.event.EventService;
+import org.hisp.dhis.tracker.imports.TrackerImportParams;
+import org.hisp.dhis.tracker.imports.TrackerImportService;
+import org.hisp.dhis.tracker.imports.domain.Note;
+import org.hisp.dhis.user.User;
+import org.hisp.dhis.user.UserDetails;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+class NoteServiceTest extends TrackerTest {
+ @Autowired private TrackerImportService trackerImportService;
+
+ @Autowired private EventService eventService;
+
+ @Autowired private EnrollmentService enrollmentService;
+
+ @Autowired private NoteService noteService;
+
+ private UserDetails userDetails;
+
+ @BeforeAll
+ void setUp() throws IOException {
+ setUpMetadata("tracker/simple_metadata.json");
+
+ User importUser = userService.getUser("tTgjgobT1oS");
+ userDetails = UserDetails.fromUser(importUser);
+ injectSecurityContext(userDetails);
+
+ TrackerImportParams params = TrackerImportParams.builder().build();
+ assertNoErrors(
+ trackerImportService.importTracker(params, fromJson("tracker/event_and_enrollment.json")));
+ }
+
+ @BeforeEach
+ void initUser() {
+ injectSecurityContext(userDetails);
+ }
+
+ @Test
+ void shouldCreateEnrollmentNote()
+ throws ForbiddenException, NotFoundException, BadRequestException {
+ Note note = note();
+ noteService.addNoteForEnrollment(note, UID.of("nxP7UnKhomJ"));
+
+ manager.clear();
+ manager.flush();
+
+ Enrollment dbEnrollment = enrollmentService.getEnrollment(UID.of("nxP7UnKhomJ"));
+ assertNotes(List.of(note), dbEnrollment.getNotes(), userDetails);
+ }
+
+ @Test
+ void shouldFailToCreateEnrollmentNoteWhenNoteValueIsNull() {
+ Note note = note();
+ note.setValue(null);
+
+ assertThrows(
+ BadRequestException.class,
+ () -> noteService.addNoteForEnrollment(note, UID.of("nxP7UnKhomJ")));
+ }
+
+ @Test
+ void shouldFailToCreateEnrollmentNoteWhenEnrollmentIsNotPresent() {
+ Note note = note();
+ assertThrows(
+ NotFoundException.class,
+ () -> noteService.addNoteForEnrollment(note, UID.of("jPP9AnKh34U")));
+ }
+
+ @Test
+ void shouldFailToCreateDuplicateEnrollmentNote()
+ throws ForbiddenException, NotFoundException, BadRequestException {
+ Note note = note();
+ noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ"));
+ assertThrows(
+ BadRequestException.class,
+ () -> noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ")));
+ }
+
+ @Test
+ void shouldFailToCreateEnrollmentNoteIfUserHasNoAccessToEnrollment() {
+ User importUser = userService.getUser("nIidJVYpQQK");
+ injectSecurityContext(UserDetails.fromUser(importUser));
+
+ Note note = note();
+
+ assertThrows(
+ ForbiddenException.class,
+ () -> noteService.addNoteForEnrollment(note, UID.of("nxP8UnKhomJ")));
+ }
+
+ @Test
+ void shouldCreateEventNote() throws ForbiddenException, NotFoundException, BadRequestException {
+ Note note = note();
+ noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72"));
+
+ manager.clear();
+ manager.flush();
+
+ Event dbEvent = eventService.getEvent(UID.of("pTzf9KYMk72"));
+ assertNotes(List.of(note), dbEvent.getNotes(), userDetails);
+ }
+
+ @Test
+ void shouldFailToCreateEventNoteWhenNoteValueIsNull() {
+ Note note = note();
+ note.setValue(null);
+
+ assertThrows(
+ BadRequestException.class, () -> noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72")));
+ }
+
+ @Test
+ void shouldFailToCreateEventNoteWhenEnrollmentIsNotPresent() {
+ Note note = note();
+ assertThrows(
+ NotFoundException.class, () -> noteService.addNoteForEvent(note, UID.of("jPP9AnKh34U")));
+ }
+
+ @Test
+ void shouldFailToCreateDuplicateEventNote()
+ throws ForbiddenException, NotFoundException, BadRequestException {
+ Note note = note();
+ noteService.addNoteForEvent(note, UID.of("D9PbzJY8bJM"));
+ assertThrows(
+ BadRequestException.class, () -> noteService.addNoteForEvent(note, UID.of("D9PbzJY8bJM")));
+ }
+
+ @Test
+ void shouldFailToCreateEventNoteIfUserHasNoAccessToEvent() {
+ User importUser = userService.getUser("nIidJVYpQQK");
+ injectSecurityContext(UserDetails.fromUser(importUser));
+
+ Note note = note();
+
+ assertThrows(
+ NotFoundException.class, () -> noteService.addNoteForEvent(note, UID.of("pTzf9KYMk72")));
+ }
+
+ private void assertNotes(
+ List notes, List dbNotes, UserDetails updatedBy) {
+ for (org.hisp.dhis.tracker.imports.domain.Note note : notes) {
+ org.hisp.dhis.note.Note dbNote =
+ dbNotes.stream()
+ .filter(n -> n.getUid().equals(note.getNote().getValue()))
+ .findFirst()
+ .orElse(null);
+ assertNotNull(dbNote);
+ assertEquals(note.getValue(), dbNote.getNoteText());
+ assertEquals(note.getStoredBy(), dbNote.getCreator());
+ assertEquals(updatedBy.getUid(), dbNote.getLastUpdatedBy().getUid());
+ }
+ }
+
+ private Note note() {
+ return Note.builder()
+ .note(UID.generate())
+ .storedBy("This is the creator")
+ .value("This is a note")
+ .build();
+ }
+}
diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java
index 5394088d2d09..d46d77161021 100644
--- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java
+++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java
@@ -336,7 +336,6 @@ void testValidateAndAddNotesToEvent() throws IOException {
Note note = getByNote(event.getNotes(), t);
assertTrue(CodeGenerator.isValidUid(note.getUid()));
assertTrue(note.getCreated().getTime() > now.getTime());
- assertTrue(note.getLastUpdated().getTime() > now.getTime());
assertNull(note.getCreator());
assertEquals(importUser.getUid(), note.getLastUpdatedBy().getUid());
});
@@ -360,7 +359,6 @@ void testValidateAndAddNotesToUpdatedEvent() throws IOException {
Note note = getByNote(event.getNotes(), t);
assertTrue(CodeGenerator.isValidUid(note.getUid()));
assertTrue(note.getCreated().getTime() > now.getTime());
- assertTrue(note.getLastUpdated().getTime() > now.getTime());
assertNull(note.getCreator());
assertEquals(importUser.getUid(), note.getLastUpdatedBy().getUid());
});
diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java
new file mode 100644
index 000000000000..3ce3693bb9cf
--- /dev/null
+++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportNoteControllerTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2004-2022, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.hisp.dhis.webapi.controller.tracker.imports;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Date;
+import java.util.Set;
+import org.hisp.dhis.category.CategoryOptionCombo;
+import org.hisp.dhis.common.CodeGenerator;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.http.HttpStatus;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.program.Enrollment;
+import org.hisp.dhis.program.EnrollmentStatus;
+import org.hisp.dhis.program.Event;
+import org.hisp.dhis.program.Program;
+import org.hisp.dhis.program.ProgramStage;
+import org.hisp.dhis.security.acl.AccessStringHelper;
+import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase;
+import org.hisp.dhis.test.webapi.json.domain.JsonWebMessage;
+import org.hisp.dhis.trackedentity.TrackedEntity;
+import org.hisp.dhis.trackedentity.TrackedEntityType;
+import org.hisp.dhis.user.User;
+import org.hisp.dhis.user.sharing.UserAccess;
+import org.hisp.dhis.webapi.controller.tracker.JsonNote;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class TrackerImportNoteControllerTest extends PostgresControllerIntegrationTestBase {
+ private User importUser;
+
+ private Event event;
+
+ private Enrollment enrollment;
+
+ @BeforeAll
+ void setUp() {
+ importUser = makeUser("o");
+ manager.save(importUser, false);
+
+ CategoryOptionCombo coc = categoryService.getDefaultCategoryOptionCombo();
+
+ OrganisationUnit orgUnit = createOrganisationUnit('A');
+ manager.save(orgUnit);
+
+ importUser.addOrganisationUnit(orgUnit);
+ manager.update(importUser);
+
+ Program program = createProgram('A');
+ program.getSharing().addUserAccess(new UserAccess(importUser, AccessStringHelper.DATA_READ));
+ manager.save(program, false);
+
+ TrackedEntityType trackedEntityType = createTrackedEntityType('A');
+ manager.save(trackedEntityType);
+
+ ProgramStage programStage = createProgramStage('A', program);
+ programStage
+ .getSharing()
+ .addUserAccess(new UserAccess(importUser, AccessStringHelper.DATA_READ));
+ manager.save(programStage, false);
+
+ TrackedEntity te = createTrackedEntity(orgUnit);
+ te.setTrackedEntityType(trackedEntityType);
+ manager.save(te);
+
+ enrollment = enrollment(te, program, orgUnit);
+ event = event(enrollment, programStage, coc);
+ enrollment.setEvents(Set.of(event));
+ manager.update(enrollment);
+ }
+
+ @BeforeEach
+ void injectUser() {
+ injectSecurityContextUser(importUser);
+ }
+
+ @Test
+ void shouldReturnBadRequestWhenValueIsNullForEventNote() {
+ JsonWebMessage webMessage =
+ POST(
+ "/tracker/events/" + event.getUid() + "/note",
+ """
+
+ {
+ "creator": "I am the creator"
+ }
+ """)
+ .content(HttpStatus.BAD_REQUEST)
+ .as(JsonWebMessage.class);
+
+ assertEquals("Value cannot be empty", webMessage.getMessage());
+ }
+
+ @Test
+ void shouldCreateEventNote() {
+ JsonNote note =
+ POST(
+ "/tracker/events/" + event.getUid() + "/note",
+ """
+
+ {
+ "value": "This is a note"
+ }
+ """)
+ .content(HttpStatus.OK)
+ .as(JsonNote.class);
+
+ assertEquals("This is a note", note.getValue());
+ assertTrue(CodeGenerator.isValidUid(note.getNote()));
+ }
+
+ @Test
+ void shouldCreateEventNoteWhenNoteUidIsProvided() {
+ UID noteUid = UID.generate();
+ JsonNote note =
+ POST(
+ "/tracker/events/" + event.getUid() + "/note",
+ """
+
+ {
+ "note": "%s",
+ "value": "This is a note"
+ }
+ """
+ .formatted(noteUid.getValue()))
+ .content(HttpStatus.OK)
+ .as(JsonNote.class);
+
+ assertEquals("This is a note", note.getValue());
+ assertEquals(noteUid.getValue(), note.getNote());
+ }
+
+ @Test
+ void shouldReturnBadRequestWhenValueIsNullForEnrollmentNote() {
+ JsonWebMessage webMessage =
+ POST(
+ "/tracker/enrollments/" + enrollment.getUid() + "/note",
+ """
+
+ {
+ "creator": "I am the creator"
+ }
+ """)
+ .content(HttpStatus.BAD_REQUEST)
+ .as(JsonWebMessage.class);
+
+ assertEquals("Value cannot be empty", webMessage.getMessage());
+ }
+
+ @Test
+ void shouldCreateEnrollmentNote() {
+ JsonNote note =
+ POST(
+ "/tracker/enrollments/" + enrollment.getUid() + "/note",
+ """
+
+ {
+ "value": "This is a note"
+ }
+ """)
+ .content(HttpStatus.OK)
+ .as(JsonNote.class);
+
+ assertEquals("This is a note", note.getValue());
+ assertTrue(CodeGenerator.isValidUid(note.getNote()));
+ }
+
+ @Test
+ void shouldCreateEnrollmentNoteWhenNoteUidIsProvided() {
+ UID noteUid = UID.generate();
+ JsonNote note =
+ POST(
+ "/tracker/enrollments/" + enrollment.getUid() + "/note",
+ """
+
+ {
+ "note": "%s",
+ "value": "This is a note"
+ }
+ """
+ .formatted(noteUid.getValue()))
+ .content(HttpStatus.OK)
+ .as(JsonNote.class);
+
+ assertEquals("This is a note", note.getValue());
+ assertEquals(noteUid.getValue(), note.getNote());
+ }
+
+ private Event event(Enrollment enrollment, ProgramStage programStage, CategoryOptionCombo coc) {
+ Event eventA = new Event(enrollment, programStage, enrollment.getOrganisationUnit(), coc);
+ eventA.setAutoFields();
+ manager.save(eventA);
+ return eventA;
+ }
+
+ private Enrollment enrollment(TrackedEntity te, Program program, OrganisationUnit orgUnit) {
+ Enrollment enrollmentA = new Enrollment(program, te, orgUnit);
+ enrollmentA.setAutoFields();
+ enrollmentA.setEnrollmentDate(new Date());
+ enrollmentA.setOccurredDate(new Date());
+ enrollmentA.setStatus(EnrollmentStatus.COMPLETED);
+ enrollmentA.setFollowup(true);
+ manager.save(enrollmentA, false);
+ te.setEnrollments(Set.of(enrollmentA));
+ manager.save(te, false);
+ return enrollmentA;
+ }
+}
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java
index 058de6068cc4..e4dac5a1d9df 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/NoteMapper.java
@@ -30,12 +30,12 @@
import org.hisp.dhis.tracker.TrackerIdSchemeParams;
import org.hisp.dhis.webapi.controller.tracker.view.InstantMapper;
import org.hisp.dhis.webapi.controller.tracker.view.Note;
-import org.mapstruct.Context;
import org.mapstruct.Mapper;
@Mapper(uses = {InstantMapper.class, UserMapper.class})
public interface NoteMapper extends DomainMapper {
- org.hisp.dhis.tracker.imports.domain.Note from(
- org.hisp.dhis.tracker.imports.domain.Note note,
- @Context TrackerIdSchemeParams idSchemeParams);
+
+ default org.hisp.dhis.tracker.imports.domain.Note from(Note note) {
+ return from(note, TrackerIdSchemeParams.builder().build());
+ }
}
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java
index 58a76f04a23a..bc02eaef0a50 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportController.java
@@ -43,9 +43,12 @@
import lombok.RequiredArgsConstructor;
import org.hisp.dhis.common.DhisApiVersion;
import org.hisp.dhis.common.OpenApi;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.commons.util.StreamUtils;
import org.hisp.dhis.dxf2.webmessage.WebMessage;
+import org.hisp.dhis.feedback.BadRequestException;
import org.hisp.dhis.feedback.ConflictException;
+import org.hisp.dhis.feedback.ForbiddenException;
import org.hisp.dhis.feedback.NotFoundException;
import org.hisp.dhis.scheduling.JobConfiguration;
import org.hisp.dhis.scheduling.JobConfigurationService;
@@ -58,16 +61,20 @@
import org.hisp.dhis.tracker.imports.TrackerImportParams;
import org.hisp.dhis.tracker.imports.TrackerImportService;
import org.hisp.dhis.tracker.imports.domain.TrackerObjects;
+import org.hisp.dhis.tracker.imports.note.NoteService;
import org.hisp.dhis.tracker.imports.report.ImportReport;
import org.hisp.dhis.tracker.imports.report.Status;
import org.hisp.dhis.user.CurrentUser;
import org.hisp.dhis.user.UserDetails;
import org.hisp.dhis.webapi.controller.tracker.export.CsvService;
import org.hisp.dhis.webapi.controller.tracker.view.Event;
+import org.hisp.dhis.webapi.controller.tracker.view.Note;
import org.hisp.dhis.webapi.mvc.annotation.ApiVersion;
import org.hisp.dhis.webapi.utils.ContextUtils;
import org.locationtech.jts.io.ParseException;
+import org.mapstruct.factory.Mappers;
import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.GetMapping;
@@ -105,6 +112,10 @@ public class TrackerImportController {
private final ObjectMapper jsonMapper;
+ private final NoteService noteService;
+
+ private final NoteMapper noteMapper = Mappers.getMapper(NoteMapper.class);
+
@PostMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
@ResponseBody
public WebMessage asyncPostJsonTracker(
@@ -254,4 +265,22 @@ public ImportReport getJobReport(
.map(report -> trackerImportService.buildImportReport((ImportReport) report, reportMode))
.orElseThrow(() -> new NotFoundException("Summary for job " + uid + " does not exist"));
}
+
+ @PostMapping(value = "/enrollments/{uid}/note", consumes = APPLICATION_JSON_VALUE)
+ public ResponseEntity addNoteToEnrollment(@RequestBody Note note, @PathVariable UID uid)
+ throws ForbiddenException, NotFoundException, BadRequestException {
+
+ noteService.addNoteForEnrollment(noteMapper.from(note), uid);
+
+ return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(note);
+ }
+
+ @PostMapping(value = "/events/{uid}/note", consumes = APPLICATION_JSON_VALUE)
+ public ResponseEntity addNoteToEvent(@RequestBody Note note, @PathVariable UID uid)
+ throws ForbiddenException, NotFoundException, BadRequestException {
+
+ noteService.addNoteForEvent(noteMapper.from(note), uid);
+
+ return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(note);
+ }
}
diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java
index fed94b0ba405..0b00f6a5ca3e 100644
--- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/view/Note.java
@@ -51,7 +51,8 @@
public class Note {
@OpenApi.Property({UID.class, org.hisp.dhis.note.Note.class})
@JsonProperty
- private UID note;
+ @Builder.Default
+ private UID note = UID.generate();
@JsonProperty private Instant storedAt;
diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java
index a89f4ec1ad81..6d77650330d4 100644
--- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java
+++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java
@@ -59,6 +59,7 @@
import org.hisp.dhis.system.notification.Notification;
import org.hisp.dhis.system.notification.Notifier;
import org.hisp.dhis.tracker.imports.DefaultTrackerImportService;
+import org.hisp.dhis.tracker.imports.note.NoteService;
import org.hisp.dhis.tracker.imports.report.ImportReport;
import org.hisp.dhis.tracker.imports.report.PersistenceReport;
import org.hisp.dhis.tracker.imports.report.Status;
@@ -100,6 +101,8 @@ class TrackerImportControllerTest {
@Mock private UserService userService;
+ @Mock private NoteService noteService;
+
private RenderService renderService;
@BeforeEach
@@ -120,7 +123,8 @@ public void setUp() {
notifier,
jobSchedulerService,
jobConfigurationService,
- new ObjectMapper());
+ new ObjectMapper(),
+ noteService);
mockMvc =
MockMvcBuilders.standaloneSetup(controller)