diff --git a/app/src/androidTest/java/org/simple/clinic/di/TestAppComponent.kt b/app/src/androidTest/java/org/simple/clinic/di/TestAppComponent.kt index 88a4c669a0e..6dfa32c577a 100644 --- a/app/src/androidTest/java/org/simple/clinic/di/TestAppComponent.kt +++ b/app/src/androidTest/java/org/simple/clinic/di/TestAppComponent.kt @@ -36,6 +36,7 @@ import org.simple.clinic.patient.PatientRepositoryAndroidTest import org.simple.clinic.patient.download.PatientLineListCsvGeneratorTest import org.simple.clinic.patient.download.PatientLineListDownloaderTest import org.simple.clinic.patient.onlinelookup.api.LookupPatientOnlineApiIntegrationTest +import org.simple.clinic.patientattribute.PatientAttributeRepositoryAndroidTest import org.simple.clinic.protocolv2.ProtocolRepositoryAndroidTest import org.simple.clinic.protocolv2.sync.ProtocolSyncAndroidTest import org.simple.clinic.rules.LocalAuthenticationRule @@ -162,4 +163,5 @@ interface TestAppComponent { fun inject(target: QuestionnaireResponseRepositoryAndroidTest) fun inject(target: QuestionnaireResponseSyncIntegrationTest) fun inject(target: DatabaseEncryptorTest) + fun inject(target: PatientAttributeRepositoryAndroidTest) } diff --git a/app/src/androidTest/java/org/simple/clinic/patientattribute/PatientAttributeRepositoryAndroidTest.kt b/app/src/androidTest/java/org/simple/clinic/patientattribute/PatientAttributeRepositoryAndroidTest.kt new file mode 100644 index 00000000000..0d8bbfdb05b --- /dev/null +++ b/app/src/androidTest/java/org/simple/clinic/patientattribute/PatientAttributeRepositoryAndroidTest.kt @@ -0,0 +1,47 @@ +package org.simple.clinic.patientattribute + +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.TestClinicApp +import org.simple.clinic.rules.LocalAuthenticationRule +import org.simple.clinic.rules.SaveDatabaseRule +import org.simple.sharedTestCode.TestData +import org.simple.sharedTestCode.util.Rules +import javax.inject.Inject + +class PatientAttributeRepositoryAndroidTest { + + @Inject + lateinit var repository: PatientAttributeRepository + + @Inject + lateinit var testData: TestData + + @get:Rule + val rules: RuleChain = Rules + .global() + .around(LocalAuthenticationRule()) + .around(SaveDatabaseRule()) + + @Before + fun setup() { + TestClinicApp.appComponent().inject(this) + } + + @Test + fun saving_a_patient_attribute_should_work_correctly() { + //given + val bmiReading = BMIReading(height = "177", weight = "64") + val patientAttribute = testData.patientAttribute(reading = bmiReading) + + //when + repository.save(listOf(patientAttribute)) + + //then + val savedPatientAttribute = repository.getPatientAttribute(patientAttribute.patientUuid) + assertThat(savedPatientAttribute).isEqualTo(patientAttribute) + } +} diff --git a/app/src/main/java/org/simple/clinic/AppDatabase.kt b/app/src/main/java/org/simple/clinic/AppDatabase.kt index d293cdf4bc4..66a7838ceab 100644 --- a/app/src/main/java/org/simple/clinic/AppDatabase.kt +++ b/app/src/main/java/org/simple/clinic/AppDatabase.kt @@ -201,6 +201,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun questionnaireResponseDao(): QuestionnaireResponse.RoomDao + abstract fun patientAttributeDao(): PatientAttribute.RoomDao + fun clearAppData() { runInTransaction { patientDao().clear() @@ -218,6 +220,7 @@ abstract class AppDatabase : RoomDatabase() { callResultDao().clear() questionnaireDao().clear() questionnaireResponseDao().clear() + patientAttributeDao().clear() } } diff --git a/app/src/main/java/org/simple/clinic/patientattribute/PatientAttribute.kt b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttribute.kt index 32d2adafa9d..4a1fef28037 100644 --- a/app/src/main/java/org/simple/clinic/patientattribute/PatientAttribute.kt +++ b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttribute.kt @@ -1,10 +1,17 @@ package org.simple.clinic.patientattribute import android.os.Parcelable +import androidx.room.Dao import androidx.room.Embedded import androidx.room.Entity import androidx.room.Index +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SimpleSQLiteQuery +import io.reactivex.Flowable import kotlinx.parcelize.Parcelize import org.simple.clinic.patient.SyncStatus import org.simple.clinic.storage.Timestamps @@ -32,4 +39,54 @@ data class PatientAttribute( val timestamps: Timestamps, val syncStatus: SyncStatus -) : Parcelable +) : Parcelable { + + @Dao + interface RoomDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun save(patientAttributes: List) + + @Query("UPDATE PatientAttribute SET syncStatus = :to WHERE syncStatus = :from") + fun updateSyncStatus(from: SyncStatus, to: SyncStatus) + + @RawQuery + fun updateSyncStatusForIdsRaw(query: SimpleSQLiteQuery): Int + + fun updateSyncStatusForIds(ids: List, to: SyncStatus) { + updateSyncStatusForIdsRaw(SimpleSQLiteQuery( + "UPDATE PatientAttribute SET syncStatus = '$to' WHERE uuid IN (${ids.joinToString(prefix = "'", postfix = "'", separator = "','")})" + )) + } + + @Query("SELECT COUNT(uuid) FROM PatientAttribute") + fun count(): Flowable + + @Query("SELECT COUNT(uuid) FROM PatientAttribute WHERE syncStatus = :syncStatus") + fun countWithStatus(syncStatus: SyncStatus): Flowable + + @Query(""" + SELECT * FROM PatientAttribute + WHERE syncStatus = :syncStatus + LIMIT :limit OFFSET :offset + """) + fun recordsWithSyncStatusBatched( + syncStatus: SyncStatus, + limit: Int, + offset: Int + ): List + + @Query("SELECT uuid FROM PatientAttribute WHERE syncStatus = :syncStatus") + fun recordIdsWithSyncStatus(syncStatus: SyncStatus): List + + @Query(""" + SELECT * FROM PatientAttribute + WHERE patientUuid = :patientUuid AND deletedAt IS NULL + ORDER BY updatedAt DESC + LIMIT 1 + """) + fun patientImmediate(patientUuid: UUID): PatientAttribute? + + @Query("DELETE FROM PatientAttribute") + fun clear() + } +} diff --git a/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeModule.kt b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeModule.kt new file mode 100644 index 00000000000..bffe046cab0 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeModule.kt @@ -0,0 +1,13 @@ +package org.simple.clinic.patientattribute + +import dagger.Module +import dagger.Provides +import org.simple.clinic.AppDatabase + +@Module +class PatientAttributeModule { + @Provides + fun dao(appDatabase: AppDatabase): PatientAttribute.RoomDao { + return appDatabase.patientAttributeDao() + } +} diff --git a/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeRepository.kt b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeRepository.kt new file mode 100644 index 00000000000..9962304d0c0 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/patientattribute/PatientAttributeRepository.kt @@ -0,0 +1,81 @@ +package org.simple.clinic.patientattribute + +import io.reactivex.Completable +import io.reactivex.Observable +import org.simple.clinic.di.AppScope +import org.simple.clinic.patient.SyncStatus +import org.simple.clinic.patient.SyncStatus.PENDING +import org.simple.clinic.patientattribute.sync.PatientAttributePayload +import org.simple.clinic.storage.Timestamps +import org.simple.clinic.sync.SynceableRepository +import org.simple.clinic.user.User +import org.simple.clinic.util.UtcClock +import java.util.UUID +import javax.inject.Inject + +@AppScope +class PatientAttributeRepository @Inject constructor( + val dao: PatientAttribute.RoomDao, + private val utcClock: UtcClock +) : SynceableRepository { + fun save( + reading: BMIReading, + patientUuid: UUID, + loggedInUser: User, + uuid: UUID, + ): Completable { + val patientAttribute = PatientAttribute( + uuid = uuid, + patientUuid = patientUuid, + userUuid = loggedInUser.uuid, + reading = reading, + timestamps = Timestamps.create(utcClock), + syncStatus = PENDING + ) + return Completable.fromAction { save(listOf(patientAttribute)) } + } + + override fun save(records: List) { + dao.save(records) + } + + override fun setSyncStatus(from: SyncStatus, to: SyncStatus) { + dao.updateSyncStatus(from, to) + } + + override fun setSyncStatus(ids: List, to: SyncStatus) { + if (ids.isEmpty()) { + throw AssertionError() + } + + dao.updateSyncStatusForIds(ids, to) + } + + override fun recordCount(): Observable = dao.count().toObservable() + + override fun pendingSyncRecordCount(): Observable = + dao.countWithStatus(PENDING).toObservable() + + override fun pendingSyncRecords(limit: Int, offset: Int): List { + return dao + .recordsWithSyncStatusBatched( + syncStatus = PENDING, + limit = limit, + offset = offset + ) + } + + override fun mergeWithLocalData(payloads: List) { + val dirtyRecords = dao.recordIdsWithSyncStatus(PENDING) + + val payloadsToSave = payloads + .filterNot { it.uuid in dirtyRecords } + .map { it.toDatabaseModel(SyncStatus.DONE) } + + dao.save(payloadsToSave) + } + + fun getPatientAttribute(patientUuid: UUID): PatientAttribute? { + return dao.patientImmediate(patientUuid) + } +} diff --git a/app/src/main/java/org/simple/clinic/patientattribute/sync/PatientAttributePayload.kt b/app/src/main/java/org/simple/clinic/patientattribute/sync/PatientAttributePayload.kt new file mode 100644 index 00000000000..58e0daf6b16 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/patientattribute/sync/PatientAttributePayload.kt @@ -0,0 +1,54 @@ +package org.simple.clinic.patientattribute.sync + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.simple.clinic.patient.SyncStatus +import org.simple.clinic.patientattribute.BMIReading +import org.simple.clinic.patientattribute.PatientAttribute +import org.simple.clinic.storage.Timestamps +import java.time.Instant +import java.util.UUID + +@JsonClass(generateAdapter = false) +data class PatientAttributePayload( + @Json(name = "id") + val uuid: UUID, + + @Json(name = "height") + val height: Float, + + @Json(name = "weight") + val weight: Float, + + @Json(name = "patient_id") + val patientUuid: UUID, + + @Json(name = "user_id") + val userUuid: UUID, + + @Json(name = "created_at") + val createdAt: Instant, + + @Json(name = "updated_at") + val updatedAt: Instant, + + @Json(name = "deleted_at") + val deletedAt: Instant?, +) { + + fun toDatabaseModel(syncStatus: SyncStatus) = PatientAttribute( + uuid = uuid, + patientUuid = patientUuid, + userUuid = userUuid, + reading = BMIReading( + height = height.toString(), + weight = weight.toString() + ), + timestamps = Timestamps( + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ), + syncStatus = syncStatus, + ) +} diff --git a/app/src/main/java/org/simple/clinic/sync/SyncModule.kt b/app/src/main/java/org/simple/clinic/sync/SyncModule.kt index caa78d79d3c..ec36389fb3f 100644 --- a/app/src/main/java/org/simple/clinic/sync/SyncModule.kt +++ b/app/src/main/java/org/simple/clinic/sync/SyncModule.kt @@ -26,8 +26,6 @@ import org.simple.clinic.main.TypedPreference.Type.FacilitySyncGroupSwitchedAt import org.simple.clinic.medicalhistory.MedicalHistoryModule import org.simple.clinic.medicalhistory.MedicalHistoryRepository import org.simple.clinic.medicalhistory.sync.MedicalHistorySync -import org.simple.clinic.questionnaire.di.QuestionnaireModule -import org.simple.clinic.questionnaireresponse.di.QuestionnaireResponseModule import org.simple.clinic.overdue.AppointmentModule import org.simple.clinic.overdue.AppointmentRepository import org.simple.clinic.overdue.AppointmentSync @@ -38,9 +36,12 @@ import org.simple.clinic.overdue.download.di.OverdueListDownloadModule import org.simple.clinic.patient.PatientRepository import org.simple.clinic.patient.sync.PatientSync import org.simple.clinic.patient.sync.PatientSyncModule +import org.simple.clinic.patientattribute.PatientAttributeModule import org.simple.clinic.protocol.ProtocolModule import org.simple.clinic.protocol.sync.ProtocolSync +import org.simple.clinic.questionnaire.di.QuestionnaireModule import org.simple.clinic.questionnaire.sync.QuestionnaireSync +import org.simple.clinic.questionnaireresponse.di.QuestionnaireResponseModule import org.simple.clinic.questionnaireresponse.sync.QuestionnaireResponseSync import org.simple.clinic.reports.ReportsModule import org.simple.clinic.reports.ReportsSync @@ -68,7 +69,8 @@ import javax.inject.Named CallResultModule::class, OverdueListDownloadModule::class, QuestionnaireModule::class, - QuestionnaireResponseModule::class + QuestionnaireResponseModule::class, + PatientAttributeModule::class, ]) class SyncModule { diff --git a/sharedTestCode/src/main/java/org/simple/sharedTestCode/TestData.kt b/sharedTestCode/src/main/java/org/simple/sharedTestCode/TestData.kt index 07a3b1f7c36..9f759f0f4aa 100644 --- a/sharedTestCode/src/main/java/org/simple/sharedTestCode/TestData.kt +++ b/sharedTestCode/src/main/java/org/simple/sharedTestCode/TestData.kt @@ -64,6 +64,8 @@ import org.simple.clinic.patient.sync.BusinessIdPayload import org.simple.clinic.patient.sync.PatientAddressPayload import org.simple.clinic.patient.sync.PatientPayload import org.simple.clinic.patient.sync.PatientPhoneNumberPayload +import org.simple.clinic.patientattribute.BMIReading +import org.simple.clinic.patientattribute.PatientAttribute import org.simple.clinic.protocol.Protocol import org.simple.clinic.protocol.ProtocolDrug import org.simple.clinic.protocol.sync.ProtocolDrugPayload @@ -1647,4 +1649,27 @@ object TestData { "monthly_screening_reports.is_smoking" to true, ) } + + fun patientAttribute( + uuid: UUID = UUID.randomUUID(), + patientUuid: UUID = UUID.randomUUID(), + userUuid: UUID = UUID.randomUUID(), + reading: BMIReading, + syncStatus: SyncStatus = randomOfEnum(SyncStatus::class), + createdAt: Instant = Instant.now(), + updatedAt: Instant = Instant.now(), + deletedAt: Instant? = null + ): PatientAttribute { + return PatientAttribute( + uuid = uuid, + patientUuid = patientUuid, + userUuid = userUuid, + reading = reading, + timestamps = Timestamps( + createdAt, updatedAt, deletedAt + ), + syncStatus = syncStatus + ) + + } }