Skip to content

Commit 62b899b

Browse files
authored
feat: [ANDROAPP-7605] Add granular sync usecase for programs (#4954)
1 parent 510a142 commit 62b899b

5 files changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.model.ProgramType
4+
import org.hisp.dhis.android.core.D2
5+
import org.hisp.dhis.android.core.common.State
6+
import org.hisp.dhis.android.core.program.ProgramType as CoreProgramType
7+
8+
internal class SyncProgramRepositoryImpl(
9+
private val d2: D2,
10+
) : SyncProgramRepository {
11+
override suspend fun getProgramType(programUid: String): ProgramType =
12+
when (
13+
d2
14+
.programModule()
15+
.programs()
16+
.uid(programUid)
17+
.suspendGet()
18+
?.programType
19+
) {
20+
CoreProgramType.WITH_REGISTRATION -> ProgramType.Tracker
21+
CoreProgramType.WITHOUT_REGISTRATION -> ProgramType.Event
22+
null -> ProgramType.None
23+
}
24+
25+
override suspend fun uploadTrackerProgram(programUid: String) {
26+
d2
27+
.trackedEntityModule()
28+
.trackedEntityInstances()
29+
.byProgramUids(listOf(programUid))
30+
.blockingUpload()
31+
}
32+
33+
override suspend fun downloadTrackerProgram(programUid: String) {
34+
d2
35+
.trackedEntityModule()
36+
.trackedEntityInstanceDownloader()
37+
.byProgramUid(programUid)
38+
.blockingDownload()
39+
}
40+
41+
override suspend fun uploadEventProgram(programUid: String) {
42+
d2
43+
.eventModule()
44+
.events()
45+
.byProgramUid()
46+
.eq(programUid)
47+
.blockingUpload()
48+
}
49+
50+
override suspend fun downloadEventProgram(programUid: String) {
51+
d2
52+
.eventModule()
53+
.eventDownloader()
54+
.byProgramUid(programUid)
55+
.blockingDownload()
56+
}
57+
58+
override suspend fun downloadFileResources(programUid: String) {
59+
d2
60+
.fileResourceModule()
61+
.fileResourceDownloader()
62+
.byProgramUid()
63+
.eq(programUid)
64+
.blockingDownload()
65+
}
66+
67+
override suspend fun allTeisAreSynced(programUid: String) =
68+
d2
69+
.trackedEntityModule()
70+
.trackedEntityInstances()
71+
.byProgramUids(listOf(programUid))
72+
.byAggregatedSyncState()
73+
.notIn(State.SYNCED, State.RELATIONSHIP)
74+
.blockingGet()
75+
.isEmpty()
76+
77+
override suspend fun allEventsAreSynced(programUid: String) =
78+
d2
79+
.eventModule()
80+
.events()
81+
.byProgramUid()
82+
.eq(programUid)
83+
.byAggregatedSyncState()
84+
.notIn(State.SYNCED)
85+
.blockingGet()
86+
.isEmpty()
87+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.model.ProgramType
4+
5+
typealias IsSynced = Boolean
6+
7+
internal interface SyncProgramRepository {
8+
suspend fun getProgramType(programUid: String): ProgramType
9+
10+
suspend fun uploadTrackerProgram(programUid: String)
11+
12+
suspend fun downloadTrackerProgram(programUid: String)
13+
14+
suspend fun uploadEventProgram(programUid: String)
15+
16+
suspend fun downloadEventProgram(programUid: String)
17+
18+
suspend fun downloadFileResources(programUid: String)
19+
20+
suspend fun allTeisAreSynced(programUid: String): Boolean
21+
22+
suspend fun allEventsAreSynced(programUid: String): Boolean
23+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import org.dhis2.mobile.commons.domain.UseCase
4+
import org.dhis2.mobile.sync.data.SyncProgramRepository
5+
import org.dhis2.mobile.sync.model.ProgramType
6+
7+
internal typealias ProgramUid = String
8+
9+
internal class SyncProgram(
10+
private val syncProgramRepository: SyncProgramRepository,
11+
private val syncStatusController: SyncStatusController,
12+
) : UseCase<ProgramUid, Unit> {
13+
override suspend fun invoke(input: ProgramUid): Result<Unit> =
14+
try {
15+
val isSynced =
16+
when (syncProgramRepository.getProgramType(input)) {
17+
ProgramType.Event -> syncEvents(input)
18+
ProgramType.Tracker -> syncTracker(input)
19+
ProgramType.None -> return Result.failure(Exception("Unknown program"))
20+
}
21+
22+
if (isSynced) {
23+
syncStatusController.updateSingleProgramToSuccess(input)
24+
Result.success(Unit)
25+
} else {
26+
Result.failure(Exception("Status is not synced"))
27+
}
28+
} catch (e: Exception) {
29+
Result.failure(e)
30+
}
31+
32+
private suspend fun syncEvents(programUid: ProgramUid): Boolean =
33+
with(syncProgramRepository) {
34+
uploadEventProgram(programUid)
35+
val isSynced = allEventsAreSynced(programUid)
36+
if (isSynced) {
37+
downloadEventProgram(programUid)
38+
downloadFileResources(programUid)
39+
}
40+
isSynced
41+
}
42+
43+
private suspend fun syncTracker(programUid: ProgramUid): Boolean =
44+
with(syncProgramRepository) {
45+
uploadTrackerProgram(programUid)
46+
val isSynced = allTeisAreSynced(programUid)
47+
if (isSynced) {
48+
downloadTrackerProgram(programUid)
49+
downloadFileResources(programUid)
50+
}
51+
isSynced
52+
}
53+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.dhis2.mobile.sync.model
2+
3+
sealed interface ProgramType {
4+
data object Event : ProgramType
5+
6+
data object Tracker : ProgramType
7+
8+
data object None : ProgramType
9+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import kotlinx.coroutines.runBlocking
4+
import org.dhis2.mobile.sync.data.SyncProgramRepository
5+
import org.dhis2.mobile.sync.model.ProgramType
6+
import org.junit.Test
7+
import org.mockito.kotlin.mock
8+
import org.mockito.kotlin.never
9+
import org.mockito.kotlin.verify
10+
import org.mockito.kotlin.whenever
11+
import kotlin.test.assertTrue
12+
13+
class SyncProgramTest {
14+
private val repository: SyncProgramRepository = mock()
15+
private val syncStatusController = SyncStatusController()
16+
private val useCase = SyncProgram(repository, syncStatusController)
17+
18+
@Test
19+
fun `should sync event program successfully when all events are synced`() =
20+
runBlocking {
21+
val programUid = "eventProgramUid"
22+
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Event)
23+
whenever(repository.allEventsAreSynced(programUid)).thenReturn(true)
24+
25+
val result = useCase(programUid)
26+
27+
assertTrue(result.isSuccess)
28+
verify(repository).uploadEventProgram(programUid)
29+
verify(repository).downloadEventProgram(programUid)
30+
verify(repository).downloadFileResources(programUid)
31+
}
32+
33+
@Test
34+
fun `should not download event program when events are not synced`() =
35+
runBlocking {
36+
val programUid = "eventProgramUid"
37+
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Event)
38+
whenever(repository.allEventsAreSynced(programUid)).thenReturn(false)
39+
40+
val result = useCase(programUid)
41+
42+
assertTrue(result.isFailure)
43+
verify(repository).uploadEventProgram(programUid)
44+
verify(repository, never()).downloadEventProgram(programUid)
45+
verify(repository, never()).downloadFileResources(programUid)
46+
}
47+
48+
@Test
49+
fun `should sync tracker program successfully when all teis are synced`() =
50+
runBlocking {
51+
val programUid = "trackerProgramUid"
52+
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Tracker)
53+
whenever(repository.allTeisAreSynced(programUid)).thenReturn(true)
54+
55+
val result = useCase(programUid)
56+
57+
assertTrue(result.isSuccess)
58+
verify(repository).uploadTrackerProgram(programUid)
59+
verify(repository).downloadTrackerProgram(programUid)
60+
verify(repository).downloadFileResources(programUid)
61+
}
62+
63+
@Test
64+
fun `should not download tracker program when teis are not synced`() =
65+
runBlocking {
66+
val programUid = "trackerProgramUid"
67+
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Tracker)
68+
whenever(repository.allTeisAreSynced(programUid)).thenReturn(false)
69+
70+
val result = useCase(programUid)
71+
72+
assertTrue(result.isFailure)
73+
verify(repository).uploadTrackerProgram(programUid)
74+
verify(repository, never()).downloadTrackerProgram(programUid)
75+
verify(repository, never()).downloadFileResources(programUid)
76+
}
77+
78+
@Test
79+
fun `should return failure for unknown program type`() =
80+
runBlocking {
81+
val programUid = "unknownProgramUid"
82+
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.None)
83+
84+
val result = useCase(programUid)
85+
86+
assertTrue(result.isFailure)
87+
}
88+
89+
@Test
90+
fun `should return failure when getProgramType throws`() =
91+
runBlocking {
92+
val programUid = "programUid"
93+
whenever(repository.getProgramType(programUid)).thenThrow(RuntimeException("type error"))
94+
95+
val result = useCase(programUid)
96+
97+
assertTrue(result.isFailure)
98+
}
99+
}

0 commit comments

Comments
 (0)