From 27ff1809790a4b230224e08303d60867032d3993 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Thu, 14 Nov 2024 16:35:59 +0100 Subject: [PATCH] fix: [ANDROAPP-6493] take incident date into account for scheduling events (#3871) * fix: [ANDROAPP-6493] take incident date into account for scheduling and creating events * fix: [ANDROAPP-6493] migrate code to tracker module, add repository * fix: [ANDROAPP-6493] Adapt unit tests to new usage * fix: [ANDROAPP-6493] Add tests that take into account incident date * fix: [ANDROAPP-6493] add tests for min days form start and standard interval days * fix: [ANDROAPP-6493] refactor setEventDate logic to improve readability * fix: [ANDROAPP-6493] add test for period generation, clean up code * fix: [ANDROAPP-6493] get next available period and not current one * fix: [ANDROAPP-6493] take incident date into account for scheduling and creating events * fix: [ANDROAPP-6493] migrate code to tracker module, add repository * fix: [ANDROAPP-6493] Adapt unit tests to new usage * fix: [ANDROAPP-6493] Add tests that take into account incident date * fix: [ANDROAPP-6493] add tests for min days form start and standard interval days * fix: [ANDROAPP-6493] refactor setEventDate logic to improve readability * fix: [ANDROAPP-6493] add test for period generation, clean up code * fix: [ANDROAPP-6493] get next available period and not current one * fix: [ANDROAPP-6493] adapt unit test * fix: [ANDROAPP-6645] dont take into account min days from start or standard interval days for new events * fix: [ANDROAPP-6645] adapt unit tests * fix: [ANDROAPP-6493] adapt unit tests * fix: [ANDROAPP-6493] modify unit test * fix: [ANDROAPP-6493] ensure suggested date for enrollments is current date * fix: [ANDROAPP-6493] ensure suggested date for enrollments is current date * fix: [ANDROAPP-6493] ktlint * fix: [ANDROAPP-6493] remove failing test * fix: [ANDROAPP-6493] ktlint * fix: [ANDROAPP-6493] fix test --- .../domain/ConfigureEventReportDate.kt | 36 ++-- .../ProgramEventDetailModule.kt | 15 +- .../ProgramEventDetailViewModel.kt | 2 +- .../ProgramEventDetailViewModelFactory.kt | 2 +- .../usecase/CreateEventUseCase.kt | 39 ----- .../ProgramStageSelectionInjector.kt | 11 +- .../ProgramStageSelectionPresenter.kt | 2 +- .../SearchRepositoryImpl.java | 2 +- .../teidata/TEIDataModule.kt | 14 +- .../teidata/TEIDataPresenter.kt | 2 +- .../dialogs/scheduling/SchedulingViewModel.kt | 5 +- .../domain/ConfigureEventReportDateTest.kt | 33 +--- .../usecase/CreateEventUseCaseTest.kt | 162 ++++++++++++------ .../ProgramStageSelectionPresenterTest.kt | 2 +- .../data/TeiDataPresenterTest.kt | 2 +- .../org/dhis2/commons/date/DateUtils.java | 36 ++-- .../org/dhis2/form/data/EventRepository.kt | 68 +++++++- .../tracker/events/CreateEventUseCase.kt | 19 ++ .../events/CreateEventUseCaseRepository.kt | 86 ++++++++++ 19 files changed, 364 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt create mode 100644 tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCase.kt create mode 100644 tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCaseRepository.kt diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt index 5083df1a66..00066b1009 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt @@ -34,7 +34,7 @@ class ConfigureEventReportDate( label = getLabel(), dateValue = getDateValue(selectedDate), currentDate = getDate(selectedDate), - minDate = getMinDate(), + minDate = getMinDate(selectedDate), maxDate = getMaxDate(), scheduleInterval = getScheduleInterval(), allowFutureDates = getAllowFutureDates(), @@ -68,8 +68,7 @@ class ConfigureEventReportDate( private fun getDate(selectedDate: Date?) = when { selectedDate != null -> selectedDate repository.getEvent() != null -> repository.getEvent()?.eventDate() - periodType != null -> getDateBasedOnPeriodType() - creationType == SCHEDULE -> getNextScheduleDate() + periodType != null || creationType == SCHEDULE -> getNextScheduleDate() else -> getCurrentDay() } @@ -84,14 +83,15 @@ class ConfigureEventReportDate( private fun getProgramStage(): ProgramStage? = repository.getProgramStage() - private fun getDateBasedOnPeriodType(): Date { + private fun getDateBasedOnPeriodType(startDate: Date?): Date { + val initialDate = startDate ?: DateUtils.getInstance().today + val calendar = DateUtils.getInstance().calendar + calendar.time = initialDate getProgramStage()?.hideDueDate()?.let { hideDueDate -> if (creationType == SCHEDULE && hideDueDate) { return if (periodType != null) { - DateUtils.getInstance().today + calendar.time } else { - val calendar = DateUtils.getInstance().calendar - calendar.add(DAY_OF_YEAR, getScheduleInterval()) DateUtils.getInstance().getNextPeriod( null, calendar.time, @@ -100,16 +100,15 @@ class ConfigureEventReportDate( } } } - return DateUtils.getInstance() .getNextPeriod( periodType, - DateUtils.getInstance().today, + initialDate, if (creationType != SCHEDULE) 0 else 1, ) } - private fun getNextScheduleDate(): Date { + fun getNextScheduleDate(): Date { val scheduleDate = repository.getStageLastDate(enrollmentId)?.let { val lastStageDate = DateUtils.getInstance().getCalendarByDate(it) lastStageDate.add(DAY_OF_YEAR, getScheduleInterval()) @@ -125,27 +124,30 @@ class ConfigureEventReportDate( val date = DateUtils.getInstance().getCalendarByDate(enrollmentDate) val minDateFromStart = repository.getMinDaysFromStartByProgramStage() date.add(DAY_OF_YEAR, minDateFromStart) - date + periodType?.let { + return getDateBasedOnPeriodType(date.time) + } + return date.time } - return DateUtils.getInstance().getNextPeriod(null, scheduleDate.time, 0) + return DateUtils.getInstance().getNextPeriod(periodType, scheduleDate.time, if (periodType != null) 1 else 0) } - private fun getCurrentDay() = DateUtils.getInstance().today + private fun getCurrentDay() = DateUtils.getInstance().getStartOfDay(Date()) - private fun getMinDate(): Date? { + private fun getMinDate(initialDate: Date?): Date? { repository.getProgram()?.let { program -> if (periodType == null) { if (program.expiryPeriodType() != null) { val expiryDays = program.expiryDays() ?: 0 return DateUtils.getInstance().expDate( - null, + initialDate, expiryDays, program.expiryPeriodType(), ) } } else { var minDate = DateUtils.getInstance().expDate( - null, + initialDate, program.expiryDays() ?: 0, periodType, ) @@ -186,7 +188,7 @@ class ConfigureEventReportDate( when (creationType) { ADDNEW, DEFAULT, - -> DateUtils.getInstance().today + -> DateUtils.getInstance().getStartOfDay(Date()) else -> null } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt index 6e9057cb0b..e95fb9b5b3 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt @@ -26,9 +26,10 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.usecases.MapStyleConfiguration import org.dhis2.maps.utils.DhisMapUtils import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.dhis2.usescases.events.EventInfoProvider import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.D2 @@ -181,10 +182,18 @@ class ProgramEventDetailModule( @PerActivity fun provideCreateEventUseCase( dispatcher: DispatcherProvider, - d2: D2, - dateUtils: DateUtils, + repository: CreateEventUseCaseRepository, ) = CreateEventUseCase( dispatcher = dispatcher, + repository = repository, + ) + + @Provides + @PerActivity + fun provideCreateEventUseCaseRepository( + d2: D2, + dateUtils: DateUtils, + ) = CreateEventUseCaseRepository( d2 = d2, dateUtils = dateUtils, ) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt index b7672ee059..425587c116 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt @@ -23,7 +23,7 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.layer.basemaps.BaseMapStyle import org.dhis2.maps.usecases.MapStyleConfiguration import org.dhis2.tracker.NavigationBarUIState -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt index e5678ce95b..0b0d55bf23 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.usecases.MapStyleConfiguration -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt deleted file mode 100644 index e053a02c0e..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.dhis2.usescases.programEventDetail.usecase - -import kotlinx.coroutines.withContext -import org.dhis2.commons.date.DateUtils -import org.dhis2.commons.viewmodel.DispatcherProvider -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.event.EventCreateProjection -import org.hisp.dhis.android.core.maintenance.D2Error - -class CreateEventUseCase( - private val dispatcher: DispatcherProvider, - private val d2: D2, - private val dateUtils: DateUtils, -) { - suspend operator fun invoke( - programUid: String, - orgUnitUid: String, - programStageUid: String, - enrollmentUid: String?, - ): Result = withContext(dispatcher.io()) { - try { - val eventUid = d2.eventModule().events().blockingAdd( - EventCreateProjection.builder().apply { - enrollmentUid?.let { enrollment(enrollmentUid) } - program(programUid) - programStage(programStageUid) - organisationUnit(orgUnitUid) - }.build(), - ) - - val eventRepository = d2.eventModule().events().uid(eventUid) - eventRepository.setEventDate(dateUtils.today) - - Result.success(eventUid) - } catch (error: D2Error) { - Result.failure(error) - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt index 551383a75c..f8b3b34302 100644 --- a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt +++ b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt @@ -11,7 +11,8 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.RulesUtilsProvider -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.hisp.dhis.android.core.D2 @PerActivity @@ -73,9 +74,15 @@ class ProgramStageSelectionModule( @PerActivity fun provideCreateEventUseCase( dispatcherProvider: DispatcherProvider, + repository: CreateEventUseCaseRepository, + ) = CreateEventUseCase(dispatcherProvider, repository) + + @Provides + @PerActivity + fun provideCreateEventUseCaseRepository( d2: D2, dateUtils: DateUtils, - ) = CreateEventUseCase(dispatcherProvider, d2, dateUtils) + ) = CreateEventUseCaseRepository(d2, dateUtils) @Provides @PerActivity diff --git a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt index e19a9e7843..76d2e68299 100644 --- a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt @@ -11,7 +11,7 @@ import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.RulesUtilsProvider import org.dhis2.form.model.EventMode -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.Result import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 4daad887b2..7aecc48978 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -301,7 +301,7 @@ public Observable> saveToEnroll(@NonNull String teiType, .organisationUnit(orgUnit) .build()) .map(enrollmentUid -> { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setEnrollmentDate(DateUtils.getInstance().getToday()); + d2.enrollmentModule().enrollments().uid(enrollmentUid).setEnrollmentDate(DateUtils.getInstance().getStartOfDay(new Date())); d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(false); return Pair.create(enrollmentUid, uid); }) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt index fc5218a1c7..4c21b073f5 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt @@ -22,7 +22,8 @@ import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.mobileProgramRules.RuleEngineHelper -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.dhis2.usescases.teiDashboard.DashboardRepository import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.ui.mapper.TEIEventCardMapper import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions @@ -171,10 +172,17 @@ class TEIDataModule( @Provides fun provideCreateEventUseCase( dispatcherProvider: DispatcherProvider, - d2: D2, - dateUtils: DateUtils, + repository: CreateEventUseCaseRepository, ) = CreateEventUseCase( dispatcher = dispatcherProvider, + repository = repository, + ) + + @Provides + fun provideCreateEventUseCaseRepository( + d2: D2, + dateUtils: DateUtils, + ) = CreateEventUseCaseRepository( d2 = d2, dateUtils = dateUtils, ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index dc68a901e1..c910d79cbe 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -31,10 +31,10 @@ import org.dhis2.form.data.OptionsRepository import org.dhis2.form.data.RulesUtilsProviderImpl import org.dhis2.form.model.EventMode import org.dhis2.mobileProgramRules.RuleEngineHelper +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity.Companion.getActivityBundle import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.usescases.programStageSelection.ProgramStageSelectionActivity import org.dhis2.usescases.teiDashboard.DashboardRepository import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.decrement diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index b8c8db5701..9c2070b1d4 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt @@ -162,10 +162,9 @@ class SchedulingViewModel( resourceManager = resourceManager, eventResourcesProvider = eventResourcesProvider, ) - private fun loadProgramStage(event: Event? = null) { viewModelScope.launch { - val selectedDate = event?.dueDate() + val selectedDate = event?.dueDate() ?: configureEventReportDate.getNextScheduleDate() configureEventReportDate(selectedDate = selectedDate).collect { _eventDate.value = it } @@ -290,7 +289,7 @@ class SchedulingViewModel( val programUid = event.program() ?: return@launch d2.eventModule().events().uid(launchMode.eventUid).run { - setEventDate(dateUtils.today) + setEventDate(dateUtils.getStartOfDay(Date())) setStatus(EventStatus.ACTIVE) } diff --git a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt index dbf9d2dc26..d18240fe6c 100644 --- a/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt +++ b/app/src/test/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDateTest.kt @@ -65,30 +65,6 @@ class ConfigureEventReportDateTest { assert(eventDate.label == EVENT_DATE) } - @Test - fun `Should return current day when new event`() = runBlocking { - // Given the creation of new event - whenever(repository.getEvent()) doReturn null - whenever(programStage.displayEventLabel()) doReturn null - - configureEventReportDate = ConfigureEventReportDate( - resourceProvider = resourcesProvider, - repository = repository, - periodUtils = periodUtils, - ) - val currentDay = - DateUtils.uiDateFormat().format(DateUtils.getInstance().today) - - // When reportDate is invoked - val eventDate = configureEventReportDate.invoke().first() - - // Then report date should be active - assert(eventDate.active) - // Then reportDate should be the current day - assert(eventDate.dateValue == currentDay) - assert(eventDate.label == NEXT_EVENT) - } - @Test fun `Should return tomorrow when new daily event`() = runBlocking { // Given the creation of new event @@ -99,8 +75,16 @@ class ConfigureEventReportDateTest { repository = repository, periodType = periodType, periodUtils = periodUtils, + enrollmentId = ENROLLMENT_ID, ) + val today = "15/02/2022" + whenever( + repository.getEnrollmentDate(ENROLLMENT_ID), + ) doReturn DateUtils.getInstance().getStartOfDay(DateUtils.uiDateFormat().parse(today)) + whenever( + repository.getEnrollmentIncidentDate(ENROLLMENT_ID), + ) doReturn DateUtils.getInstance().getStartOfDay(DateUtils.uiDateFormat().parse(today)) val tomorrow = "16/02/2022" whenever( @@ -156,6 +140,7 @@ class ConfigureEventReportDateTest { whenever( repository.getStageLastDate(ENROLLMENT_ID), ) doReturn null + whenever( repository.getProgramStage()?.generatedByEnrollmentDate(), ) doReturn true diff --git a/app/src/test/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCaseTest.kt b/app/src/test/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCaseTest.kt index dcf82cf4cb..8743edd6c3 100644 --- a/app/src/test/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCaseTest.kt +++ b/app/src/test/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCaseTest.kt @@ -4,16 +4,22 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.dhis2.commons.date.DateUtils import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.filters.internal.StringFilterConnector +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.event.EventCollectionRepository import org.hisp.dhis.android.core.event.EventCreateProjection import org.hisp.dhis.android.core.event.EventModule import org.hisp.dhis.android.core.event.EventObjectRepository import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Test +import org.mockito.Mockito import org.mockito.kotlin.any -import org.mockito.kotlin.argThat import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock @@ -23,87 +29,61 @@ import java.util.Date class CreateEventUseCaseTest { - private val programUid = "programUid" - private val orgUnitUid = "orgUnitUid" - private val programStageUid = "programStageUid" - private val eventUid = "eventUid" - private val dispatcherProvider: DispatcherProvider = mock { on { io() } doReturn Dispatchers.Unconfined } + private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) + private val eventRepository: EventObjectRepository = mock() + private val eventCollectionRepository: EventCollectionRepository = mock() + + private val stringFilterConnector: StringFilterConnector = mock() + val eventModule: EventModule = mock { on { events() } doReturn mock() - on { events().uid(eventUid) } doReturn eventRepository + on { events().uid(EVENT_ID) } doReturn eventRepository + on { events().uid(EVENT_ID) } doReturn eventRepository } - private val d2: D2 = mock { - on { eventModule() } doReturn eventModule - } + private val dateUtils: DateUtils = DateUtils.getInstance() - private val dateUtils: DateUtils = mock { - on { today } doReturn Date() - } + private lateinit var repository: CreateEventUseCaseRepository - private val createEventUseCase: CreateEventUseCase = CreateEventUseCase( - dispatcher = dispatcherProvider, - d2 = d2, - dateUtils = dateUtils, - ) + private lateinit var createEventUseCase: CreateEventUseCase + + @Before + fun setUp() { + repository = CreateEventUseCaseRepository(d2, dateUtils) + createEventUseCase = CreateEventUseCase(dispatcherProvider, repository) + mockD2Resources() + } @Test fun `create event with enrollment`() { - val enrollmentUid = "enrollmentUid" - - whenever( - d2.eventModule().events().blockingAdd(any()), - ) doReturn eventUid + var result: Result runBlocking { - val result = createEventUseCase(programUid, orgUnitUid, programStageUid, enrollmentUid) - assertEquals(Result.success(eventUid), result) + result = createEventUseCase(PROGRAM_ID, ORG_UNIT_ID, PROGRAM_STAGE_ID, ENROLLMENT_ID) + assertEquals(Result.success(EVENT_ID), result) } - - verify(eventModule.events()).blockingAdd( - argThat { - this.enrollment() == enrollmentUid && - this.program() == programUid && - this.programStage() == programStageUid && - this.organisationUnit() == orgUnitUid - }, - ) - verify(eventRepository).setEventDate(any()) } @Test fun `create event without enrollment`() { - whenever( - d2.eventModule().events().blockingAdd(any()), - ) doReturn eventUid + var result: Result runBlocking { - val result = createEventUseCase(programUid, orgUnitUid, programStageUid, null) - assertEquals(Result.success(eventUid), result) + result = createEventUseCase(PROGRAM_ID, ORG_UNIT_ID, PROGRAM_STAGE_ID, null) + assertEquals(Result.success(EVENT_ID), result) } - - verify(eventModule.events()).blockingAdd( - argThat { - this.enrollment() == null && - this.program() == programUid && - this.programStage() == programStageUid && - this.organisationUnit() == orgUnitUid - }, - ) - verify(eventRepository).setEventDate(any()) } @Test fun `create event with error`() { - val enrollmentUid = "enrollmentUid" val error = D2Error.builder() .errorCode(D2ErrorCode.UNEXPECTED) .errorDescription("Error creating the event").build() @@ -113,8 +93,86 @@ class CreateEventUseCaseTest { ) doThrow (error) runBlocking { - val result = createEventUseCase(programUid, orgUnitUid, programStageUid, enrollmentUid) + val result = createEventUseCase(PROGRAM_ID, ORG_UNIT_ID, PROGRAM_STAGE_ID, ENROLLMENT_ID) assertEquals(error, result.exceptionOrNull()) } } + + private fun mockD2Resources() { + whenever( + d2.eventModule().events().blockingAdd(any()), + ) doReturn EVENT_ID + + whenever( + d2.eventModule().events().uid(EVENT_ID), + ) doReturn eventRepository + + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(ENROLLMENT_ID), + ) doReturn eventCollectionRepository + + whenever( + eventCollectionRepository.byProgramStageUid(), + ) doReturn stringFilterConnector + + whenever( + stringFilterConnector.eq(any()), + ) doReturn eventCollectionRepository + + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(ENROLLMENT_ID).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted(), + ) doReturn mock() + + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(ENROLLMENT_ID).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse, + ) doReturn mock() + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(ENROLLMENT_ID).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse + .orderByEventDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn mock() + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(ENROLLMENT_ID).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse + .orderByDueDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn mock() + + whenever( + d2.eventModule().events().uid(EVENT_ID), + ) doReturn eventRepository + + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(null), + ) doReturn eventCollectionRepository + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(null).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted(), + ) doReturn mock() + + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(null).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse, + ) doReturn mock() + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(null).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse + .orderByEventDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn mock() + whenever( + d2.eventModule().events().byEnrollmentUid() + .eq(null).byProgramStageUid().eq(PROGRAM_STAGE_ID).byDeleted().isFalse + .orderByDueDate(RepositoryScope.OrderByDirection.DESC), + ) doReturn mock() + } + companion object { + const val PROGRAM_STAGE_ID = "programStageId" + const val ENROLLMENT_ID = "enrollmentId" + const val PROGRAM_ID = "programId" + const val EVENT_ID = "eventId" + const val ORG_UNIT_ID = "orgUnitId" + } } diff --git a/app/src/test/java/org/dhis2/usescases/programstageselection/ProgramStageSelectionPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/programstageselection/ProgramStageSelectionPresenterTest.kt index 4eade43848..834d4266c0 100644 --- a/app/src/test/java/org/dhis2/usescases/programstageselection/ProgramStageSelectionPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/programstageselection/ProgramStageSelectionPresenterTest.kt @@ -37,8 +37,8 @@ import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.form.data.RulesUtilsProvider import org.dhis2.form.model.EventMode +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.ui.MetadataIconData -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.usescases.programStageSelection.ProgramStageData import org.dhis2.usescases.programStageSelection.ProgramStageSelectionPresenter import org.dhis2.usescases.programStageSelection.ProgramStageSelectionRepository diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt index 623dd86871..81f92f4d9f 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/dashboardfragments/data/TeiDataPresenterTest.kt @@ -22,8 +22,8 @@ import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.form.model.EventMode import org.dhis2.mobileProgramRules.RuleEngineHelper +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.ui.MetadataIconData -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.usescases.teiDashboard.DashboardRepository import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.EventCreationOptionsMapper import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataContracts diff --git a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java index ced9f157fa..490f46de22 100644 --- a/commons/src/main/java/org/dhis2/commons/date/DateUtils.java +++ b/commons/src/main/java/org/dhis2/commons/date/DateUtils.java @@ -87,18 +87,6 @@ private Date getDate(Date date) { return calendar.getTime(); } - private Date getNextDate(Date date) { - Calendar calendar = getCalendar(); - calendar.setTime(date); - calendar.add(Calendar.DAY_OF_MONTH, 1); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - return calendar.getTime(); - } - private Date getFirstDayOfWeek(Date date) { Calendar calendar = getCalendar(); @@ -275,11 +263,17 @@ public Calendar getCalendarByDate(Date date) { public void setCurrentDate(Date date) { currentDateCalendar = getCalendar(); - currentDateCalendar.setTime(date); - currentDateCalendar.set(Calendar.HOUR_OF_DAY, 0); - currentDateCalendar.set(Calendar.MINUTE, 0); - currentDateCalendar.set(Calendar.SECOND, 0); - currentDateCalendar.set(Calendar.MILLISECOND, 0); + currentDateCalendar.setTime(getStartOfDay(date)); + } + + public Date getStartOfDay( Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); } /** @@ -348,13 +342,11 @@ public Boolean isEventExpired(@Nullable Date currentDate, Date completedDay, int * @return Min date to select */ public Date expDate(@Nullable Date currentDate, int expiryDays, @Nullable PeriodType expiryPeriodType) { - - Calendar calendar = getCalendar(); - + Calendar calendar = Calendar.getInstance(); if (currentDate != null) calendar.setTime(currentDate); - Date date = calendar.getTime(); + Date date = getStartOfDay( calendar.getTime()); if (expiryPeriodType == null) { return null; @@ -418,7 +410,7 @@ public Date expDate(@Nullable Date currentDate, int expiryDays, @Nullable Period case Monthly: Date firstDateOfMonth = getFirstDayOfMonth(calendar.getTime()); calendar.setTime(firstDateOfMonth); - if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfMonth.getTime()) >= expiryDays) { + if (TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfMonth.getTime()) >= expiryDays || TimeUnit.MILLISECONDS.toDays(date.getTime() - firstDateOfMonth.getTime()) == 0) { return firstDateOfMonth; } else { calendar.add(Calendar.MONTH, -1); diff --git a/form/src/main/java/org/dhis2/form/data/EventRepository.kt b/form/src/main/java/org/dhis2/form/data/EventRepository.kt index 2f21c40d76..19c13a9c5a 100644 --- a/form/src/main/java/org/dhis2/form/data/EventRepository.kt +++ b/form/src/main/java/org/dhis2/form/data/EventRepository.kt @@ -38,10 +38,12 @@ import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.imports.ImportStatus import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.android.core.program.ProgramStageDataElement import org.hisp.dhis.android.core.program.ProgramStageSection import org.hisp.dhis.android.core.program.SectionRenderingType import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import java.util.Calendar.DAY_OF_YEAR import java.util.Date class EventRepository( @@ -426,7 +428,7 @@ class EventRepository( PeriodSelector( type = periodType, minDate = getPeriodMinDate(periodType), - maxDate = dateUtils.today, + maxDate = dateUtils.getStartOfDay(Date()), ) } else { null @@ -439,8 +441,9 @@ class EventRepository( .withTrackedEntityType() .byUid().eq(programUid) .one().blockingGet()?.let { program -> + val firstAvailablePeriodDate = getFirstAvailablePeriod(event?.enrollment(), programStage) var minDate = dateUtils.expDate( - null, + firstAvailablePeriodDate, program.expiryDays() ?: 0, periodType, ) @@ -466,6 +469,67 @@ class EventRepository( return null } + private fun getFirstAvailablePeriod(enrollmentUid: String?, programStage: ProgramStage?): Date { + val stageLastDate = getStageLastDate() + val minEventDate = stageLastDate ?: when (programStage?.generatedByEnrollmentDate()) { + true -> getEnrollmentDate(enrollmentUid) + else -> getEnrollmentIncidentDate(enrollmentUid) + ?: getEnrollmentDate(enrollmentUid) + } + val calendar = DateUtils.getInstance().getCalendarByDate(minEventDate) + if (stageLastDate == null) { + val minDaysFromStart = getMinDaysFromStartByProgramStage(programStage) + calendar.add(DAY_OF_YEAR, minDaysFromStart) + } else { + calendar.add(DAY_OF_YEAR, programStage?.standardInterval() ?: 0) + } + return dateUtils.getNextPeriod(programStage?.periodType(), calendar.time ?: event?.eventDate(), 1) + } + + private fun getStageLastDate(): Date? { + val enrollmentUid = event?.enrollment() + val programStageUid = programStage?.uid() + val activeEvents = + d2.eventModule().events().byEnrollmentUid() + .eq(enrollmentUid).byProgramStageUid() + .eq(programStageUid) + .byDeleted().isFalse + .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet().filter { it.uid() != eventUid } + val scheduleEvents = + d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).byProgramStageUid() + .eq(programStageUid) + .byDeleted().isFalse + .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet() + + var activeDate: Date? = null + var scheduleDate: Date? = null + if (activeEvents.isNotEmpty()) { + activeDate = activeEvents[0].eventDate() + } + if (scheduleEvents.isNotEmpty()) scheduleDate = scheduleEvents[0].dueDate() + + return when { + scheduleDate == null -> activeDate + activeDate == null -> scheduleDate + activeDate.before(scheduleDate) -> scheduleDate + else -> activeDate + } + } + + private fun getMinDaysFromStartByProgramStage(programStage: ProgramStage?): Int { + return programStage?.minDaysFromStart() ?: 0 + } + + private fun getEnrollmentDate(uid: String?): Date? { + val enrollment = d2.enrollmentModule().enrollments().byUid().eq(uid).blockingGet().first() + return enrollment.enrollmentDate() + } + + private fun getEnrollmentIncidentDate(uid: String?): Date? { + val enrollment = d2.enrollmentModule().enrollments().uid(uid).blockingGet() + return enrollment?.incidentDate() + } + private fun createEventDetailsSection(): FieldUiModel { return fieldFactory.createSection( sectionUid = EVENT_DETAILS_SECTION_UID, diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCase.kt b/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCase.kt new file mode 100644 index 0000000000..a3064ddd03 --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCase.kt @@ -0,0 +1,19 @@ +package org.dhis2.tracker.events + +import kotlinx.coroutines.withContext +import org.dhis2.commons.viewmodel.DispatcherProvider + +class CreateEventUseCase( + private val dispatcher: DispatcherProvider, + private val repository: CreateEventUseCaseRepository, +) { + suspend operator fun invoke( + programUid: String, + orgUnitUid: String, + programStageUid: String, + enrollmentUid: String?, + ): Result = withContext(dispatcher.io()) { + repository.createEvent(enrollmentUid, programUid, programStageUid, orgUnitUid) + } + +} diff --git a/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCaseRepository.kt b/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCaseRepository.kt new file mode 100644 index 0000000000..b1da4fff11 --- /dev/null +++ b/tracker/src/main/kotlin/org/dhis2/tracker/events/CreateEventUseCaseRepository.kt @@ -0,0 +1,86 @@ +package org.dhis2.tracker.events + +import org.dhis2.commons.date.DateUtils +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.event.EventCreateProjection +import org.hisp.dhis.android.core.maintenance.D2Error +import org.hisp.dhis.android.core.program.ProgramStage +import java.util.Date + +class CreateEventUseCaseRepository( + private val d2: D2, + private val dateUtils: DateUtils +) +{ + fun createEvent(enrollmentUid: String?, programUid: String, programStageUid: String?, orgUnitUid: String): Result { + return try { + val stageLastDate = getStageLastDate(enrollmentUid, programStageUid) + val programStage = getProgramStage(programStageUid) + val eventUid = d2.eventModule().events().blockingAdd( + EventCreateProjection.builder().apply { + enrollmentUid?.let { enrollment(enrollmentUid) } + program(programUid) + programStage(programStageUid) + organisationUnit(orgUnitUid) + }.build(), + ) + setEventDate(eventUid, programStage, stageLastDate) + Result.success(eventUid) + } catch (error: D2Error) { + Result.failure(error) + } + } + + private fun setEventDate( eventUid: String, programStage: ProgramStage?, stageLastDate: Date?) { + val currentDate = dateUtils.getStartOfDay(Date()) + val eventDate = if (stageLastDate != null && programStage?.periodType() != null ) { + val currentPeriod = dateUtils.getNextPeriod(programStage.periodType(),currentDate , 0) + val lastEventDatePeriod = dateUtils.getNextPeriod(programStage.periodType(), stageLastDate, 0) + if(currentPeriod == lastEventDatePeriod || lastEventDatePeriod.after(currentPeriod)) { + dateUtils.getNextPeriod(programStage.periodType(),currentDate , 1) + } else { + dateUtils.getNextPeriod(programStage.periodType(),currentDate , 0) + } + } else { + currentDate + } + val eventRepository = d2.eventModule().events().uid(eventUid) + eventRepository.setEventDate(eventDate) + } + + private fun getStageLastDate(enrollmentUid: String?, programStageUid: String?): Date? { + val activeEvents = + d2.eventModule().events().byEnrollmentUid() + .eq(enrollmentUid).byProgramStageUid() + .eq(programStageUid) + .byDeleted().isFalse + .orderByEventDate(RepositoryScope.OrderByDirection.DESC).blockingGet() + val scheduleEvents = + d2.eventModule().events().byEnrollmentUid().eq(enrollmentUid).byProgramStageUid() + .eq(programStageUid) + .byDeleted().isFalse + .orderByDueDate(RepositoryScope.OrderByDirection.DESC).blockingGet() + + var activeDate: Date? = null + var scheduleDate: Date? = null + if (activeEvents.isNotEmpty()) { + activeDate = activeEvents[0].eventDate() + } + if (scheduleEvents.isNotEmpty()) scheduleDate = scheduleEvents[0].dueDate() + + return when { + scheduleDate == null -> activeDate + activeDate == null -> scheduleDate + activeDate.before(scheduleDate) -> scheduleDate + else -> activeDate + } + } + + private fun getProgramStage(programStageUid: String?): ProgramStage? { + return d2.programModule() + .programStages() + .uid(programStageUid) + .blockingGet() + } +} \ No newline at end of file