diff --git a/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java b/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java index e5e9e9792..6e56ac48a 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/EncounterService.java @@ -154,6 +154,7 @@ public void updateEncounterWithVisitSchedule(Encounter encounter, VisitSchedule encounter.setEarliestVisitDateTime(visitSchedule.getEarliestDate()); encounter.setMaxVisitDateTime(visitSchedule.getMaxDate()); encounter.setName(visitSchedule.getName()); + encounter.setLastModifiedBy(userService.getCurrentUser()); } public Encounter createEmptyEncounter(Individual individual, EncounterType encounterType) { @@ -164,6 +165,9 @@ public Encounter createEmptyEncounter(Individual individual, EncounterType encou encounter.setVoided(false); encounter.setObservations(new ObservationCollection()); encounter.setCancelObservations(new ObservationCollection()); + encounter.setFilledBy(userService.getCurrentUser()); + encounter.setCreatedBy(userService.getCurrentUser()); + encounter.setLastModifiedBy(userService.getCurrentUser()); return encounter; } diff --git a/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java b/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java index 05bd5375b..c74344d4b 100644 --- a/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java +++ b/avni-server-api/src/main/java/org/avni/server/service/ProgramEncounterService.java @@ -125,6 +125,7 @@ public void updateProgramEncounterWithVisitSchedule(ProgramEncounter programEnco programEncounter.setEarliestVisitDateTime(visitSchedule.getEarliestDate()); programEncounter.setMaxVisitDateTime(visitSchedule.getMaxDate()); programEncounter.setName(visitSchedule.getName()); + programEncounter.setLastModifiedBy(userService.getCurrentUser()); } public ProgramEncounter createEmptyProgramEncounter(ProgramEnrolment programEnrolment, OperationalEncounterType operationalEncounterType) { @@ -135,7 +136,11 @@ public ProgramEncounter createEmptyProgramEncounter(ProgramEnrolment programEnro programEncounter.setVoided(false); programEncounter.setObservations(new ObservationCollection()); programEncounter.setCancelObservations(new ObservationCollection()); - return programEncounter; + final User currentUser = userService.getCurrentUser(); + programEncounter.setFilledBy(null); + programEncounter.setCreatedBy(currentUser); + programEncounter.setLastModifiedBy(currentUser); + return programEncounter; } @Messageable(EntityType.ProgramEncounter) @@ -169,6 +174,11 @@ public ProgramEncounter saveProgramEncounter(ProgramEncounterRequest request) th encounter.setMaxVisitDateTime(request.getMaxVisitDateTime()); encounter.setCancelDateTime(request.getCancelDateTime()); encounter.setCancelObservations(observationService.createObservations(request.getCancelObservations())); + final User currentUser = userService.getCurrentUser(); ++ if (encounter.getCreatedBy() == null) { ++ encounter.setCreatedBy(currentUser); ++ } ++ encounter.setLastModifiedBy(currentUser); PointRequest encounterLocation = request.getEncounterLocation(); if (encounterLocation != null) @@ -227,6 +237,11 @@ public ProgramEncounter save(ProgramEncounter programEncounter) { Individual individual = programEnrolment.getIndividual(); programEncounter.addConceptSyncAttributeValues(individual.getSubjectType(), individual.getObservations()); programEncounter.setIndividual(individual); + final User currentUser = userService.getCurrentUser(); + if (programEncounter.getCreatedBy() == null) { + programEncounter.setCreatedBy(currentUser); + } + programEncounter.setLastModifiedBy(currentUser); if (individual.getAddressLevel() != null) { programEncounter.setAddressId(individual.getAddressLevel().getId()); } diff --git a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java index 6ec3d7a99..c6b28d6a2 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/EncounterController.java @@ -170,6 +170,9 @@ private Encounter createEncounter(EncounterRequest request) throws ValidationExc encounter.setCancelDateTime(request.getCancelDateTime()); encounter.setCancelObservations(observationService.createObservations(request.getCancelObservations())); encounter.setVoided(request.isVoided()); + encounter.setCreatedBy(userService.getCurrentUser()); + encounter.setLastModifiedBy(userService.getCurrentUser()); + encounter.setFilledBy(userService.getCurrentUser()); PointRequest encounterLocation = request.getEncounterLocation(); if (encounterLocation != null) encounter.setEncounterLocation(new Point(encounterLocation.getX(), encounterLocation.getY())); @@ -280,6 +283,8 @@ public EntityModel process(EntityModel resource) { resource.add(Link.of(encounter.getIndividual().getUuid(), "individualUUID")); addAuditFields(encounter, resource); addUserFields(encounter.getFilledBy(), resource, "filledBy"); + addUserFields(encounter.getCreatedBy(), resource, "createdBy"); + addUserFields(encounter.getLastModifiedBy(), resource, "lastModifiedBy"); return resource; } } diff --git a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java index be42d8eae..319d0bf63 100644 --- a/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java +++ b/avni-server-api/src/main/java/org/avni/server/web/ProgramEncounterController.java @@ -184,6 +184,8 @@ public EntityModel process(EntityModel resou resource.add(Link.of(programEncounter.getProgramEnrolment().getUuid(), "programEnrolmentUUID")); addAuditFields(programEncounter, resource); addUserFields(programEncounter.getFilledBy(), resource, "filledBy"); + addUserFields(programEncounter.getLastModifiedBy(), resource, "lastModifiedBy"); + addUserFields(programEncounter.getCreatedBy(), resource, "createdBy"); return resource; } } diff --git a/avni-server-api/src/main/resources/db/migration/V1_353__AddAuditFields.sql b/avni-server-api/src/main/resources/db/migration/V1_353__AddAuditFields.sql new file mode 100644 index 000000000..feabb507f --- /dev/null +++ b/avni-server-api/src/main/resources/db/migration/V1_353__AddAuditFields.sql @@ -0,0 +1,29 @@ +-- Add audit fields to existing tables +ALTER TABLE encounter + ADD COLUMN IF NOT EXISTS created_by_id bigint REFERENCES users(id), + ADD COLUMN IF NOT EXISTS last_modified_by_id bigint REFERENCES users(id), + ADD COLUMN IF NOT EXISTS filled_by_id bigint REFERENCES users(id); + +ALTER TABLE program_encounter + ADD COLUMN IF NOT EXISTS created_by_id bigint REFERENCES users(id), + ADD COLUMN IF NOT EXISTS last_modified_by_id bigint REFERENCES users(id), + ADD COLUMN IF NOT EXISTS filled_by_id bigint REFERENCES users(id); + +-- Create policies for the new audit fields +CREATE POLICY encounter_created_by_id_rls_policy ON encounter + USING (created_by_id = current_user_id()) + WITH CHECK (created_by_id = current_user_id()); +CREATE POLICY encounter_last_modified_by_id_rls_policy ON encounter + USING (last_modified_by_id = current_user_id()) + WITH CHECK (last_modified_by_id = current_user_id()); +CREATE POLICY encounter_filled_by_id_rls_policy ON encounter + USING (filled_by_id = current_user_id()) + WITH CHECK (filled_by_id = current_user_id()); + +CREATE INDEX IF NOT EXISTS encounter_created_by_id_idx ON encounter(created_by_id); +CREATE INDEX IF NOT EXISTS encounter_last_modified_by_id_idx ON encounter(last_modified_by_id); +CREATE INDEX IF NOT EXISTS encounter_filled_by_id_idx ON encounter(filled_by_id); + +CREATE INDEX IF NOT EXISTS program_encounter_created_by_id_idx ON program_encounter(created_by_id); +CREATE INDEX IF NOT EXISTS program_encounter_last_modified_by_id_idx ON program_encounter(last_modified_by_id); +CREATE INDEX IF NOT EXISTS program_encounter_filled_by_id_idx ON program_encounter(filled_by_id); \ No newline at end of file diff --git a/avni-server-api/src/test/java/org/avni/server/service/ProgramEncounterServiceTest.java b/avni-server-api/src/test/java/org/avni/server/service/ProgramEncounterServiceTest.java new file mode 100644 index 000000000..84c8b3111 --- /dev/null +++ b/avni-server-api/src/test/java/org/avni/server/service/ProgramEncounterServiceTest.java @@ -0,0 +1,52 @@ +package org.avni.server.service; + +import org.avni.server.dao.ProgramEncounterRepository; +import org.avni.server.domain.ProgramEncounter; +import org.avni.server.domain.User; +import org.avni.server.web.request.ProgramEncounterRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ProgramEncounterServiceTest { + + @Mock + private ProgramEncounterRepository programEncounterRepository; + + @Mock + private UserService userService; + + @InjectMocks + private ProgramEncounterService programEncounterService; + + @Test + public void shouldSetAuditFieldsWhenSavingProgramEncounter() { + // Arrange + User currentUser = new User(); + currentUser.setId(1L); + currentUser.setUsername("testUser"); + + when(userService.getCurrentUser()).thenReturn(currentUser); + when(programEncounterRepository.saveEntity(any())).thenAnswer(i -> i.getArguments()[0]); + + ProgramEncounter encounter = new ProgramEncounter(); + + // Act + ProgramEncounter savedEncounter = programEncounterService.save(encounter); + + // Assert + assertNotNull(savedEncounter.getCreatedBy()); + assertNotNull(savedEncounter.getLastModifiedBy()); + assertNotNull(savedEncounter.getFilledBy()); + assertEquals(currentUser, savedEncounter.getCreatedBy()); + assertEquals(currentUser, savedEncounter.getLastModifiedBy()); + assertEquals(currentUser, savedEncounter.getFilledBy()); + } +} \ No newline at end of file diff --git a/avni-server-api/src/test/java/org/avni/server/web/ProgramEncounterControllerIntegrationTest.java b/avni-server-api/src/test/java/org/avni/server/web/ProgramEncounterControllerIntegrationTest.java index ce8eb4f78..c890d05d0 100644 --- a/avni-server-api/src/test/java/org/avni/server/web/ProgramEncounterControllerIntegrationTest.java +++ b/avni-server-api/src/test/java/org/avni/server/web/ProgramEncounterControllerIntegrationTest.java @@ -41,4 +41,4 @@ public void createNewEncounter() { Assert.fail(); } } -} +} \ No newline at end of file diff --git a/avni-server-data/src/main/java/org/avni/server/domain/AbstractEncounter.java b/avni-server-data/src/main/java/org/avni/server/domain/AbstractEncounter.java index 59248e854..114aedc4b 100644 --- a/avni-server-data/src/main/java/org/avni/server/domain/AbstractEncounter.java +++ b/avni-server-data/src/main/java/org/avni/server/domain/AbstractEncounter.java @@ -65,6 +65,13 @@ public class AbstractEncounter extends SyncAttributeEntity { @ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY) private User filledBy; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by_id") + private User createdBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "last_modified_by_id") + private User lastModifiedBy; @Column(updatable = false) private boolean syncDisabled; @@ -211,7 +218,25 @@ public void setFilledBy(User filledBy) { this.filledBy = filledBy; } - public boolean isSyncDisabled() { + @JsonIgnore + public User getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(User createdBy) { + this.createdBy = createdBy; + } + + @JsonIgnore + public User getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(User lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public boolean isSyncDisabled() { return syncDisabled; }