diff --git a/.gitignore b/.gitignore index 1e18bfbe7818..1fcb063cb78d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ dhis-2/projectFilesBackup coverage node_modules **/rebel.xml +.vscode # ignore generated artifact directory docker/artifacts # ignore local docker override files diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/trackedentityattributevalue/TrackedEntityAttributeValue.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/trackedentityattributevalue/TrackedEntityAttributeValue.java index 07cea81ea1e1..a263f2ec48e9 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/trackedentityattributevalue/TrackedEntityAttributeValue.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/trackedentityattributevalue/TrackedEntityAttributeValue.java @@ -147,7 +147,7 @@ public void setEncryptedValue(String encryptedValue) { } /** - * Retrieves the plain-text value is the attribute isn't confidential. If the value is + * Retrieves the plain-text value if the attribute isn't confidential. If the value is * confidential, this value should be null, unless it was non-confidential at an earlier stage. * * @return String with plain-text value or null. diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java index 8ffa41841a99..c2a174e99a00 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java @@ -43,6 +43,7 @@ import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.changelog.ChangeLogType; import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.UID; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.feedback.ForbiddenException; @@ -386,21 +387,26 @@ private Set getEnrollments( private Set getTrackedEntityAttributeValues( TrackedEntity trackedEntity, Program program) { - Set readableAttributes = - new HashSet<>(trackedEntity.getTrackedEntityType().getTrackedEntityAttributes()); + Set readableAttributes = + trackedEntity.getTrackedEntityType().getTrackedEntityAttributes().stream() + .map(IdentifiableObject::getUid) + .collect(Collectors.toSet()); if (program != null) { - readableAttributes.addAll(program.getTrackedEntityAttributes()); + readableAttributes.addAll( + program.getTrackedEntityAttributes().stream() + .map(IdentifiableObject::getUid) + .collect(Collectors.toSet())); } return trackedEntity.getTrackedEntityAttributeValues().stream() - .filter(av -> readableAttributes.contains(av.getAttribute())) - .collect(Collectors.toCollection(LinkedHashSet::new)); + .filter(av -> readableAttributes.contains(av.getAttribute().getUid())) + .collect(Collectors.toSet()); } private RelationshipItem withNestedEntity( TrackedEntity trackedEntity, RelationshipItem item, boolean includeDeleted) - throws ForbiddenException, NotFoundException { + throws NotFoundException { // relationships of relationship items are not mapped to JSON so there is no need to fetch them RelationshipItem result = new RelationshipItem(); @@ -546,7 +552,7 @@ private void mapRelationshipItems( } private void mapRelationshipItems(TrackedEntity trackedEntity, boolean includeDeleted) - throws ForbiddenException, NotFoundException { + throws NotFoundException { Set result = new HashSet<>(); for (RelationshipItem item : trackedEntity.getRelationshipItems()) { @@ -562,7 +568,7 @@ private void mapRelationshipItems(TrackedEntity trackedEntity, boolean includeDe private void mapRelationshipItems( Enrollment enrollment, TrackedEntity trackedEntity, boolean includeDeleted) - throws ForbiddenException, NotFoundException { + throws NotFoundException { Set result = new HashSet<>(); for (RelationshipItem item : enrollment.getRelationshipItems()) { @@ -573,8 +579,7 @@ private void mapRelationshipItems( } private void mapRelationshipItems( - Event event, TrackedEntity trackedEntity, boolean includeDeleted) - throws ForbiddenException, NotFoundException { + Event event, TrackedEntity trackedEntity, boolean includeDeleted) throws NotFoundException { Set result = new HashSet<>(); for (RelationshipItem item : event.getRelationshipItems()) { @@ -589,7 +594,7 @@ private RelationshipItem mapRelationshipItem( BaseIdentifiableObject itemOwner, TrackedEntity trackedEntity, boolean includeDeleted) - throws ForbiddenException, NotFoundException { + throws NotFoundException { Relationship rel = item.getRelationship(); RelationshipItem from = withNestedEntity(trackedEntity, rel.getFrom(), includeDeleted); RelationshipItem to = withNestedEntity(trackedEntity, rel.getTo(), includeDeleted); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/TrackerImportService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/TrackerImportService.java index c6a6488e82e6..3f03b13847d3 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/TrackerImportService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/TrackerImportService.java @@ -37,12 +37,18 @@ */ public interface TrackerImportService { /** - * Import object using provided params. Takes the objects through all phases of the importer from - * preheating to validation, and then finished with a commit (unless its validate only) + * Import objects using provided params. Takes the objects through all phases of the importer from + * preheating to validation, and then finished with a commit (unless the {@link + * TrackerImportParams#getImportMode()} is {@link + * org.hisp.dhis.tracker.imports.bundle.TrackerBundleMode#VALIDATE} only). * - * @param params Parameters for import + *

{@link TrackerObjects} need to be flat. Each entity needs to be in the top level field like + * {@link TrackerObjects#getEnrollments()} for an enrollment, even though you could put its' data + * onto its tracked entitys' enrollment field in {@link TrackerObjects#getTrackedEntities()}. + * + * @param params import parameters * @param trackerObjects the objects to import - * @return Report giving status of import (and any errors) + * @return report giving status of import (and any errors) */ default ImportReport importTracker(TrackerImportParams params, TrackerObjects trackerObjects) { return importTracker(params, trackerObjects, RecordingJobProgress.transitory()); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/DeleteEventSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/DeleteEventSMSListener.java index a275b96aebba..e6620f83f8fb 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/DeleteEventSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/DeleteEventSMSListener.java @@ -28,6 +28,7 @@ package org.hisp.dhis.tracker.imports.sms; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.dataelement.DataElementService; @@ -95,10 +96,7 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use TrackerImportParams params = TrackerImportParams.builder().importStrategy(TrackerImportStrategy.DELETE).build(); - TrackerObjects trackerObjects = - TrackerObjects.builder() - .events(List.of(Event.builder().event(subm.getEvent().getUid()).build())) - .build(); + TrackerObjects trackerObjects = map(subm); ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); if (Status.OK == importReport.getStatus()) { @@ -109,6 +107,13 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use return SmsResponse.INVALID_EVENT.set(subm.getEvent()); } + @Nonnull + private static TrackerObjects map(@Nonnull DeleteSmsSubmission submission) { + return TrackerObjects.builder() + .events(List.of(Event.builder().event(submission.getEvent().getUid()).build())) + .build(); + } + @Override protected boolean handlesType(SubmissionType type) { return (type == SubmissionType.DELETE); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListener.java index 01a52a1f2623..1b9dd4d70f81 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListener.java @@ -27,72 +27,49 @@ */ package org.hisp.dhis.tracker.imports.sms; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.hisp.dhis.category.CategoryOptionCombo; +import static org.hisp.dhis.tracker.imports.sms.SmsImportMapper.map; + import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.feedback.BadRequestException; 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.program.Enrollment; 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.sms.listener.CompressionSMSListener; import org.hisp.dhis.sms.listener.SMSProcessingException; import org.hisp.dhis.smscompression.SmsConsts.SubmissionType; import org.hisp.dhis.smscompression.SmsResponse; import org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission; -import org.hisp.dhis.smscompression.models.SmsAttributeValue; -import org.hisp.dhis.smscompression.models.SmsEvent; import org.hisp.dhis.smscompression.models.SmsSubmission; -import org.hisp.dhis.smscompression.models.Uid; import org.hisp.dhis.trackedentity.TrackedEntity; -import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; -import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; -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.export.trackedentity.TrackedEntityParams; import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityService; -import org.hisp.dhis.tracker.trackedentityattributevalue.TrackedEntityAttributeValueService; +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.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.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Slf4j @Component("org.hisp.dhis.tracker.sms.EnrollmentSMSListener") @Transactional -public class EnrollmentSMSListener extends EventSavingSMSListener { - private final TrackedEntityService trackedEntityService; - - private final EnrollmentService enrollmentService; - - private final TrackedEntityAttributeValueService attributeValueService; +public class EnrollmentSMSListener extends CompressionSMSListener { + private final TrackerImportService trackerImportService; - private final ProgramStageService programStageService; - - private final SMSEnrollmentService smsEnrollmentService; + private final TrackedEntityService trackedEntityService; public EnrollmentSMSListener( IncomingSmsService incomingSmsService, @@ -104,16 +81,9 @@ public EnrollmentSMSListener( OrganisationUnitService organisationUnitService, CategoryService categoryService, DataElementService dataElementService, - ProgramStageService programStageService, - EventService eventService, - EventChangeLogService eventChangeLogService, - FileResourceService fileResourceService, - DhisConfigurationProvider config, - TrackedEntityAttributeValueService attributeValueService, - TrackedEntityService trackedEntityService, - EnrollmentService enrollmentService, - IdentifiableObjectManager manager, - SMSEnrollmentService smsEnrollmentService) { + IdentifiableObjectManager identifiableObjectManager, + TrackerImportService trackerImportService, + TrackedEntityService trackedEntityService) { super( incomingSmsService, smsSender, @@ -124,16 +94,9 @@ public EnrollmentSMSListener( organisationUnitService, categoryService, dataElementService, - manager, - eventService, - eventChangeLogService, - fileResourceService, - config); + identifiableObjectManager); + this.trackerImportService = trackerImportService; this.trackedEntityService = trackedEntityService; - this.programStageService = programStageService; - this.enrollmentService = enrollmentService; - this.attributeValueService = attributeValueService; - this.smsEnrollmentService = smsEnrollmentService; } @Override @@ -141,229 +104,42 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use throws SMSProcessingException { EnrollmentSmsSubmission subm = (EnrollmentSmsSubmission) submission; - Date enrollmentDate = subm.getEnrollmentDate(); - Date occurredDate = subm.getIncidentDate(); - Uid teUid = subm.getTrackedEntityInstance(); - Uid progid = subm.getTrackerProgram(); - Uid tetid = subm.getTrackedEntityType(); - Uid ouid = subm.getOrgUnit(); - Uid enrollmentid = subm.getEnrollment(); - OrganisationUnit orgUnit = organisationUnitService.getOrganisationUnit(ouid.getUid()); - - Program program = programService.getProgram(progid.getUid()); - + Program program = programService.getProgram(subm.getTrackerProgram().getUid()); if (program == null) { - throw new SMSProcessingException(SmsResponse.INVALID_PROGRAM.set(progid)); - } - - TrackedEntityType entityType = trackedEntityTypeService.getTrackedEntityType(tetid.getUid()); - - if (entityType == null) { - throw new SMSProcessingException(SmsResponse.INVALID_TETYPE.set(tetid)); - } - - if (!programService.hasOrgUnit(program, orgUnit)) { - throw new SMSProcessingException(SmsResponse.OU_NOTIN_PROGRAM.set(ouid, progid)); - } - - TrackedEntity trackedEntity; - boolean teExists = this.manager.exists(TrackedEntity.class, teUid.getUid()); - - if (teExists) { - log.info("Tracked entity exists: '{}'. Updating.", teUid); - try { - trackedEntity = - trackedEntityService.getTrackedEntity( - teUid.getUid(), null, TrackedEntityParams.FALSE, false); - } catch (NotFoundException | ForbiddenException | BadRequestException e) { - // TODO(tracker) Find a better error message for these exceptions - throw new SMSProcessingException(SmsResponse.UNKNOWN_ERROR); - } - } else { - log.info("Tracked entity does not exist: '{}'. Creating.", teUid); - trackedEntity = new TrackedEntity(); - trackedEntity.setUid(teUid.getUid()); - trackedEntity.setOrganisationUnit(orgUnit); - trackedEntity.setTrackedEntityType(entityType); - } - - Set attributeValues = getSMSAttributeValues(subm, trackedEntity); - - if (teExists) { - updateAttributeValues(attributeValues, trackedEntity.getTrackedEntityAttributeValues()); - trackedEntity.setTrackedEntityAttributeValues(attributeValues); - this.manager.update(trackedEntity); - } else { - manager.save(trackedEntity); - - for (TrackedEntityAttributeValue pav : attributeValues) { - attributeValueService.addTrackedEntityAttributeValue(pav); - trackedEntity.getTrackedEntityAttributeValues().add(pav); - } - - manager.update(trackedEntity); + return SmsResponse.INVALID_PROGRAM.set(subm.getTrackerProgram()); } - TrackedEntity te; + TrackedEntity trackedEntity = null; try { - te = + trackedEntity = trackedEntityService.getTrackedEntity( - teUid.getUid(), null, TrackedEntityParams.FALSE, false); - } catch (NotFoundException | ForbiddenException | BadRequestException e) { - // TODO(tracker) Improve this error message - throw new SMSProcessingException(SmsResponse.INVALID_TEI.set(trackedEntity.getUid())); - } - - Enrollment enrollment = null; - try { - enrollment = - enrollmentService.getEnrollment(enrollmentid.getUid(), UserDetails.fromUser(user)); - enrollment.setEnrollmentDate(enrollmentDate); - enrollment.setOccurredDate(occurredDate); - } catch (ForbiddenException e) { - throw new SMSProcessingException(SmsResponse.INVALID_ENROLL.set(enrollmentid.getUid())); + subm.getTrackedEntityInstance().getUid(), + subm.getTrackerProgram().getUid(), + TrackedEntityParams.FALSE.withIncludeAttributes(true), + false); } catch (NotFoundException e) { - // we'll create a new enrollment if none was found - // TODO(tracker) a NFE might be thrown if the user has no metadata access, we shouldn't create - // a new enrollment in that case - } - - if (enrollment == null) { - enrollment = - smsEnrollmentService.enrollTrackedEntity( - te, program, orgUnit, occurredDate, enrollmentid.getUid()); - - if (enrollment == null) { - throw new SMSProcessingException(SmsResponse.ENROLL_FAILED.set(teUid, progid)); - } - } - - enrollment.setStatus(getCoreEnrollmentStatus(subm.getEnrollmentStatus())); - enrollment.setGeometry(convertGeoPointToGeometry(subm.getCoordinates())); - this.manager.update(enrollment); - - // We now check if the enrollment has events to process - List errorUIDs = new ArrayList<>(); - if (subm.getEvents() != null) { - for (SmsEvent event : subm.getEvents()) { - errorUIDs.addAll(processEvent(event, user, enrollment)); - } - } - enrollment.setStatus(getCoreEnrollmentStatus(subm.getEnrollmentStatus())); - enrollment.setGeometry(convertGeoPointToGeometry(subm.getCoordinates())); - this.manager.update(enrollment); - - if (!errorUIDs.isEmpty()) { - return SmsResponse.WARN_DVERR.setList(errorUIDs); + // new TE will be created + } catch (ForbiddenException | BadRequestException e) { + // TODO(DHIS2-18003) we need to map tracker import report errors/warnings to an sms + return SmsResponse.UNKNOWN_ERROR; } - if (attributeValues.isEmpty()) { - // TODO: Is this correct handling? - return SmsResponse.WARN_AVEMPTY; - } - - return SmsResponse.SUCCESS; - } - - private TrackedEntityAttributeValue findAttributeValue( - TrackedEntityAttributeValue attributeValue, - Set attributeValues) { - return attributeValues.stream() - .filter(v -> v.getAttribute().getUid().equals(attributeValue.getAttribute().getUid())) - .findAny() - .orElse(null); - } - - private void updateAttributeValues( - Set attributeValues, - Set oldAttributeValues) { - // Update existing and add new values - for (TrackedEntityAttributeValue attributeValue : attributeValues) { - TrackedEntityAttributeValue oldAttributeValue = - findAttributeValue(attributeValue, oldAttributeValues); - if (oldAttributeValue != null) { - oldAttributeValue.setValue(attributeValue.getValue()); - attributeValueService.updateTrackedEntityAttributeValue(oldAttributeValue); - } else { - attributeValueService.addTrackedEntityAttributeValue(attributeValue); - } - } + TrackerImportParams params = + TrackerImportParams.builder() + .importStrategy(TrackerImportStrategy.CREATE_AND_UPDATE) + .build(); + TrackerObjects trackerObjects = map(subm, program, trackedEntity, user); + ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); - // Delete any that don't exist anymore - for (TrackedEntityAttributeValue oldAttributeValue : oldAttributeValues) { - if (findAttributeValue(oldAttributeValue, attributeValues) == null) { - attributeValueService.deleteTrackedEntityAttributeValue(oldAttributeValue); - } + 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_ENROLL.set(subm.getEnrollment()); } @Override protected boolean handlesType(SubmissionType type) { return (type == SubmissionType.ENROLLMENT); } - - private Set getSMSAttributeValues( - EnrollmentSmsSubmission submission, TrackedEntity trackedEntity) { - if (submission.getValues() == null) { - return Collections.emptySet(); - } - return submission.getValues().stream() - .map(v -> createTrackedEntityValue(v, trackedEntity)) - .collect(Collectors.toSet()); - } - - protected TrackedEntityAttributeValue createTrackedEntityValue( - SmsAttributeValue SMSAttributeValue, TrackedEntity te) { - Uid attribUid = SMSAttributeValue.getAttribute(); - String val = SMSAttributeValue.getValue(); - - TrackedEntityAttribute attribute = - trackedEntityAttributeService.getTrackedEntityAttribute(attribUid.getUid()); - - if (attribute == null) { - throw new SMSProcessingException(SmsResponse.INVALID_ATTRIB.set(attribUid)); - } else if (val == null) { - // TODO: Is this an error we can't recover from? - throw new SMSProcessingException(SmsResponse.NULL_ATTRIBVAL.set(attribUid)); - } - TrackedEntityAttributeValue trackedEntityAttributeValue = new TrackedEntityAttributeValue(); - trackedEntityAttributeValue.setAttribute(attribute); - trackedEntityAttributeValue.setTrackedEntity(te); - trackedEntityAttributeValue.setValue(val); - return trackedEntityAttributeValue; - } - - protected List processEvent(SmsEvent event, User user, Enrollment enrollment) { - Uid stageid = event.getProgramStage(); - Uid aocid = event.getAttributeOptionCombo(); - Uid orgunitid = event.getOrgUnit(); - - OrganisationUnit orgUnit = organisationUnitService.getOrganisationUnit(orgunitid.getUid()); - if (orgUnit == null) { - throw new SMSProcessingException(SmsResponse.INVALID_ORGUNIT.set(orgunitid)); - } - - ProgramStage programStage = programStageService.getProgramStage(stageid.getUid()); - if (programStage == null) { - throw new SMSProcessingException(SmsResponse.INVALID_STAGE.set(stageid)); - } - - CategoryOptionCombo aoc = categoryService.getCategoryOptionCombo(aocid.getUid()); - if (aoc == null) { - throw new SMSProcessingException(SmsResponse.INVALID_AOC.set(aocid)); - } - - return saveEvent( - event.getEvent().getUid(), - orgUnit, - programStage, - enrollment, - aoc, - user, - event.getValues(), - event.getEventStatus(), - event.getEventDate(), - event.getDueDate(), - event.getCoordinates()); - } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/RelationshipSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/RelationshipSMSListener.java index bc93c1dc1d4b..21349739bb9c 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/RelationshipSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/RelationshipSMSListener.java @@ -111,18 +111,7 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use TrackerImportParams params = TrackerImportParams.builder().importStrategy(TrackerImportStrategy.CREATE).build(); - TrackerObjects trackerObjects = - TrackerObjects.builder() - .relationships( - List.of( - Relationship.builder() - .relationshipType(MetadataIdentifier.ofUid(relType)) - .relationship( - subm.getRelationship() != null ? subm.getRelationship().getUid() : null) - .from(relationshipItem(relType.getFromConstraint(), subm.getFrom())) - .to(relationshipItem(relType.getToConstraint(), subm.getTo())) - .build())) - .build(); + TrackerObjects trackerObjects = map(subm, relType); ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); if (Status.OK == importReport.getStatus()) { @@ -133,7 +122,24 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use return SmsResponse.UNKNOWN_ERROR; } - private RelationshipItem relationshipItem(RelationshipConstraint constraint, Uid uid) { + private static TrackerObjects map( + RelationshipSmsSubmission submission, RelationshipType relType) { + return TrackerObjects.builder() + .relationships( + List.of( + Relationship.builder() + .relationshipType(MetadataIdentifier.ofUid(relType)) + .relationship( + submission.getRelationship() != null + ? submission.getRelationship().getUid() + : null) + .from(relationshipItem(relType.getFromConstraint(), submission.getFrom())) + .to(relationshipItem(relType.getToConstraint(), submission.getTo())) + .build())) + .build(); + } + + private static RelationshipItem relationshipItem(RelationshipConstraint constraint, Uid uid) { return switch (constraint.getRelationshipEntity()) { case TRACKED_ENTITY_INSTANCE -> RelationshipItem.builder().trackedEntity(uid.getUid()).build(); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapper.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapper.java new file mode 100644 index 000000000000..01118155d8c2 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapper.java @@ -0,0 +1,346 @@ +/* + * 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.sms; + +import static org.apache.commons.collections4.CollectionUtils.emptyIfNull; + +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.hisp.dhis.common.collection.CollectionUtils; +import org.hisp.dhis.event.EventStatus; +import org.hisp.dhis.program.EnrollmentStatus; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramTrackedEntityAttribute; +import org.hisp.dhis.smscompression.SmsConsts.SmsEnrollmentStatus; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; +import org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission; +import org.hisp.dhis.smscompression.models.GeoPoint; +import org.hisp.dhis.smscompression.models.SmsAttributeValue; +import org.hisp.dhis.smscompression.models.SmsDataValue; +import org.hisp.dhis.smscompression.models.SmsEvent; +import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; +import org.hisp.dhis.smscompression.models.Uid; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; +import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +import org.hisp.dhis.tracker.imports.domain.Attribute; +import org.hisp.dhis.tracker.imports.domain.DataValue; +import org.hisp.dhis.tracker.imports.domain.Enrollment; +import org.hisp.dhis.tracker.imports.domain.Event; +import org.hisp.dhis.tracker.imports.domain.MetadataIdentifier; +import org.hisp.dhis.tracker.imports.domain.TrackedEntity; +import org.hisp.dhis.tracker.imports.domain.TrackerObjects; +import org.hisp.dhis.user.User; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +/** + * SmsImportMapper maps tracker SMS types found in {@link org.hisp.dhis.smscompression.models} to + * {@link org.hisp.dhis.tracker.imports.domain.TrackerObjects} so they can be imported into tracker. + * This class should only include pure functions that do not need any other dependency. + * + *

Note that all "id" fields in a compressed SMS are mandatory, meaning you will get an NPE if + * you try to encode an sms using {@link org.hisp.dhis.smscompression}. Android will therefore + * always send an id even for newly created entities. Some fields like dates or collections are + * optional. + * + *

The output {@link org.hisp.dhis.tracker.imports.domain.TrackerObjects} need to flattened as + * that is what the tracker import expects. + */ +class SmsImportMapper { + private static final GeometryFactory geometryFactory = new GeometryFactory(); + + private SmsImportMapper() { + throw new IllegalStateException("Utility class"); + } + + /** + * {@link EnrollmentSmsSubmission} can create or update a tracked entity, enrollment with tracked + * entity attributes. It can also create or update events with data values. Refer to attribute + * mappers for the tracked entity and enrollment to understand how attributes are translated. + */ + @Nonnull + static TrackerObjects map( + @Nonnull EnrollmentSmsSubmission submission, + @Nonnull Program program, + @Nullable org.hisp.dhis.trackedentity.TrackedEntity trackedEntity, + @Nonnull User user) { + Set programAttributes = + emptyIfNull(program.getProgramAttributes()).stream() + .map(ProgramTrackedEntityAttribute::getAttribute) + .map(TrackedEntityAttribute::getUid) + .collect(Collectors.toSet()); + + Set existingAttributeValues = Set.of(); + if (trackedEntity != null) { + existingAttributeValues = + CollectionUtils.emptyIfNull(trackedEntity.getTrackedEntityAttributeValues()).stream() + .map(TrackedEntityAttributeValue::getAttribute) + .map(TrackedEntityAttribute::getUid) + .collect(Collectors.toSet()); + } + + return TrackerObjects.builder() + .trackedEntities( + List.of(mapTrackedEntity(submission, programAttributes, existingAttributeValues))) + .enrollments( + List.of(mapToEnrollment(submission, programAttributes, existingAttributeValues))) + .events( + emptyIfNull(submission.getEvents()).stream() + .map(e -> mapToEvent(e, user, submission.getEnrollment())) + .toList()) + .build(); + } + + @Nonnull + private static TrackedEntity mapTrackedEntity( + EnrollmentSmsSubmission submission, + Set programAttributes, + Set existingAttributeValues) { + return TrackedEntity.builder() + .orgUnit(metadataUid(submission.getOrgUnit())) + .trackedEntity(submission.getTrackedEntityInstance().getUid()) + .trackedEntityType(metadataUid(submission.getTrackedEntityType())) + .attributes( + mapTrackedEntityTypeAttributes( + submission.getValues(), existingAttributeValues, programAttributes)) + .enrollments( + List.of(Enrollment.builder().enrollment(submission.getEnrollment().getUid()).build())) + .build(); + } + + /** + * mapTrackedEntityTypeAttributes works like {@link #mapProgramAttributeValues(List, Set, Set)} + * only using non-program attributes which are assumed to be tracked entity type attributes. + */ + @Nonnull + private static List mapTrackedEntityTypeAttributes( + @Nullable List smsAttributeValues, + @Nonnull Set existingAttributeValues, + @Nonnull Set programAttributes) { + List smsTrackedEntityTypeAttributeValues = + emptyIfNull(smsAttributeValues).stream() + .filter(av -> !programAttributes.contains(av.getAttribute().getUid())) + .toList(); + Set existingTrackedEntityTypeAttributeValues = + existingAttributeValues.stream() + .filter(Predicate.not(programAttributes::contains)) + .collect(Collectors.toSet()); + return mapAttributeValues( + smsTrackedEntityTypeAttributeValues, existingTrackedEntityTypeAttributeValues); + } + + /** + * mapAttributeValues translates {@link SmsAttributeValue}s to {@link Attribute}s for the tracker + * importer. Any TEAV that is not present in the SMS will turn into an element to delete the TEAV + * {@code {"attribute": "uid", "value": null}}. This logic was part of the {@link + * EnrollmentSMSListener} processing before we migrated it to the tracker importer. It might have + * been added as users cannot add an attribute with a null value in the {@link + * EnrollmentSmsSubmission} model. That is speculation as users might be able to send an empty "" + * value which is also understood as a deletion request for attributes. + */ + @Nonnull + private static List mapAttributeValues( + @Nonnull List smsAttributeValues, + @Nonnull Set existingAttributeValues) { + Map attributeValues = new HashMap<>(); + + for (String attributeUid : existingAttributeValues) { + attributeValues.put(attributeUid, null); + } + + for (SmsAttributeValue smsAttributeValue : smsAttributeValues) { + // either add a new attribute value or update an existing one + attributeValues.put(smsAttributeValue.getAttribute().getUid(), smsAttributeValue.getValue()); + } + + return attributeValues.entrySet().stream() + .map( + entry -> + Attribute.builder() + .attribute(MetadataIdentifier.ofUid(entry.getKey())) + .value(entry.getValue()) + .build()) + .toList(); + } + + @Nonnull + private static Enrollment mapToEnrollment( + @Nonnull EnrollmentSmsSubmission submission, + @Nonnull Set programAttributes, + @Nonnull Set existingAttributeValues) { + return Enrollment.builder() + .orgUnit(metadataUid(submission.getOrgUnit())) + .program(metadataUid(submission.getTrackerProgram())) + .trackedEntity(submission.getTrackedEntityInstance().getUid()) + .enrollment(submission.getEnrollment().getUid()) + .enrolledAt(toInstant(submission.getEnrollmentDate())) + .occurredAt(toInstant(submission.getIncidentDate())) + .status(map(submission.getEnrollmentStatus())) + .geometry(map(submission.getCoordinates())) + .attributes( + mapProgramAttributeValues( + submission.getValues(), programAttributes, existingAttributeValues)) + .build(); + } + + /** + * mapProgramAttributeValues translates {@link EnrollmentSmsSubmission#getValues()} into {@link + * Enrollment#getAttributes()}. The tracker importer only accepts program attributes in {@link + * Enrollment#getAttributes()}. Since attribute values in the sms can be tracked entity type + * and/or program attributes they have to be split. Tracked entity type attributes go to the + * {@link TrackedEntity#getAttributes()} while program attributes go to the {@link + * Enrollment#getAttributes()}. The confusion in our tracker model is that a program attribute + * can also be a tracked entity attribute. This is why its legal to put such attributes + * into both attribute collections. This is what Capture app currently does. This only works in + * the Capture app as it can guarantee that tracker programs will always contain all the tracked + * entity type attributes of the programs tracked entity as its program attributes. + * + * @param smsAttributeValues all attribute values of an {@link EnrollmentSmsSubmission} + * @param programAttributes program attributes of the tracker program the {@link + * EnrollmentSmsSubmission} is for + * @param existingAttributeValues program and tracked entity type attribute values of an existing + * tracked entity + * @return enrollment (program) attributes for the tracker importer + */ + @Nonnull + static List mapProgramAttributeValues( + @Nullable List smsAttributeValues, + @Nonnull Set programAttributes, + @Nonnull Set existingAttributeValues) { + List smsProgramAttributeValues = + emptyIfNull(smsAttributeValues).stream() + .filter(av -> programAttributes.contains(av.getAttribute().getUid())) + .toList(); + Set existingProgramAttributeValues = + existingAttributeValues.stream() + .filter(programAttributes::contains) + .collect(Collectors.toSet()); + return mapAttributeValues(smsProgramAttributeValues, existingProgramAttributeValues); + } + + @Nonnull + private static Event mapToEvent( + @Nonnull SmsEvent submission, @Nonnull User user, @Nonnull Uid enrollment) { + return Event.builder() + .event(submission.getEvent().getUid()) + .enrollment(enrollment.getUid()) + .orgUnit(metadataUid(submission.getOrgUnit())) + .programStage(metadataUid(submission.getProgramStage())) + .attributeOptionCombo(metadataUid(submission.getAttributeOptionCombo())) + .storedBy(user.getUsername()) + .occurredAt(toInstant(submission.getEventDate())) + .scheduledAt(toInstant(submission.getDueDate())) + .status(map(submission.getEventStatus())) + .geometry(map(submission.getCoordinates())) + .dataValues(map(submission.getValues(), user)) + .build(); + } + + @Nonnull + static TrackerObjects map(@Nonnull TrackerEventSmsSubmission submission, @Nonnull User user) { + return TrackerObjects.builder().events(List.of(mapEvent(submission, user))).build(); + } + + @Nonnull + private static Event mapEvent(@Nonnull TrackerEventSmsSubmission submission, @Nonnull User user) { + return Event.builder() + .event(submission.getEvent().getUid()) + .enrollment(submission.getEnrollment().getUid()) + .orgUnit(metadataUid(submission.getOrgUnit())) + .programStage(metadataUid(submission.getProgramStage())) + .attributeOptionCombo(metadataUid(submission.getAttributeOptionCombo())) + .storedBy(user.getUsername()) + .occurredAt(toInstant(submission.getEventDate())) + .scheduledAt(toInstant(submission.getDueDate())) + .status(map(submission.getEventStatus())) + .geometry(map(submission.getCoordinates())) + .dataValues(map(submission.getValues(), user)) + .build(); + } + + @Nonnull + private static MetadataIdentifier metadataUid(Uid uid) { + return MetadataIdentifier.ofUid(uid.getUid()); + } + + @Nullable + private static Instant toInstant(@Nullable Date date) { + return date != null ? date.toInstant() : null; + } + + @Nonnull + private static Set map(@Nullable List dataValues, @Nonnull User user) { + return emptyIfNull(dataValues).stream() + .map( + dv -> + DataValue.builder() + .dataElement(metadataUid(dv.getDataElement())) + .value(dv.getValue()) + .storedBy(user.getUsername()) + .build()) + .collect(Collectors.toSet()); + } + + @Nullable + private static Point map(@Nullable GeoPoint coordinates) { + if (coordinates == null) { + return null; + } + + return geometryFactory.createPoint( + new Coordinate(coordinates.getLongitude(), coordinates.getLatitude())); + } + + private static EnrollmentStatus map(SmsEnrollmentStatus status) { + return switch (status) { + case ACTIVE -> EnrollmentStatus.ACTIVE; + case COMPLETED -> EnrollmentStatus.COMPLETED; + case CANCELLED -> EnrollmentStatus.CANCELLED; + }; + } + + private static EventStatus map(SmsEventStatus status) { + return switch (status) { + case ACTIVE -> EventStatus.ACTIVE; + case COMPLETED -> EventStatus.COMPLETED; + case VISITED -> EventStatus.VISITED; + case SCHEDULE -> EventStatus.SCHEDULE; + case OVERDUE -> EventStatus.OVERDUE; + case SKIPPED -> EventStatus.SKIPPED; + }; + } +} 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 21f804135388..90cf06514dc4 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,14 +27,11 @@ */ package org.hisp.dhis.tracker.imports.sms; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import static org.hisp.dhis.tracker.imports.sms.SmsImportMapper.map; + 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.event.EventStatus; import org.hisp.dhis.message.MessageSender; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.program.ProgramService; @@ -42,11 +39,8 @@ 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.trackedentity.TrackedEntityAttributeService; @@ -54,17 +48,11 @@ 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.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; @@ -105,25 +93,11 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use throws SMSProcessingException { TrackerEventSmsSubmission subm = (TrackerEventSmsSubmission) submission; - 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())); - TrackerImportParams params = TrackerImportParams.builder() .importStrategy(TrackerImportStrategy.CREATE_AND_UPDATE) .build(); - TrackerObjects trackerObjects = TrackerObjects.builder().events(List.of(event.build())).build(); + TrackerObjects trackerObjects = map(subm, user); ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); if (Status.OK == importReport.getStatus()) { @@ -133,42 +107,6 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use return SmsResponse.INVALID_EVENT.set(subm.getEvent()); } - 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; - } - - 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 dataValues.stream() - .map( - dv -> - DataValue.builder() - .dataElement(MetadataIdentifier.ofUid(dv.getDataElement().getUid())) - .value(dv.getValue()) - .storedBy(user.getUsername()) - .build()) - .collect(Collectors.toSet()); - } - @Override protected boolean handlesType(SubmissionType type) { return (type == SubmissionType.TRACKER_EVENT); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListenerTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListenerTest.java deleted file mode 100644 index d838a0b351b7..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/EnrollmentSMSListenerTest.java +++ /dev/null @@ -1,487 +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.common.ValueType; -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.program.ProgramTrackedEntityAttribute; -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.SmsEnrollmentStatus; -import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; -import org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission; -import org.hisp.dhis.smscompression.models.GeoPoint; -import org.hisp.dhis.smscompression.models.SmsAttributeValue; -import org.hisp.dhis.smscompression.models.SmsDataValue; -import org.hisp.dhis.smscompression.models.SmsEvent; -import org.hisp.dhis.trackedentity.TrackedEntity; -import org.hisp.dhis.trackedentity.TrackedEntityAttribute; -import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; -import org.hisp.dhis.trackedentity.TrackedEntityType; -import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; -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.export.trackedentity.TrackedEntityService; -import org.hisp.dhis.tracker.trackedentityattributevalue.TrackedEntityAttributeValueService; -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; - -@ExtendWith(MockitoExtension.class) -class EnrollmentSMSListenerTest 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 ProgramStageService programStageService; - - @Mock private EventService eventService; - - // Needed for this test - - @Mock private TrackedEntityService trackedEntityService; - - @Mock private EnrollmentService enrollmentService; - - @Mock private TrackedEntityAttributeValueService attributeValueService; - - @Mock private IdentifiableObjectManager identifiableObjectManager; - - @Mock private EventChangeLogService eventChangeLogService; - - @Mock private FileResourceService fileResourceService; - - @Mock private DhisConfigurationProvider config; - - @Mock private SMSEnrollmentService smsEnrollmentService; - - EnrollmentSMSListener subject; - - // Needed for all - - private User user; - - private final OutboundMessageResponse response = new OutboundMessageResponse(); - - private IncomingSms updatedIncomingSms; - - private String message = ""; - - // Needed for this test - - private IncomingSms incomingSmsEnrollmentNoEvents; - - private IncomingSms incomingSmsEnrollmentWithEvents; - - private IncomingSms incomingSmsEnrollmentWithNulls; - - private IncomingSms incomingSmsEnrollmentNoAttribs; - - private IncomingSms incomingSmsEnrollmentEventWithNulls; - - private IncomingSms incomingSmsEnrollmentEventNoValues; - - private OrganisationUnit organisationUnit; - - private Program program; - - private ProgramStage programStage; - - private Enrollment enrollment; - - private Event event; - - private TrackedEntityAttribute trackedEntityAttribute; - - private TrackedEntityAttributeValue trackedEntityAttributeValue; - - private TrackedEntityType trackedEntityType; - - private TrackedEntity trackedEntity; - - private CategoryOptionCombo categoryOptionCombo; - - private DataElement dataElement; - - @BeforeEach - public void initTest() throws SmsCompressionException, ForbiddenException, NotFoundException { - subject = - new EnrollmentSMSListener( - incomingSmsService, - smsSender, - userService, - trackedEntityTypeService, - trackedEntityAttributeService, - programService, - organisationUnitService, - categoryService, - dataElementService, - programStageService, - eventService, - eventChangeLogService, - fileResourceService, - config, - attributeValueService, - trackedEntityService, - enrollmentService, - identifiableObjectManager, - smsEnrollmentService); - - 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(programService.getProgram(anyString())).thenReturn(program); - when(trackedEntityTypeService.getTrackedEntityType(anyString())).thenReturn(trackedEntityType); - when(programService.hasOrgUnit(any(Program.class), any(OrganisationUnit.class))) - .thenReturn(true); - when(enrollmentService.getEnrollment(anyString(), any(UserDetails.class))) - .thenThrow(NotFoundException.class); - - doAnswer( - invocation -> { - updatedIncomingSms = (IncomingSms) invocation.getArguments()[0]; - return updatedIncomingSms; - }) - .when(incomingSmsService) - .update(any()); - - trackedEntity.setTrackedEntityType(trackedEntityType); - when(smsEnrollmentService.enrollTrackedEntity(any(), any(), any(), any(), any())) - .thenReturn(enrollment); - } - - @Test - void testEnrollmentNoEvents() { - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentNoEvents); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testEnrollmentWithEvents() { - when(dataElementService.getDataElement(anyString())).thenReturn(dataElement); - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentWithEvents); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testEnrollmentWithEventsRepeat() { - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - when(dataElementService.getDataElement(anyString())).thenReturn(dataElement); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentWithEvents); - subject.receive(incomingSmsEnrollmentWithEvents); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(2)).update(any()); - } - - @Test - void testEnrollmentWithNulls() { - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentWithNulls); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testEnrollmentNoAttribs() { - subject.receive(incomingSmsEnrollmentNoAttribs); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(NOATTRIBS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testEnrollmentEventWithNulls() { - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - when(dataElementService.getDataElement(anyString())).thenReturn(dataElement); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentEventWithNulls); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - // For now there's no warning if an event within the event - // list has no values. This might be changed in the future. - @Test - void testEnrollmentEventNoValues() { - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(trackedEntityAttributeService.getTrackedEntityAttribute(anyString())) - .thenReturn(trackedEntityAttribute); - - subject.receive(incomingSmsEnrollmentEventNoValues); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - private void setUpInstances() throws SmsCompressionException { - trackedEntityType = createTrackedEntityType('T'); - organisationUnit = createOrganisationUnit('O'); - program = createProgram('P'); - programStage = createProgramStage('S', program); - - user = makeUser("U"); - user.setPhoneNumber(ORIGINATOR); - user.setOrganisationUnits(Sets.newHashSet(organisationUnit)); - - trackedEntityAttribute = createTrackedEntityAttribute('A', ValueType.TEXT); - final ProgramTrackedEntityAttribute programTrackedEntityAttribute = - createProgramTrackedEntityAttribute(program, trackedEntityAttribute); - program.getProgramAttributes().add(programTrackedEntityAttribute); - program.getOrganisationUnits().add(organisationUnit); - program.setTrackedEntityType(trackedEntityType); - HashSet stages = new HashSet<>(); - stages.add(programStage); - program.setProgramStages(stages); - - enrollment = new Enrollment(); - enrollment.setAutoFields(); - enrollment.setProgram(program); - - event = new Event(); - event.setAutoFields(); - - trackedEntity = createTrackedEntity(organisationUnit); - trackedEntity.getTrackedEntityAttributeValues().add(trackedEntityAttributeValue); - trackedEntity.setOrganisationUnit(organisationUnit); - - trackedEntityAttributeValue = - createTrackedEntityAttributeValue('A', trackedEntity, trackedEntityAttribute); - trackedEntityAttributeValue.setValue(ATTRIBUTE_VALUE); - - categoryOptionCombo = createCategoryOptionCombo('C'); - dataElement = createDataElement('D'); - - incomingSmsEnrollmentNoEvents = createSMSFromSubmission(createEnrollmentSubmissionNoEvents()); - incomingSmsEnrollmentWithEvents = - createSMSFromSubmission(createEnrollmentSubmissionWithEvents()); - incomingSmsEnrollmentWithNulls = createSMSFromSubmission(createEnrollmentSubmissionWithNulls()); - incomingSmsEnrollmentNoAttribs = createSMSFromSubmission(createEnrollmentSubmissionNoAttribs()); - incomingSmsEnrollmentEventWithNulls = - createSMSFromSubmission(createEnrollmentSubmissionEventWithNulls()); - incomingSmsEnrollmentEventNoValues = - createSMSFromSubmission(createEnrollmentSubmissionEventNoValues()); - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionNoEvents() { - EnrollmentSmsSubmission subm = new EnrollmentSmsSubmission(); - - subm.setUserId(user.getUid()); - subm.setOrgUnit(organisationUnit.getUid()); - subm.setTrackerProgram(program.getUid()); - subm.setTrackedEntityType(trackedEntityType.getUid()); - subm.setTrackedEntityInstance(trackedEntity.getUid()); - subm.setEnrollment(enrollment.getUid()); - subm.setEnrollmentDate(new Date()); - subm.setIncidentDate(new Date()); - subm.setEnrollmentStatus(SmsEnrollmentStatus.ACTIVE); - subm.setCoordinates(new GeoPoint(59.9399586f, 10.7195609f)); - ArrayList values = new ArrayList<>(); - values.add(new SmsAttributeValue(trackedEntityAttribute.getUid(), ATTRIBUTE_VALUE)); - subm.setValues(values); - subm.setSubmissionId(1); - - return subm; - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionWithEvents() { - EnrollmentSmsSubmission subm = createEnrollmentSubmissionNoEvents(); - - ArrayList events = new ArrayList<>(); - events.add(createEvent()); - subm.setEvents(events); - return subm; - } - - private SmsEvent createEvent() { - SmsEvent event = new SmsEvent(); - event.setOrgUnit(organisationUnit.getUid()); - event.setProgramStage(programStage.getUid()); - event.setAttributeOptionCombo(categoryOptionCombo.getUid()); - event.setEvent(this.event.getUid()); - event.setEventStatus(SmsEventStatus.COMPLETED); - event.setEventDate(new Date()); - event.setDueDate(new Date()); - event.setCoordinates(new GeoPoint(59.9399586f, 10.7195609f)); - ArrayList eventValues = new ArrayList<>(); - eventValues.add(new SmsDataValue(categoryOptionCombo.getUid(), dataElement.getUid(), "10")); - event.setValues(eventValues); - - return event; - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionWithNulls() { - EnrollmentSmsSubmission subm = createEnrollmentSubmissionNoEvents(); - subm.setEnrollmentDate(null); - subm.setIncidentDate(null); - subm.setCoordinates(null); - subm.setEvents(null); - - return subm; - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionNoAttribs() { - EnrollmentSmsSubmission subm = createEnrollmentSubmissionNoEvents(); - subm.setValues(null); - - return subm; - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionEventWithNulls() { - EnrollmentSmsSubmission subm = createEnrollmentSubmissionNoEvents(); - SmsEvent event = createEvent(); - event.setEventDate(null); - event.setDueDate(null); - event.setCoordinates(null); - ArrayList events = new ArrayList<>(); - events.add(event); - subm.setEvents(events); - - return subm; - } - - private EnrollmentSmsSubmission createEnrollmentSubmissionEventNoValues() { - EnrollmentSmsSubmission subm = createEnrollmentSubmissionNoEvents(); - SmsEvent event = createEvent(); - event.setValues(null); - ArrayList events = new ArrayList<>(); - events.add(event); - subm.setEvents(events); - - return subm; - } -} diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapperTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapperTest.java new file mode 100644 index 000000000000..e603c1df2b95 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/SmsImportMapperTest.java @@ -0,0 +1,429 @@ +/* + * 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.sms; + +import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; +import static org.hisp.dhis.test.utils.Assertions.assertNotEmpty; +import static org.hisp.dhis.tracker.imports.sms.SmsImportMapper.map; +import static org.hisp.dhis.tracker.imports.sms.SmsImportMapper.mapProgramAttributeValues; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Set; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.event.EventStatus; +import org.hisp.dhis.program.EnrollmentStatus; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramTrackedEntityAttribute; +import org.hisp.dhis.smscompression.SmsConsts.SmsEnrollmentStatus; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; +import org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission; +import org.hisp.dhis.smscompression.models.GeoPoint; +import org.hisp.dhis.smscompression.models.SmsAttributeValue; +import org.hisp.dhis.smscompression.models.SmsDataValue; +import org.hisp.dhis.smscompression.models.SmsEvent; +import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; +import org.hisp.dhis.test.TestBase; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; +import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +import org.hisp.dhis.tracker.imports.domain.Attribute; +import org.hisp.dhis.tracker.imports.domain.DataValue; +import org.hisp.dhis.tracker.imports.domain.Enrollment; +import org.hisp.dhis.tracker.imports.domain.Event; +import org.hisp.dhis.tracker.imports.domain.MetadataIdentifier; +import org.hisp.dhis.tracker.imports.domain.TrackedEntity; +import org.hisp.dhis.tracker.imports.domain.TrackerObjects; +import org.hisp.dhis.user.User; +import org.hisp.dhis.util.DateUtils; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; + +class SmsImportMapperTest extends TestBase { + @Test + void mapEnrollmentWithNewTrackedEntityAndOnlyMandatoryFields() { + EnrollmentSmsSubmission input = new EnrollmentSmsSubmission(); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setTrackerProgram(CodeGenerator.generateUid()); + input.setTrackedEntityInstance(CodeGenerator.generateUid()); + input.setTrackedEntityType(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setEnrollmentStatus(SmsEnrollmentStatus.COMPLETED); + + TrackerObjects actual = map(input, program(), null, user("francis")); + + TrackerObjects expected = + TrackerObjects.builder() + .enrollments( + List.of( + Enrollment.builder() + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .program(MetadataIdentifier.ofUid(input.getTrackerProgram().getUid())) + .trackedEntity(input.getTrackedEntityInstance().getUid()) + .enrollment(input.getEnrollment().getUid()) + .status(EnrollmentStatus.COMPLETED) + .build())) + .trackedEntities( + List.of( + TrackedEntity.builder() + .trackedEntity(input.getTrackedEntityInstance().getUid()) + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .trackedEntityType( + MetadataIdentifier.ofUid(input.getTrackedEntityType().getUid())) + .enrollments( + List.of( + Enrollment.builder() + .enrollment(input.getEnrollment().getUid()) + .build())) + .build())) + .build(); + assertEquals(expected, actual); + } + + @Test + void mapEnrollmentWithNewTrackedEntityAndAttributes() { + EnrollmentSmsSubmission input = new EnrollmentSmsSubmission(); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setTrackerProgram(CodeGenerator.generateUid()); + input.setTrackedEntityInstance(CodeGenerator.generateUid()); + input.setTrackedEntityType(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setEnrollmentStatus(SmsEnrollmentStatus.COMPLETED); + // non-program attribute values are mapped onto the tracked entity + input.setValues(List.of(new SmsAttributeValue("fN8skWVI8JS", "soap"))); + + TrackerObjects actual = map(input, program("YjToz9y10ZZ"), null, user("francis")); + + TrackerObjects expected = + TrackerObjects.builder() + .enrollments( + List.of( + Enrollment.builder() + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .program(MetadataIdentifier.ofUid(input.getTrackerProgram().getUid())) + .trackedEntity(input.getTrackedEntityInstance().getUid()) + .enrollment(input.getEnrollment().getUid()) + .status(EnrollmentStatus.COMPLETED) + .build())) + .trackedEntities( + List.of( + TrackedEntity.builder() + .trackedEntity(input.getTrackedEntityInstance().getUid()) + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .trackedEntityType( + MetadataIdentifier.ofUid(input.getTrackedEntityType().getUid())) + .attributes( + List.of( + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("fN8skWVI8JS")) + .value("soap") + .build())) + .enrollments( + List.of( + Enrollment.builder() + .enrollment(input.getEnrollment().getUid()) + .build())) + .build())) + .build(); + assertEquals(expected, actual); + } + + @Test + void mapEnrollmentWithNewTrackedEntityAndOptionalNonCollectionFields() { + EnrollmentSmsSubmission input = new EnrollmentSmsSubmission(); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setTrackerProgram(CodeGenerator.generateUid()); + input.setTrackedEntityInstance(CodeGenerator.generateUid()); + input.setTrackedEntityType(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setEnrollmentStatus(SmsEnrollmentStatus.CANCELLED); + + Date enrollmentDate = DateUtils.getDate(2024, 9, 2, 10, 15); + input.setEnrollmentDate(enrollmentDate); + Date occurredDate = DateUtils.getDate(2024, 9, 3, 16, 23); + input.setIncidentDate(occurredDate); + input.setCoordinates(new GeoPoint(48.8575f, 2.3514f)); + + TrackerObjects actual = map(input, program(), null, user("francis")); + + List expected = + List.of( + Enrollment.builder() + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .program(MetadataIdentifier.ofUid(input.getTrackerProgram().getUid())) + .trackedEntity(input.getTrackedEntityInstance().getUid()) + .enrollment(input.getEnrollment().getUid()) + .enrolledAt(enrollmentDate.toInstant()) + .occurredAt(occurredDate.toInstant()) + .status(EnrollmentStatus.CANCELLED) + .geometry(new GeometryFactory().createPoint(new Coordinate(2.3514f, 48.8575f))) + .build()); + assertEquals(expected, actual.getEnrollments()); + } + + @Test + void mapEnrollmentWithEvents() { + EnrollmentSmsSubmission input = new EnrollmentSmsSubmission(); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setTrackerProgram(CodeGenerator.generateUid()); + input.setTrackedEntityInstance(CodeGenerator.generateUid()); + input.setTrackedEntityType(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setEnrollmentStatus(SmsEnrollmentStatus.CANCELLED); + + SmsEvent smsEvent = new SmsEvent(); + smsEvent.setOrgUnit(input.getOrgUnit().getUid()); + smsEvent.setProgramStage(CodeGenerator.generateUid()); + smsEvent.setEventStatus(SmsEventStatus.SCHEDULE); + smsEvent.setAttributeOptionCombo(CodeGenerator.generateUid()); + smsEvent.setEvent(CodeGenerator.generateUid()); + // The coc has to be set so the sms-compression library can encode the data value. Not sure why + // that is necessary though. + smsEvent.setValues( + List.of(new SmsDataValue(CodeGenerator.generateUid(), "oHvZHthw9Y0", "hello"))); + input.setEvents(List.of(smsEvent)); + + TrackerObjects actual = map(input, program(), null, user("francis")); + + List expected = + List.of( + Event.builder() + .event(smsEvent.getEvent().getUid()) + .orgUnit(MetadataIdentifier.ofUid(smsEvent.getOrgUnit().getUid())) + .programStage(MetadataIdentifier.ofUid(smsEvent.getProgramStage().getUid())) + .attributeOptionCombo( + MetadataIdentifier.ofUid(smsEvent.getAttributeOptionCombo().getUid())) + .status(EventStatus.SCHEDULE) + .storedBy("francis") + .dataValues( + Set.of( + DataValue.builder() + .dataElement(MetadataIdentifier.ofUid("oHvZHthw9Y0")) + .value("hello") + .storedBy("francis") + .build())) + .enrollment(input.getEnrollment().getUid()) + .build()); + assertEquals(expected, actual.getEvents()); + } + + @Test + void mapEnrollmentWithExistingTrackedEntity() { + TrackedEntityAttribute tea1 = new TrackedEntityAttribute(); + tea1.setUid("uE1OF7DDawz"); + TrackedEntityAttribute tea2 = new TrackedEntityAttribute(); + tea2.setUid("cCR4QVathUM"); + org.hisp.dhis.trackedentity.TrackedEntity trackedEntity = + new org.hisp.dhis.trackedentity.TrackedEntity(); + trackedEntity.setUid(CodeGenerator.generateUid()); + trackedEntity.setTrackedEntityAttributeValues( + Set.of( + new TrackedEntityAttributeValue(tea1, trackedEntity, "oneWillBeDeleted"), + new TrackedEntityAttributeValue(tea2, trackedEntity, "twoWillBeUpdated"))); + + EnrollmentSmsSubmission input = new EnrollmentSmsSubmission(); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setTrackerProgram(CodeGenerator.generateUid()); + input.setTrackedEntityInstance(trackedEntity.getUid()); + input.setTrackedEntityType(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setEnrollmentStatus(SmsEnrollmentStatus.CANCELLED); + // attribute values are only mapped onto the tracked entity + input.setValues( + List.of( + new SmsAttributeValue("cCR4QVathUM", "twoWasUpdated"), + new SmsAttributeValue("zjOPAEZyQxu", "threeWasAdded"))); + + TrackerObjects actual = map(input, program("YjToz9y10ZZ"), trackedEntity, user("francis")); + + List expected = + List.of( + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("uE1OF7DDawz")) + .value(null) + .build(), + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("cCR4QVathUM")) + .value("twoWasUpdated") + .build(), + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("zjOPAEZyQxu")) + .value("threeWasAdded") + .build()); + assertNotEmpty(actual.getTrackedEntities()); + assertContainsOnly(expected, actual.getTrackedEntities().get(0).getAttributes()); + } + + @Test + void + mapSmsAttributeValuesToProgramAttributesGivenTETAndProgramAttributesAndNoExistingAttributes() { + List input = + List.of( + new SmsAttributeValue("uE1OF7DDawz", "firstTrackedEntityTypeAttribute"), + new SmsAttributeValue("YjToz9y10ZZ", "firstProgramAttribute"), + new SmsAttributeValue("fN8skWVI8JS", "secondProgramAttribute")); + Set programAttributes = Set.of("YjToz9y10ZZ", "fN8skWVI8JS"); + + List actual = mapProgramAttributeValues(input, programAttributes, Set.of()); + + List expected = + List.of( + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("YjToz9y10ZZ")) + .value("firstProgramAttribute") + .build(), + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("fN8skWVI8JS")) + .value("secondProgramAttribute") + .build()); + assertContainsOnly(expected, actual); + } + + @Test + void mapSmsAttributeValuesToProgramAttributesGivenTETAndProgramAttributesAndExistingAttributes() { + List input = + List.of( + new SmsAttributeValue("uE1OF7DDawz", "firstTrackedEntityTypeAttribute"), + new SmsAttributeValue("YjToz9y10ZZ", "firstProgramAttribute"), + new SmsAttributeValue("fN8skWVI8JS", "secondProgramAttribute")); + Set existingAttributeValues = + Set.of("uE1OF7DDawz", "cCR4QVathUM", "fN8skWVI8JS", "IKfH08xtgWG"); + Set programAttributes = Set.of("YjToz9y10ZZ", "fN8skWVI8JS", "IKfH08xtgWG"); + + List actual = + mapProgramAttributeValues(input, programAttributes, existingAttributeValues); + + List expected = + List.of( + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("YjToz9y10ZZ")) + .value("firstProgramAttribute") + .build(), + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("fN8skWVI8JS")) + .value("secondProgramAttribute") + .build(), + Attribute.builder() + .attribute(MetadataIdentifier.ofUid("IKfH08xtgWG")) + .value(null) + .build()); + assertContainsOnly(expected, actual); + } + + @Test + void mapEventWithMandatoryFields() { + TrackerEventSmsSubmission input = new TrackerEventSmsSubmission(); + input.setEvent(CodeGenerator.generateUid()); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setProgramStage(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setAttributeOptionCombo(CodeGenerator.generateUid()); + input.setEventStatus(SmsEventStatus.COMPLETED); + + TrackerObjects actual = map(input, user("francis")); + + Event expected = + Event.builder() + .event(input.getEvent().getUid()) + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .programStage(MetadataIdentifier.ofUid(input.getProgramStage().getUid())) + .enrollment(input.getEnrollment().getUid()) + .attributeOptionCombo( + MetadataIdentifier.ofUid(input.getAttributeOptionCombo().getUid())) + .status(EventStatus.COMPLETED) + .storedBy("francis") + .build(); + assertContainsOnly(List.of(expected), actual.getEvents()); + } + + @Test + void mapEventWithOptionalFields() { + TrackerEventSmsSubmission input = new TrackerEventSmsSubmission(); + input.setEvent(CodeGenerator.generateUid()); + input.setOrgUnit(CodeGenerator.generateUid()); + input.setProgramStage(CodeGenerator.generateUid()); + input.setEnrollment(CodeGenerator.generateUid()); + input.setAttributeOptionCombo(CodeGenerator.generateUid()); + input.setEventStatus(SmsEventStatus.ACTIVE); + Date occurredDate = DateUtils.getDate(2024, 10, 7, 14, 12); + input.setEventDate(occurredDate); + Date scheduledDate = DateUtils.getDate(2024, 10, 9, 14, 12); + input.setDueDate(scheduledDate); + input.setCoordinates(new GeoPoint(48.8575f, 2.3514f)); + // The coc has to be set so the sms-compression library can encode the data value. Not sure why + // that is necessary though. + input.setValues(List.of(new SmsDataValue(CodeGenerator.generateUid(), "oHvZHthw9Y0", "hello"))); + + TrackerObjects actual = map(input, user("francis")); + + Event expected = + Event.builder() + .event(input.getEvent().getUid()) + .orgUnit(MetadataIdentifier.ofUid(input.getOrgUnit().getUid())) + .programStage(MetadataIdentifier.ofUid(input.getProgramStage().getUid())) + .enrollment(input.getEnrollment().getUid()) + .attributeOptionCombo( + MetadataIdentifier.ofUid(input.getAttributeOptionCombo().getUid())) + .status(EventStatus.ACTIVE) + .storedBy("francis") + .occurredAt(occurredDate.toInstant()) + .scheduledAt(scheduledDate.toInstant()) + .geometry(new GeometryFactory().createPoint(new Coordinate(2.3514f, 48.8575f))) + .dataValues( + Set.of( + DataValue.builder() + .dataElement(MetadataIdentifier.ofUid("oHvZHthw9Y0")) + .value("hello") + .storedBy("francis") + .build())) + .build(); + assertContainsOnly(List.of(expected), actual.getEvents()); + } + + private static Program program(String... programAttributes) { + Program program = new Program(); + program.setProgramAttributes( + Arrays.stream(programAttributes) + .map( + a -> { + TrackedEntityAttribute tea = new TrackedEntityAttribute(); + tea.setUid(a); + return new ProgramTrackedEntityAttribute(null, tea); + }) + .toList()); + return program; + } + + private static User user(String username) { + User user = new User(); + user.setUsername(username); + return user; + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEnrollmentSMSTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEnrollmentSMSTest.java new file mode 100644 index 000000000000..e9c05a59dcac --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEnrollmentSMSTest.java @@ -0,0 +1,447 @@ +/* + * 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.webapi.controller.tracker.imports; + +import static java.lang.String.format; +import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; +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.assertNotNull; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.hisp.dhis.analytics.AggregationType; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObject; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.ValueType; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.feedback.BadRequestException; +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; +import org.hisp.dhis.program.EnrollmentStatus; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; +import org.hisp.dhis.program.ProgramStageDataElement; +import org.hisp.dhis.program.ProgramType; +import org.hisp.dhis.security.Authorities; +import org.hisp.dhis.security.acl.AccessStringHelper; +import org.hisp.dhis.sms.incoming.IncomingSms; +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.SmsEnrollmentStatus; +import org.hisp.dhis.smscompression.SmsResponse; +import org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission; +import org.hisp.dhis.smscompression.models.SmsAttributeValue; +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; +import org.hisp.dhis.test.webapi.json.domain.JsonWebMessage; +import org.hisp.dhis.trackedentity.TrackedEntity; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; +import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.hisp.dhis.trackedentity.TrackedEntityTypeAttribute; +import org.hisp.dhis.trackedentityattributevalue.TrackedEntityAttributeValue; +import org.hisp.dhis.tracker.export.enrollment.EnrollmentParams; +import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; +import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityParams; +import org.hisp.dhis.tracker.export.trackedentity.TrackedEntityService; +import org.hisp.dhis.tracker.trackedentityattributevalue.TrackedEntityAttributeValueService; +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.springframework.beans.factory.annotation.Autowired; + +/** + * Tests tracker SMS to enroll an existing or new tracked entity via a {@link + * org.hisp.dhis.smscompression.models.EnrollmentSmsSubmission} implemented via {@link + * org.hisp.dhis.tracker.imports.sms.EnrollmentSMSListener} + */ +class TrackerEnrollmentSMSTest extends PostgresControllerIntegrationTestBase { + @Autowired private IdentifiableObjectManager manager; + + @Autowired private TrackedEntityService trackedEntityService; + + @Autowired private EnrollmentService enrollmentService; + + @Autowired private IncomingSmsService incomingSmsService; + + @Autowired private FakeMessageSender messageSender; + + @Autowired private TrackedEntityAttributeValueService attributeValueService; + + private OrganisationUnit orgUnit; + + private Program program; + + private User user; + + private TrackedEntityType trackedEntityType; + private TrackedEntityAttribute teaA; + private TrackedEntityAttribute teaB; + private TrackedEntityAttribute teaC; + + @BeforeEach + void setUp() { + orgUnit = createOrganisationUnit('A'); + + user = createUserWithAuth("tester", Authorities.toStringArray(Authorities.F_MOBILE_SETTINGS)); + user.addOrganisationUnit(orgUnit); + user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); + user.setPhoneNumber("7654321"); + userService.updateUser(user); + + orgUnit.getSharing().setOwner(user); + manager.save(orgUnit, false); + + trackedEntityType = trackedEntityTypeAccessible(); + + teaA = createTrackedEntityAttribute('A', ValueType.TEXT); + teaA.setConfidential(false); + teaA.getSharing().setOwner(user); + teaA.getSharing().addUserAccess(fullAccess(user)); + manager.save(teaA, false); + + // this TEA will be a tracked entity type attribute and also a program attribute + teaB = createTrackedEntityAttribute('B', ValueType.TEXT); + teaB.getSharing().setOwner(user); + teaB.getSharing().addUserAccess(fullAccess(user)); + teaB.setConfidential(false); + manager.save(teaB, false); + + // this TEA will only be a program attribute + teaC = createTrackedEntityAttribute('C', ValueType.TEXT); + teaC.getSharing().setOwner(user); + teaC.getSharing().addUserAccess(fullAccess(user)); + teaC.setConfidential(false); + manager.save(teaC, false); + + trackedEntityType.setTrackedEntityTypeAttributes( + List.of( + new TrackedEntityTypeAttribute(trackedEntityType, teaA), + new TrackedEntityTypeAttribute(trackedEntityType, teaB))); + manager.save(trackedEntityType, false); + + program = createProgram('A'); + program.addOrganisationUnit(orgUnit); + program.getSharing().setOwner(user); + program.getSharing().addUserAccess(fullAccess(user)); + program.setTrackedEntityType(trackedEntityType); + program.setProgramType(ProgramType.WITH_REGISTRATION); + program.setProgramAttributes( + List.of( + createProgramTrackedEntityAttribute(program, teaB), + createProgramTrackedEntityAttribute(program, teaC))); + manager.save(program, false); + + DataElement de = createDataElement('A', ValueType.TEXT, AggregationType.NONE); + de.getSharing().setOwner(user); + manager.save(de, false); + + ProgramStage 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(Set.of(programStageDataElement)); + manager.save(programStage, false); + } + + @AfterEach + void afterEach() { + messageSender.clearMessages(); + } + + @Test + void shouldCreateTrackedEntityAndEnrollIt() + throws SmsCompressionException, ForbiddenException, NotFoundException, BadRequestException { + EnrollmentSmsSubmission submission = new EnrollmentSmsSubmission(); + int submissionId = 1; + submission.setSubmissionId(submissionId); + submission.setUserId(user.getUid()); + submission.setOrgUnit(orgUnit.getUid()); + submission.setTrackerProgram(program.getUid()); + submission.setTrackedEntityInstance(CodeGenerator.generateUid()); + submission.setTrackedEntityType(trackedEntityType.getUid()); + String enrollmentUid = CodeGenerator.generateUid(); + submission.setEnrollment(enrollmentUid); + submission.setEnrollmentDate(DateUtils.getDate(2024, 9, 2, 10, 15)); + submission.setIncidentDate(DateUtils.getDate(2024, 9, 3, 16, 23)); + submission.setEnrollmentStatus(SmsEnrollmentStatus.COMPLETED); + submission.setValues( + List.of( + new SmsAttributeValue(teaA.getUid(), "TrackedEntityTypeAttributeValue"), + new SmsAttributeValue(teaC.getUid(), "ProgramAttributeValue"))); + + 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()), + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + assertDoesNotThrow(() -> enrollmentService.getEnrollment(enrollmentUid)); + Enrollment actual = enrollmentService.getEnrollment(enrollmentUid); + assertAll( + "created enrollment", + () -> assertEquals(enrollmentUid, actual.getUid()), + () -> assertEqualUids(submission.getTrackedEntityInstance(), actual.getTrackedEntity())); + assertDoesNotThrow( + () -> + trackedEntityService.getTrackedEntity( + submission.getTrackedEntityInstance().getUid(), + submission.getTrackerProgram().getUid(), + TrackedEntityParams.FALSE, + false)); + TrackedEntity actualTe = + trackedEntityService.getTrackedEntity( + submission.getTrackedEntityInstance().getUid(), + submission.getTrackerProgram().getUid(), + TrackedEntityParams.FALSE.withIncludeAttributes(true), + false); + assertAll( + "created tracked entity with tracked entity attribute values", + () -> assertEqualUids(submission.getTrackedEntityInstance(), actualTe), + () -> { + Map actualTeav = + actualTe.getTrackedEntityAttributeValues().stream() + .collect( + Collectors.toMap( + teav -> teav.getAttribute().getUid(), + TrackedEntityAttributeValue::getValue)); + assertEquals( + Map.of( + teaA.getUid(), + "TrackedEntityTypeAttributeValue", + teaC.getUid(), + "ProgramAttributeValue"), + actualTeav); + }); + } + + @Test + void shouldEnrollExistingTrackedEntityAndAddUpdateAndDeleteAttributes() + throws SmsCompressionException, ForbiddenException, NotFoundException, BadRequestException { + TrackedEntity trackedEntity = trackedEntity(); + // add two tracked entity type value to the TE (one will be updated, the other deleted) + TrackedEntityAttributeValue teavA = createTrackedEntityAttributeValue('A', trackedEntity, teaA); + attributeValueService.addTrackedEntityAttributeValue(teavA); + trackedEntity.getTrackedEntityAttributeValues().add(teavA); + + TrackedEntityAttributeValue teavB = createTrackedEntityAttributeValue('B', trackedEntity, teaB); + attributeValueService.addTrackedEntityAttributeValue(teavB); + trackedEntity.getTrackedEntityAttributeValues().add(teavB); + + manager.save(trackedEntity, false); + Enrollment enrollment = enrollment(trackedEntity); + + switchContextToUser(user); + + assertEquals( + EnrollmentStatus.ACTIVE, + enrollment.getStatus(), + "enrollment status should be updated from active to completed"); + + EnrollmentSmsSubmission submission = new EnrollmentSmsSubmission(); + int submissionId = 2; + submission.setSubmissionId(submissionId); + submission.setUserId(user.getUid()); + submission.setOrgUnit(orgUnit.getUid()); + submission.setTrackerProgram(program.getUid()); + submission.setTrackedEntityInstance(trackedEntity.getUid()); + submission.setTrackedEntityType(trackedEntityType.getUid()); + submission.setEnrollment(enrollment.getUid()); + submission.setEnrollmentDate(enrollment.getEnrollmentDate()); + submission.setIncidentDate(enrollment.getOccurredDate()); + submission.setEnrollmentStatus(SmsEnrollmentStatus.COMPLETED); + submission.setValues( + List.of( + new SmsAttributeValue(teaA.getUid(), "AttributeAUpdated"), + new SmsAttributeValue(teaC.getUid(), "AttributeCAdded"))); + + String text = encodeSms(submission); + String originator = user.getPhoneNumber(); + + JsonWebMessage response = + POST( + "/sms/inbound", + format( + """ + { + "text": "%s", + "originator": "%s" + } + """, + text, originator)) + .content(HttpStatus.OK) + .as(JsonWebMessage.class); + + manager.clear(); + IncomingSms sms = getSms(response); + assertAll( + () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), + () -> { + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = + new OutboundMessage(null, expectedText, Set.of(originator)); + assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages()); + }); + Enrollment actual = + enrollmentService.getEnrollment( + enrollment.getUid(), EnrollmentParams.FALSE.withIncludeAttributes(true), false); + assertAll( + "update enrollment and program attributes", + () -> assertEqualUids(submission.getTrackedEntityInstance(), actual.getTrackedEntity())); + assertDoesNotThrow( + () -> + trackedEntityService.getTrackedEntity( + submission.getTrackedEntityInstance().getUid(), + submission.getTrackerProgram().getUid(), + TrackedEntityParams.FALSE, + false)); + TrackedEntity actualTe = + trackedEntityService.getTrackedEntity( + submission.getTrackedEntityInstance().getUid(), + submission.getTrackerProgram().getUid(), + TrackedEntityParams.FALSE.withIncludeAttributes(true), + false); + assertAll( + "update tracked entity with tracked entity attribute values", + () -> assertEqualUids(submission.getTrackedEntityInstance(), actualTe), + () -> { + Map actualTeav = + actualTe.getTrackedEntityAttributeValues().stream() + .collect( + Collectors.toMap( + teav -> teav.getAttribute().getUid(), + TrackedEntityAttributeValue::getValue)); + assertEquals( + Map.of(teaA.getUid(), "AttributeAUpdated", teaC.getUid(), "AttributeCAdded"), + actualTeav); + }); + } + + private IncomingSms getSms(JsonWebMessage response) { + assertStartsWith("Received SMS: ", response.getMessage()); + + String smsUid = response.getMessage().replaceFirst("^Received SMS: ", ""); + IncomingSms sms = incomingSmsService.get(smsUid); + assertNotNull(sms, "failed to find SMS in DB with UID " + smsUid); + return sms; + } + + private TrackedEntityType trackedEntityTypeAccessible() { + TrackedEntityType type = trackedEntityType('A'); + type.getSharing().setOwner(user); + type.getSharing().addUserAccess(fullAccess(user)); + manager.save(type, false); + return type; + } + + private Enrollment enrollment(TrackedEntity te) { + Enrollment enrollment = new Enrollment(program, te, te.getOrganisationUnit()); + enrollment.setAutoFields(); + enrollment.setEnrollmentDate(new Date()); + enrollment.setOccurredDate(new Date()); + enrollment.setStatus(EnrollmentStatus.ACTIVE); + manager.save(enrollment); + te.getEnrollments().add(enrollment); + manager.save(te); + return enrollment; + } + + private TrackedEntity trackedEntity() { + TrackedEntity te = trackedEntity(orgUnit); + manager.save(te, false); + return te; + } + + private TrackedEntity trackedEntity(OrganisationUnit orgUnit) { + return trackedEntity(orgUnit, trackedEntityType); + } + + private TrackedEntity trackedEntity( + OrganisationUnit orgUnit, TrackedEntityType trackedEntityType) { + TrackedEntity te = createTrackedEntity(orgUnit); + te.setTrackedEntityType(trackedEntityType); + te.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); + te.getSharing().setOwner(user); + return te; + } + + private TrackedEntityType trackedEntityType(char uniqueChar) { + TrackedEntityType type = createTrackedEntityType(uniqueChar); + type.getSharing().setOwner(user); + type.getSharing().setPublicAccess(AccessStringHelper.DEFAULT); + return type; + } + + private UserAccess fullAccess(User user) { + UserAccess a = new UserAccess(); + a.setUser(user); + a.setAccess(AccessStringHelper.FULL); + return a; + } + + private static void assertEqualUids(Uid expected, IdentifiableObject actual) { + assertEquals(expected.getUid(), actual.getUid()); + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java index d98aa508a458..79f6b9742aba 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java @@ -40,7 +40,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.google.common.collect.Sets; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -172,7 +171,7 @@ void setUp() { programStage.getSharing().addUserAccess(fullAccess(user)); ProgramStageDataElement programStageDataElement = createProgramStageDataElement(programStage, de, 1, false); - programStage.setProgramStageDataElements(Sets.newHashSet(programStageDataElement)); + programStage.setProgramStageDataElements(Set.of(programStageDataElement)); manager.save(programStage, false); }