Skip to content

Commit f5f8fc7

Browse files
authored
feat: [ANDROAPP-7605] Add granular sync use-case for datasets and datavalues (#4957)
1 parent 9edd5c9 commit f5f8fc7

9 files changed

Lines changed: 381 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.domain.DataSetUid
4+
import org.hisp.dhis.android.core.D2
5+
6+
internal class SyncDataSetRepositoryImpl(
7+
private val d2: D2,
8+
) : SyncDataSetRepository {
9+
override suspend fun uploadDataSet(dataSetUid: DataSetUid) {
10+
d2
11+
.dataSetModule()
12+
.dataSetInstances()
13+
.byDataSetUid()
14+
.eq(dataSetUid)
15+
.blockingGet()
16+
.forEach { dataSetInstance ->
17+
d2
18+
.dataValueModule()
19+
.dataValues()
20+
.byOrganisationUnitUid()
21+
.eq(dataSetInstance.organisationUnitUid())
22+
.byPeriod()
23+
.eq(dataSetInstance.period())
24+
.byAttributeOptionComboUid()
25+
.eq(dataSetInstance.attributeOptionComboUid())
26+
.blockingUpload()
27+
}
28+
}
29+
30+
override suspend fun uploadCompleteRegistration(dataSetUid: DataSetUid) {
31+
d2
32+
.dataSetModule()
33+
.dataSetCompleteRegistrations()
34+
.byDataSetUid()
35+
.eq(dataSetUid)
36+
.blockingUpload()
37+
}
38+
39+
// TODO: The sdk does not provide methods to download data values for a given dataset
40+
override suspend fun downloadDataSet(dataSetUid: DataSetUid) {
41+
// Do nothing
42+
}
43+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.domain.DataSetUid
4+
import org.dhis2.mobile.sync.model.CategoryOptionComboUid
5+
import org.dhis2.mobile.sync.model.OrgUnitUid
6+
import org.dhis2.mobile.sync.model.PeriodId
7+
import org.hisp.dhis.android.core.D2
8+
9+
internal class SyncDataValueRepositoryImpl(
10+
private val d2: D2,
11+
) : SyncDataValueRepository {
12+
override suspend fun uploadDataValues(
13+
dataSetUid: DataSetUid,
14+
orgUnitUid: OrgUnitUid,
15+
periodId: PeriodId,
16+
attributeOptionComboUid: CategoryOptionComboUid,
17+
categoryOptionComboUids: List<CategoryOptionComboUid>,
18+
) {
19+
d2
20+
.dataValueModule()
21+
.dataValues()
22+
.byAttributeOptionComboUid()
23+
.eq(attributeOptionComboUid)
24+
.byOrganisationUnitUid()
25+
.eq(orgUnitUid)
26+
.byPeriod()
27+
.eq(periodId)
28+
.byCategoryOptionComboUid()
29+
.`in`(categoryOptionComboUids)
30+
.blockingUpload()
31+
}
32+
33+
override suspend fun uploadCompleteRegistrations(
34+
dataSetUid: DataSetUid,
35+
orgUnitUid: OrgUnitUid,
36+
periodId: PeriodId,
37+
attributeOptionComboUid: CategoryOptionComboUid,
38+
) {
39+
d2
40+
.dataSetModule()
41+
.dataSetCompleteRegistrations()
42+
.byDataSetUid()
43+
.eq(dataSetUid)
44+
.byAttributeOptionComboUid()
45+
.eq(attributeOptionComboUid)
46+
.byOrganisationUnitUid()
47+
.eq(orgUnitUid)
48+
.byPeriod()
49+
.eq(periodId)
50+
.blockingUpload()
51+
}
52+
53+
// TODO: The sdk does not provide methods to download data values for a given data set instance
54+
override suspend fun downloadDataValues(
55+
dataSetUid: DataSetUid,
56+
orgUnitUid: OrgUnitUid,
57+
periodId: PeriodId,
58+
attributeOptionComboUid: CategoryOptionComboUid,
59+
) {
60+
// Do nothing
61+
}
62+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.domain.DataSetUid
4+
5+
internal interface SyncDataSetRepository {
6+
suspend fun uploadDataSet(dataSetUid: DataSetUid)
7+
8+
suspend fun uploadCompleteRegistration(dataSetUid: DataSetUid)
9+
10+
suspend fun downloadDataSet(dataSetUid: DataSetUid)
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.dhis2.mobile.sync.data
2+
3+
import org.dhis2.mobile.sync.domain.DataSetUid
4+
import org.dhis2.mobile.sync.model.CategoryOptionComboUid
5+
import org.dhis2.mobile.sync.model.OrgUnitUid
6+
import org.dhis2.mobile.sync.model.PeriodId
7+
8+
internal interface SyncDataValueRepository {
9+
suspend fun uploadDataValues(
10+
dataSetUid: DataSetUid,
11+
orgUnitUid: OrgUnitUid,
12+
periodId: PeriodId,
13+
attributeOptionComboUid: CategoryOptionComboUid,
14+
categoryOptionComboUids: List<CategoryOptionComboUid>,
15+
)
16+
17+
suspend fun uploadCompleteRegistrations(
18+
dataSetUid: DataSetUid,
19+
orgUnitUid: OrgUnitUid,
20+
periodId: PeriodId,
21+
attributeOptionComboUid: CategoryOptionComboUid,
22+
)
23+
24+
suspend fun downloadDataValues(
25+
dataSetUid: DataSetUid,
26+
orgUnitUid: OrgUnitUid,
27+
periodId: PeriodId,
28+
attributeOptionComboUid: CategoryOptionComboUid,
29+
)
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import org.dhis2.mobile.commons.domain.UseCase
4+
import org.dhis2.mobile.sync.data.SyncDataSetRepository
5+
6+
internal typealias DataSetUid = String
7+
8+
internal class SyncDataSet(
9+
private val syncDataSetRepository: SyncDataSetRepository,
10+
) : UseCase<DataSetUid, Unit> {
11+
override suspend fun invoke(input: DataSetUid): Result<Unit> =
12+
try {
13+
syncDataSetRepository.uploadDataSet(input)
14+
syncDataSetRepository.uploadCompleteRegistration(input)
15+
syncDataSetRepository.downloadDataSet(input)
16+
return Result.success(Unit)
17+
} catch (e: Exception) {
18+
Result.failure(e)
19+
}
20+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import org.dhis2.mobile.commons.domain.UseCase
4+
import org.dhis2.mobile.sync.data.SyncDataValueRepository
5+
import org.dhis2.mobile.sync.model.SyncDataValueInput
6+
7+
internal class SyncDataValue(
8+
private val syncDataValueRepository: SyncDataValueRepository,
9+
) : UseCase<SyncDataValueInput, Unit> {
10+
override suspend fun invoke(input: SyncDataValueInput): Result<Unit> =
11+
try {
12+
syncDataValueRepository.uploadDataValues(
13+
dataSetUid = input.dataSetUid,
14+
orgUnitUid = input.orgUnitUid,
15+
periodId = input.periodId,
16+
attributeOptionComboUid = input.attrOptionComboUid,
17+
categoryOptionComboUids = input.categoryOptionComboUid,
18+
)
19+
syncDataValueRepository.uploadCompleteRegistrations(
20+
dataSetUid = input.dataSetUid,
21+
orgUnitUid = input.orgUnitUid,
22+
periodId = input.periodId,
23+
attributeOptionComboUid = input.attrOptionComboUid,
24+
)
25+
syncDataValueRepository.downloadDataValues(
26+
dataSetUid = input.dataSetUid,
27+
orgUnitUid = input.orgUnitUid,
28+
periodId = input.periodId,
29+
attributeOptionComboUid = input.attrOptionComboUid,
30+
)
31+
Result.success(Unit)
32+
} catch (e: Exception) {
33+
Result.failure(e)
34+
}
35+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.dhis2.mobile.sync.model
2+
3+
import org.dhis2.mobile.sync.domain.DataSetUid
4+
5+
internal typealias OrgUnitUid = String
6+
internal typealias CategoryOptionComboUid = String
7+
internal typealias PeriodId = String
8+
9+
internal data class SyncDataValueInput(
10+
val dataSetUid: DataSetUid,
11+
val orgUnitUid: OrgUnitUid,
12+
val periodId: PeriodId,
13+
val attrOptionComboUid: CategoryOptionComboUid,
14+
val categoryOptionComboUid: List<CategoryOptionComboUid>,
15+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import kotlinx.coroutines.runBlocking
4+
import org.dhis2.mobile.sync.data.SyncDataSetRepository
5+
import org.junit.Test
6+
import org.mockito.kotlin.mock
7+
import org.mockito.kotlin.verify
8+
import org.mockito.kotlin.whenever
9+
import kotlin.test.assertTrue
10+
11+
class SyncDataSetTest {
12+
private val repository: SyncDataSetRepository = mock()
13+
private val useCase = SyncDataSet(repository)
14+
15+
@Test
16+
fun `should return success and call all repository methods in order`() =
17+
runBlocking {
18+
val dataSetUid = "dataSetUid"
19+
20+
val result = useCase(dataSetUid)
21+
22+
assertTrue(result.isSuccess)
23+
verify(repository).uploadDataSet(dataSetUid)
24+
verify(repository).uploadCompleteRegistration(dataSetUid)
25+
verify(repository).downloadDataSet(dataSetUid)
26+
}
27+
28+
@Test
29+
fun `should return failure when uploadDataSet throws`() =
30+
runBlocking {
31+
val dataSetUid = "dataSetUid"
32+
whenever(repository.uploadDataSet(dataSetUid)).thenThrow(RuntimeException("upload error"))
33+
34+
val result = useCase(dataSetUid)
35+
36+
assertTrue(result.isFailure)
37+
}
38+
39+
@Test
40+
fun `should return failure when uploadCompleteRegistration throws`() =
41+
runBlocking {
42+
val dataSetUid = "dataSetUid"
43+
whenever(repository.uploadCompleteRegistration(dataSetUid))
44+
.thenThrow(RuntimeException("registration error"))
45+
46+
val result = useCase(dataSetUid)
47+
48+
assertTrue(result.isFailure)
49+
}
50+
51+
@Test
52+
fun `should return failure when downloadDataSet throws`() =
53+
runBlocking {
54+
val dataSetUid = "dataSetUid"
55+
whenever(repository.downloadDataSet(dataSetUid))
56+
.thenThrow(RuntimeException("download error"))
57+
58+
val result = useCase(dataSetUid)
59+
60+
assertTrue(result.isFailure)
61+
}
62+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.dhis2.mobile.sync.domain
2+
3+
import kotlinx.coroutines.runBlocking
4+
import org.dhis2.mobile.sync.data.SyncDataValueRepository
5+
import org.dhis2.mobile.sync.model.SyncDataValueInput
6+
import org.junit.Test
7+
import org.mockito.kotlin.mock
8+
import org.mockito.kotlin.verify
9+
import org.mockito.kotlin.whenever
10+
import kotlin.test.assertTrue
11+
12+
class SyncDataValueTest {
13+
private val repository: SyncDataValueRepository = mock()
14+
private val useCase = SyncDataValue(repository)
15+
16+
private val input =
17+
SyncDataValueInput(
18+
dataSetUid = "dataSetUid",
19+
orgUnitUid = "orgUnitUid",
20+
periodId = "periodId",
21+
attrOptionComboUid = "attrOptionComboUid",
22+
categoryOptionComboUid = listOf("combo1", "combo2"),
23+
)
24+
25+
@Test
26+
fun `should return success and call all repository methods in order`() =
27+
runBlocking {
28+
val result = useCase(input)
29+
30+
assertTrue(result.isSuccess)
31+
verify(repository).uploadDataValues(
32+
dataSetUid = input.dataSetUid,
33+
orgUnitUid = input.orgUnitUid,
34+
periodId = input.periodId,
35+
attributeOptionComboUid = input.attrOptionComboUid,
36+
categoryOptionComboUids = input.categoryOptionComboUid,
37+
)
38+
verify(repository).uploadCompleteRegistrations(
39+
dataSetUid = input.dataSetUid,
40+
orgUnitUid = input.orgUnitUid,
41+
periodId = input.periodId,
42+
attributeOptionComboUid = input.attrOptionComboUid,
43+
)
44+
verify(repository).downloadDataValues(
45+
dataSetUid = input.dataSetUid,
46+
orgUnitUid = input.orgUnitUid,
47+
periodId = input.periodId,
48+
attributeOptionComboUid = input.attrOptionComboUid,
49+
)
50+
}
51+
52+
@Test
53+
fun `should return failure when uploadDataValues throws`() =
54+
runBlocking {
55+
whenever(
56+
repository.uploadDataValues(
57+
dataSetUid = input.dataSetUid,
58+
orgUnitUid = input.orgUnitUid,
59+
periodId = input.periodId,
60+
attributeOptionComboUid = input.attrOptionComboUid,
61+
categoryOptionComboUids = input.categoryOptionComboUid,
62+
),
63+
).thenThrow(RuntimeException("upload error"))
64+
65+
val result = useCase(input)
66+
67+
assertTrue(result.isFailure)
68+
}
69+
70+
@Test
71+
fun `should return failure when uploadCompleteRegistrations throws`() =
72+
runBlocking {
73+
whenever(
74+
repository.uploadCompleteRegistrations(
75+
dataSetUid = input.dataSetUid,
76+
orgUnitUid = input.orgUnitUid,
77+
periodId = input.periodId,
78+
attributeOptionComboUid = input.attrOptionComboUid,
79+
),
80+
).thenThrow(RuntimeException("complete registration error"))
81+
82+
val result = useCase(input)
83+
84+
assertTrue(result.isFailure)
85+
}
86+
87+
@Test
88+
fun `should return failure when downloadDataValues throws`() =
89+
runBlocking {
90+
whenever(
91+
repository.downloadDataValues(
92+
dataSetUid = input.dataSetUid,
93+
orgUnitUid = input.orgUnitUid,
94+
periodId = input.periodId,
95+
attributeOptionComboUid = input.attrOptionComboUid,
96+
),
97+
).thenThrow(RuntimeException("download error"))
98+
99+
val result = useCase(input)
100+
101+
assertTrue(result.isFailure)
102+
}
103+
}

0 commit comments

Comments
 (0)