diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index f98dfae648..d173c69c57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -160,6 +160,7 @@ interface TangemTechApi { @GET("/v1/wallets/{walletId}/accounts/archived") suspend fun getWalletArchivedAccounts( @Path("walletId") walletId: String, + @Header("If-None-Match") eTag: String? = null, ): ApiResponse // endregion } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index 61056db1c3..df7c1a3919 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -3,9 +3,13 @@ package com.tangem.data.account.di import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.account.converter.AccountConverterFactoryContainer import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles +import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher import com.tangem.data.account.repository.DefaultAccountsCRUDRepository import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.common.account.WalletAccountsSaver +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -31,18 +35,30 @@ internal object AccountDataModule { @Singleton fun provideAccountsCRUDRepository( tangemTechApi: TangemTechApi, + walletAccountsSaver: WalletAccountsSaver, accountsResponseStoreFactory: AccountsResponseStoreFactory, userWalletsStore: UserWalletsStore, + eTagsStore: ETagsStore, accountConverterFactoryContainer: AccountConverterFactoryContainer, dispatchers: CoroutineDispatcherProvider, ): AccountsCRUDRepository { return DefaultAccountsCRUDRepository( tangemTechApi = tangemTechApi, + walletAccountsSaver = walletAccountsSaver, accountsResponseStoreFactory = accountsResponseStoreFactory, archivedAccountsStoreFactory = ArchivedAccountsStoreFactory, userWalletsStore = userWalletsStore, + eTagsStore = eTagsStore, convertersContainer = accountConverterFactoryContainer, dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideWalletAccountsFetcher(impl: DefaultWalletAccountsFetcher): WalletAccountsFetcher = impl + + @Provides + @Singleton + fun provideWalletAccountsSaver(impl: DefaultWalletAccountsFetcher): WalletAccountsSaver = impl } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 45a16f947b..17bc75d8ff 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -21,6 +21,8 @@ import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton /** * Default implementation of [WalletAccountsFetcher] and [WalletAccountsSaver] @@ -34,7 +36,8 @@ import kotlinx.coroutines.withContext * [REDACTED_AUTHOR] */ -internal class DefaultWalletAccountsFetcher( +@Singleton +internal class DefaultWalletAccountsFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val userTokensSaver: UserTokensSaver, diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 73a4766449..80e69fc536 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -5,15 +5,17 @@ import arrow.core.raise.option import arrow.core.toOption import com.tangem.data.account.converter.AccountConverterFactoryContainer import com.tangem.data.account.converter.ArchivedAccountConverter -import com.tangem.data.account.converter.SaveWalletAccountsResponseConverter import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.store.ArchivedAccountsStore import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.data.common.account.WalletAccountsSaver +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository @@ -23,25 +25,23 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext /** [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultAccountsCRUDRepository( private val tangemTechApi: TangemTechApi, + private val walletAccountsSaver: WalletAccountsSaver, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory, private val userWalletsStore: UserWalletsStore, + private val eTagsStore: ETagsStore, private val convertersContainer: AccountConverterFactoryContainer, private val dispatchers: CoroutineDispatcherProvider, ) : AccountsCRUDRepository { - private val saveAccountsMutex = Mutex() - override suspend fun getAccountListSync(userWalletId: UserWalletId): Option = option { val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId) @@ -85,7 +85,10 @@ internal class DefaultAccountsCRUDRepository( override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) { val response = withContext(dispatchers.io) { - tangemTechApi.getWalletArchivedAccounts(walletId = userWalletId.stringValue).getOrThrow() + tangemTechApi.getWalletArchivedAccounts( + walletId = userWalletId.stringValue, + eTag = getETag(userWalletId), + ).getOrThrow() } val store = getArchivedAccountsStore(userWalletId = userWalletId) @@ -97,27 +100,12 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun saveAccounts(accountList: AccountList) { - saveAccountsMutex.withLock { - val store = getAccountsResponseStore(userWalletId = accountList.userWallet.walletId) + val userWalletId = accountList.userWallet.walletId - store.data.firstOrNull()?.wallet?.version ?: 0 - val body = SaveWalletAccountsResponseConverter.convert(value = accountList) + val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet) + val accountsResponse = converter.convert(value = accountList) - withContext(dispatchers.io) { - tangemTechApi.saveWalletAccounts( - walletId = accountList.userWallet.walletId.stringValue, - eTag = "", // TODO("[REDACTED_JIRA]") - body = body, - ) - .getOrThrow() - } - - val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet) - - val accountsResponse = converter.convert(value = accountList) - - store.updateData { accountsResponse } - } + walletAccountsSaver.pushAndStore(userWalletId = userWalletId, response = accountsResponse) } override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option = option { @@ -132,9 +120,13 @@ internal class DefaultAccountsCRUDRepository( return userWalletsStore.getSyncStrict(userWalletId) } + private suspend fun getETag(userWalletId: UserWalletId): String? { + return eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts) + } + private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? { val store = getAccountsResponseStore(userWalletId = userWalletId) - return store.data.firstOrNull() + return store.getSyncOrNull() } private fun getAccountsResponseStore(userWalletId: UserWalletId): AccountsResponseStore { diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 6c620a5b8a..5f18006b72 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -9,8 +9,9 @@ import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.store.ArchivedAccountsStore import com.tangem.data.account.store.ArchivedAccountsStoreFactory +import com.tangem.data.common.account.WalletAccountsSaver +import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse @@ -40,6 +41,7 @@ import kotlin.time.Duration.Companion.minutes class DefaultAccountsCRUDRepositoryTest { private val tangemTechApi: TangemTechApi = mockk() + private val walletAccountsSaver: WalletAccountsSaver = mockk(relaxUnitFun = true) private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk() private val accountsResponseStore: AccountsResponseStore = mockk() @@ -50,6 +52,7 @@ class DefaultAccountsCRUDRepositoryTest { private val archivedAccountsStore = ArchivedAccountsStore(runtimeStore = archivedAccountsInnerStore) private val userWalletsStore: UserWalletsStore = mockk() + private val eTagsStore: ETagsStore = mockk() private val convertersContainer: AccountConverterFactoryContainer = mockk() private val accountListConverter: AccountListConverter = mockk() @@ -57,9 +60,11 @@ class DefaultAccountsCRUDRepositoryTest { private val repository = DefaultAccountsCRUDRepository( tangemTechApi = tangemTechApi, + walletAccountsSaver = walletAccountsSaver, accountsResponseStoreFactory = accountsResponseStoreFactory, archivedAccountsStoreFactory = archivedAccountsStoreFactory, userWalletsStore = userWalletsStore, + eTagsStore = eTagsStore, convertersContainer = convertersContainer, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -517,14 +522,17 @@ class DefaultAccountsCRUDRepositoryTest { totalTokens = 0, ) + val eTag = "etag123" val apiResponse = mockk { every { this@mockk.accounts } returns listOf(accountDTO) } val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO) + coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag + coEvery { - tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) + tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } returns ApiResponse.Success(apiResponse) // Act @@ -535,15 +543,20 @@ class DefaultAccountsCRUDRepositoryTest { Truth.assertThat(actual).containsExactly(archivedAccount) coVerifyOrder { - tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) + eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) + tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) archivedAccountsStoreFactory.create(userWalletId) } } @Test - fun `fetchArchivedAccounts should throw exception if API returns error`() = runTest { // Arrange + fun `fetchArchivedAccounts should throw exception if API returns error`() = runTest { + // Arrange + val eTag = "etag123" val exception = Exception("API error") - coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) } throws exception + + coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag + coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } throws exception // Act val actual = runCatching { repository.fetchArchivedAccounts(userWalletId) }.exceptionOrNull()!! @@ -553,7 +566,10 @@ class DefaultAccountsCRUDRepositoryTest { Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull() - coVerify { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue) } + coVerifyOrder { + eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) + tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) + } } } @@ -561,8 +577,6 @@ class DefaultAccountsCRUDRepositoryTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class SaveAccounts { - private val version = 1 - @Test fun `saveAccounts should call API and update store`() = runTest { // Arrange @@ -572,24 +586,9 @@ class DefaultAccountsCRUDRepositoryTest { val accountList = AccountList.empty(userWallet = userWallet) - val accountsResponse = mockk { - every { this@mockk.wallet.version } returns version - } - + val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse - val body = SaveWalletAccountsResponseConverter.convert(value = accountList) - - val apiResponse = ApiResponse.Success(Unit) - - coEvery { - tangemTechApi.saveWalletAccounts( - walletId = userWalletId.stringValue, - eTag = "", - body = body, - ) - } returns apiResponse - val converter = mockk { every { this@mockk.convert(accountList) } returns accountsResponse } @@ -598,8 +597,6 @@ class DefaultAccountsCRUDRepositoryTest { convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter - coEvery { accountsResponseStore.updateData(transform = any()) } returns accountsResponse - // Act repository.saveAccounts(accountList) @@ -607,12 +604,9 @@ class DefaultAccountsCRUDRepositoryTest { Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse) coVerifyOrder { - accountsResponseStoreFactory.create(userWalletId) - accountsResponseStore.data - tangemTechApi.saveWalletAccounts(userWalletId.stringValue, "", body) convertersContainer.getWalletAccountsResponseCF.create(userWallet) converter.convert(accountList) - accountsResponseStore.updateData(any()) + walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } } @@ -625,39 +619,32 @@ class DefaultAccountsCRUDRepositoryTest { val accountList = AccountList.empty(userWallet = userWallet) - val accountsResponse = mockk { - every { this@mockk.wallet.version } returns version - } - + val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse - val body = SaveWalletAccountsResponseConverter.convert(value = accountList) + val converter = mockk { + every { this@mockk.convert(accountList) } returns accountsResponse + } - val apiResponse = ApiResponse.Error(cause = ApiResponseError.NetworkException) as ApiResponse + every { + convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) + } returns converter - coEvery { - tangemTechApi.saveWalletAccounts( - walletId = userWalletId.stringValue, - eTag = "", - body = body, - ) - } returns apiResponse + val exception = Exception("Test error") + + coEvery { walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } throws exception // Act val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!! // Assert - Truth.assertThat(actual).isEqualTo(ApiResponseError.NetworkException) + Truth.assertThat(actual).isInstanceOf(exception::class.java) + Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message) coVerifyOrder { - accountsResponseStoreFactory.create(userWalletId) - accountsResponseStore.data - tangemTechApi.saveWalletAccounts(userWalletId.stringValue, "", body) - } - - coVerify(inverse = true) { - convertersContainer.getWalletAccountsResponseCF.create(any()) - accountsResponseStore.updateData(any()) + convertersContainer.getWalletAccountsResponseCF.create(userWallet) + converter.convert(accountList) + walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } } }