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 25e8b3f9a29a..7e1270969f1a 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 @@ -63,7 +63,7 @@ public enum ValidationCode { E1025("Property enrolledAt is null or has an invalid format: `{0}`."), E1029("Event OrganisationUnit: `{0}`, and Program: `{1}`, don't match."), E1030("Event: `{0}`, already exists."), - E1031("Event OccurredAt date is missing."), + E1031("Event occurredAt date is missing."), E1032("Event: `{0}`, do not exist."), E1033("Event: `{0}`, Enrollment value is null."), E1035("Event: `{0}`, ProgramStage value is null."), @@ -160,7 +160,7 @@ public enum ValidationCode { E1314("Generated by program rule (`{0}`) - DataElement `{1}` is mandatory and cannot be deleted"), E1315( "Status `{0}` does not allow defining data values. Statuses that do allow defining data values are: {1}"), - E1316("No event can transition from status `{0}` to status `{1}.`"), + E1316("No event can transition from status `{0}` to status `{1}`."), E1317("Generated by program rule (`{0}`) - Attribute `{1}` is mandatory and cannot be deleted"), /* Relationship */ diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java index bc94f1abbd06..3905b1459c0e 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java @@ -136,8 +136,8 @@ protected List saveEvent( User user, List values, SmsEventStatus eventStatus, - Date eventDate, - Date dueDate, + Date occurredDate, + Date scheduledDate, GeoPoint coordinates) { ArrayList errorUids = new ArrayList<>(); @@ -163,8 +163,8 @@ protected List saveEvent( event.setOrganisationUnit(orgUnit); event.setProgramStage(programStage); event.setEnrollment(enrollment); - event.setOccurredDate(eventDate); - event.setScheduledDate(dueDate); + event.setOccurredDate(occurredDate); + event.setScheduledDate(scheduledDate); event.setAttributeOptionCombo(aoc); event.setStoredBy(user.getUsername()); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java index 368261c2359d..2ee84e905b27 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java @@ -27,48 +27,53 @@ */ package org.hisp.dhis.tracker.imports.sms; +import java.time.Instant; import java.util.List; -import org.hisp.dhis.category.CategoryOptionCombo; +import java.util.Set; +import java.util.stream.Collectors; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.collection.CollectionUtils; import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; -import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fileresource.FileResourceService; +import org.hisp.dhis.event.EventStatus; import org.hisp.dhis.message.MessageSender; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; -import org.hisp.dhis.program.Enrollment; import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.ProgramStageService; import org.hisp.dhis.sms.incoming.IncomingSms; import org.hisp.dhis.sms.incoming.IncomingSmsService; +import org.hisp.dhis.sms.listener.CompressionSMSListener; import org.hisp.dhis.sms.listener.SMSProcessingException; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; import org.hisp.dhis.smscompression.SmsConsts.SubmissionType; import org.hisp.dhis.smscompression.SmsResponse; +import org.hisp.dhis.smscompression.models.GeoPoint; +import org.hisp.dhis.smscompression.models.SmsDataValue; import org.hisp.dhis.smscompression.models.SmsSubmission; import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; -import org.hisp.dhis.smscompression.models.Uid; import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; -import org.hisp.dhis.tracker.export.event.EventChangeLogService; -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.TrackerImportStrategy; +import org.hisp.dhis.tracker.imports.domain.DataValue; +import org.hisp.dhis.tracker.imports.domain.Event.EventBuilder; +import org.hisp.dhis.tracker.imports.domain.MetadataIdentifier; +import org.hisp.dhis.tracker.imports.domain.TrackerObjects; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Status; import org.hisp.dhis.user.User; -import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserService; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component("org.hisp.dhis.tracker.sms.TrackerEventSMSListener") @Transactional -public class TrackerEventSMSListener extends EventSavingSMSListener { - private final ProgramStageService programStageService; - - private final EnrollmentService enrollmentService; +public class TrackerEventSMSListener extends CompressionSMSListener { + private final TrackerImportService trackerImportService; public TrackerEventSMSListener( IncomingSmsService incomingSmsService, @@ -81,12 +86,7 @@ public TrackerEventSMSListener( CategoryService categoryService, DataElementService dataElementService, IdentifiableObjectManager identifiableObjectManager, - EventService eventService, - EventChangeLogService eventChangeLogService, - FileResourceService fileResourceService, - DhisConfigurationProvider config, - ProgramStageService programStageService, - EnrollmentService enrollmentService) { + TrackerImportService trackerImportService) { super( incomingSmsService, smsSender, @@ -97,13 +97,8 @@ public TrackerEventSMSListener( organisationUnitService, categoryService, dataElementService, - identifiableObjectManager, - eventService, - eventChangeLogService, - fileResourceService, - config); - this.programStageService = programStageService; - this.enrollmentService = enrollmentService; + identifiableObjectManager); + this.trackerImportService = trackerImportService; } @Override @@ -111,52 +106,77 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use throws SMSProcessingException { TrackerEventSmsSubmission subm = (TrackerEventSmsSubmission) submission; - Uid ouid = subm.getOrgUnit(); - Uid stageid = subm.getProgramStage(); - Uid enrolmentid = subm.getEnrollment(); - Uid aocid = subm.getAttributeOptionCombo(); - - OrganisationUnit orgUnit = organisationUnitService.getOrganisationUnit(ouid.getUid()); + EventBuilder event = + org.hisp.dhis.tracker.imports.domain.Event.builder() + .event(subm.getEvent() != null ? subm.getEvent().getUid() : null) + .enrollment(subm.getEnrollment().getUid()) + .orgUnit(MetadataIdentifier.ofUid(subm.getOrgUnit().getUid())) + .programStage(MetadataIdentifier.ofUid(subm.getProgramStage().getUid())) + .attributeOptionCombo(MetadataIdentifier.ofUid(subm.getAttributeOptionCombo().getUid())) + .storedBy(user.getUsername()) + .occurredAt(subm.getEventDate() != null ? subm.getEventDate().toInstant() : null) + .scheduledAt(subm.getDueDate() != null ? subm.getDueDate().toInstant() : null) + .status(map(subm.getEventStatus())) + .geometry(map(subm.getCoordinates())) + .dataValues(map(user, subm.getValues())); - Enrollment enrollment; - try { - enrollment = - enrollmentService.getEnrollment(enrolmentid.getUid(), UserDetails.fromUser(user)); - } catch (ForbiddenException | NotFoundException e) { - throw new SMSProcessingException(SmsResponse.INVALID_ENROLL.set(enrolmentid)); + if (subm.getEventStatus() == SmsEventStatus.COMPLETED) { + event.completedBy(user.getUsername()); + event.completedAt(Instant.now()); } - ProgramStage programStage = programStageService.getProgramStage(stageid.getUid()); - if (programStage == null) { - throw new SMSProcessingException(SmsResponse.INVALID_STAGE.set(stageid)); + TrackerImportParams params = + TrackerImportParams.builder() + .importStrategy(TrackerImportStrategy.CREATE_AND_UPDATE) + .userId( + user.getUid()) // SMS processing is done inside a job executed as the user that sent + // the SMS. We might want to remove the params user in favor of the currentUser set on + // the thread. + .build(); + TrackerObjects trackerObjects = TrackerObjects.builder().events(List.of(event.build())).build(); + ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); + + if (Status.OK == importReport.getStatus()) { + return SmsResponse.SUCCESS; } + // TODO(DHIS2-18003) we need to map tracker import report errors/warnings to an sms + return SmsResponse.INVALID_EVENT.set(subm.getEvent()); + } - CategoryOptionCombo aoc = categoryService.getCategoryOptionCombo(aocid.getUid()); - if (aoc == null) { - throw new SMSProcessingException(SmsResponse.INVALID_AOC.set(aocid)); + private EventStatus map(SmsEventStatus eventStatus) { + return switch (eventStatus) { + case ACTIVE -> EventStatus.ACTIVE; + case COMPLETED -> EventStatus.COMPLETED; + case VISITED -> EventStatus.VISITED; + case SCHEDULE -> EventStatus.SCHEDULE; + case OVERDUE -> EventStatus.OVERDUE; + case SKIPPED -> EventStatus.SKIPPED; + }; + } + + private Geometry map(GeoPoint coordinates) { + if (coordinates == null) { + return null; } - List errorUIDs = - saveEvent( - subm.getEvent().getUid(), - orgUnit, - programStage, - enrollment, - aoc, - user, - subm.getValues(), - subm.getEventStatus(), - subm.getEventDate(), - subm.getDueDate(), - subm.getCoordinates()); - if (!errorUIDs.isEmpty()) { - return SmsResponse.WARN_DVERR.setList(errorUIDs); - } else if (subm.getValues() == null || subm.getValues().isEmpty()) { - // TODO: Should we save the event if there are no data values? - return SmsResponse.WARN_DVEMPTY; + return new GeometryFactory() + .createPoint(new Coordinate(coordinates.getLongitude(), coordinates.getLatitude())); + } + + private Set map(User user, List dataValues) { + if (CollectionUtils.isEmpty(dataValues)) { + return Set.of(); } - return SmsResponse.SUCCESS; + return dataValues.stream() + .map( + dv -> + DataValue.builder() + .dataElement(MetadataIdentifier.ofUid(dv.getDataElement().getUid())) + .value(dv.getValue()) + .storedBy(user.getUsername()) + .build()) + .collect(Collectors.toSet()); } @Override diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/MessageFormatter.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/MessageFormatter.java index 36fc64e60456..764291cd5d3a 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/MessageFormatter.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/MessageFormatter.java @@ -111,7 +111,7 @@ private static String formatArgument(TrackerIdSchemeParams idSchemes, Object arg if (Event.class.isAssignableFrom(type)) return ((Event) argument).getEvent(); if (TrackedEntity.class.isAssignableFrom(type)) return ((TrackedEntity) argument).getTrackedEntity(); - return ""; + return argument.toString(); } private static String getIdAndName(MetadataIdentifier identifier) { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java deleted file mode 100644 index 9fc351b684a4..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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.tracker.imports.sms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.common.collect.Sets; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import org.hisp.dhis.category.CategoryOptionCombo; -import org.hisp.dhis.category.CategoryService; -import org.hisp.dhis.common.IdentifiableObjectManager; -import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; -import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fileresource.FileResourceService; -import org.hisp.dhis.message.MessageSender; -import org.hisp.dhis.organisationunit.OrganisationUnit; -import org.hisp.dhis.organisationunit.OrganisationUnitService; -import org.hisp.dhis.outboundmessage.OutboundMessageResponse; -import org.hisp.dhis.program.Enrollment; -import org.hisp.dhis.program.Event; -import org.hisp.dhis.program.Program; -import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.ProgramStageService; -import org.hisp.dhis.sms.incoming.IncomingSms; -import org.hisp.dhis.sms.incoming.IncomingSmsService; -import org.hisp.dhis.smscompression.SmsCompressionException; -import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; -import org.hisp.dhis.smscompression.models.GeoPoint; -import org.hisp.dhis.smscompression.models.SmsDataValue; -import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; -import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; -import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; -import org.hisp.dhis.tracker.export.event.EventChangeLogService; -import org.hisp.dhis.tracker.export.event.EventService; -import org.hisp.dhis.user.User; -import org.hisp.dhis.user.UserDetails; -import org.hisp.dhis.user.UserService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@MockitoSettings(strictness = Strictness.LENIENT) -@ExtendWith(MockitoExtension.class) -class TrackerEventSMSListenerTest extends CompressionSMSListenerTest { - - @Mock private UserService userService; - - @Mock private IncomingSmsService incomingSmsService; - - @Mock private MessageSender smsSender; - - @Mock private DataElementService dataElementService; - - @Mock private TrackedEntityTypeService trackedEntityTypeService; - - @Mock private TrackedEntityAttributeService trackedEntityAttributeService; - - @Mock private ProgramService programService; - - @Mock private OrganisationUnitService organisationUnitService; - - @Mock private CategoryService categoryService; - - @Mock private EventService eventService; - - @Mock private IdentifiableObjectManager identifiableObjectManager; - - private User user; - - private final OutboundMessageResponse response = new OutboundMessageResponse(); - - private IncomingSms updatedIncomingSms; - - private String message = ""; - - @Mock private EnrollmentService enrollmentService; - - @Mock private ProgramStageService programStageService; - - @Mock private EventChangeLogService eventChangeLogService; - - @Mock private FileResourceService fileResourceService; - - @Mock private DhisConfigurationProvider config; - - TrackerEventSMSListener subject; - - private IncomingSms incomingSmsTrackerEvent; - - private IncomingSms incomingSmsTrackerEventWithNulls; - - private IncomingSms incomingSmsTrackerEventNoValues; - - private OrganisationUnit organisationUnit; - - private CategoryOptionCombo categoryOptionCombo; - - private DataElement dataElement; - - private ProgramStage programStage; - - private Enrollment enrollment; - - private Event event; - - @BeforeEach - public void initTest() throws SmsCompressionException, ForbiddenException, NotFoundException { - subject = - new TrackerEventSMSListener( - incomingSmsService, - smsSender, - userService, - trackedEntityTypeService, - trackedEntityAttributeService, - programService, - organisationUnitService, - categoryService, - dataElementService, - identifiableObjectManager, - eventService, - eventChangeLogService, - fileResourceService, - config, - programStageService, - enrollmentService); - - setUpInstances(); - - when(userService.getUser(anyString())).thenReturn(user); - when(smsSender.isConfigured()).thenReturn(true); - when(smsSender.sendMessage(any(), any(), anyString())) - .thenAnswer( - invocation -> { - message = (String) invocation.getArguments()[1]; - return response; - }); - - when(organisationUnitService.getOrganisationUnit(anyString())).thenReturn(organisationUnit); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(enrollmentService.getEnrollment(anyString(), any(UserDetails.class))) - .thenReturn(enrollment); - when(dataElementService.getDataElement(anyString())).thenReturn(dataElement); - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - - doAnswer( - invocation -> { - updatedIncomingSms = (IncomingSms) invocation.getArguments()[0]; - return updatedIncomingSms; - }) - .when(incomingSmsService) - .update(any()); - } - - @Test - void testTrackerEvent() { - subject.receive(incomingSmsTrackerEvent); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testTrackerEventRepeat() { - subject.receive(incomingSmsTrackerEvent); - subject.receive(incomingSmsTrackerEvent); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(2)).update(any()); - } - - @Test - void testTrackerEventWithNulls() { - subject.receive(incomingSmsTrackerEventWithNulls); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testTrackerEventNoValues() { - subject.receive(incomingSmsTrackerEventNoValues); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(NOVALUES_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - private void setUpInstances() throws SmsCompressionException { - organisationUnit = createOrganisationUnit('O'); - Program program = createProgram('P'); - programStage = createProgramStage('S', program); - - user = makeUser("U"); - user.setPhoneNumber(ORIGINATOR); - user.setOrganisationUnits(Sets.newHashSet(organisationUnit)); - - categoryOptionCombo = createCategoryOptionCombo('C'); - dataElement = createDataElement('D'); - - program.getOrganisationUnits().add(organisationUnit); - HashSet stages = new HashSet<>(); - stages.add(programStage); - program.setProgramStages(stages); - - enrollment = new Enrollment(); - enrollment.setAutoFields(); - enrollment.setProgram(program); - - event = new Event(); - event.setAutoFields(); - - incomingSmsTrackerEvent = createSMSFromSubmission(createTrackerEventSubmission()); - incomingSmsTrackerEventWithNulls = - createSMSFromSubmission(createTrackerEventSubmissionWithNulls()); - incomingSmsTrackerEventNoValues = - createSMSFromSubmission(createTrackerEventSubmissionNoValues()); - } - - private TrackerEventSmsSubmission createTrackerEventSubmission() { - TrackerEventSmsSubmission subm = new TrackerEventSmsSubmission(); - - subm.setUserId(user.getUid()); - subm.setOrgUnit(organisationUnit.getUid()); - subm.setProgramStage(programStage.getUid()); - subm.setAttributeOptionCombo(categoryOptionCombo.getUid()); - subm.setEnrollment(enrollment.getUid()); - subm.setEvent(event.getUid()); - subm.setEventStatus(SmsEventStatus.COMPLETED); - subm.setEventDate(new Date()); - subm.setDueDate(new Date()); - subm.setCoordinates(new GeoPoint(59.9399586f, 10.7195609f)); - ArrayList values = new ArrayList<>(); - values.add(new SmsDataValue(categoryOptionCombo.getUid(), dataElement.getUid(), "10")); - - subm.setValues(values); - subm.setSubmissionId(1); - - return subm; - } - - private TrackerEventSmsSubmission createTrackerEventSubmissionWithNulls() { - TrackerEventSmsSubmission subm = createTrackerEventSubmission(); - subm.setEventDate(null); - subm.setDueDate(null); - subm.setCoordinates(null); - - return subm; - } - - private TrackerEventSmsSubmission createTrackerEventSubmissionNoValues() { - TrackerEventSmsSubmission subm = createTrackerEventSubmission(); - subm.setValues(null); - - return subm; - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java index 27a7cdfdb613..77bb39afb9e7 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java @@ -42,7 +42,9 @@ import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.event.EventStatus; import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.program.EnrollmentStatus; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.relationship.RelationshipType; @@ -186,9 +188,18 @@ void formatArgumentsShouldTurnEventIntoArguments() { assertContainsOnly(List.of("zwccdzhk5zc"), args); } + @Test + void formatArgumentsShouldTurnEnumsIntoArguments() { + List args = + MessageFormatter.formatArguments( + idSchemes, EventStatus.COMPLETED, EnrollmentStatus.CANCELLED); + + assertContainsOnly(List.of("COMPLETED", "CANCELLED"), args); + } + @Test void formatArgumentsWithNumber() { - assertEquals(List.of(""), MessageFormatter.formatArguments(idSchemes, 2)); + assertEquals(List.of("2"), MessageFormatter.formatArguments(idSchemes, 2)); } @Test diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/tracker/imports/events/EventValidationTests.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/tracker/imports/events/EventValidationTests.java index 0129d1eeaf43..6cc39df54606 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/tracker/imports/events/EventValidationTests.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/tracker/imports/events/EventValidationTests.java @@ -123,7 +123,7 @@ public void shouldNotImportDeletedEvents() throws Exception { response.validateErrorReport().body("errorCode", hasItem("E1082")); } - @CsvSource({"ACTIVE,,OccurredAt date is missing.", "SCHEDULE,,ScheduledAt date is missing."}) + @CsvSource({"ACTIVE,,occurredAt date is missing.", "SCHEDULE,,ScheduledAt date is missing."}) @ParameterizedTest public void shouldValidateEventProperties(String status, String occurredAt, String error) { JsonObject object = diff --git a/dhis-2/dhis-test-web-api/pom.xml b/dhis-2/dhis-test-web-api/pom.xml index 2fed95eefbd5..fc656280b750 100644 --- a/dhis-2/dhis-test-web-api/pom.xml +++ b/dhis-2/dhis-test-web-api/pom.xml @@ -291,6 +291,11 @@ jsonassert test + + org.locationtech.jts + jts-core + test + com.cronutils cron-utils diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java similarity index 56% rename from dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java rename to dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java index f6a01656ce9d..b02893534189 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java @@ -32,9 +32,11 @@ import static org.hisp.dhis.test.utils.Assertions.assertStartsWith; import static org.hisp.dhis.webapi.controller.tracker.imports.SmsTestUtils.encodeSms; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -49,11 +51,16 @@ import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.UID; import org.hisp.dhis.common.ValueType; import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.event.EventStatus; +import org.hisp.dhis.eventdatavalue.EventDataValue; +import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.organisationunit.FeatureType; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.outboundmessage.OutboundMessage; import org.hisp.dhis.program.Enrollment; @@ -68,8 +75,13 @@ import org.hisp.dhis.sms.incoming.IncomingSmsService; import org.hisp.dhis.sms.incoming.SmsMessageStatus; import org.hisp.dhis.smscompression.SmsCompressionException; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; import org.hisp.dhis.smscompression.SmsResponse; import org.hisp.dhis.smscompression.models.DeleteSmsSubmission; +import org.hisp.dhis.smscompression.models.GeoPoint; +import org.hisp.dhis.smscompression.models.SmsDataValue; +import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; +import org.hisp.dhis.smscompression.models.Uid; import org.hisp.dhis.test.message.FakeMessageSender; import org.hisp.dhis.test.web.HttpStatus; import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; @@ -79,18 +91,29 @@ import org.hisp.dhis.tracker.export.event.EventService; import org.hisp.dhis.user.User; import org.hisp.dhis.user.sharing.UserAccess; +import org.hisp.dhis.util.DateUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.springframework.beans.factory.annotation.Autowired; /** - * Tests tracker SMS to delete an event implemented via {@link - * org.hisp.dhis.tracker.imports.sms.DeleteEventSMSListener}. It also tests parts of {@link - * org.hisp.dhis.webapi.controller.sms.SmsInboundController} and other SMS classes in the SMS class - * hierarchy. + * Tests tracker SMS + * + *
    + *
  • to delete an event via a {@link DeleteSmsSubmission} implemented via {@link + * org.hisp.dhis.tracker.imports.sms.DeleteEventSMSListener} + *
  • to create an event via a {@link TrackerEventSmsSubmission} implemented via {@link + * org.hisp.dhis.tracker.imports.sms.TrackerEventSMSListener} + *
+ * + * It also tests parts of {@link org.hisp.dhis.webapi.controller.sms.SmsInboundController} and other + * SMS classes in the SMS class hierarchy. */ -class TrackerDeleteEventSMSTest extends PostgresControllerIntegrationTestBase { +class TrackerEventSMSTest extends PostgresControllerIntegrationTestBase { @Autowired private IdentifiableObjectManager manager; @Autowired private CategoryService categoryService; @@ -113,7 +136,7 @@ class TrackerDeleteEventSMSTest extends PostgresControllerIntegrationTestBase { private TrackedEntityType trackedEntityType; - private Event event; + private DataElement de; @BeforeEach void setUp() { @@ -139,19 +162,18 @@ void setUp() { program.setTrackedEntityType(trackedEntityType); manager.save(program, false); - DataElement de = createDataElement('A', ValueType.TEXT, AggregationType.NONE); + de = createDataElement('A', ValueType.TEXT, AggregationType.NONE); de.getSharing().setOwner(user); manager.save(de, false); programStage = createProgramStage('A', program); + programStage.setFeatureType(FeatureType.POINT); programStage.getSharing().setOwner(user); programStage.getSharing().addUserAccess(fullAccess(user)); ProgramStageDataElement programStageDataElement = createProgramStageDataElement(programStage, de, 1, false); programStage.setProgramStageDataElements(Sets.newHashSet(programStageDataElement)); manager.save(programStage, false); - - event = event(enrollment(trackedEntity())); } @AfterEach @@ -161,6 +183,8 @@ void afterEach() { @Test void shouldDeleteEvent() throws SmsCompressionException { + Event event = event(enrollment(trackedEntity())); + DeleteSmsSubmission submission = new DeleteSmsSubmission(); int submissionId = 1; submission.setSubmissionId(submissionId); @@ -187,8 +211,6 @@ void shouldDeleteEvent() throws SmsCompressionException { .as(JsonWebMessage.class); IncomingSms sms = getSms(response); - String expectedText = submissionId + ":" + SmsResponse.SUCCESS; - OutboundMessage expectedMessage = new OutboundMessage(null, expectedText, Set.of(originator)); assertAll( () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), () -> assertTrue(sms.isParsed()), @@ -197,16 +219,21 @@ void shouldDeleteEvent() throws SmsCompressionException { () -> assertNotNull(sms.getReceivedDate()), () -> assertEquals(sms.getReceivedDate(), sms.getSentDate()), () -> assertEquals("default", sms.getGatewayId()), - () -> - assertThrows( - NotFoundException.class, () -> eventService.getEvent(UID.of(event.getUid()))), - () -> assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages())); + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + assertThrows(NotFoundException.class, () -> eventService.getEvent(UID.of(event.getUid()))); } @Test void shouldDeleteEventViaRequestParameters() throws SmsCompressionException { + Event event = event(enrollment(trackedEntity())); + DeleteSmsSubmission submission = new DeleteSmsSubmission(); - int submissionId = 1; + int submissionId = 2; submission.setSubmissionId(submissionId); submission.setUserId(user.getUid()); submission.setEvent(event.getUid()); @@ -227,8 +254,6 @@ void shouldDeleteEventViaRequestParameters() throws SmsCompressionException { .as(JsonWebMessage.class); IncomingSms sms = getSms(response); - String expectedText = submissionId + ":" + SmsResponse.SUCCESS; - OutboundMessage expectedMessage = new OutboundMessage(null, expectedText, Set.of(originator)); assertAll( () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), () -> assertTrue(sms.isParsed()), @@ -241,10 +266,13 @@ void shouldDeleteEventViaRequestParameters() throws SmsCompressionException { receivedTime, sms.getSentDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()), () -> assertEquals("Unknown", sms.getGatewayId()), - () -> - assertThrows( - NotFoundException.class, () -> eventService.getEvent(UID.of(event.getUid()))), - () -> assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages())); + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + assertThrows(NotFoundException.class, () -> eventService.getEvent(UID.of(event.getUid()))); } @Test @@ -253,7 +281,7 @@ void shouldFailDeletingNonExistingEvent() throws SmsCompressionException { assertThrows(NotFoundException.class, () -> eventService.getEvent(uid)); DeleteSmsSubmission submission = new DeleteSmsSubmission(); - int submissionId = 2; + int submissionId = 3; submission.setSubmissionId(submissionId); submission.setUserId(user.getUid()); submission.setEvent(uid.getValue()); @@ -278,14 +306,157 @@ void shouldFailDeletingNonExistingEvent() throws SmsCompressionException { .as(JsonWebMessage.class); IncomingSms sms = getSms(response); - String expectedText = submissionId + ":" + SmsResponse.INVALID_EVENT.set(uid.getValue()); - OutboundMessage expectedMessage = new OutboundMessage(null, expectedText, Set.of(originator)); assertAll( () -> assertEquals(SmsMessageStatus.FAILED, sms.getStatus()), () -> assertTrue(sms.isParsed()), () -> assertEquals(originator, sms.getOriginator()), () -> assertEquals(user, sms.getCreatedBy()), - () -> assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages())); + () -> { + String expectedText = submissionId + ":" + SmsResponse.INVALID_EVENT.set(uid.getValue()); + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + } + + @Test + void shouldCreateEvent() throws SmsCompressionException, ForbiddenException, NotFoundException { + Enrollment enrollment = enrollment(trackedEntity()); + + TrackerEventSmsSubmission submission = new TrackerEventSmsSubmission(); + int submissionId = 4; + submission.setSubmissionId(submissionId); + submission.setUserId(user.getUid()); + String eventUid = CodeGenerator.generateUid(); + submission.setEvent(eventUid); + submission.setOrgUnit(orgUnit.getUid()); + submission.setProgramStage(programStage.getUid()); + submission.setEnrollment(enrollment.getUid()); + submission.setAttributeOptionCombo(coc.getUid()); + submission.setEventStatus(SmsEventStatus.COMPLETED); + submission.setEventDate(DateUtils.getDate(2024, 9, 2, 10, 15)); + submission.setDueDate(DateUtils.getDate(2024, 9, 3, 16, 23)); + submission.setCoordinates(new GeoPoint(48.8575f, 2.3514f)); + + String text = encodeSms(submission); + String originator = user.getPhoneNumber(); + + switchContextToUser(user); + + JsonWebMessage response = + POST( + "/sms/inbound", + format( + """ + { + "text": "%s", + "originator": "%s" + } + """, + text, originator)) + .content(HttpStatus.OK) + .as(JsonWebMessage.class); + + IncomingSms sms = getSms(response); + assertAll( + () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), + () -> assertTrue(sms.isParsed()), + () -> assertEquals(originator, sms.getOriginator()), + () -> assertEquals(user, sms.getCreatedBy()), + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + assertDoesNotThrow(() -> eventService.getEvent(UID.of(eventUid))); + Event actual = eventService.getEvent(UID.of(eventUid)); + assertAll( + "created event", + () -> assertEquals(eventUid, actual.getUid()), + () -> assertEqualUids(submission.getEnrollment(), actual.getEnrollment()), + () -> assertEqualUids(submission.getOrgUnit(), actual.getOrganisationUnit()), + () -> assertEqualUids(submission.getProgramStage(), actual.getProgramStage()), + () -> + assertEqualUids(submission.getAttributeOptionCombo(), actual.getAttributeOptionCombo()), + () -> assertEquals(user.getUsername(), actual.getStoredBy()), + () -> assertEquals(submission.getEventDate(), actual.getOccurredDate()), + () -> assertEquals(submission.getDueDate(), actual.getScheduledDate()), + () -> assertEquals(EventStatus.COMPLETED, actual.getStatus()), + () -> assertEquals(user.getUsername(), actual.getCompletedBy()), + () -> assertNotNull(actual.getCompletedDate()), + () -> assertGeometry(submission.getCoordinates(), actual.getGeometry())); + } + + @Test + void shouldUpdateEvent() throws SmsCompressionException, ForbiddenException, NotFoundException { + Enrollment enrollment = enrollment(trackedEntity()); + Event event = event(enrollment); + + TrackerEventSmsSubmission submission = new TrackerEventSmsSubmission(); + int submissionId = 5; + submission.setSubmissionId(submissionId); + submission.setUserId(user.getUid()); + submission.setEvent(event.getUid()); + submission.setOrgUnit(event.getOrganisationUnit().getUid()); + submission.setProgramStage(event.getProgramStage().getUid()); + submission.setEnrollment(enrollment.getUid()); + submission.setAttributeOptionCombo(event.getAttributeOptionCombo().getUid()); + submission.setEventStatus(SmsEventStatus.COMPLETED); + submission.setEventDate(event.getOccurredDate()); + submission.setDueDate(event.getScheduledDate()); + // The coc has to be set so the sms-compression library can encode the data value. Not sure why + // that is necessary though. + submission.setValues(List.of(new SmsDataValue(coc.getUid(), de.getUid(), "hello"))); + + String text = encodeSms(submission); + String originator = user.getPhoneNumber(); + + switchContextToUser(user); + + JsonWebMessage response = + POST("/sms/inbound", format(""" +{ +"text": "%s", +"originator": "%s" +} +""", text, originator)) + .content(HttpStatus.OK) + .as(JsonWebMessage.class); + + IncomingSms sms = getSms(response); + assertAll( + () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), + () -> assertTrue(sms.isParsed()), + () -> assertEquals(originator, sms.getOriginator()), + () -> assertEquals(user, sms.getCreatedBy()), + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + assertDoesNotThrow(() -> eventService.getEvent(UID.of(event.getUid()))); + Event actual = eventService.getEvent(UID.of(event.getUid())); + assertAll( + "updated event", + () -> assertEqualUids(submission.getEnrollment(), actual.getEnrollment()), + () -> assertEqualUids(submission.getOrgUnit(), actual.getOrganisationUnit()), + () -> assertEqualUids(submission.getProgramStage(), actual.getProgramStage()), + () -> + assertEqualUids(submission.getAttributeOptionCombo(), actual.getAttributeOptionCombo()), + () -> assertNull(actual.getStoredBy()), + () -> assertEquals(event.getOccurredDate(), actual.getOccurredDate()), + () -> assertEquals(event.getScheduledDate(), actual.getScheduledDate()), + () -> assertEquals(EventStatus.COMPLETED, event.getStatus()), + () -> assertEquals(user.getUsername(), event.getCompletedBy()), + () -> assertNotNull(event.getCompletedDate()), + () -> { + EventDataValue expected = new EventDataValue(de.getUid(), "hello"); + expected.setStoredBy(user.getUsername()); + assertContainsOnly(Set.of(expected), actual.getEventDataValues()); + }, + () -> assertNull(actual.getGeometry())); } private IncomingSms getSms(JsonWebMessage response) { @@ -336,6 +507,7 @@ private Enrollment enrollment(TrackedEntity te) { private Event event(Enrollment enrollment) { Event event = new Event(enrollment, programStage, enrollment.getOrganisationUnit(), coc); + event.setOccurredDate(new Date()); event.setAutoFields(); manager.save(event); return event; @@ -354,4 +526,15 @@ private UserAccess fullAccess(User user) { a.setAccess(AccessStringHelper.FULL); return a; } + + private static void assertEqualUids(Uid expected, IdentifiableObject actual) { + assertEquals(expected.getUid(), actual.getUid()); + } + + private static void assertGeometry(GeoPoint expected, Geometry actual) { + assertEquals( + new GeometryFactory() + .createPoint(new Coordinate(expected.getLongitude(), expected.getLatitude())), + actual); + } }