Skip to content

Commit

Permalink
Add PatientAttributeSync
Browse files Browse the repository at this point in the history
  • Loading branch information
Siddharth Agarwal committed Dec 9, 2024
1 parent 3d1b8ac commit 6b9b13c
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import org.simple.clinic.sync.BloodSugarSyncIntegrationTest
import org.simple.clinic.sync.CallResultSyncIntegrationTest
import org.simple.clinic.sync.HelpSyncIntegrationTest
import org.simple.clinic.sync.MedicalHistorySyncIntegrationTest
import org.simple.clinic.sync.PatientAttributeSyncIntegrationTest
import org.simple.clinic.sync.PatientSyncIntegrationTest
import org.simple.clinic.sync.PrescriptionSyncIntegrationTest
import org.simple.clinic.sync.ProtocolSyncIntegrationTest
Expand Down Expand Up @@ -164,4 +165,5 @@ interface TestAppComponent {
fun inject(target: QuestionnaireResponseSyncIntegrationTest)
fun inject(target: DatabaseEncryptorTest)
fun inject(target: PatientAttributeRepositoryAndroidTest)
fun inject(target: PatientAttributeSyncIntegrationTest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package org.simple.clinic.sync

import com.f2prateek.rx.preferences2.Preference
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.simple.clinic.AppDatabase
import org.simple.clinic.TestClinicApp
import org.simple.clinic.main.TypedPreference
import org.simple.clinic.main.TypedPreference.Type.LastPatientAttributePullToken
import org.simple.clinic.patient.SyncStatus
import org.simple.clinic.patientattribute.BMIReading
import org.simple.clinic.patientattribute.PatientAttribute
import org.simple.clinic.patientattribute.PatientAttributeRepository
import org.simple.clinic.patientattribute.sync.PatientAttributeSync
import org.simple.clinic.patientattribute.sync.PatientAttributeSyncApi
import org.simple.clinic.rules.RegisterPatientRule
import org.simple.clinic.rules.SaveDatabaseRule
import org.simple.clinic.rules.ServerAuthenticationRule
import org.simple.clinic.user.UserSession
import org.simple.clinic.util.unsafeLazy
import org.simple.sharedTestCode.TestData
import org.simple.sharedTestCode.util.Rules
import java.util.Optional
import java.util.UUID
import javax.inject.Inject

class PatientAttributeSyncIntegrationTest {

@Inject
lateinit var appDatabase: AppDatabase

@Inject
lateinit var repository: PatientAttributeRepository

@Inject
@TypedPreference(LastPatientAttributePullToken)
lateinit var lastPullToken: Preference<Optional<String>>

@Inject
lateinit var syncApi: PatientAttributeSyncApi

@Inject
lateinit var userSession: UserSession

@Inject
lateinit var syncInterval: SyncInterval

private val patientUuid = UUID.fromString("9af4f083-86dd-453f-91e5-9c716e859a9e")

private val userUuid: UUID by unsafeLazy { userSession.loggedInUserImmediate()!!.uuid }

@get:Rule
val ruleChain: RuleChain = Rules
.global()
.around(ServerAuthenticationRule())
.around(RegisterPatientRule(patientUuid))
.around(SaveDatabaseRule())

private lateinit var sync: PatientAttributeSync

private val batchSize = 3

private lateinit var config: SyncConfig

@Before
fun setUp() {
TestClinicApp.appComponent().inject(this)

resetLocalData()

config = SyncConfig(
syncInterval = syncInterval,
pullBatchSize = batchSize,
pushBatchSize = batchSize,
name = ""
)

sync = PatientAttributeSync(
syncCoordinator = SyncCoordinator(),
repository = repository,
api = syncApi,
lastPullToken = lastPullToken,
config = config
)
}

private fun resetLocalData() {
clearData()
lastPullToken.delete()
}

private fun clearData() {
appDatabase.patientAttributeDao().clear()
}

@Test
fun syncing_records_should_work_as_expected() {
// given
val totalNumberOfRecords = batchSize * 2 + 1
val records = (1..totalNumberOfRecords).map {
TestData.patientAttribute(
patientUuid = patientUuid,
userUuid = userUuid,
reading = BMIReading(
height = "177",
weight = "68"
),
syncStatus = SyncStatus.PENDING,
)
}
assertThat(records).containsNoDuplicates()

repository.save(records)
assertThat(repository.pendingSyncRecordCount().blockingFirst()).isEqualTo(totalNumberOfRecords)

// when
sync.push()
clearData()
sync.pull()

// then
val expectedPulledRecords = records.map { it.syncCompleted() }
val pulledRecords = repository.recordsWithSyncStatus(SyncStatus.DONE)

assertThat(pulledRecords).containsAtLeastElementsIn(expectedPulledRecords)
}

@Test
fun sync_pending_records_should_not_be_overwritten_by_server_records() {
// given
val records = (1..batchSize).map {
TestData.patientAttribute(
patientUuid = patientUuid,
userUuid = userUuid,
reading = BMIReading(
height = "177",
weight = "68"
),
syncStatus = SyncStatus.PENDING,
)
}
assertThat(records).containsNoDuplicates()

repository.save(records)
sync.push()
assertThat(repository.pendingSyncRecordCount().blockingFirst()).isEqualTo(0)

val modifiedRecord = records[1].withReading(BMIReading(height = "182", weight = "78"))
repository.save(listOf(modifiedRecord))
assertThat(repository.pendingSyncRecordCount().blockingFirst()).isEqualTo(1)

// when
sync.pull()

// then
val expectedSavedRecords = records
.map { it.syncCompleted() }
.filterNot { it.patientUuid == modifiedRecord.patientUuid }

val savedRecords = repository.recordsWithSyncStatus(SyncStatus.DONE)
val pendingSyncRecords = repository.recordsWithSyncStatus(SyncStatus.PENDING)

assertThat(savedRecords).containsAtLeastElementsIn(expectedSavedRecords)
assertThat(pendingSyncRecords).containsExactly(modifiedRecord)
}

private fun PatientAttribute.syncCompleted(): PatientAttribute = copy(syncStatus = SyncStatus.DONE)

private fun PatientAttribute.withReading(reading: BMIReading): PatientAttribute {
return copy(
reading = reading.copy(height = reading.height, weight = reading.weight),
syncStatus = SyncStatus.PENDING,
timestamps = timestamps.copy(updatedAt = timestamps.updatedAt.plusMillis(1))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ annotation class TypedPreference(val value: Type) {
LastQuestionnairePullToken,
LastQuestionnaireResponsePullToken,
DataProtectionConsent,
LastPatientAttributePullToken,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.room.RawQuery
import androidx.sqlite.db.SimpleSQLiteQuery
import io.reactivex.Flowable
import kotlinx.parcelize.Parcelize
import org.simple.clinic.medicalhistory.MedicalHistory
import org.simple.clinic.patient.SyncStatus
import org.simple.clinic.storage.Timestamps
import java.util.UUID
Expand Down Expand Up @@ -64,6 +65,9 @@ data class PatientAttribute(
@Query("SELECT COUNT(uuid) FROM PatientAttribute WHERE syncStatus = :syncStatus")
fun countWithStatus(syncStatus: SyncStatus): Flowable<Int>

@Query("SELECT * FROM MedicalHistory WHERE syncStatus = :status")
fun recordsWithSyncStatus(status: SyncStatus): List<MedicalHistory>

@Query("""
SELECT * FROM PatientAttribute
WHERE syncStatus = :syncStatus
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
package org.simple.clinic.patientattribute

import com.f2prateek.rx.preferences2.Preference
import com.f2prateek.rx.preferences2.RxSharedPreferences
import dagger.Module
import dagger.Provides
import org.simple.clinic.AppDatabase
import org.simple.clinic.main.TypedPreference
import org.simple.clinic.main.TypedPreference.Type.LastPatientAttributePullToken
import org.simple.clinic.patientattribute.sync.PatientAttributeSyncApi
import org.simple.clinic.util.preference.StringPreferenceConverter
import org.simple.clinic.util.preference.getOptional
import retrofit2.Retrofit
import java.util.Optional
import javax.inject.Named

@Module
class PatientAttributeModule {
@Provides
fun dao(appDatabase: AppDatabase): PatientAttribute.RoomDao {
return appDatabase.patientAttributeDao()
}

@Provides
fun syncApi(@Named("for_deployment") retrofit: Retrofit): PatientAttributeSyncApi {
return retrofit.create(PatientAttributeSyncApi::class.java)
}

@Provides
@TypedPreference(LastPatientAttributePullToken)
fun lastPullToken(rxSharedPrefs: RxSharedPreferences): Preference<Optional<String>> {
return rxSharedPrefs.getOptional("last_patient_attribute_pull_token", StringPreferenceConverter())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.simple.clinic.patientattribute
import io.reactivex.Completable
import io.reactivex.Observable
import org.simple.clinic.di.AppScope
import org.simple.clinic.medicalhistory.MedicalHistory
import org.simple.clinic.patient.SyncStatus
import org.simple.clinic.patient.SyncStatus.PENDING
import org.simple.clinic.patientattribute.sync.PatientAttributePayload
Expand Down Expand Up @@ -78,4 +79,8 @@ class PatientAttributeRepository @Inject constructor(
fun getPatientAttribute(patientUuid: UUID): PatientAttribute? {
return dao.patientImmediate(patientUuid)
}

fun recordsWithSyncStatus(syncStatus: SyncStatus): List<MedicalHistory> {
return dao.recordsWithSyncStatus(syncStatus)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import java.util.UUID

@JsonClass(generateAdapter = false)
data class PatientAttributePayload(

@Json(name = "id")
val uuid: UUID,

@Json(name = "height")
val height: Float,
val height: String,

@Json(name = "weight")
val weight: Float,
val weight: String,

@Json(name = "patient_id")
val patientUuid: UUID,
Expand All @@ -41,8 +42,8 @@ data class PatientAttributePayload(
patientUuid = patientUuid,
userUuid = userUuid,
reading = BMIReading(
height = height.toString(),
weight = weight.toString()
height = height,
weight = weight
),
timestamps = Timestamps(
createdAt = createdAt,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.simple.clinic.patientattribute.sync

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.simple.clinic.sync.DataPullResponse

@JsonClass(generateAdapter = true)
data class PatientAttributePullResponse(

@Json(name = "patient_attributes")
override val payloads: List<PatientAttributePayload>,

@Json(name = "process_token")
override val processToken: String

) : DataPullResponse<PatientAttributePayload>

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.simple.clinic.patientattribute.sync

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class PatientAttributePushRequest(

@Json(name = "patient_attributes")
val histories: List<PatientAttributePayload>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.simple.clinic.patientattribute.sync

import com.f2prateek.rx.preferences2.Preference
import org.simple.clinic.main.TypedPreference
import org.simple.clinic.main.TypedPreference.Type.LastPatientAttributePullToken
import org.simple.clinic.patientattribute.PatientAttribute
import org.simple.clinic.patientattribute.PatientAttributeRepository
import org.simple.clinic.sync.ModelSync
import org.simple.clinic.sync.SyncConfig
import org.simple.clinic.sync.SyncConfigType
import org.simple.clinic.sync.SyncConfigType.Type.Frequent
import org.simple.clinic.sync.SyncCoordinator
import org.simple.clinic.util.read
import java.util.Optional
import javax.inject.Inject

class PatientAttributeSync @Inject constructor(
private val syncCoordinator: SyncCoordinator,
private val repository: PatientAttributeRepository,
private val api: PatientAttributeSyncApi,
@TypedPreference(LastPatientAttributePullToken) private val lastPullToken: Preference<Optional<String>>,
@SyncConfigType(Frequent) private val config: SyncConfig
) : ModelSync {

override val name: String = "Patient Attribute"

override val requiresSyncApprovedUser = true

override fun push() {
syncCoordinator.push(repository, config.pushBatchSize) { api.push(toRequest(it)).execute().read()!! }
}

override fun pull() {
val batchSize = config.pullBatchSize
syncCoordinator.pull(repository, lastPullToken, batchSize) { api.pull(batchSize, it).execute().read()!! }
}

private fun toRequest(patientAttributes: List<PatientAttribute>): PatientAttributePushRequest {
val payloads = patientAttributes
.map {
it.run {
PatientAttributePayload(
uuid = uuid,
patientUuid = patientUuid,
userUuid = userUuid,
height = reading.height,
weight = reading.weight,
createdAt = timestamps.createdAt,
updatedAt = timestamps.updatedAt,
deletedAt = timestamps.deletedAt
)
}
}
return PatientAttributePushRequest(payloads)
}
}
Loading

0 comments on commit 6b9b13c

Please sign in to comment.