diff --git a/CHANGELOG.md b/CHANGELOG.md index c7460cbda6e..9a60da5554e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,10 +62,12 @@ - Bug fixes in CI workflows - Fetch `lab_based_cvd_risk_calculation_sheet` from remote config - Add `cholesterol_value` to `MedicalHistory` table -- Bump AGP to v8.8.1 - Fix JSON variable name in non-lab based statin calculation sheet - Add `LabBasedCVDRiskCalculator` and effect to calculate lab based cvd risk - Add `CholesterolEntrySheet` +- Bump AGP to v8.8.2 +- Calculate lab-based CVD risk score +- Update copy of statin nudge to support lab-based nudge ### Fixes diff --git a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt index 67047d67105..09a4809969d 100644 --- a/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt +++ b/app/src/main/java/org/simple/clinic/cvdrisk/CVDRiskLevel.kt @@ -6,14 +6,16 @@ import org.simple.clinic.R enum class CVDRiskLevel(val displayStringResId: Int, val color: Color) { LOW_HIGH(R.string.statin_alert_low_high_risk_patient_x, Color(0xFFFF7A00)), MEDIUM_HIGH(R.string.statin_alert_medium_high_risk_patient_x, Color(0xFFFF7A00)), - HIGH(R.string.statin_alert_high_risk_patient_x, Color(0xFFFF3355)); + HIGH(R.string.statin_alert_high_risk_patient_x, Color(0xFFFF3355)), + VERY_HIGH(R.string.statin_alert_very_high_risk_range, Color(0xFFFF3355)); companion object { fun compute(cvdRiskRange: CVDRiskRange): CVDRiskLevel { return when { cvdRiskRange.min < 5 -> LOW_HIGH cvdRiskRange.min < 10 -> MEDIUM_HIGH - else -> HIGH + cvdRiskRange.min < 20 -> HIGH + else -> VERY_HIGH } } } diff --git a/app/src/main/java/org/simple/clinic/cvdrisk/StatinInfo.kt b/app/src/main/java/org/simple/clinic/cvdrisk/StatinInfo.kt index 007742f5265..3fbb23362d4 100644 --- a/app/src/main/java/org/simple/clinic/cvdrisk/StatinInfo.kt +++ b/app/src/main/java/org/simple/clinic/cvdrisk/StatinInfo.kt @@ -7,16 +7,19 @@ import org.simple.clinic.patientattribute.BMIReading @Parcelize data class StatinInfo( - val canPrescribeStatin: Boolean, + val canShowStatinNudge: Boolean, val cvdRisk: CVDRiskRange? = null, val isSmoker: Answer = Answer.Unanswered, val bmiReading: BMIReading? = null, val hasCVD: Boolean = false, + val hasDiabetes: Boolean = false, + val age: Int = 0, + val cholesterol: Float? = null, ) : Parcelable { companion object { fun default(): StatinInfo { return StatinInfo( - canPrescribeStatin = false, + canShowStatinNudge = false, ) } } 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 0842f165093..d0e9ce626c7 100644 --- a/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt +++ b/app/src/main/java/org/simple/clinic/medicalhistory/MedicalHistory.kt @@ -12,7 +12,6 @@ import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SimpleSQLiteQuery import io.reactivex.Flowable -import io.reactivex.Observable import kotlinx.parcelize.Parcelize import org.simple.clinic.medicalhistory.Answer.Unanswered import org.simple.clinic.medicalhistory.MedicalHistoryQuestion.DiagnosedWithDiabetes @@ -69,6 +68,14 @@ data class MedicalHistory( val deletedAt: Instant? ) : Parcelable { + + companion object { + + fun convertCholesterolToMmol(cholesterol: Float): Float { + return cholesterol / 38.67f + } + } + val diagnosisRecorded: Boolean get() = diagnosedWithHypertension != Unanswered && diagnosedWithDiabetes != Unanswered diff --git a/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistoryPayload.kt b/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistoryPayload.kt index ec56bee2289..45f70f140e8 100644 --- a/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistoryPayload.kt +++ b/app/src/main/java/org/simple/clinic/medicalhistory/sync/MedicalHistoryPayload.kt @@ -46,7 +46,7 @@ data class MedicalHistoryPayload( @Json(name = "smoking") val isSmoking: Answer, - @Json(name = "cholesterol_value") + @Json(name = "cholesterol") val cholesterol: Float?, @Json(name = "created_at") diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt index 7f1f9dbd95a..5f2b74f1e2b 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffect.kt @@ -141,3 +141,5 @@ data class ShowHypertensionDiagnosisWarning(val continueToDiabetesDiagnosisWarni data object ShowSmokingStatusDialog : PatientSummaryViewEffect() data class OpenBMIEntrySheet(val patientUuid: UUID) : PatientSummaryViewEffect() + +data class OpenCholesterolEntrySheet(val patientUuid: UUID) : PatientSummaryViewEffect() diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt index e5cc820d4f8..7667c693972 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEffectHandler.kt @@ -16,13 +16,13 @@ import org.simple.clinic.cvdrisk.CVDRiskRange import org.simple.clinic.cvdrisk.CVDRiskRepository import org.simple.clinic.cvdrisk.LabBasedCVDRiskInput import org.simple.clinic.cvdrisk.NonLabBasedCVDRiskInput -import org.simple.clinic.cvdrisk.StatinInfo import org.simple.clinic.cvdrisk.calculator.LabBasedCVDRiskCalculator import org.simple.clinic.cvdrisk.calculator.NonLabBasedCVDRiskCalculator import org.simple.clinic.drugs.DiagnosisWarningPrescriptions import org.simple.clinic.drugs.PrescriptionRepository import org.simple.clinic.facility.Facility import org.simple.clinic.facility.FacilityRepository +import org.simple.clinic.medicalhistory.MedicalHistory import org.simple.clinic.medicalhistory.MedicalHistoryQuestion import org.simple.clinic.medicalhistory.MedicalHistoryRepository import org.simple.clinic.overdue.AppointmentRepository @@ -233,7 +233,7 @@ class PatientSummaryEffectHandler @AssistedInject constructor( systolic = bloodPressure.reading.systolic, isSmoker = medicalHistory.isSmoking, diagnosedWithDiabetes = medicalHistory.diagnosedWithDiabetes, - cholesterol = null //Update once the value is available in medical history + cholesterol = medicalHistory.cholesterol?.let { MedicalHistory.convertCholesterolToMmol(it) } ) ) } @@ -279,18 +279,20 @@ class PatientSummaryEffectHandler @AssistedInject constructor( .observeOn(schedulersProvider.io()) .map { effect -> val patientUuid = effect.patientUuid + val patient = patientRepository.patientImmediate(patientUuid) val medicalHistory = medicalHistoryRepository.historyForPatientOrDefaultImmediate( defaultHistoryUuid = uuidGenerator.v4(), patientUuid = patientUuid ) - val bmiReading = patientAttributeRepository.getPatientAttributeImmediate(patientUuid) - val cvdRisk = cvdRiskRepository.getCVDRiskImmediate(patientUuid) - StatinInfoLoaded(StatinInfo( - canPrescribeStatin = cvdRisk?.riskScore?.canPrescribeStatin ?: false, - cvdRisk = cvdRisk?.riskScore, - isSmoker = medicalHistory.isSmoking, - bmiReading = bmiReading?.bmiReading, - )) + val patientAttribute = patientAttributeRepository.getPatientAttributeImmediate(patientUuid) + val riskRange = cvdRiskRepository.getCVDRiskImmediate(patientUuid)?.riskScore + + StatinInfoLoaded( + age = patient!!.ageDetails.estimateAge(userClock), + medicalHistory = medicalHistory, + riskRange = riskRange, + bmiReading = patientAttribute?.bmiReading, + ) } } } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt index 47d05ee5d6b..b88c4bae137 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryEvent.kt @@ -2,13 +2,13 @@ package org.simple.clinic.summary import org.simple.clinic.cvdrisk.CVDRisk import org.simple.clinic.cvdrisk.CVDRiskRange -import org.simple.clinic.cvdrisk.StatinInfo import org.simple.clinic.drugs.DiagnosisWarningPrescriptions import org.simple.clinic.drugs.PrescribedDrug import org.simple.clinic.facility.Facility import org.simple.clinic.medicalhistory.Answer import org.simple.clinic.medicalhistory.MedicalHistory import org.simple.clinic.overdue.Appointment +import org.simple.clinic.patientattribute.BMIReading import org.simple.clinic.patientattribute.PatientAttribute import org.simple.clinic.reassignpatient.ReassignPatientSheetClosedFrom import org.simple.clinic.reassignpatient.ReassignPatientSheetOpenedFrom @@ -163,7 +163,10 @@ data class CVDRiskCalculated( data object CVDRiskUpdated : PatientSummaryEvent() data class StatinInfoLoaded( - val statinInfo: StatinInfo + val age: Int, + val medicalHistory: MedicalHistory, + val riskRange: CVDRiskRange?, + val bmiReading: BMIReading?, ) : PatientSummaryEvent() data object AddSmokingClicked : PatientSummaryEvent() @@ -175,3 +178,7 @@ data class SmokingStatusAnswered( data object BMIReadingAdded : PatientSummaryEvent() data object AddBMIClicked : PatientSummaryEvent() + +data object AddCholesterolClicked: PatientSummaryEvent() + +data object CholesterolAdded : PatientSummaryEvent() diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt index d71ee93bbcb..e9e8ea86183 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryScreen.kt @@ -71,6 +71,7 @@ import org.simple.clinic.reassignpatient.ReassignPatientSheetOpenedFrom import org.simple.clinic.remoteconfig.ConfigReader import org.simple.clinic.scheduleappointment.ScheduleAppointmentSheet import org.simple.clinic.scheduleappointment.facilityselection.FacilitySelectionScreen +import org.simple.clinic.summary.addcholesterol.CholesterolEntrySheet import org.simple.clinic.summary.addphone.AddPhoneNumberDialog import org.simple.clinic.summary.compose.StatinNudge import org.simple.clinic.summary.linkId.LinkIdWithPatientSheet.LinkIdWithPatientSheetKey @@ -327,8 +328,11 @@ class PatientSummaryScreen : StatinNudge( statinInfo = statinInfo, modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp), + isNonLabBasedStatinNudgeEnabled = features.isEnabled(Feature.NonLabBasedStatinNudge), + isLabBasedStatinNudgeEnabled = features.isEnabled(Feature.LabBasedStatinNudge), addSmokingClick = { additionalEvents.notify(AddSmokingClicked) }, - addBMIClick = { additionalEvents.notify(AddBMIClicked) } + addBMIClick = { additionalEvents.notify(AddBMIClicked) }, + addCholesterol = { additionalEvents.notify(AddCholesterolClicked) } ) } } @@ -360,6 +364,10 @@ class PatientSummaryScreen : is ScreenRequest.BMIEntrySheet -> { additionalEvents.notify(BMIReadingAdded) } + + is ScreenRequest.CholesterolEntrySheet -> { + additionalEvents.notify(CholesterolAdded) + } } } @@ -757,6 +765,10 @@ class PatientSummaryScreen : .show() } + override fun openCholesterolEntrySheet(patientUuid: UUID) { + router.pushExpectingResult(ScreenRequest.CholesterolEntrySheet, CholesterolEntrySheet.Key(patientUuid)) + } + override fun openBMIEntrySheet(patientUuid: UUID) { router.pushExpectingResult(ScreenRequest.BMIEntrySheet, BMIEntrySheet.Key(patientUuid)) } @@ -903,5 +915,8 @@ class PatientSummaryScreen : @Parcelize data object BMIEntrySheet : ScreenRequest() + + @Parcelize + data object CholesterolEntrySheet : ScreenRequest() } } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUiActions.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUiActions.kt index 6178b59267a..32964ce7af4 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUiActions.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUiActions.kt @@ -42,4 +42,5 @@ interface PatientSummaryUiActions { fun showHypertensionDiagnosisWarning(continueToDiabetesDiagnosisWarning: Boolean) fun showSmokingStatusDialog() fun openBMIEntrySheet(patientUuid: UUID) + fun openCholesterolEntrySheet(patientUuid: UUID) } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt index 3e46dd8eac4..edad60f74ab 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryUpdate.kt @@ -5,6 +5,7 @@ import com.spotify.mobius.Next.next import com.spotify.mobius.Next.noChange import com.spotify.mobius.Update import org.simple.clinic.cvdrisk.CVDRiskLevel +import org.simple.clinic.cvdrisk.CVDRiskRange import org.simple.clinic.cvdrisk.StatinInfo import org.simple.clinic.drugs.DiagnosisWarningPrescriptions import org.simple.clinic.drugs.PrescribedDrug @@ -33,7 +34,8 @@ class PatientSummaryUpdate( private val isNonLabBasedStatinNudgeEnabled: Boolean, private val isLabBasedStatinNudgeEnabled: Boolean, private val minAgeForStatin: Int = 40, - private val maxAgeForCVDRisk: Int = 74 + private val maxAgeForCVDRisk: Int = 74, + private val minReqMaxRiskRangeForLabBasedNudge: Int = 10, ) : Update { override fun update( @@ -109,12 +111,89 @@ class PatientSummaryUpdate( is SmokingStatusAnswered -> dispatch(UpdateSmokingStatus(model.patientUuid, event.isSmoker)) is BMIReadingAdded -> dispatch(CalculateNonLabBasedCVDRisk(model.patientSummaryProfile!!.patient)) is AddBMIClicked -> dispatch(OpenBMIEntrySheet(model.patientUuid)) + is AddCholesterolClicked -> dispatch(OpenCholesterolEntrySheet(model.patientUuid)) + CholesterolAdded -> dispatch(CalculateLabBasedCVDRisk(model.patientSummaryProfile!!.patient)) } } private fun statinPrescriptionCheckInfoLoaded( event: StatinPrescriptionCheckInfoLoaded, model: PatientSummaryModel + ): Next { + return when { + isLabBasedStatinNudgeEnabled -> labBasedStatinNudge(event, model) + isNonLabBasedStatinNudgeEnabled -> nonLabBasedStatinNudge(event, model) + else -> { + throw IllegalArgumentException("Unknown case, statin prescription check info is unhandled") + } + } + } + + private fun labBasedStatinNudge( + event: StatinPrescriptionCheckInfoLoaded, + model: PatientSummaryModel + ): Next { + val hasHadStroke = event.medicalHistory.hasHadStroke == Yes + val hasHadHeartAttack = event.medicalHistory.hasHadHeartAttack == Yes + val hasDiabetes = event.medicalHistory.diagnosedWithDiabetes == Yes + + val hasCVD = hasHadStroke || hasHadHeartAttack + val areStatinsPrescribedAlready = event.prescriptions.any { it.name.contains("statin", ignoreCase = true) } + val canPrescribeStatin = event.isPatientDead.not() && + event.wasBPMeasuredWithin90Days && + areStatinsPrescribedAlready.not() + + val isEligibleForLabBasedCvdRisk = + event.age in minAgeForStatin..maxAgeForCVDRisk && isLabBasedStatinNudgeEnabled && canPrescribeStatin + val shouldCalculateCVDRisk = event.cvdRiskRange == null || event.hasMedicalHistoryChanged || !event.wasCVDCalculatedWithin90Days + val isCvdRiskCalculationRequired = isEligibleForLabBasedCvdRisk && shouldCalculateCVDRisk + + return when { + hasCVD -> { + val updatedModel = model.updateStatinInfo( + StatinInfo( + canShowStatinNudge = canPrescribeStatin, + hasCVD = true + ) + ) + + next(updatedModel) + } + + hasDiabetes && event.age > maxAgeForCVDRisk -> { + val updatedModel = model.updateStatinInfo( + StatinInfo( + canShowStatinNudge = canPrescribeStatin, + hasDiabetes = true + ) + ) + + next(updatedModel) + } + + (hasDiabetes && isCvdRiskCalculationRequired) || isCvdRiskCalculationRequired -> { + dispatch(CalculateLabBasedCVDRisk(model.patientSummaryProfile!!.patient)) + } + + isEligibleForLabBasedCvdRisk -> { + dispatch(LoadStatinInfo(model.patientUuid)) + } + + else -> { + val updatedModel = model.updateStatinInfo( + StatinInfo( + canShowStatinNudge = false, + hasCVD = false + ) + ) + next(updatedModel) + } + } + } + + private fun nonLabBasedStatinNudge( + event: StatinPrescriptionCheckInfoLoaded, + model: PatientSummaryModel ): Next { val hasHadStroke = event.medicalHistory.hasHadStroke == Yes val hasHadHeartAttack = event.medicalHistory.hasHadHeartAttack == Yes @@ -140,7 +219,7 @@ class PatientSummaryUpdate( hasCVD || (hasDiabetes && event.age >= minAgeForStatin) -> { val updatedModel = model.updateStatinInfo( StatinInfo( - canPrescribeStatin = canPrescribeStatin, + canShowStatinNudge = canPrescribeStatin, hasCVD = hasCVD ) ) @@ -158,7 +237,7 @@ class PatientSummaryUpdate( else -> { val updatedModel = model.updateStatinInfo( StatinInfo( - canPrescribeStatin = false, + canShowStatinNudge = false, hasCVD = false ) ) @@ -182,19 +261,63 @@ class PatientSummaryUpdate( event: StatinInfoLoaded, model: PatientSummaryModel ): Next { - val canShowSmokingStatusDialog = - event.statinInfo.canPrescribeStatin && - (event.statinInfo.cvdRisk?.level == CVDRiskLevel.LOW_HIGH || - event.statinInfo.cvdRisk?.level == CVDRiskLevel.MEDIUM_HIGH) && - event.statinInfo.isSmoker == MedicalHistoryAnswer.Unanswered && + val age = event.age + val medicalHistory = event.medicalHistory + val hasCVD = medicalHistory.hasHadStroke == Yes || medicalHistory.hasHadHeartAttack == Yes + val hasDiabetes = medicalHistory.diagnosedWithDiabetes == Yes + val cholesterol = medicalHistory.cholesterol + val bmiReading = event.bmiReading + val calculatedRiskRange = event.riskRange + val canPrescribeStatin = if (isLabBasedStatinNudgeEnabled) { + checkIfLabBasedNudgeCanBeShown(event.medicalHistory, event.riskRange) + } else { + calculatedRiskRange?.canPrescribeStatin ?: false + } + + val canShowSmokingStatusDialog = canPrescribeStatin && + (calculatedRiskRange?.level == CVDRiskLevel.LOW_HIGH || + calculatedRiskRange?.level == CVDRiskLevel.MEDIUM_HIGH) && + medicalHistory.isSmoking == MedicalHistoryAnswer.Unanswered && !model.hasShownSmokingStatusDialog + val riskRange = when { + isLabBasedStatinNudgeEnabled -> labBasedRiskRange(calculatedRiskRange) + else -> calculatedRiskRange + } + + val statinInfo = StatinInfo( + canShowStatinNudge = canPrescribeStatin, + cvdRisk = riskRange, + isSmoker = medicalHistory.isSmoking, + bmiReading = bmiReading, + hasCVD = hasCVD, + hasDiabetes = hasDiabetes, + age = age, + cholesterol = cholesterol, + ) + return if (canShowSmokingStatusDialog) { - next(model.updateStatinInfo(event.statinInfo) - .showSmokingStatusDialog(), - ShowSmokingStatusDialog) + next(model.updateStatinInfo(statinInfo).showSmokingStatusDialog(), ShowSmokingStatusDialog) + } else { + next(model.updateStatinInfo(statinInfo)) + } + } + + private fun checkIfLabBasedNudgeCanBeShown( + medicalHistory: MedicalHistory, + riskRange: CVDRiskRange? + ): Boolean { + val maxRiskRange = riskRange?.max ?: 0 + return !(medicalHistory.diagnosedWithDiabetes != Yes && maxRiskRange < minReqMaxRiskRangeForLabBasedNudge) + } + + private fun labBasedRiskRange(calculatedRiskRange: CVDRiskRange?): CVDRiskRange? { + if (calculatedRiskRange == null) return null + + return if (calculatedRiskRange.max < minReqMaxRiskRangeForLabBasedNudge) { + null } else { - next(model.updateStatinInfo(event.statinInfo)) + calculatedRiskRange } } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewEffectHandler.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewEffectHandler.kt index 8de7986e9bf..200e3819414 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewEffectHandler.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewEffectHandler.kt @@ -40,6 +40,7 @@ class PatientSummaryViewEffectHandler( is ShowHypertensionDiagnosisWarning -> uiActions.showHypertensionDiagnosisWarning(viewEffect.continueToDiabetesDiagnosisWarning) is ShowSmokingStatusDialog -> uiActions.showSmokingStatusDialog() is OpenBMIEntrySheet -> uiActions.openBMIEntrySheet(viewEffect.patientUuid) + is OpenCholesterolEntrySheet -> uiActions.openCholesterolEntrySheet(viewEffect.patientUuid) }.exhaustive() } } diff --git a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt index fdcdf9b1066..aa772fbfc9f 100644 --- a/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt +++ b/app/src/main/java/org/simple/clinic/summary/PatientSummaryViewRenderer.kt @@ -47,7 +47,7 @@ class PatientSummaryViewRenderer( } private fun renderClinicalDecisionBasedOnAppointment(model: PatientSummaryModel) { - if (model.statinInfo?.canPrescribeStatin == true) + if (model.statinInfo?.canShowStatinNudge == true) return if (model.hasScheduledAppointment) { @@ -154,7 +154,7 @@ class PatientSummaryViewRenderer( if (model.hasStatinInfoLoaded.not()) return ui.updateStatinAlert(model.statinInfo!!) - if (model.statinInfo.canPrescribeStatin) { + if (model.statinInfo.canShowStatinNudge) { ui.hideClinicalDecisionSupportAlertWithoutAnimation() } } diff --git a/app/src/main/java/org/simple/clinic/summary/addcholesterol/CholesterolEntrySheet.kt b/app/src/main/java/org/simple/clinic/summary/addcholesterol/CholesterolEntrySheet.kt index d8dc941fee9..e8eb7476653 100644 --- a/app/src/main/java/org/simple/clinic/summary/addcholesterol/CholesterolEntrySheet.kt +++ b/app/src/main/java/org/simple/clinic/summary/addcholesterol/CholesterolEntrySheet.kt @@ -1,6 +1,7 @@ package org.simple.clinic.summary.addcholesterol import android.content.Context +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import android.view.inputmethod.EditorInfo @@ -20,6 +21,7 @@ import org.simple.clinic.mobius.ViewEffectsHandler import org.simple.clinic.mobius.ViewRenderer import org.simple.clinic.navigation.v2.Router import org.simple.clinic.navigation.v2.ScreenKey +import org.simple.clinic.navigation.v2.Succeeded import org.simple.clinic.navigation.v2.fragments.BaseBottomSheet import org.simple.clinic.widgets.UiEvent import org.simple.clinic.widgets.textChanges @@ -96,7 +98,7 @@ class CholesterolEntrySheet : BaseBottomSheet< } override fun dismissSheet() { - router.pop() + router.popWithResult(Succeeded(CholesterolAdded)) } override fun showReqMaxCholesterolError() { @@ -158,4 +160,7 @@ class CholesterolEntrySheet : BaseBottomSheet< interface Injector { fun inject(target: CholesterolEntrySheet) } + + @Parcelize + data object CholesterolAdded : Parcelable } diff --git a/app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt b/app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt index eec63789256..961913f1c8a 100644 --- a/app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt +++ b/app/src/main/java/org/simple/clinic/summary/compose/StatinNudgeView.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,15 +47,21 @@ import org.simple.clinic.cvdrisk.StatinInfo import org.simple.clinic.medicalhistory.Answer import org.simple.clinic.util.toAnnotatedString +private const val LAB_BASED_MIN_REQ_MAX_RISK_RANGE = 10 +private const val MIN_AGE_FOR_STATIN = 40 + @Composable fun StatinNudge( statinInfo: StatinInfo, - modifier: Modifier = Modifier, + isNonLabBasedStatinNudgeEnabled: Boolean, + isLabBasedStatinNudgeEnabled: Boolean, addSmokingClick: () -> Unit, addBMIClick: () -> Unit, + addCholesterol: () -> Unit, + modifier: Modifier = Modifier, ) { AnimatedVisibility( - visible = statinInfo.canPrescribeStatin, + visible = statinInfo.canShowStatinNudge, enter = expandVertically( animationSpec = tween(500), expandFrom = Alignment.Top @@ -77,6 +84,8 @@ fun StatinNudge( endOffset = endOffset, cvdRiskRange = statinInfo.cvdRisk, hasCVD = statinInfo.hasCVD, + hasDiabetes = statinInfo.hasDiabetes, + isLabBasedStatinNudgeEnabled = isLabBasedStatinNudgeEnabled, parentWidth = constraints.maxWidth, ) Spacer(modifier = Modifier.height(12.dp)) @@ -85,13 +94,21 @@ fun StatinNudge( endOffset = endOffset ) Spacer(modifier = Modifier.height(16.dp)) - DescriptionText(statinInfo = statinInfo) - if (statinInfo.cvdRisk != null) { + DescriptionText( + isLabBasedStatinNudgeEnabled = isLabBasedStatinNudgeEnabled, + statinInfo = statinInfo + ) + + val onlyOneTypeOfNudgeIsEnabled = isLabBasedStatinNudgeEnabled xor isNonLabBasedStatinNudgeEnabled + if (statinInfo.cvdRisk != null && onlyOneTypeOfNudgeIsEnabled) { StainNudgeAddButtons( modifier = Modifier.padding(top = 16.dp), statinInfo = statinInfo, + isNonLabBasedStatinNudgeEnabled = isNonLabBasedStatinNudgeEnabled, + isLabBasedStatinNudgeEnabled = isLabBasedStatinNudgeEnabled, addSmokingClick = addSmokingClick, - addBMIClick = addBMIClick + addBMIClick = addBMIClick, + addCholesterol = addCholesterol, ) } } @@ -106,7 +123,9 @@ fun RiskText( endOffset: Float, cvdRiskRange: CVDRiskRange?, hasCVD: Boolean, + hasDiabetes: Boolean, parentWidth: Int, + isLabBasedStatinNudgeEnabled: Boolean, parentPadding: Float = 16f ) { val midpoint = (startOffset + endOffset) / 2 @@ -119,7 +138,12 @@ fun RiskText( val riskText = when { hasCVD -> stringResource(R.string.statin_alert_very_high_risk_patient) - cvdRiskRange == null -> stringResource(R.string.statin_alert_at_risk_patient) + + cvdRiskRange == null || + (hasDiabetes && (cvdRiskRange.max < LAB_BASED_MIN_REQ_MAX_RISK_RANGE && isLabBasedStatinNudgeEnabled)) -> { + stringResource(R.string.statin_alert_at_risk_patient) + } + else -> stringResource(cvdRiskRange.level.displayStringResId, riskPercentage) } @@ -141,10 +165,10 @@ fun RiskText( Text( modifier = Modifier .offset { - IntOffset( - x = clampedOffsetX.toInt(), - y = 0 - ) + IntOffset( + x = clampedOffsetX.toInt(), + y = 0 + ) } .background(riskColor, shape = RoundedCornerShape(50)) .padding(horizontal = 8.dp, vertical = 4.dp), @@ -174,20 +198,20 @@ fun RiskProgressBar( .fillMaxWidth() .height(14.dp) .drawWithContent { - drawContent() - - drawLine( - color = indicatorColor, - start = Offset(startOffset, 0f), - end = Offset(startOffset, size.height), - strokeWidth = 2.dp.toPx() - ) - drawLine( - color = indicatorColor, - start = Offset(endOffset, 0f), - end = Offset(endOffset, size.height), - strokeWidth = 2.dp.toPx() - ) + drawContent() + + drawLine( + color = indicatorColor, + start = Offset(startOffset, 0f), + end = Offset(startOffset, size.height), + strokeWidth = 2.dp.toPx() + ) + drawLine( + color = indicatorColor, + start = Offset(endOffset, 0f), + end = Offset(endOffset, size.height), + strokeWidth = 2.dp.toPx() + ) }, contentAlignment = Alignment.Center, ) { @@ -209,21 +233,21 @@ fun RiskProgressBar( .weight(1f) .fillMaxHeight() .drawWithContent { - drawRect(color.copy(alpha = 0.5f)) - - val visibleStart = maxOf(segmentStartPx, startOffset) - val visibleEnd = minOf(segmentEndPx, endOffset) - - if (visibleStart < visibleEnd) { - drawRect( - color = color.copy(alpha = 1.0f), - topLeft = Offset(x = visibleStart - segmentStartPx, y = 0f), - size = Size( - width = visibleEnd - visibleStart, - height = size.height - ) + drawRect(color.copy(alpha = 0.5f)) + + val visibleStart = maxOf(segmentStartPx, startOffset) + val visibleEnd = minOf(segmentEndPx, endOffset) + + if (visibleStart < visibleEnd) { + drawRect( + color = color.copy(alpha = 1.0f), + topLeft = Offset(x = visibleStart - segmentStartPx, y = 0f), + size = Size( + width = visibleEnd - visibleStart, + height = size.height ) - } + ) + } } ) } @@ -233,29 +257,19 @@ fun RiskProgressBar( @Composable fun DescriptionText( - statinInfo: StatinInfo + statinInfo: StatinInfo, + isLabBasedStatinNudgeEnabled: Boolean, ) { - val text = when { - statinInfo.cvdRisk == null || statinInfo.cvdRisk.level == CVDRiskLevel.HIGH -> - stringResource(R.string.statin_alert_refer_to_doctor) - - statinInfo.isSmoker == Answer.Unanswered && statinInfo.bmiReading == null -> - stringResource(R.string.statin_alert_add_smoking_and_bmi_info) - - statinInfo.isSmoker == Answer.Unanswered && statinInfo.bmiReading != null -> - stringResource(R.string.statin_alert_add_smoking_info) - - statinInfo.isSmoker != Answer.Unanswered && statinInfo.bmiReading == null -> - stringResource(R.string.statin_alert_add_bmi_info) - - else -> stringResource(R.string.statin_alert_refer_to_doctor) - }.toAnnotatedString() + val text = descriptionText( + isLabBasedStatinNudgeEnabled = isLabBasedStatinNudgeEnabled, + statinInfo = statinInfo, + ) val textColor = when { - statinInfo.cvdRisk == null || statinInfo.cvdRisk.level == CVDRiskLevel.HIGH + statinInfo.cvdRisk == null || statinInfo.cvdRisk.level == CVDRiskLevel.HIGH || statinInfo.hasDiabetes -> SimpleTheme.colors.material.error - statinInfo.isSmoker == Answer.Unanswered || statinInfo.bmiReading == null -> + statinInfo.isSmoker == Answer.Unanswered || statinInfo.bmiReading == null || statinInfo.cholesterol == null -> SimpleTheme.colors.onSurface67 else -> SimpleTheme.colors.material.error @@ -274,12 +288,60 @@ fun DescriptionText( } } +@Composable +@ReadOnlyComposable +private fun descriptionText( + isLabBasedStatinNudgeEnabled: Boolean, + statinInfo: StatinInfo +): AnnotatedString { + val maxCvdRiskRange = statinInfo.cvdRisk?.max ?: 0 + + return when { + statinInfo.hasCVD -> stringResource(R.string.statin_alert_refer_to_doctor) + + statinInfo.hasDiabetes && statinInfo.age >= MIN_AGE_FOR_STATIN && isLabBasedStatinNudgeEnabled.not() -> + stringResource(R.string.statin_alert_refer_to_doctor_diabetic_40) + + statinInfo.hasDiabetes && statinInfo.age >= MIN_AGE_FOR_STATIN && isLabBasedStatinNudgeEnabled && maxCvdRiskRange < LAB_BASED_MIN_REQ_MAX_RISK_RANGE -> + stringResource(R.string.statin_alert_refer_to_doctor_diabetic_40) + + statinInfo.cvdRisk == null || statinInfo.cvdRisk.level == CVDRiskLevel.HIGH -> + stringResource(R.string.statin_alert_refer_to_doctor) + + statinInfo.hasDiabetes && isLabBasedStatinNudgeEnabled -> + stringResource(R.string.statin_alert_refer_to_doctor_diabetic) + + statinInfo.isSmoker == Answer.Unanswered && statinInfo.bmiReading == null && isLabBasedStatinNudgeEnabled.not() -> + stringResource(R.string.statin_alert_add_smoking_and_bmi_info) + + statinInfo.isSmoker == Answer.Unanswered && statinInfo.bmiReading != null && isLabBasedStatinNudgeEnabled.not() -> + stringResource(R.string.statin_alert_add_smoking_info) + + statinInfo.isSmoker != Answer.Unanswered && statinInfo.bmiReading == null && isLabBasedStatinNudgeEnabled.not() -> + stringResource(R.string.statin_alert_add_bmi_info) + + statinInfo.isSmoker == Answer.Unanswered && statinInfo.cholesterol == null && isLabBasedStatinNudgeEnabled -> + stringResource(R.string.statin_alert_add_smoking_and_cholesterol_info) + + statinInfo.isSmoker == Answer.Unanswered && statinInfo.cholesterol != null && isLabBasedStatinNudgeEnabled -> + stringResource(R.string.statin_alert_add_smoking_info) + + statinInfo.isSmoker != Answer.Unanswered && statinInfo.cholesterol == null && isLabBasedStatinNudgeEnabled -> + stringResource(R.string.statin_alert_add_cholesterol_info) + + else -> stringResource(R.string.statin_alert_refer_to_doctor) + }.toAnnotatedString() +} + @Composable fun StainNudgeAddButtons( modifier: Modifier, statinInfo: StatinInfo, + isNonLabBasedStatinNudgeEnabled: Boolean, + isLabBasedStatinNudgeEnabled: Boolean, addSmokingClick: () -> Unit, addBMIClick: () -> Unit, + addCholesterol: () -> Unit, ) { SimpleInverseTheme { Row( @@ -301,7 +363,8 @@ fun StainNudgeAddButtons( ) } } - if (statinInfo.bmiReading == null) { + + if (isNonLabBasedStatinNudgeEnabled && statinInfo.bmiReading == null) { FilledButton( modifier = modifier .height(36.dp) @@ -316,6 +379,22 @@ fun StainNudgeAddButtons( ) } } + + if (isLabBasedStatinNudgeEnabled && statinInfo.cholesterol == null) { + FilledButton( + modifier = modifier + .height(36.dp) + .fillMaxWidth() + .weight(1f) + .clip(RoundedCornerShape(50)), + onClick = { addCholesterol() } + ) { + Text( + text = stringResource(R.string.statin_alert_add_cholesterol), + fontSize = 14.sp, + ) + } + } } } } @@ -362,6 +441,13 @@ fun List.findSegmentRatio(value: Int): Float { @Composable fun StatinNudgePreview() { SimpleTheme { - StatinNudge(StatinInfo(canPrescribeStatin = true), addSmokingClick = {}, addBMIClick = {}) + StatinNudge( + statinInfo = StatinInfo(canShowStatinNudge = true, hasDiabetes = true), + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + addSmokingClick = {}, + addBMIClick = {}, + addCholesterol = {} + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d161818d5ae..4bfbda20727 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1089,13 +1089,19 @@ The Simple app contains private health information of patients (“Data”).\n\n MEDIUM-HIGH RISK %s HIGH RISK %s VERY HIGH RISK PATIENT + VERY HIGH RISK %s AT RISK PATIENT Refer to doctor for <b>statin medicine</b> + Diabetic patient, age above 40. Refer to doctor for <b>statin medicine</b> + Diabetic patient. Refer to doctor for <b>statin medicine</b> Add smoking Add BMI + Add cholesterol Add smoking for more accurate risk score. Add BMI for more accurate risk score. Add smoking status and BMI for more accurate risk score. + Add cholesterol for more accurate risk score. + Add smoking status and cholesterol for more accurate risk score. Optimising database diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryEffectHandlerTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryEffectHandlerTest.kt index e878381d883..8bff95e5a1d 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryEffectHandlerTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryEffectHandlerTest.kt @@ -934,7 +934,11 @@ class PatientSummaryEffectHandlerTest { dateOfBirth = null, ) ) - val medicalHistory = TestData.medicalHistory(isSmoking = Yes) + val medicalHistory = TestData.medicalHistory( + isSmoking = Yes, + hasDiabetes = Yes, + cholesterol = 400f, + ) val bloodPressure = TestData.bloodPressureMeasurement( UUID.fromString("3e8c246f-91b9-4f8c-81fe-91b67ac0a2d5"), systolic = 130, @@ -948,14 +952,13 @@ class PatientSummaryEffectHandlerTest { patientUuid = patientUuid, defaultHistoryUuid = uuidGenerator.v4() )) doReturn medicalHistory - whenever(cvdRiskRepository.getCVDRiskImmediate(patientUuid)) doReturn - cvdRisk + whenever(cvdRiskRepository.getCVDRiskImmediate(patientUuid)) doReturn cvdRisk //when testCase.dispatch(CalculateLabBasedCVDRisk(patient = patient)) //then - testCase.assertOutgoingEvents(CVDRiskCalculated(cvdRisk, CVDRiskRange(7, 14))) + testCase.assertOutgoingEvents(CVDRiskCalculated(cvdRisk, CVDRiskRange(14, 14))) } @Test @@ -983,11 +986,21 @@ class PatientSummaryEffectHandlerTest { fun `when load statin info effect is received, then load statin info`() { //given val bmiReading = BMIReading(height = 177f, weight = 53f) + + whenever(patientRepository.patientImmediate(patientUuid)) doReturn TestData.patient( + uuid = patientUuid, + patientAgeDetails = PatientAgeDetails( + ageValue = 55, + ageUpdatedAt = Instant.parse("2018-01-01T00:00:00Z"), + dateOfBirth = null, + ) + ) + val medicalHistory = TestData.medicalHistory(isSmoking = Yes) + whenever(medicalHistoryRepository.historyForPatientOrDefaultImmediate( defaultHistoryUuid = uuidGenerator.v4(), patientUuid = patientUuid - )) doReturn - TestData.medicalHistory(isSmoking = Yes) + )) doReturn medicalHistory whenever(patientAttributeRepository.getPatientAttributeImmediate(patientUuid)) doReturn TestData.patientAttribute(reading = bmiReading) @@ -999,12 +1012,12 @@ class PatientSummaryEffectHandlerTest { testCase.dispatch(LoadStatinInfo(patientUuid)) //then - testCase.assertOutgoingEvents(StatinInfoLoaded(StatinInfo( - canPrescribeStatin = true, - cvdRisk = CVDRiskRange(27, 27), - isSmoker = Yes, - bmiReading = bmiReading - ))) + testCase.assertOutgoingEvents(StatinInfoLoaded( + age = 55, + medicalHistory = medicalHistory, + riskRange = CVDRiskRange(27, 27), + bmiReading = bmiReading, + )) } @Test @@ -1030,4 +1043,15 @@ class PatientSummaryEffectHandlerTest { verify(uiActions).openBMIEntrySheet(patientUuid) verifyNoMoreInteractions(uiActions) } + + @Test + fun `when open cholesterol entry sheet effect is received, then open cholesterol entry sheet`() { + // when + testCase.dispatch(OpenCholesterolEntrySheet(patientUuid = patientUuid)) + + // then + testCase.assertNoOutgoingEvents() + verify(uiActions).openCholesterolEntrySheet(patientUuid = patientUuid) + verifyNoMoreInteractions(uiActions) + } } diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt index 02c172f8f14..3858f91364e 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryUpdateTest.kt @@ -20,6 +20,7 @@ import org.simple.clinic.patient.PatientStatus import org.simple.clinic.patient.businessid.Identifier import org.simple.clinic.patient.businessid.Identifier.IdentifierType.BangladeshNationalId import org.simple.clinic.patient.businessid.Identifier.IdentifierType.BpPassport +import org.simple.clinic.patientattribute.BMIReading import org.simple.clinic.reassignpatient.ReassignPatientSheetClosedFrom import org.simple.clinic.reassignpatient.ReassignPatientSheetOpenedFrom import org.simple.clinic.summary.AppointmentSheetOpenedFrom.BACK_CLICK @@ -2190,7 +2191,7 @@ class PatientSummaryUpdateTest { wasCVDCalculatedWithin90Days = false, )) .then(assertThatNext( - hasModel(defaultModel.updateStatinInfo(StatinInfo(canPrescribeStatin = false, hasCVD = false))), + hasModel(defaultModel.updateStatinInfo(StatinInfo(canShowStatinNudge = false, hasCVD = false))), hasNoEffects() )) } @@ -2223,7 +2224,7 @@ class PatientSummaryUpdateTest { wasCVDCalculatedWithin90Days = false, )) .then(assertThatNext( - hasModel(defaultModel.updateStatinInfo(StatinInfo(canPrescribeStatin = true, hasCVD = true))), + hasModel(defaultModel.updateStatinInfo(StatinInfo(canShowStatinNudge = true, hasCVD = true))), hasNoEffects() )) } @@ -2256,7 +2257,7 @@ class PatientSummaryUpdateTest { wasCVDCalculatedWithin90Days = false, )) .then(assertThatNext( - hasModel(defaultModel.updateStatinInfo(StatinInfo(canPrescribeStatin = true, hasCVD = false))), + hasModel(defaultModel.updateStatinInfo(StatinInfo(canShowStatinNudge = true, hasCVD = false))), hasNoEffects() )) } @@ -2375,12 +2376,68 @@ class PatientSummaryUpdateTest { @Test fun `when statin info is loaded, then update the state`() { val statinInfo = StatinInfo( - canPrescribeStatin = true + canShowStatinNudge = true, + cvdRisk = CVDRiskRange(11, 11), + isSmoker = Yes, + bmiReading = BMIReading(165f, 60f), + hasCVD = true, + hasDiabetes = false, + age = 55, + cholesterol = null, ) + + updateSpec + .given(defaultModel) + .whenEvent(StatinInfoLoaded( + age = 55, + medicalHistory = TestData.medicalHistory( + hasHadStroke = Yes, + hasHadHeartAttack = Yes, + hasDiabetes = No, + isSmoking = Yes, + cholesterol = null, + ), + riskRange = CVDRiskRange(11, 11), + bmiReading = BMIReading(165f, 60f), + )) + .then(assertThatNext( + hasModel(defaultModel.updateStatinInfo(statinInfo)), + hasNoEffects() + )) + } + + @Test + fun `when statin info is loaded and lab based statin nudge is not enabled and max risk is below 10, then statin cannot be prescribed`() { + val statinInfo = StatinInfo( + canShowStatinNudge = false, + cvdRisk = CVDRiskRange(9, 9), + isSmoker = Yes, + bmiReading = BMIReading(165f, 60f), + hasCVD = true, + hasDiabetes = false, + age = 55, + cholesterol = null, + ) + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = true, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = true, + isLabBasedStatinNudgeEnabled = false, + )) + updateSpec .given(defaultModel) .whenEvent(StatinInfoLoaded( - statinInfo = statinInfo + age = 55, + medicalHistory = TestData.medicalHistory( + hasHadStroke = Yes, + hasHadHeartAttack = Yes, + hasDiabetes = No, + isSmoking = Yes, + cholesterol = null, + ), + riskRange = CVDRiskRange(9, 9), + bmiReading = BMIReading(165f, 60f), )) .then(assertThatNext( hasModel(defaultModel.updateStatinInfo(statinInfo)), @@ -2388,16 +2445,109 @@ class PatientSummaryUpdateTest { )) } + @Test + fun `when statin info is loaded and lab-based statin is enabled and has diabetes, then statin can be prescribed`() { + val statinInfo = StatinInfo( + canShowStatinNudge = true, + cvdRisk = null, + isSmoker = Yes, + bmiReading = BMIReading(165f, 60f), + hasCVD = true, + hasDiabetes = true, + age = 55, + cholesterol = null, + ) + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = true, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = true, + isLabBasedStatinNudgeEnabled = true, + )) + + updateSpec + .given(defaultModel) + .whenEvent(StatinInfoLoaded( + age = 55, + medicalHistory = TestData.medicalHistory( + hasHadStroke = Yes, + hasHadHeartAttack = Yes, + hasDiabetes = Yes, + isSmoking = Yes, + cholesterol = null, + ), + riskRange = CVDRiskRange(9, 9), + bmiReading = BMIReading(165f, 60f), + )) + .then(assertThatNext( + hasModel(defaultModel.updateStatinInfo(statinInfo)), + hasNoEffects() + )) + } + + @Test + fun `when statin info is loaded and lab-based statin is enabled and has diabetes and max risk is less than 10, then statin cannot be prescribed`() { + val statinInfo = StatinInfo( + canShowStatinNudge = false, + cvdRisk = null, + isSmoker = Yes, + bmiReading = BMIReading(165f, 60f), + hasCVD = true, + hasDiabetes = false, + age = 55, + cholesterol = null, + ) + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = true, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = true, + isLabBasedStatinNudgeEnabled = true, + )) + + updateSpec + .given(defaultModel) + .whenEvent(StatinInfoLoaded( + age = 55, + medicalHistory = TestData.medicalHistory( + hasHadStroke = Yes, + hasHadHeartAttack = Yes, + hasDiabetes = No, + isSmoking = Yes, + cholesterol = null, + ), + riskRange = CVDRiskRange(9, 9), + bmiReading = BMIReading(165f, 60f), + )) + .then(assertThatNext( + hasModel(defaultModel.updateStatinInfo(statinInfo)), + hasNoEffects() + )) + } + @Test fun `when statin info is loaded and risk is low-high, then update the state and show smoking status dialog`() { val statinInfo = StatinInfo( - canPrescribeStatin = true, - CVDRiskRange(7, 14), + canShowStatinNudge = true, + cvdRisk = CVDRiskRange(4, 11), + isSmoker = Unanswered, + bmiReading = BMIReading(165f, 60f), + hasCVD = true, + hasDiabetes = false, + age = 55, + cholesterol = null, ) updateSpec .given(defaultModel) .whenEvent(StatinInfoLoaded( - statinInfo = statinInfo + age = 55, + medicalHistory = TestData.medicalHistory( + hasHadStroke = Yes, + hasHadHeartAttack = Yes, + hasDiabetes = No, + isSmoking = Unanswered, + cholesterol = null, + ), + riskRange = CVDRiskRange(4, 11), + bmiReading = BMIReading(165f, 60f), )) .then(assertThatNext( hasModel(defaultModel.updateStatinInfo(statinInfo).showSmokingStatusDialog()), @@ -2457,6 +2607,253 @@ class PatientSummaryUpdateTest { )) } + @Test + fun `when lab based statin feature is enabled and statin prescription check info is loaded and person is below 40 without cvd, then update the state with false`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 39, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = No, + hasHadStroke = No, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = null, + hasMedicalHistoryChanged = false, + wasCVDCalculatedWithin90Days = false, + )) + .then(assertThatNext( + hasModel(model.updateStatinInfo(StatinInfo(canShowStatinNudge = false, hasDiabetes = false))), + hasNoEffects() + )) + } + + @Test + fun `when lab based statin feature is enabled statin prescription check info is loaded and person has cvd, then update the state with true`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 39, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = No, + hasHadStroke = Yes, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = null, + hasMedicalHistoryChanged = false, + wasCVDCalculatedWithin90Days = false, + )) + .then(assertThatNext( + hasModel(model.updateStatinInfo(StatinInfo(canShowStatinNudge = true, hasCVD = true))), + hasNoEffects() + )) + } + + @Test + fun `when lab based statin feature is enabled statin prescription check info is loaded and person has diabetes and age is greater than 74, then update the state with true`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + updateSpec + .given(defaultModel) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 75, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = Yes, + hasHadStroke = No, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = null, + hasMedicalHistoryChanged = false, + wasCVDCalculatedWithin90Days = false, + )) + .then(assertThatNext( + hasModel(defaultModel.updateStatinInfo(StatinInfo(canShowStatinNudge = true, hasDiabetes = true))), + hasNoEffects() + )) + } + + @Test + fun `when lab based statin feature is enabled statin prescription check info is loaded and has diabetes and is eligible for calculating lab based CVD risk and should calculate CVD risk then calculate CVD risk`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 40, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = Yes, + hasHadStroke = No, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = null, + hasMedicalHistoryChanged = true, + wasCVDCalculatedWithin90Days = false, + )) + .then(assertThatNext( + hasNoModel(), + hasEffects(CalculateLabBasedCVDRisk(model.patientSummaryProfile!!.patient)) + )) + } + + @Test + fun `when lab based statin feature is enabled statin prescription check info is loaded and doesnt have diabetes and is eligible for calculating lab based CVD risk and should calculate CVD risk then calculate CVD risk`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 40, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = No, + hasHadStroke = No, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = null, + hasMedicalHistoryChanged = true, + wasCVDCalculatedWithin90Days = false, + )) + .then(assertThatNext( + hasNoModel(), + hasEffects(CalculateLabBasedCVDRisk(model.patientSummaryProfile!!.patient)) + )) + } + + @Test + fun `when lab based statin feature is enabled and statin prescription check info is loaded and person is above 40 with diabetes and is eligible for lab based CVD risk, then load statin info`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(StatinPrescriptionCheckInfoLoaded( + age = 48, + isPatientDead = false, + wasBPMeasuredWithin90Days = true, + medicalHistory = TestData.medicalHistory( + hasDiabetes = No, + hasHadStroke = No, + hasHadHeartAttack = No, + ), + patientAttribute = null, + prescriptions = listOf( + TestData.prescription(name = "losartin") + ), + cvdRiskRange = CVDRiskRange(14, 21), + hasMedicalHistoryChanged = false, + wasCVDCalculatedWithin90Days = true, + )) + .then(assertThatNext( + hasNoModel(), + hasEffects(LoadStatinInfo(patientUuid)) + )) + } + + @Test + fun `when add cholesterol is clicked, then open cholesterol entry sheet`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + + updateSpec + .given(defaultModel) + .whenEvent(AddCholesterolClicked) + .then(assertThatNext( + hasNoModel(), + hasEffects(OpenCholesterolEntrySheet(patientUuid)) + )) + } + + @Test + fun `when cholesterol is added, then calculate lab based cvd risk`() { + val updateSpec = UpdateSpec(PatientSummaryUpdate( + isPatientReassignmentFeatureEnabled = false, + isPatientStatinNudgeV1Enabled = true, + isNonLabBasedStatinNudgeEnabled = false, + isLabBasedStatinNudgeEnabled = true, + )) + val model = defaultModel.patientSummaryProfileLoaded(patientSummaryProfile) + + updateSpec + .given(model) + .whenEvent(CholesterolAdded) + .then(assertThatNext( + hasNoModel(), + hasEffects(CalculateLabBasedCVDRisk(patient)) + )) + } + private fun PatientSummaryModel.forExistingPatient(): PatientSummaryModel { return copy(openIntention = ViewExistingPatient) } diff --git a/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt b/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt index c9a406a35d0..4466393cc68 100644 --- a/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt +++ b/app/src/test/java/org/simple/clinic/summary/PatientSummaryViewRendererTest.kt @@ -766,7 +766,7 @@ class PatientSummaryViewRendererTest { @Test fun `when statin info is loaded then update the statin alert`() { //given - val statinInfo = StatinInfo(canPrescribeStatin = true) + val statinInfo = StatinInfo(canShowStatinNudge = true) val model = defaultModel .currentFacilityLoaded(facilityWithDiabetesManagementDisabled) .updateStatinInfo(statinInfo) @@ -820,7 +820,7 @@ class PatientSummaryViewRendererTest { .patientSummaryProfileLoaded(patientSummaryProfile = patientSummaryProfile) .clinicalDecisionSupportInfoLoaded(isNewestBpEntryHigh = true, hasPrescribedDrugsChangedToday = false) .scheduledAppointmentLoaded(appointment) - .updateStatinInfo(StatinInfo(canPrescribeStatin = true)) + .updateStatinInfo(StatinInfo(canShowStatinNudge = true)) val uiRenderer = PatientSummaryViewRenderer( ui = ui, @@ -839,7 +839,7 @@ class PatientSummaryViewRendererTest { verify(ui).showPatientDiedStatus() verify(ui).hideDiabetesView() verify(ui).hideTeleconsultButton() - verify(ui).updateStatinAlert(StatinInfo(canPrescribeStatin = true)) + verify(ui).updateStatinAlert(StatinInfo(canShowStatinNudge = true)) verify(ui).hideClinicalDecisionSupportAlertWithoutAnimation() verify(ui).showNextAppointmentCard() verifyNoMoreInteractions(ui) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 033b0338aa4..01fe5e5fda2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.8.1" +agp = "8.8.2" androidx-cameraView = "1.4.1" androidx-camera = "1.4.1"