Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add configuration to generate MeasureReports in the background #2515

Merged
merged 7 commits into from
Jul 4, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.Composition
import org.hl7.fhir.r4.model.ListResource
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.json.JSONObject
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.data.remote.fhir.resource.FhirResourceDataSource
Expand Down Expand Up @@ -104,6 +105,26 @@ constructor(
return decodedConfig
}

inline fun <reified T : Configuration> retrieveConfigurations(configType: ConfigType): List<T> =
configsJsonMap.values
.filter {
try {
JSONObject(it).getString("configType").equals(configType.name, ignoreCase = true)
} catch (e: Exception) {
Timber.w(e.localizedMessage)
false
}
}
.map {
localizationHelper
.parseTemplate(
bundleName = LocalizationHelper.STRINGS_BASE_BUNDLE_NAME,
locale = Locale.getDefault(),
template = it
)
.decodeJson<T>()
}

/**
* This function interpolates the value for the given [configKey] by replacing the string
* placeholders e.g. {{ placeholder }} with value retrieved from the [paramsMap] using [configKey]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ data class MeasureReportConfiguration(
override var configType: String = ConfigType.MeasureReport.name,
val id: String,
val registerId: String,
val registerDate: String? = null,
val startPeriod: String? = "2023-01-01",
val showFixedRangeSelection: Boolean? = null,
val showSubjectSelection: Boolean? = null,
val scheduledGenerationDuration: String? = null,
val reports: List<MeasureReportConfig> = emptyList()
) : Configuration()
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ package org.smartregister.fhircore.engine.util.extension

import ca.uhn.fhir.rest.param.ParamPrefixEnum
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.search
import org.apache.commons.lang3.StringUtils
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.Group
import org.hl7.fhir.r4.model.MeasureReport
import org.hl7.fhir.r4.model.ResourceType
import org.opencds.cqf.cql.evaluator.measure.common.MeasurePopulationType
import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfig
import org.smartregister.fhircore.engine.data.local.DefaultRepository

// TODO: Enhancement - use FhirPathEngine evaluator for data extraction
fun MeasureReport.StratifierGroupComponent.findPopulation(
Expand Down Expand Up @@ -118,3 +122,21 @@ suspend inline fun retrievePreviouslyGeneratedMeasureReports(

return fhirEngine.search(search)
}

suspend inline fun fetchReportSubjects(
config: MeasureReportConfig,
fhirEngine: FhirEngine,
defaultRepository: DefaultRepository
): List<String> {
return if (config.subjectXFhirQuery?.isNotEmpty() == true) {
fhirEngine.search(config.subjectXFhirQuery!!).map {
// prevent missing subject where MeasureEvaluator looks for Group members and skips the Group
// itself
if (it is Group && !it.hasMember()) {
it.addMember(Group.GroupMemberComponent(it.asReference()))
defaultRepository.update(it)
}
"${it.resourceType.name}/${it.logicalId}"
}
} else emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"configType": "measureReport",
"id": "serviceDeliveryMeasureReport",
"registerId": "inventoryRegister",
"registerDate": "2020-10-27",
"startPeriod": "2020-10-27",
"showFixedRangeSelection": true,
"reports": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"appId": "app",
"configType": "measureReport",
"registerDate": "2020-10-27",
"startPeriod": "2020-10-27",
"showFixedRangeSelection": true,
"id": "supplyChainMeasureReport",
"registerId": "inventoryRegister",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2021-2023 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.quest.data.report.measure

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.google.android.fhir.search.search
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration
import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfig
import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfiguration
import org.smartregister.fhircore.engine.data.local.register.RegisterRepository
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor

class MeasureReportPagingSource(
private val measureReportConfiguration: MeasureReportConfiguration,
private val registerConfiguration: RegisterConfiguration,
private val registerRepository: RegisterRepository,
private val resourceDataRulesExecutor: ResourceDataRulesExecutor
) : PagingSource<Int, MeasureReportConfig>() {

override fun getRefreshKey(state: PagingState<Int, MeasureReportConfig>): Int? {
return state.anchorPosition
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MeasureReportConfig> {
return try {
LoadResult.Page(data = measureReportConfiguration.reports, prevKey = null, nextKey = null)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}

suspend fun retrieveSubjects(count: Int): List<ResourceData> {
val xFhirQuery =
measureReportConfiguration.reports.firstOrNull()?.subjectXFhirQuery
?: ResourceType.Patient.name
return registerRepository.fhirEngine.search(xFhirQuery).map {
resourceDataRulesExecutor.processResourceData(
repositoryResourceData =
RepositoryResourceData(resourceRulesEngineFactId = it.resourceType.name, resource = it),
ruleConfigs = registerConfiguration.registerCard.rules,
params = emptyMap()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,130 @@

package org.smartregister.fhircore.quest.data.report.measure

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.logicalId
import com.google.android.fhir.search.search
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.register.RegisterConfiguration
import com.google.android.fhir.workflow.FhirOperator
import javax.inject.Inject
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.Group
import org.hl7.fhir.r4.model.MeasureReport
import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry
import org.smartregister.fhircore.engine.configuration.app.ConfigService
import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfig
import org.smartregister.fhircore.engine.configuration.report.measure.MeasureReportConfiguration
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.data.local.register.RegisterRepository
import org.smartregister.fhircore.engine.domain.model.RepositoryResourceData
import org.smartregister.fhircore.engine.domain.model.ResourceData
import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor
import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.asReference
import org.smartregister.fhircore.quest.ui.report.measure.MeasureReportViewModel

class MeasureReportRepository(
private val measureReportConfiguration: MeasureReportConfiguration,
private val registerConfiguration: RegisterConfiguration,
private val registerRepository: RegisterRepository,
private val resourceDataRulesExecutor: ResourceDataRulesExecutor
) : PagingSource<Int, MeasureReportConfig>() {
class MeasureReportRepository
@Inject
constructor(
override val fhirEngine: FhirEngine,
override val dispatcherProvider: DispatcherProvider,
override val sharedPreferencesHelper: SharedPreferencesHelper,
override val configurationRegistry: ConfigurationRegistry,
override val configService: ConfigService,
override val configRulesExecutor: ConfigRulesExecutor,
val registerRepository: RegisterRepository,
val fhirOperator: FhirOperator
) :
DefaultRepository(
fhirEngine = fhirEngine,
dispatcherProvider = dispatcherProvider,
sharedPreferencesHelper = sharedPreferencesHelper,
configurationRegistry = configurationRegistry,
configService = configService,
configRulesExecutor = configRulesExecutor
) {

override fun getRefreshKey(state: PagingState<Int, MeasureReportConfig>): Int? {
return state.anchorPosition
}
suspend fun evaluatePopulationMeasure(
measureUrl: String,
startDateFormatted: String,
endDateFormatted: String,
subjects: List<String>,
existing: List<MeasureReport>
): List<MeasureReport> {
val measureReport = mutableListOf<MeasureReport>()
withContext(dispatcherProvider.io()) {
if (subjects.isNotEmpty()) {
subjects
.map {
runMeasureReport(
measureUrl,
MeasureReportViewModel.SUBJECT,
startDateFormatted,
endDateFormatted,
it
)
}
.forEach { measureReport.add(it) }
} else
runMeasureReport(
measureUrl,
MeasureReportViewModel.POPULATION,
startDateFormatted,
endDateFormatted,
null
)
.also { measureReport.add(it) }

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MeasureReportConfig> {
return try {
LoadResult.Page(data = measureReportConfiguration.reports, prevKey = null, nextKey = null)
} catch (exception: Exception) {
LoadResult.Error(exception)
measureReport.forEach { report ->
// if report exists override instead of creating a new one
existing
.find {
it.measure == report.measure &&
(!it.hasSubject() || it.subject.reference == report.subject.reference)
}
?.let { existing -> report.id = existing.id }
addOrUpdate(resource = report)
}
}
return measureReport
}

suspend fun retrieveSubjects(count: Int): List<ResourceData> {
val xFhirQuery =
measureReportConfiguration.reports.firstOrNull()?.subjectXFhirQuery
?: ResourceType.Patient.name
return registerRepository.fhirEngine.search(xFhirQuery).map {
resourceDataRulesExecutor.processResourceData(
repositoryResourceData =
RepositoryResourceData(resourceRulesEngineFactId = it.resourceType.name, resource = it),
ruleConfigs = registerConfiguration.registerCard.rules,
params = emptyMap()
)
}
/**
* Run and generate MeasureReport for given measure and subject.
*
* @param measureUrl url of measure to generate report for
* @param reportType type of report (population | subject)
* @param startDateFormatted start date of measure period with format yyyy-MM-dd
* @param endDateFormatted end date of measure period with format yyyy-MM-dd
* @param subject the individual subject reference (ResourceType/id) to run report for
*/
private fun runMeasureReport(
measureUrl: String,
reportType: String,
startDateFormatted: String,
endDateFormatted: String,
subject: String?
): MeasureReport {
return fhirOperator.evaluateMeasure(
measureUrl = measureUrl,
start = startDateFormatted,
end = endDateFormatted,
reportType = reportType,
subject = subject,
practitioner = null
/* TODO DO NOT pass this id to MeasureProcessor as this is treated as subject if subject is null.
practitionerId?.asReference(ResourceType.Practitioner)?.reference*/ ,
)
}

suspend fun fetchSubjects(config: MeasureReportConfig): List<String> {
return if (config.subjectXFhirQuery?.isNotEmpty() == true) {
fhirEngine.search(config.subjectXFhirQuery!!).map {
// prevent missing subject where MeasureEvaluator looks for Group members and skips the
// Group itself
if (it is Group && !it.hasMember()) {
it.addMember(Group.GroupMemberComponent(it.asReference()))
update(it)
}
"${it.resourceType.name}/${it.logicalId}"
}
} else emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.smartregister.fhircore.quest.ui.shared.models.MeasureReportSubjectVie
import org.smartregister.fhircore.quest.util.mappers.MeasureReportSubjectViewDataMapper

class MeasureReportSubjectsPagingSource(
private val measureReportRepository: MeasureReportRepository,
private val measureReportPagingSource: MeasureReportPagingSource,
val measureReportSubjectViewDataMapper: MeasureReportSubjectViewDataMapper
) : PagingSource<Int, MeasureReportSubjectViewData>() {

Expand All @@ -33,7 +33,7 @@ class MeasureReportSubjectsPagingSource(
val currentPage = params.key ?: 0
val pageSize = params.loadSize
val data =
measureReportRepository.retrieveSubjects(currentPage).map { resourceData ->
measureReportPagingSource.retrieveSubjects(currentPage).map { resourceData ->
measureReportSubjectViewDataMapper.transformInputToOutputModel(resourceData)
}
val prevKey = if (currentPage == 0) null else currentPage - 1
Expand Down
Loading