Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PatientAttribute sync resource #5153

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.0",
weight = "68.0"
),
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 @@ -67,6 +67,9 @@ data class PatientAttribute(
@Query("SELECT COUNT(uuid) FROM PatientAttribute WHERE syncStatus = :syncStatus")
fun countWithStatus(syncStatus: SyncStatus): Flowable<Int>

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

@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 @@ -76,4 +76,8 @@ class PatientAttributeRepository @Inject constructor(
fun getPatientAttributeImmediate(patientUuid: UUID): PatientAttribute? {
return dao.patientAttributeImmediate(patientUuid)
}

fun recordsWithSyncStatus(syncStatus: SyncStatus): List<PatientAttribute> {
return dao.recordsWithSyncStatus(syncStatus)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import org.simple.clinic.storage.Timestamps
import java.time.Instant
import java.util.UUID

@JsonClass(generateAdapter = false)
@JsonClass(generateAdapter = true)
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,
siddh1004 marked this conversation as resolved.
Show resolved Hide resolved

@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 patientAttributes: 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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.simple.clinic.patientattribute.sync

import org.simple.clinic.sync.DataPushResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

interface PatientAttributeSyncApi {

@POST("v4/patient_attributes/sync")
fun push(
@Body body: PatientAttributePushRequest
): Call<DataPushResponse>

@GET("v4/patient_attributes/sync")
fun pull(
@Query("limit") recordsToPull: Int,
@Query("process_token") lastPullToken: String? = null
): Call<PatientAttributePullResponse>
}
Loading