Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 10:51:33 +04:00
parent 7135ce83fa
commit 772272285a
5 changed files with 79 additions and 80 deletions

View file

@ -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<GetWalletArchivedAccountsResponse>
// endregion
}

View file

@ -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
}

View file

@ -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,

View file

@ -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<AccountList> = 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<Int> = 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 {

View file

@ -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<GetWalletArchivedAccountsResponse> {
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<GetWalletAccountsResponse> {
every { this@mockk.wallet.version } returns version
}
val accountsResponse = mockk<GetWalletAccountsResponse>()
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<GetWalletAccountsResponseConverter> {
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<GetWalletAccountsResponse> {
every { this@mockk.wallet.version } returns version
}
val accountsResponse = mockk<GetWalletAccountsResponse>()
accountsResponseStoreFlow.value = accountsResponse
val body = SaveWalletAccountsResponseConverter.convert(value = accountList)
val converter = mockk<GetWalletAccountsResponseConverter> {
every { this@mockk.convert(accountList) } returns accountsResponse
}
val apiResponse = ApiResponse.Error(cause = ApiResponseError.NetworkException) as ApiResponse<Unit>
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)
}
}
}