diff --git a/android/buildSrc/src/main/kotlin/BuildConfigs.kt b/android/buildSrc/src/main/kotlin/BuildConfigs.kt index 78176a7a61..fe8057f58e 100644 --- a/android/buildSrc/src/main/kotlin/BuildConfigs.kt +++ b/android/buildSrc/src/main/kotlin/BuildConfigs.kt @@ -3,7 +3,7 @@ object BuildConfigs { const val compileSdk = 34 const val targetSdk = 34 const val versionCode = 11 - const val versionName = "2.0.0" + const val versionName = "2.0.0.2" const val applicationId = "org.smartregister.opensrp" const val jvmToolchain = 17 const val kotlinCompilerExtensionVersion = "1.5.8" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt index a3daf5035a..757bb0a355 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/data/remote/shared/TokenAuthenticator.kt @@ -40,6 +40,8 @@ import java.util.Base64 import javax.inject.Inject import javax.inject.Singleton import javax.net.ssl.SSLHandshakeException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.remote.auth.OAuthService @@ -215,8 +217,11 @@ constructor( addAccountExplicitly(newAccount, oAuthResponse.refreshToken, null) setAuthToken(newAccount, AUTH_TOKEN_TYPE, oAuthResponse.accessToken) } + // Save credentials - secureSharedPreference.saveCredentials(username, password) + CoroutineScope(dispatcherProvider.io()).launch { + secureSharedPreference.saveCredentials(username, password) + } } } diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ContentCache.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ContentCache.kt new file mode 100644 index 0000000000..135764e009 --- /dev/null +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/datastore/ContentCache.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2021-2024 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.engine.datastore + +import androidx.collection.LruCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.hl7.fhir.r4.model.Resource +import timber.log.Timber + +object ContentCache { + private val maxMemory: Int = (Runtime.getRuntime().maxMemory() / 1024).toInt() + private val cacheSize: Int = maxMemory / 8 + private val cache = LruCache(cacheSize) + + @JvmStatic + suspend fun saveResource(resourceId: String, resource: Resource) = + withContext(Dispatchers.IO) { + cache.put("${resource::class.simpleName}/$resourceId", resource) + Timber.i("ContentCache:saveResource: $resourceId") + } + + @JvmStatic + fun getResource(resourceId: String): Resource? { + return cache[resourceId]?.also { Timber.i("ContentCache:getResource: $resourceId") } + } + + suspend fun invalidate() = + withContext(Dispatchers.IO) { + cache.evictAll() + Timber.i("ContentCache: clearing cache") + } +} diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt index 55c2df7da2..e21a00638d 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/task/FhirCarePlanGenerator.kt @@ -57,6 +57,7 @@ import org.smartregister.fhircore.engine.R import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.event.EventType import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.datastore.ContentCache import org.smartregister.fhircore.engine.util.extension.addResourceParameter import org.smartregister.fhircore.engine.util.extension.asReference import org.smartregister.fhircore.engine.util.extension.batchedSearch @@ -214,8 +215,16 @@ constructor( } source.setParameter(Task.SP_PERIOD, period) source.setParameter(ActivityDefinition.SP_VERSION, IntegerType(index)) - - val structureMap = fhirEngine.get(IdType(action.transform).idPart) + val structureMapId = IdType(action.transform).idPart + val structureMap = + ContentCache.getResource(ResourceType.StructureMap.name + "/" + structureMapId)?.let { + it as StructureMap + } + ?: run { + fhirEngine.get(structureMapId).also { + ContentCache.saveResource(structureMapId, it) + } + } structureMapUtilities.transform( transformSupportServices.simpleWorkerContext, source, diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt index a099937789..f6f2ff9eb4 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModel.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.RequestBody.Companion.toRequestBody import org.apache.commons.lang3.StringUtils -import org.hl7.fhir.r4.model.Binary import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Composition import org.hl7.fhir.r4.model.ResourceType diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt index f873e96df6..b66bb05165 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginActivity.kt @@ -24,10 +24,13 @@ import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.compose.material.ExperimentalMaterialApi import androidx.core.os.bundleOf +import androidx.lifecycle.viewModelScope import androidx.work.WorkManager import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.launch import org.smartregister.fhircore.engine.data.remote.shared.TokenAuthenticator +import org.smartregister.fhircore.engine.datastore.ContentCache import org.smartregister.fhircore.engine.p2p.dao.P2PReceiverTransferDao import org.smartregister.fhircore.engine.p2p.dao.P2PSenderTransferDao import org.smartregister.fhircore.engine.sync.AppSyncWorker @@ -84,7 +87,7 @@ open class LoginActivity : BaseMultiLanguageActivity() { navigateToPinLogin(launchSetup = false) } } - + viewModelScope.launch { ContentCache.invalidate() } navigateToHome.observe(loginActivity) { launchHomeScreen -> if (launchHomeScreen) { downloadNowWorkflowConfigs() diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index f01d407979..b56c5ee111 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -60,6 +60,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComp import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType +import org.hl7.fhir.r4.model.StructureMap import org.smartregister.fhircore.engine.BuildConfig import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry @@ -69,6 +70,7 @@ import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.app.CodingSystemUsage import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.datastore.ContentCache import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType import org.smartregister.fhircore.engine.domain.model.isEditable @@ -147,7 +149,21 @@ constructor( questionnaireConfig: QuestionnaireConfig, ): Questionnaire? { if (questionnaireConfig.id.isEmpty() || questionnaireConfig.id.isBlank()) return null - return defaultRepository.loadResource(questionnaireConfig.id) + var result = + ContentCache.getResource(ResourceType.Questionnaire.name + "/" + questionnaireConfig.id) + ?.copy() + if (result == null) { + result = + defaultRepository.loadResource(questionnaireConfig.id)?.also { questionnaire, + -> + ContentCache.saveResource( + questionnaireConfig.id, + questionnaire.copy(), + ) + } + } + + return result as Questionnaire } /** @@ -196,63 +212,67 @@ constructor( context = context, ) - saveExtractedResources( - bundle = bundle, - questionnaire = questionnaire, - questionnaireConfig = questionnaireConfig, - questionnaireResponse = currentQuestionnaireResponse, - context = context, - ) - - updateResourcesLastUpdatedProperty(actionParameters) + val doSaveOperations: suspend () -> Unit = { + saveExtractedResources( + bundle = bundle, + questionnaire = questionnaire, + questionnaireConfig = questionnaireConfig, + questionnaireResponse = currentQuestionnaireResponse, + context = context, + ) - // Important to load subject resource to retrieve ID (as reference) correctly - val subjectIdType: IdType? = - if (currentQuestionnaireResponse.subject.reference.isNullOrEmpty()) { - null - } else { - IdType(currentQuestionnaireResponse.subject.reference) - } + updateResourcesLastUpdatedProperty(actionParameters) - if (subjectIdType != null) { - val subject = - loadResource( - ResourceType.valueOf(subjectIdType.resourceType), - subjectIdType.idPart, - ) + // Important to load subject resource to retrieve ID (as reference) correctly + val subjectIdType: IdType? = + if (currentQuestionnaireResponse.subject.reference.isNullOrEmpty()) { + null + } else { + IdType(currentQuestionnaireResponse.subject.reference) + } - if (subject != null && !questionnaireConfig.isReadOnly()) { - val newBundle = bundle.copyBundle(currentQuestionnaireResponse) + if (subjectIdType != null) { + val subject = + loadResource( + ResourceType.valueOf(subjectIdType.resourceType), + subjectIdType.idPart, + ) - val extractedResources = newBundle.entry.map { it.resource } - validateWithFhirValidator(*extractedResources.toTypedArray()) + if (subject != null && !questionnaireConfig.isReadOnly()) { + val newBundle = bundle.copyBundle(currentQuestionnaireResponse) - generateCarePlan( - subject = subject, - bundle = newBundle, - questionnaireConfig = questionnaireConfig, - ) + val extractedResources = newBundle.entry.map { it.resource } + validateWithFhirValidator(*extractedResources.toTypedArray()) - withContext(dispatcherProvider.io()) { - executeCql( + generateCarePlan( subject = subject, bundle = newBundle, - questionnaire = questionnaire, questionnaireConfig = questionnaireConfig, ) - } - fhirCarePlanGenerator.conditionallyUpdateResourceStatus( - questionnaireConfig = questionnaireConfig, - subject = subject, - bundle = newBundle, - ) + withContext(dispatcherProvider.io()) { + executeCql( + subject = subject, + bundle = newBundle, + questionnaire = questionnaire, + questionnaireConfig = questionnaireConfig, + ) + } + + fhirCarePlanGenerator.conditionallyUpdateResourceStatus( + questionnaireConfig = questionnaireConfig, + subject = subject, + bundle = newBundle, + ) + } } - } - softDeleteResources(questionnaireConfig) + softDeleteResources(questionnaireConfig) + + retireUsedQuestionnaireUniqueId(questionnaireConfig, currentQuestionnaireResponse) + } - retireUsedQuestionnaireUniqueId(questionnaireConfig, currentQuestionnaireResponse) + defaultRepository.fhirEngine.withTransaction { doSaveOperations.invoke() } val idTypes = bundle.entry?.map { IdType(it.resource.resourceType.name, it.resource.logicalId) } @@ -637,8 +657,15 @@ constructor( StructureMapExtractionContext( transformSupportServices = transformSupportServices, structureMapProvider = { structureMapUrl: String?, _: IWorkerContext -> - structureMapUrl?.substringAfterLast("/")?.let { - defaultRepository.loadResource(it) + structureMapUrl?.substringAfterLast("/")?.let { smID -> + ContentCache.getResource(ResourceType.StructureMap.name + "/" + smID)?.let { + it as StructureMap + } + ?: run { + defaultRepository.loadResource(smID)?.also { + ContentCache.saveResource(smID, it) + } + } } }, ), diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ContentCacheTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ContentCacheTest.kt new file mode 100644 index 0000000000..4a94ca9f10 --- /dev/null +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ContentCacheTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2021-2024 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 + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Resource +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.smartregister.fhircore.engine.datastore.ContentCache + +@OptIn(ExperimentalCoroutinesApi::class) +class ContentCacheTest { + + private val testDispatcher = StandardTestDispatcher() + private val resourceId = "123" + private val mockResource: Resource = Questionnaire() + + @Before + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `saveResource should store resource in cache`() = runTest { + ContentCache.saveResource(resourceId, mockResource) + advanceUntilIdle() // Ensure coroutine has finished + + val cachedResource = ContentCache.getResource("${mockResource::class.simpleName}/$resourceId") + assertNotNull(cachedResource) + assertEquals(mockResource, cachedResource) + } + + @Test + fun `getResource should return the correct resource from cache`() = runTest { + ContentCache.saveResource(resourceId, mockResource) + advanceUntilIdle() // Ensure coroutine has finished + + val result = ContentCache.getResource("${mockResource::class.simpleName}/$resourceId") + assertEquals(mockResource, result) + } + + @Test + fun `getResource should return null if resource does not exist`() = runTest { + val result = ContentCache.getResource("non_existing_id") + assertNull(result) + } + + @Test + fun `invalidate should clear all resources from cache`() = runTest { + ContentCache.saveResource(resourceId, mockResource) + advanceUntilIdle() // Ensure coroutine has finished + + ContentCache.invalidate() + advanceUntilIdle() // Ensure coroutine has finished + + val result = ContentCache.getResource("${mockResource::class.simpleName}/$resourceId") + assertNull(result) + } +} diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModelTest.kt index 9c63ea2390..408d98b3b0 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/profile/ProfileViewModelTest.kt @@ -54,7 +54,6 @@ import org.smartregister.fhircore.engine.domain.model.OverflowMenuItemConfig 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.util.DefaultDispatcherProvider import org.smartregister.fhircore.engine.util.DispatcherProvider import org.smartregister.fhircore.engine.util.extension.BLACK_COLOR_HEX_CODE import org.smartregister.fhircore.engine.util.extension.getActivity @@ -99,7 +98,6 @@ class ProfileViewModelTest : RobolectricTest() { spyk( RegisterRepository( fhirEngine = mockk(), - dispatcherProvider = DefaultDispatcherProvider(), sharedPreferencesHelper = mockk(), configurationRegistry = configurationRegistry, configService = mockk(), @@ -107,6 +105,7 @@ class ProfileViewModelTest : RobolectricTest() { fhirPathDataExtractor = mockk(), parser = parser, context = ApplicationProvider.getApplicationContext(), + dispatcherProvider = dispatcherProvider, ), ) coEvery { diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index 578a91120f..73911597b9 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -51,6 +51,7 @@ import kotlin.test.assertEquals import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Address import org.hl7.fhir.r4.model.Attachment @@ -94,6 +95,7 @@ import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.local.DefaultRepository +import org.smartregister.fhircore.engine.datastore.ContentCache import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType import org.smartregister.fhircore.engine.domain.model.QuestionnaireType @@ -639,6 +641,26 @@ class QuestionnaireViewModelTest : RobolectricTest() { Assert.assertEquals(questionnaireConfig.id, questionnaire?.id?.extractLogicalIdUuid()) } + @Test + fun testRetrieveQuestionnaireShouldReturnValidQuestionnaireFromCache() = runBlocking { + coEvery { fhirEngine.get(ResourceType.Questionnaire, questionnaireConfig.id) } returns + samplePatientRegisterQuestionnaire + + ContentCache.saveResource(questionnaireConfig.id, samplePatientRegisterQuestionnaire) + + val questionnaire = + questionnaireViewModel.retrieveQuestionnaire( + questionnaireConfig = questionnaireConfig, + ) + + Assert.assertEquals( + samplePatientRegisterQuestionnaire, + ContentCache.getResource(ResourceType.Questionnaire.name + "/" + questionnaireConfig.id), + ) + Assert.assertNotNull(questionnaire) + Assert.assertEquals(questionnaireConfig.id, questionnaire?.id?.extractLogicalIdUuid()) + } + @Test fun testPopulateQuestionnaireShouldPrePopulatedQuestionnaireWithComputedValues() = runTest { val questionnaireViewModelInstance = @@ -1047,57 +1069,58 @@ class QuestionnaireViewModelTest : RobolectricTest() { } @Test - fun testExecuteCqlShouldInvokeRunCqlLibrary() = runTest { - val bundle = - Bundle().apply { addEntry(Bundle.BundleEntryComponent().apply { resource = patient }) } + fun testExecuteCqlShouldInvokeRunCqlLibrary() = + runTest(UnconfinedTestDispatcher()) { + val bundle = + Bundle().apply { addEntry(Bundle.BundleEntryComponent().apply { resource = patient }) } - val questionnaire = - samplePatientRegisterQuestionnaire.copy().apply { - addExtension( - Extension().apply { - url = "https://sample.cqf-library.url" - setValue(StringType("http://smartreg.org/Library/123")) - }, - ) - } - - coEvery { fhirOperator.evaluateLibrary(any(), any(), any(), any()) } returns Parameters() + val questionnaire = + samplePatientRegisterQuestionnaire.copy().apply { + addExtension( + Extension().apply { + url = "https://sample.cqf-library.url" + setValue(StringType("http://smartreg.org/Library/123")) + }, + ) + } - val cqlLibrary = - Library().apply { - id = "Library/123" - url = "http://smartreg.org/Library/123" - name = "123" - version = "1.0.0" - status = Enumerations.PublicationStatus.ACTIVE - addContent( - Attachment().apply { - contentType = "text/cql" - data = "someCQL".toByteArray() - }, - ) - } + coEvery { fhirOperator.evaluateLibrary(any(), any(), any(), any()) } returns Parameters() + + val cqlLibrary = + Library().apply { + id = "Library/123" + url = "http://smartreg.org/Library/123" + name = "123" + version = "1.0.0" + status = Enumerations.PublicationStatus.ACTIVE + addContent( + Attachment().apply { + contentType = "text/cql" + data = "someCQL".toByteArray() + }, + ) + } - knowledgeManager.install( - File.createTempFile(cqlLibrary.name, ".json").apply { - this.writeText(cqlLibrary.encodeResourceToString()) - }, - ) + knowledgeManager.install( + File.createTempFile(cqlLibrary.name, ".json").apply { + this.writeText(cqlLibrary.encodeResourceToString()) + }, + ) - fhirEngine.create(patient) + fhirEngine.create(patient) - questionnaireViewModel.executeCql(patient, bundle, questionnaire) + questionnaireViewModel.executeCql(patient, bundle, questionnaire) - coVerify { - fhirOperator.evaluateLibrary( - "http://smartreg.org/Library/123", - patient.asReference().reference, - null, - bundle, - null, - ) + coVerify { + fhirOperator.evaluateLibrary( + "http://smartreg.org/Library/123", + patient.asReference().reference, + null, + bundle, + null, + ) + } } - } @Test fun testGenerateCarePlan() = runTest {