diff --git a/CHANGELOG.md b/CHANGELOG.md index 802700cbac4..fb6aa6be31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ ### Internal --Update `ci-checks.yml` & `story-link-check.sh` to verify valid jira reference +- Update `ci-checks.yml` & `story-link-check.sh` to verify valid jira reference +- Add effect to fetch complete medical records +- Bump sqlcipher to v4.13.0 +- Bump dagger to v2.59.1 + +### Changes + +- Add `Sync Medical Records` button on setting page behind feature flag ## 2026.02.02 diff --git a/app/src/androidTest/java/org/simple/clinic/settings/SettingsScreenTest.kt b/app/src/androidTest/java/org/simple/clinic/settings/SettingsScreenTest.kt index 223a183550a..c63989157fc 100644 --- a/app/src/androidTest/java/org/simple/clinic/settings/SettingsScreenTest.kt +++ b/app/src/androidTest/java/org/simple/clinic/settings/SettingsScreenTest.kt @@ -18,6 +18,7 @@ class SettingsScreenTest { private val defaultSettingsModel = SettingsModel.default( isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true ) @Test @@ -30,7 +31,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -58,7 +60,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -78,7 +81,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -97,7 +101,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -115,7 +120,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -133,7 +139,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -150,7 +157,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -168,7 +176,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -186,7 +195,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -198,6 +208,7 @@ class SettingsScreenTest { fun whenChangeLanguageFeatureIsNotEnabledThenDoNotShowChangeLanguageSetting() { val model = SettingsModel.default( isChangeLanguageFeatureEnabled = false, + showDiagnosisButton = true ) composeRule.setContent { SettingsScreen( @@ -205,7 +216,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -217,6 +229,7 @@ class SettingsScreenTest { fun whenChangeLanguageFeatureIsEnabledThenShowChangeLanguageSetting() { val model = SettingsModel.default( isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true, ) composeRule.setContent { SettingsScreen( @@ -224,7 +237,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } @@ -236,6 +250,7 @@ class SettingsScreenTest { fun logoutButtonShouldBeVisible() { val model = SettingsModel.default( isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true ) composeRule.setContent { SettingsScreen( @@ -243,7 +258,8 @@ class SettingsScreenTest { navigationIconClick = { /*no-op*/ }, changeLanguageButtonClick = { /*no-op*/ }, updateButtonClick = { /*no-op*/ }, - logoutButtonClick = { /*no-op*/ } + logoutButtonClick = { /*no-op*/ }, + syncMedicalRecordClick = { /*no-op*/ } ) } diff --git a/app/src/main/java/org/simple/clinic/bloodsugar/BloodSugarMeasurement.kt b/app/src/main/java/org/simple/clinic/bloodsugar/BloodSugarMeasurement.kt index 6222bf6e2a0..15f1fc6333a 100644 --- a/app/src/main/java/org/simple/clinic/bloodsugar/BloodSugarMeasurement.kt +++ b/app/src/main/java/org/simple/clinic/bloodsugar/BloodSugarMeasurement.kt @@ -99,6 +99,12 @@ data class BloodSugarMeasurement( ORDER BY recordedAt DESC """) fun allBloodSugars(patientUuid: UUID): Observable> + @Query(""" + SELECT * FROM BloodSugarMeasurements + WHERE patientUuid == :patientUuid AND deletedAt IS NULL + ORDER BY recordedAt DESC + """) + fun allBloodSugarsImmediate(patientUuid: UUID): List @Query(""" SELECT * FROM BloodSugarMeasurements diff --git a/app/src/main/java/org/simple/clinic/feature/Feature.kt b/app/src/main/java/org/simple/clinic/feature/Feature.kt index a08845d928a..e08e558b7aa 100644 --- a/app/src/main/java/org/simple/clinic/feature/Feature.kt +++ b/app/src/main/java/org/simple/clinic/feature/Feature.kt @@ -26,5 +26,6 @@ enum class Feature( PatientStatinNudge(false, "patient_statin_nudge_v0"), NonLabBasedStatinNudge(false, "non_lab_based_statin_nudge"), LabBasedStatinNudge(false, "lab_based_statin_nudge"), - Screening(false, "screening_feature_v0") + Screening(false, "screening_feature_v0"), + ShowDiagnosisButton(false, "show_diagnosis_button"), } diff --git a/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt b/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt index a7d29d4d3a0..e2e613fb297 100644 --- a/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt +++ b/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt @@ -24,6 +24,7 @@ import org.simple.clinic.medicalhistory.MedicalHistoryQuestion.IsOnDiabetesTreat import org.simple.clinic.medicalhistory.MedicalHistoryQuestion.IsOnHypertensionTreatment import org.simple.clinic.medicalhistory.MedicalHistoryQuestion.IsSmoking import org.simple.clinic.medicalhistory.MedicalHistoryQuestion.IsUsingSmokelessTobacco +import org.simple.clinic.medicalhistory.sync.MedicalHistoryPayload import org.simple.clinic.patient.PatientUuid import org.simple.clinic.patient.SyncStatus import java.time.Instant @@ -114,6 +115,28 @@ data class MedicalHistory( return copy(cholesterol = cholesterol) } + fun toPayload(): MedicalHistoryPayload { + return MedicalHistoryPayload( + uuid = uuid, + patientUuid = patientUuid, + diagnosedWithHypertension = diagnosedWithHypertension, + isOnTreatmentForHypertension = isOnHypertensionTreatment, + isOnDiabetesTreatment = isOnDiabetesTreatment, + hasHadHeartAttack = hasHadHeartAttack, + hasHadStroke = hasHadStroke, + hasHadKidneyDisease = hasHadKidneyDisease, + hasDiabetes = diagnosedWithDiabetes, + hasHypertension = diagnosedWithHypertension, + isSmoking = isSmoking, + isUsingSmokelessTobacco = isUsingSmokelessTobacco, + cholesterol = cholesterol, + hypertensionDiagnosedAt = hypertensionDiagnosedAt, + diabetesDiagnosedAt = diabetesDiagnosedAt, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt) + } + @Dao interface RoomDao { diff --git a/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistorySync.kt b/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistorySync.kt index b096fd25e15..9ed909cda7a 100644 --- a/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistorySync.kt +++ b/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistorySync.kt @@ -35,30 +35,7 @@ class MedicalHistorySync @Inject constructor( } private fun toRequest(histories: List): MedicalHistoryPushRequest { - val payloads = histories - .map { - it.run { - MedicalHistoryPayload( - uuid = uuid, - patientUuid = patientUuid, - diagnosedWithHypertension = diagnosedWithHypertension, - isOnTreatmentForHypertension = isOnHypertensionTreatment, - isOnDiabetesTreatment = isOnDiabetesTreatment, - hasHadHeartAttack = hasHadHeartAttack, - hasHadStroke = hasHadStroke, - hasHadKidneyDisease = hasHadKidneyDisease, - hasDiabetes = diagnosedWithDiabetes, - hasHypertension = diagnosedWithHypertension, - isSmoking = isSmoking, - isUsingSmokelessTobacco = isUsingSmokelessTobacco, - cholesterol = cholesterol, - hypertensionDiagnosedAt = hypertensionDiagnosedAt, - diabetesDiagnosedAt = diabetesDiagnosedAt, - createdAt = createdAt, - updatedAt = updatedAt, - deletedAt = deletedAt) - } - } + val payloads = histories.map { it.toPayload() } return MedicalHistoryPushRequest(payloads) } } diff --git a/app/src/main/java/org/simple/clinic/overdue/Appointment.kt b/app/src/main/java/org/simple/clinic/overdue/Appointment.kt index 415082caaa5..aa54beacfa8 100644 --- a/app/src/main/java/org/simple/clinic/overdue/Appointment.kt +++ b/app/src/main/java/org/simple/clinic/overdue/Appointment.kt @@ -51,6 +51,23 @@ data class Appointment( val creationFacilityUuid: UUID? ) : Parcelable { + fun toPayload(): AppointmentPayload { + return AppointmentPayload( + uuid = uuid, + patientUuid = patientUuid, + facilityUuid = facilityUuid, + creationFacilityUuid = creationFacilityUuid, + date = scheduledDate, + status = status, + cancelReason = cancelReason, + remindOn = remindOn, + agreedToVisit = agreedToVisit, + appointmentType = appointmentType, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt) + } + fun wasCancelledBecauseOfInvalidPhoneNumber(): Boolean = status == Status.Cancelled && cancelReason == AppointmentCancelReason.InvalidPhoneNumber sealed class Status : Parcelable { diff --git a/app/src/main/java/org/simple/clinic/overdue/AppointmentSync.kt b/app/src/main/java/org/simple/clinic/overdue/AppointmentSync.kt index 49504117518..4abbd917880 100644 --- a/app/src/main/java/org/simple/clinic/overdue/AppointmentSync.kt +++ b/app/src/main/java/org/simple/clinic/overdue/AppointmentSync.kt @@ -33,26 +33,7 @@ class AppointmentSync @Inject constructor( } private fun toRequest(appointments: List): AppointmentPushRequest { - val payloads = appointments - .map { - it.run { - AppointmentPayload( - uuid = uuid, - patientUuid = patientUuid, - facilityUuid = facilityUuid, - creationFacilityUuid = creationFacilityUuid, - date = scheduledDate, - status = status, - cancelReason = cancelReason, - remindOn = remindOn, - agreedToVisit = agreedToVisit, - appointmentType = appointmentType, - createdAt = createdAt, - updatedAt = updatedAt, - deletedAt = deletedAt) - } - } - .toList() + val payloads = appointments.map { it.toPayload() } return AppointmentPushRequest(payloads) } } diff --git a/app/src/main/java/org/simple/clinic/patient/PatientPhoneNumber.kt b/app/src/main/java/org/simple/clinic/patient/PatientPhoneNumber.kt index 0b68f502299..1d567815930 100644 --- a/app/src/main/java/org/simple/clinic/patient/PatientPhoneNumber.kt +++ b/app/src/main/java/org/simple/clinic/patient/PatientPhoneNumber.kt @@ -12,6 +12,7 @@ import androidx.room.PrimaryKey import androidx.room.Query import io.reactivex.Flowable import kotlinx.parcelize.Parcelize +import org.simple.clinic.patient.sync.PatientPhoneNumberPayload import java.time.Instant import java.util.UUID @@ -47,6 +48,18 @@ data class PatientPhoneNumber( val deletedAt: Instant? ) : Parcelable { + fun toPayload(): PatientPhoneNumberPayload { + return PatientPhoneNumberPayload( + uuid = uuid, + number = number, + type = phoneType, + active = active, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt + ) + } + fun withNumber(number: String): PatientPhoneNumber = copy(number = number) diff --git a/app/src/main/java/org/simple/clinic/patient/PatientRepository.kt b/app/src/main/java/org/simple/clinic/patient/PatientRepository.kt index b80d8937b8d..c089b48943a 100644 --- a/app/src/main/java/org/simple/clinic/patient/PatientRepository.kt +++ b/app/src/main/java/org/simple/clinic/patient/PatientRepository.kt @@ -13,7 +13,7 @@ import org.simple.clinic.facility.Facility import org.simple.clinic.medicalhistory.Answer import org.simple.clinic.overdue.Appointment.AppointmentType.Manual import org.simple.clinic.overdue.Appointment.Status.Scheduled -import org.simple.clinic.patient.Answer.* +import org.simple.clinic.patient.Answer.Unanswered import org.simple.clinic.patient.PatientSearchCriteria.Name import org.simple.clinic.patient.PatientSearchCriteria.NumericCriteria import org.simple.clinic.patient.SyncStatus.DONE @@ -741,6 +741,35 @@ class PatientRepository @Inject constructor( } } + fun fetchCompleteMedicalRecord(): List { + val patientProfiles = database.patientDao().allPatientProfiles() + return patientProfiles.mapNotNull { patientProfile -> + try { + val patientUuid = patientProfile.patientUuid + val medicalHistory = database.medicalHistoryDao().historyForPatientImmediate(patientUuid) + val appointments = database.appointmentDao().getAllAppointmentsForPatient(patientUuid) + val bloodPressures = database.bloodPressureDao().allBloodPressuresRecordedSinceImmediate( + patientUuid, + Instant.EPOCH + ) + val bloodSugars = database.bloodSugarDao().allBloodSugarsImmediate(patientUuid) + + val prescribedDrugs = database.prescriptionDao().forPatientImmediate(patientUuid) + + CompleteMedicalRecord( + patient = patientProfile, + medicalHistory = medicalHistory, + appointments = appointments, + bloodPressures = bloodPressures, + bloodSugars = bloodSugars, + prescribedDrugs = prescribedDrugs + ) + } catch (_: Exception) { + null + } + } + } + fun addIdentifiersToPatient( patientUuid: UUID, businessIds: List diff --git a/app/src/main/java/org/simple/clinic/patient/medicalRecords/CompleteMedicalRecordsPushRequest.kt b/app/src/main/java/org/simple/clinic/patient/medicalRecords/CompleteMedicalRecordsPushRequest.kt new file mode 100644 index 00000000000..94f129d765a --- /dev/null +++ b/app/src/main/java/org/simple/clinic/patient/medicalRecords/CompleteMedicalRecordsPushRequest.kt @@ -0,0 +1,13 @@ +package org.simple.clinic.patient.medicalRecords + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.simple.clinic.patient.CompleteMedicalRecord +import org.simple.clinic.patient.onlinelookup.api.CompleteMedicalRecordPayload + +@JsonClass(generateAdapter = true) +data class CompleteMedicalRecordsPushRequest( + + @Json(name = "patients") + val patients: List +) diff --git a/app/src/main/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnline.kt b/app/src/main/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnline.kt new file mode 100644 index 00000000000..a599e332923 --- /dev/null +++ b/app/src/main/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnline.kt @@ -0,0 +1,128 @@ +package org.simple.clinic.patient.medicalRecords + +import org.simple.clinic.patient.CompleteMedicalRecord +import org.simple.clinic.patient.onlinelookup.api.CompleteMedicalRecordPayload +import org.simple.clinic.patient.onlinelookup.api.RecordRetention +import org.simple.clinic.patient.onlinelookup.api.RetentionType +import org.simple.clinic.patient.onlinelookup.api.SecondsDuration +import org.simple.clinic.patient.sync.PatientSyncApi +import org.simple.clinic.util.UtcClock +import java.time.Duration +import java.time.Instant +import javax.inject.Inject + +class PushMedicalRecordsOnline @Inject constructor( + private val patientSyncApi: PatientSyncApi, + private val clock: UtcClock, +) { + fun pushAllMedicalRecordsOnServer( + medicalRecords: List + ): Result { + + if (medicalRecords.isEmpty()) { + return Result.NothingToPush + } + + val request = CompleteMedicalRecordsPushRequest( + patients = medicalRecords.map { mapToPayload(it) } + ) + + return try { + val response = patientSyncApi + .pushAllPatientsData(request) + .execute() + + return when (response.code()) { + 200 -> Result.Success + else -> Result.ServerError( + code = response.code(), + message = response.errorBody()?.string() + ) + } + + } catch (e: Exception) { + Result.NetworkError(e) + } + } + + + fun mapToPayload( + completeMedicalRecord: CompleteMedicalRecord + ): CompleteMedicalRecordPayload { + + val patientProfile = completeMedicalRecord.patient + val patient = patientProfile.patient + + return CompleteMedicalRecordPayload( + id = patient.uuid, + fullName = patient.fullName, + gender = patient.gender, + dateOfBirth = patient.ageDetails.dateOfBirth, + age = patient.ageDetails.ageValue, + ageUpdatedAt = patient.ageDetails.ageUpdatedAt, + status = patient.status, + createdAt = patient.createdAt, + updatedAt = patient.updatedAt, + deletedAt = patient.deletedAt, + + address = patientProfile.address.toPayload(), + + phoneNumbers = patientProfile.phoneNumbers + .map { it.toPayload() }, + + businessIds = patientProfile.businessIds + .map { it.toPayload() }, + + recordedAt = patient.recordedAt, + + reminderConsent = patient.reminderConsent, + + deletedReason = patient.deletedReason, + + registeredFacilityId = patient.registeredFacilityId, + + assignedFacilityId = patient.assignedFacilityId, + + appointments = completeMedicalRecord.appointments + .map { it.toPayload() }, + + bloodPressures = completeMedicalRecord.bloodPressures + .map { it.toPayload() }, + + bloodSugars = completeMedicalRecord.bloodSugars + .map { it.toPayload() }, + + medicalHistory = completeMedicalRecord.medicalHistory + ?.toPayload(), + + prescribedDrugs = completeMedicalRecord.prescribedDrugs + .map { it.toPayload() }, + + retention = patient.retainUntil?.let { retainUntil -> + val duration = Duration.between( + Instant.now(clock), + retainUntil + ).coerceAtLeast(Duration.ZERO) + + RecordRetention( + type = RetentionType.Temporary, + retainFor = SecondsDuration(duration) + ) + } ?: RecordRetention( + type = RetentionType.Permanent, + retainFor = null + ) + ) + } + + sealed class Result { + data object Success : Result() + data object NothingToPush : Result() + data class ServerError( + val code: Int, + val message: String? + ) : Result() + + data class NetworkError(val throwable: Throwable) : Result() + } +} diff --git a/app/src/main/java/org/simple/clinic/patient/sync/PatientSync.kt b/app/src/main/java/org/simple/clinic/patient/sync/PatientSync.kt index 8a3a474b230..541490705e8 100644 --- a/app/src/main/java/org/simple/clinic/patient/sync/PatientSync.kt +++ b/app/src/main/java/org/simple/clinic/patient/sync/PatientSync.kt @@ -38,9 +38,8 @@ class PatientSync @Inject constructor( private fun toRequest(patients: List): PatientPushRequest { return PatientPushRequest( patients.map { (patient, address, phoneNumbers, businessIds) -> - val numberPayloads = phoneNumbers - .map(::phoneNumberPayload) - .let { payloads -> if (payloads.isEmpty()) null else payloads } + val numberPayloads = phoneNumbers.map { it.toPayload() } + .let { payloads -> payloads.ifEmpty { null } } val businessIdPayloads = businessIds .map { it.toPayload() } @@ -71,17 +70,4 @@ class PatientSync @Inject constructor( } ) } - - private fun phoneNumberPayload(phoneNumber: PatientPhoneNumber): PatientPhoneNumberPayload { - return phoneNumber.run { - PatientPhoneNumberPayload( - uuid = uuid, - number = number, - type = phoneType, - active = active, - createdAt = createdAt, - updatedAt = updatedAt, - deletedAt = deletedAt) - } - } } diff --git a/app/src/main/java/org/simple/clinic/patient/sync/PatientSyncApi.kt b/app/src/main/java/org/simple/clinic/patient/sync/PatientSyncApi.kt index 2ca97df5f98..186ed874fae 100644 --- a/app/src/main/java/org/simple/clinic/patient/sync/PatientSyncApi.kt +++ b/app/src/main/java/org/simple/clinic/patient/sync/PatientSyncApi.kt @@ -1,6 +1,7 @@ package org.simple.clinic.patient.sync import org.simple.clinic.di.network.Timeout +import org.simple.clinic.patient.medicalRecords.CompleteMedicalRecordsPushRequest import org.simple.clinic.patient.onlinelookup.api.OnlineLookupResponsePayload import org.simple.clinic.patient.onlinelookup.api.PatientOnlineLookupRequest import org.simple.clinic.sync.DataPushResponse @@ -31,4 +32,9 @@ interface PatientSyncApi { fun lookup( @Body body: PatientOnlineLookupRequest ): Call + + @POST("v4/legacy_data_dumps") + fun pushAllPatientsData( + @Body body: CompleteMedicalRecordsPushRequest + ): Call } diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsEffect.kt b/app/src/main/java/org/simple/clinic/settings/SettingsEffect.kt index adf52a757eb..0de8ff9fcb0 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsEffect.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsEffect.kt @@ -1,5 +1,7 @@ package org.simple.clinic.settings +import org.simple.clinic.patient.CompleteMedicalRecord + sealed class SettingsEffect data object LoadUserDetailsEffect : SettingsEffect() @@ -23,3 +25,10 @@ data object ShowConfirmLogoutDialog : SettingsViewEffect() data object RestartApp : SettingsViewEffect() data object GoBack : SettingsViewEffect() + +data object FetchCompleteMedicalRecords : SettingsEffect() + +data class PushCompleteMedicalRecordsOnline( + val medicalRecords: List +) : SettingsEffect() + diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsEffectHandler.kt b/app/src/main/java/org/simple/clinic/settings/SettingsEffectHandler.kt index 9e6fc213e2b..3f348b859e2 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsEffectHandler.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsEffectHandler.kt @@ -8,6 +8,8 @@ import dagger.assisted.AssistedInject import io.reactivex.ObservableTransformer import org.simple.clinic.appupdate.AppUpdateState import org.simple.clinic.appupdate.CheckAppUpdateAvailability +import org.simple.clinic.patient.PatientRepository +import org.simple.clinic.patient.medicalRecords.PushMedicalRecordsOnline import org.simple.clinic.storage.DatabaseEncryptor import org.simple.clinic.user.UserSession import org.simple.clinic.util.filterAndUnwrapJust @@ -16,10 +18,12 @@ import org.simple.clinic.util.scheduler.SchedulersProvider class SettingsEffectHandler @AssistedInject constructor( private val userSession: UserSession, private val settingsRepository: SettingsRepository, + private val patientRepository: PatientRepository, private val schedulersProvider: SchedulersProvider, private val appVersionFetcher: AppVersionFetcher, private val appUpdateAvailability: CheckAppUpdateAvailability, private val databaseEncryptor: DatabaseEncryptor, + private val pushMedicalRecordsOnline: PushMedicalRecordsOnline, @Assisted private val viewEffectsConsumer: Consumer ) { @@ -39,6 +43,8 @@ class SettingsEffectHandler @AssistedInject constructor( .addConsumer(SettingsViewEffect::class.java, viewEffectsConsumer::accept) .addTransformer(LogoutUser::class.java, logoutUser()) .addTransformer(LoadDatabaseEncryptionStatus::class.java, loadDatabaseEncryptionStatus()) + .addTransformer(FetchCompleteMedicalRecords::class.java, fetchCompleteMedicalRecords()) + .addTransformer(PushCompleteMedicalRecordsOnline::class.java, pushCompleteMedicalRecordsOnline()) .build() private fun loadDatabaseEncryptionStatus(): ObservableTransformer { @@ -97,4 +103,24 @@ class SettingsEffectHandler @AssistedInject constructor( } } } + + private fun fetchCompleteMedicalRecords(): ObservableTransformer { + return ObservableTransformer { effects -> + effects + .observeOn(schedulersProvider.io()) + .map { patientRepository.fetchCompleteMedicalRecord() } + .map(::MedicalRecordsFetched) + } + } + + private fun pushCompleteMedicalRecordsOnline(): ObservableTransformer { + return ObservableTransformer { effects -> + effects + .observeOn(schedulersProvider.io()) + .map { + val results = pushMedicalRecordsOnline.pushAllMedicalRecordsOnServer(it.medicalRecords) + PushMedicalRecordsOnlineCompleted(results) + } + } + } } diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsEvent.kt b/app/src/main/java/org/simple/clinic/settings/SettingsEvent.kt index 72f33a8f69c..b3fe1a5e58a 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsEvent.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsEvent.kt @@ -1,5 +1,7 @@ package org.simple.clinic.settings +import org.simple.clinic.patient.CompleteMedicalRecord +import org.simple.clinic.patient.medicalRecords.PushMedicalRecordsOnline import org.simple.clinic.user.UserSession import org.simple.clinic.widgets.UiEvent @@ -32,3 +34,15 @@ data object BackClicked : SettingsEvent() { } data class DatabaseEncryptionStatusLoaded(val isDatabaseEncrypted: Boolean) : SettingsEvent() + +data object PushAllMedicalRecordsClicked : SettingsEvent() { + override val analyticsName: String = "Settings:Push all medical records Clicked" +} + +data class MedicalRecordsFetched( + val completeMedicalRecords: List +) : SettingsEvent() + +data class PushMedicalRecordsOnlineCompleted( + val result: PushMedicalRecordsOnline.Result, +) : SettingsEvent() diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsModel.kt b/app/src/main/java/org/simple/clinic/settings/SettingsModel.kt index 9437b662296..0cb71c9ffc8 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsModel.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsModel.kt @@ -12,11 +12,14 @@ data class SettingsModel( val isUpdateAvailable: Boolean?, val isUserLoggingOut: Boolean?, val isDatabaseEncrypted: Boolean?, + val isPushingMedicalRecords: Boolean?, val isChangeLanguageFeatureEnabled: Boolean, -) : Parcelable { + val showDiagnosisButton: Boolean, + + ) : Parcelable { companion object { - fun default(isChangeLanguageFeatureEnabled: Boolean) = SettingsModel( + fun default(isChangeLanguageFeatureEnabled: Boolean, showDiagnosisButton: Boolean) = SettingsModel( name = null, phoneNumber = null, currentLanguage = null, @@ -24,7 +27,9 @@ data class SettingsModel( isUpdateAvailable = null, isUserLoggingOut = null, isDatabaseEncrypted = null, + isPushingMedicalRecords = null, isChangeLanguageFeatureEnabled = isChangeLanguageFeatureEnabled, + showDiagnosisButton = showDiagnosisButton ) } @@ -37,6 +42,9 @@ data class SettingsModel( val appVersionQueried: Boolean get() = appVersion != null + val isMedicalRecordsPushInProgress: Boolean + get() = isPushingMedicalRecords == true + fun userDetailsFetched(name: String, phoneNumber: String): SettingsModel { return copy(name = name, phoneNumber = phoneNumber) } @@ -68,4 +76,12 @@ data class SettingsModel( fun databaseEncryptionStatusLoaded(isDatabaseEncrypted: Boolean): SettingsModel { return copy(isDatabaseEncrypted = isDatabaseEncrypted) } + + fun medicalRecordsPushStarted(): SettingsModel { + return copy(isPushingMedicalRecords = true) + } + + fun medicalRecordsPushCompleted(): SettingsModel { + return copy(isPushingMedicalRecords = false) + } } diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsScreen.kt b/app/src/main/java/org/simple/clinic/settings/SettingsScreen.kt index 7a4c065db70..a2eeaf0933d 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsScreen.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsScreen.kt @@ -43,11 +43,16 @@ class SettingsScreen : Fragment(), UiActions, HandlesBack { features.isEnabled(Feature.ChangeLanguage) } + private val showDiagnosisButton by unsafeLazy { + features.isEnabled(Feature.ShowDiagnosisButton) + } + private val viewEffectHandler by unsafeLazy { SettingsViewEffectHandler(this) } private val viewModel by mobiusViewModels( defaultModel = { SettingsModel.default( isChangeLanguageFeatureEnabled = isChangeLanguageFeatureEnabled, + showDiagnosisButton = showDiagnosisButton ) }, init = { SettingsInit() }, @@ -73,7 +78,8 @@ class SettingsScreen : Fragment(), UiActions, HandlesBack { navigationIconClick = { onBackPressed() }, changeLanguageButtonClick = { viewModel.dispatchEvent(ChangeLanguage) }, updateButtonClick = { launchPlayStoreForUpdate() }, - logoutButtonClick = { viewModel.dispatchEvent(LogoutButtonClicked) } + logoutButtonClick = { viewModel.dispatchEvent(LogoutButtonClicked) }, + syncMedicalRecordClick = { viewModel.dispatchEvent(PushAllMedicalRecordsClicked) }, ) } } diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsUI.kt b/app/src/main/java/org/simple/clinic/settings/SettingsUI.kt index 1dad1517aa5..ed6c9f9542b 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsUI.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsUI.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Divider @@ -42,6 +41,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.simple.clinic.R +import org.simple.clinic.common.ui.components.FilledButton import org.simple.clinic.common.ui.components.OutlinedButton import org.simple.clinic.common.ui.components.TopAppBar import org.simple.clinic.common.ui.theme.SimpleRedTheme @@ -54,6 +54,7 @@ fun SettingsScreen( changeLanguageButtonClick: () -> Unit, updateButtonClick: () -> Unit, logoutButtonClick: () -> Unit, + syncMedicalRecordClick: () -> Unit, modifier: Modifier = Modifier, ) { SimpleTheme { @@ -80,11 +81,12 @@ fun SettingsScreen( paddingValues = paddingValues, changeLanguageButtonClick = changeLanguageButtonClick, updateButtonClick = updateButtonClick, - logoutButtonClick = logoutButtonClick + logoutButtonClick = logoutButtonClick, + syncMedicalRecordClick = syncMedicalRecordClick ) } - if (model.isUserLoggingOut == true) { + if (model.isUserLoggingOut == true || model.isMedicalRecordsPushInProgress) { // Scrim Box( modifier = Modifier @@ -106,7 +108,8 @@ private fun SettingsList( paddingValues: PaddingValues, changeLanguageButtonClick: () -> Unit, updateButtonClick: () -> Unit, - logoutButtonClick: () -> Unit + logoutButtonClick: () -> Unit, + syncMedicalRecordClick: () -> Unit, ) { LazyColumn( modifier = Modifier @@ -257,6 +260,17 @@ private fun SettingsList( logout = logoutButtonClick ) } + + if (model.showDiagnosisButton) { + item { + SyncMedicalRecordsButton( + modifier = Modifier + .padding(top = 48.dp) + .testTag("SETTINGS_LOGOUT_BUTTON"), + syncMedicalRecords = syncMedicalRecordClick + ) + } + } } } @@ -337,6 +351,25 @@ private fun LogoutButton( } } +@Composable +private fun SyncMedicalRecordsButton( + modifier: Modifier = Modifier, + syncMedicalRecords: () -> Unit +) { + Box(modifier) { + SimpleTheme { + FilledButton( + onClick = syncMedicalRecords, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text(text = stringResource(id = R.string.settings_sync_medical_records).uppercase()) + } + } + } +} + private val previewSettingsModel = SettingsModel( name = "Riya Murthy", phoneNumber = "1111111111", @@ -345,7 +378,9 @@ private val previewSettingsModel = SettingsModel( isUpdateAvailable = true, isUserLoggingOut = null, isDatabaseEncrypted = true, + isPushingMedicalRecords = false, isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true, ) @Preview @@ -384,6 +419,9 @@ private fun SettingsScreenContentPreview() { }, logoutButtonClick = { // no-op + }, + syncMedicalRecordClick = { + // no-op } ) } diff --git a/app/src/main/java/org/simple/clinic/settings/SettingsUpdate.kt b/app/src/main/java/org/simple/clinic/settings/SettingsUpdate.kt index f43a509f42d..f8f36965144 100644 --- a/app/src/main/java/org/simple/clinic/settings/SettingsUpdate.kt +++ b/app/src/main/java/org/simple/clinic/settings/SettingsUpdate.kt @@ -26,6 +26,10 @@ class SettingsUpdate : Update { is DatabaseEncryptionStatusLoaded -> next( model.databaseEncryptionStatusLoaded(isDatabaseEncrypted = event.isDatabaseEncrypted) ) + + is PushAllMedicalRecordsClicked -> next(model.medicalRecordsPushStarted(), FetchCompleteMedicalRecords) + is MedicalRecordsFetched -> dispatch(PushCompleteMedicalRecordsOnline(event.completeMedicalRecords)) + is PushMedicalRecordsOnlineCompleted -> next(model.medicalRecordsPushCompleted()) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 566687d8917..bb1e87ffe87 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -483,6 +483,7 @@ Update Version %1$s Logout + Sync medical records Logout Are you sure you want to logout? Yes diff --git a/app/src/test/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnlineTest.kt b/app/src/test/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnlineTest.kt new file mode 100644 index 00000000000..066cb9c2ba2 --- /dev/null +++ b/app/src/test/java/org/simple/clinic/patient/medicalRecords/PushMedicalRecordsOnlineTest.kt @@ -0,0 +1,122 @@ +package org.simple.clinic.patient.medicalRecords + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.simple.clinic.TestData +import org.simple.clinic.patient.CompleteMedicalRecord +import org.simple.clinic.patient.sync.PatientSyncApi +import org.simple.clinic.sync.DataPushResponse +import org.simple.clinic.util.TestUtcClock +import retrofit2.Call +import retrofit2.Response +import java.time.Instant + +class PushMedicalRecordsOnlineTest { + + private val patientSyncApi = mock() + + private val clock = TestUtcClock( + Instant.parse("2018-01-01T00:00:00Z") + ) + + private val pushMedicalRecordsOnline = PushMedicalRecordsOnline( + patientSyncApi = patientSyncApi, + clock = clock, + ) + + + private fun mockCall(): Call = mock() + + private fun fakeMedicalRecord(): CompleteMedicalRecord = TestData.completeMedicalRecord() + + + @Test + fun `when no medical records then return NothingToPush and do not call api`() { + val result = pushMedicalRecordsOnline + .pushAllMedicalRecordsOnServer(emptyList()) + + assertEquals( + PushMedicalRecordsOnline.Result.NothingToPush, + result + ) + + verify(patientSyncApi, org.mockito.kotlin.never()) + .pushAllPatientsData(any()) + } + + @Test + fun `when server returns 200 then return Success`() { + val call = mockCall() + + whenever(patientSyncApi.pushAllPatientsData(any())) + .thenReturn(call) + + whenever(call.execute()) + .thenReturn( + Response.success( + DataPushResponse( + validationErrors = emptyList() + ) + ) + ) + + val result = pushMedicalRecordsOnline.pushAllMedicalRecordsOnServer( + listOf(fakeMedicalRecord()) + ) + + assertEquals( + PushMedicalRecordsOnline.Result.Success, + result + ) + + verify(patientSyncApi).pushAllPatientsData(any()) + } + + @Test + fun `when server returns non-200 then return ServerError`() { + val call = mockCall() + + whenever(patientSyncApi.pushAllPatientsData(any())) + .thenReturn(call) + + whenever(call.execute()) + .thenReturn( + Response.error( + 500, + "boom".toResponseBody("text/plain".toMediaType()) + ) + ) + + val result = pushMedicalRecordsOnline.pushAllMedicalRecordsOnServer( + listOf(fakeMedicalRecord()) + ) + + result as PushMedicalRecordsOnline.Result.ServerError + + assertEquals(500, result.code) + assertEquals("boom", result.message) + } + + @Test + fun `when api throws exception then return NetworkError`() { + val call = mockCall() + + whenever(patientSyncApi.pushAllPatientsData(any())) + .thenReturn(call) + + whenever(call.execute()) + .thenThrow(RuntimeException("network down")) + + val result = pushMedicalRecordsOnline.pushAllMedicalRecordsOnServer( + listOf(fakeMedicalRecord()) + ) + + assert(result is PushMedicalRecordsOnline.Result.NetworkError) + } +} diff --git a/app/src/test/java/org/simple/clinic/settings/SettingsEffectHandlerTest.kt b/app/src/test/java/org/simple/clinic/settings/SettingsEffectHandlerTest.kt index ff79ea6b681..ff36836834c 100644 --- a/app/src/test/java/org/simple/clinic/settings/SettingsEffectHandlerTest.kt +++ b/app/src/test/java/org/simple/clinic/settings/SettingsEffectHandlerTest.kt @@ -1,20 +1,24 @@ package org.simple.clinic.settings +import io.reactivex.Observable +import io.reactivex.Single +import org.junit.After +import org.junit.Test import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever -import io.reactivex.Observable -import io.reactivex.Single -import org.junit.After -import org.junit.Test import org.simple.clinic.TestData import org.simple.clinic.appupdate.AppUpdateState import org.simple.clinic.appupdate.AppUpdateState.ShowAppUpdate import org.simple.clinic.appupdate.CheckAppUpdateAvailability import org.simple.clinic.mobius.EffectHandlerTestCase +import org.simple.clinic.patient.PatientRepository +import org.simple.clinic.patient.businessid.Identifier +import org.simple.clinic.patient.businessid.Identifier.IdentifierType.BpPassport +import org.simple.clinic.patient.medicalRecords.PushMedicalRecordsOnline import org.simple.clinic.storage.DatabaseEncryptor import org.simple.clinic.user.User import org.simple.clinic.user.UserSession @@ -33,14 +37,20 @@ class SettingsEffectHandlerTest { private val checkAppUpdateAvailability = mock() private val databaseEncryptor = mock() + private val patientRepository = mock() + + private val pushMedicalRecordsOnline = mock() + private val effectHandler = SettingsEffectHandler( userSession = userSession, settingsRepository = settingsRepository, + patientRepository = patientRepository, schedulersProvider = TrampolineSchedulersProvider(), appVersionFetcher = appVersionFetcher, appUpdateAvailability = checkAppUpdateAvailability, databaseEncryptor = databaseEncryptor, - viewEffectsConsumer = SettingsViewEffectHandler(uiActions)::handle + viewEffectsConsumer = SettingsViewEffectHandler(uiActions)::handle, + pushMedicalRecordsOnline = pushMedicalRecordsOnline ).build() private val testCase = EffectHandlerTestCase(effectHandler) @@ -212,4 +222,51 @@ class SettingsEffectHandlerTest { testCase.assertOutgoingEvents(DatabaseEncryptionStatusLoaded(isDatabaseEncrypted = true)) } + + @Test + fun `when fetch complete medical records effect is received, then fetch all medical records`() { + //given + val identifier = Identifier("4f1cea37-70ff-498e-bd09-ad0ca75628ff", BpPassport) + val commonIdentifier = TestData.businessId(identifier = identifier) + val patientUuid1 = TestData.patientProfile(patientUuid = UUID.fromString("0b78c024-f527-4306-9e20-6ae6d7251e9b"), businessId = commonIdentifier) + val patientUuid2 = TestData.patientProfile(patientUuid = UUID.fromString("47fdb968-9512-4e50-b95f-cc83c6de4b0a"), businessId = commonIdentifier) + + val completeMedicalRecord = TestData.completeMedicalRecord(patient = patientUuid1) + val completeMedicalRecord2 = TestData.completeMedicalRecord(patient = patientUuid2) + + val medicalRecords = listOf(completeMedicalRecord, completeMedicalRecord2) + + whenever(patientRepository.fetchCompleteMedicalRecord()) doReturn medicalRecords + + // when + testCase.dispatch(FetchCompleteMedicalRecords) + + // then + testCase.assertOutgoingEvents(MedicalRecordsFetched(medicalRecords)) + verifyNoMoreInteractions(uiActions) + } + + @Test + fun `when push complete medical records online effect is received, then push all medical records online`() { + // given + val identifier = Identifier("4f1cea37-70ff-498e-bd09-ad0ca75628ff", BpPassport) + val commonIdentifier = TestData.businessId(identifier = identifier) + val patientUuid1 = TestData.patientProfile(patientUuid = UUID.fromString("0b78c024-f527-4306-9e20-6ae6d7251e9b"), businessId = commonIdentifier) + val patientUuid2 = TestData.patientProfile(patientUuid = UUID.fromString("47fdb968-9512-4e50-b95f-cc83c6de4b0a"), businessId = commonIdentifier) + + val completeMedicalRecord = TestData.completeMedicalRecord(patient = patientUuid1) + val completeMedicalRecord2 = TestData.completeMedicalRecord(patient = patientUuid2) + + val medicalRecords = listOf(completeMedicalRecord, completeMedicalRecord2) + + val results = PushMedicalRecordsOnline.Result.Success + whenever(pushMedicalRecordsOnline.pushAllMedicalRecordsOnServer(medicalRecords)) doReturn results + + // when + testCase.dispatch(PushCompleteMedicalRecordsOnline(medicalRecords)) + + // then + testCase.assertOutgoingEvents(PushMedicalRecordsOnlineCompleted(results)) + verifyNoMoreInteractions(uiActions) + } } diff --git a/app/src/test/java/org/simple/clinic/settings/SettingsInitTest.kt b/app/src/test/java/org/simple/clinic/settings/SettingsInitTest.kt index ca490caedd9..16663a7ba7f 100644 --- a/app/src/test/java/org/simple/clinic/settings/SettingsInitTest.kt +++ b/app/src/test/java/org/simple/clinic/settings/SettingsInitTest.kt @@ -9,7 +9,8 @@ import org.junit.Test class SettingsInitTest { private val defaultModel = SettingsModel.default( - isChangeLanguageFeatureEnabled = true + isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true, ) private val spec = InitSpec(SettingsInit()) diff --git a/app/src/test/java/org/simple/clinic/settings/SettingsUpdateTest.kt b/app/src/test/java/org/simple/clinic/settings/SettingsUpdateTest.kt index dc047520e7b..4ada8b959db 100644 --- a/app/src/test/java/org/simple/clinic/settings/SettingsUpdateTest.kt +++ b/app/src/test/java/org/simple/clinic/settings/SettingsUpdateTest.kt @@ -7,13 +7,18 @@ import com.spotify.mobius.test.NextMatchers.hasNoModel import com.spotify.mobius.test.UpdateSpec import com.spotify.mobius.test.UpdateSpec.assertThatNext import org.junit.Test +import org.simple.clinic.TestData +import org.simple.clinic.patient.businessid.Identifier +import org.simple.clinic.patient.businessid.Identifier.IdentifierType.BpPassport import org.simple.clinic.user.UserSession.LogoutResult.Failure import org.simple.clinic.user.UserSession.LogoutResult.Success +import java.util.UUID class SettingsUpdateTest { private val defaultModel = SettingsModel.default( isChangeLanguageFeatureEnabled = true, + showDiagnosisButton = true ) private val spec = UpdateSpec(SettingsUpdate()) @@ -152,13 +157,35 @@ class SettingsUpdateTest { } @Test - fun `when database encryption status is loaded, then update ui`() { + fun `when sync medical records button is clicked, then update model and fetch complete medical records`() { spec .given(defaultModel) - .whenEvent(DatabaseEncryptionStatusLoaded(isDatabaseEncrypted = true)) + .whenEvent(PushAllMedicalRecordsClicked) .then(assertThatNext( - hasModel(defaultModel.databaseEncryptionStatusLoaded(isDatabaseEncrypted = true)), - hasNoEffects() + hasModel(defaultModel.medicalRecordsPushStarted()), + hasEffects(FetchCompleteMedicalRecords) + )) + } + + @Test + fun `when complete medical records are fetch, then push them online`() { + // given + val identifier = Identifier("4f1cea37-70ff-498e-bd09-ad0ca75628ff", BpPassport) + val commonIdentifier = TestData.businessId(identifier = identifier) + val patientUuid1 = TestData.patientProfile(patientUuid = UUID.fromString("0b78c024-f527-4306-9e20-6ae6d7251e9b"), businessId = commonIdentifier) + val patientUuid2 = TestData.patientProfile(patientUuid = UUID.fromString("47fdb968-9512-4e50-b95f-cc83c6de4b0a"), businessId = commonIdentifier) + + val completeMedicalRecord = TestData.completeMedicalRecord(patient = patientUuid1) + val completeMedicalRecord2 = TestData.completeMedicalRecord(patient = patientUuid2) + + val medicalRecords = listOf(completeMedicalRecord, completeMedicalRecord2) + + spec + .given(defaultModel) + .whenEvent(MedicalRecordsFetched(medicalRecords)) + .then(assertThatNext( + hasNoModel(), + hasEffects(PushCompleteMedicalRecordsOnline(medicalRecords)) )) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9c1810659d..ea4245febfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidx-lifecycle = "2.10.0" androidx-activity = "1.12.2" chucker = "4.3.0" -dagger = "2.58" +dagger = "2.59.1" kotlin = "2.3.0" @@ -42,7 +42,7 @@ androidx-compose-bom = "2026.01.00" composeThemeAdapter = "0.36.0" -sqlCipher = "4.12.0" +sqlCipher = "4.13.0" [libraries] android-desugaring = "com.android.tools:desugar_jdk_libs:2.1.5"