From 5a8f4299a66c7e7390226f8f4d5f28a7415645b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Nov 2025 12:22:31 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../AccountConverterFactoryContainer.kt | 11 +- .../DefaultAccountsCRUDRepository.kt | 11 + .../repository/AccountsCRUDRepository.kt | 7 + .../status/di/AccountStatusUseCaseModule.kt | 12 + .../status/model/AccountCryptoCurrency.kt | 2 + .../usecase/ApplyTokenListSortingUseCaseV2.kt | 203 ++++++++++ .../ApplyTokenListSortingUseCaseTest.kt | 375 ++++++++++++++++++ .../tokens/error/TokenListSortingError.kt | 2 + 8 files changed, 621 insertions(+), 2 deletions(-) create mode 100644 domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt index e8bcef8cce..8a7f2de408 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -7,21 +7,28 @@ import javax.inject.Inject /** * Container for converter factories related to accounts. * - * @property accountsListCF factory for creating an account list converter * @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter + * @property accountsListCF factory for creating an account list converter * @property cryptoPortfolioCF factory for creating a crypto portfolio converter + * @property userWalletsStore store for accessing user wallet data * * @constructor Creates an instance of the container with injected factories. * [REDACTED_AUTHOR] */ internal class AccountConverterFactoryContainer @Inject constructor( - val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, + private val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, private val accountsListCF: AccountListConverter.Factory, private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, private val userWalletsStore: UserWalletsStore, ) { + fun createWalletAccountsResponseConverter(userWalletId: UserWalletId): GetWalletAccountsResponseConverter { + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + + return getWalletAccountsResponseCF.create(userWallet) + } + fun createAccountListConverter(userWalletId: UserWalletId): AccountListConverter { val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) 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 1510ff5479..b8877392d3 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 @@ -132,6 +132,17 @@ internal class DefaultAccountsCRUDRepository( } } + override suspend fun saveAccountsLocally(accountList: AccountList) { + val converter = convertersContainer.createWalletAccountsResponseConverter( + userWalletId = accountList.userWalletId, + ) + + walletAccountsSaver.store( + userWalletId = accountList.userWalletId, + response = converter.convert(accountList), + ) + } + override suspend fun saveAccounts(accountList: AccountList) { val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = accountList.userWalletId) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index 5c965fe538..8967491701 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -62,6 +62,13 @@ interface AccountsCRUDRepository { */ suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + /** + * Saves a list of accounts to the repository + * + * @param accountList the list of accounts to be saved. + */ + suspend fun saveAccountsLocally(accountList: AccountList) + /** * Saves a list of accounts to the repository * diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 15c08a52d5..311f9baa82 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -58,6 +58,18 @@ internal object AccountStatusUseCaseModule { ) } + @Provides + @Singleton + fun provideApplyTokenListSortingUseCaseV2( + accountsCRUDRepository: AccountsCRUDRepository, + dispatchers: CoroutineDispatcherProvider, + ): ApplyTokenListSortingUseCaseV2 { + return ApplyTokenListSortingUseCaseV2( + accountsCRUDRepository = accountsCRUDRepository, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun provideGetWalletTotalBalanceUseCaseV2( diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt index e4dabbc8cc..5acaea2b98 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt @@ -4,6 +4,8 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.serialization.Serializable +typealias AccountCryptoCurrencies = Map> + /** * Combines an [Account] with its corresponding [CryptoCurrency]. * diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt new file mode 100644 index 0000000000..a3a2854c45 --- /dev/null +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt @@ -0,0 +1,203 @@ +package com.tangem.domain.account.status.usecase + +import arrow.core.* +import arrow.core.raise.* +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.model.AccountCryptoCurrencies +import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import timber.log.Timber + +private typealias SortingErrorByAccountId = MutableMap + +/** + * Use case to apply token list sorting and grouping preferences for accounts. + * + * @property accountsCRUDRepository Repository for CRUD operations on accounts. + * @property dispatchers Coroutine dispatcher provider for managing threading. + */ +class ApplyTokenListSortingUseCaseV2( + private val accountsCRUDRepository: AccountsCRUDRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Applies the token list sorting and grouping preferences for the given accounts. + * + * @param sortedTokensIdsByAccount A map of accounts to their sorted token IDs. + * @param isGroupedByNetwork Boolean indicating if tokens should be grouped by network. + * @param isSortedByBalance Boolean indicating if tokens should be sorted by balance. + * @return Either a [TokenListSortingError] or Unit on success. + */ + suspend operator fun invoke( + sortedTokensIdsByAccount: AccountCryptoCurrencies, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): Either = eitherOn(dispatchers.default) { + ensure(sortedTokensIdsByAccount.isNotEmpty()) { + raise(TokenListSortingError.TokenListIsEmpty) + } + + val userWalletId = sortedTokensIdsByAccount.keys.first().userWalletId + + val accountList = getAccountList(userWalletId) + + val maybeSortedAccountList = sortAccountList( + accountList = accountList, + sortedTokensIdsByAccount = sortedTokensIdsByAccount, + isSortedByBalance = isSortedByBalance, + isGroupedByNetwork = isGroupedByNetwork, + ) + + val sortedAccountList = maybeSortedAccountList.getOrNull() + val isSortingTypeChanged = accountList.isSortedByBalance() != isSortedByBalance + val isGroupingTypeChanged = accountList.isGroupedByNetwork() != isGroupedByNetwork + + if (sortedAccountList != null || isSortingTypeChanged || isGroupingTypeChanged) { + applySorting(sortedAccountList ?: accountList) + } + + maybeSortedAccountList.toEitherNeg() + .onLeft { errorByAccountId -> + Timber.e( + """ + Unable to sort tokens for accounts: ${ + errorByAccountId.entries.joinToString { "${it.key.value}: ${it.value}" } + } + """.trimIndent(), + ) + + raise(TokenListSortingError.UnableToSortAccounts(errorByAccountId.keys.map { it.value })) + } + } + + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catchOrDataError { + accountsCRUDRepository.getAccountListSync(userWalletId = userWalletId).getOrElse { + throw IllegalStateException("Account list not found for wallet $userWalletId") + } + } + } + + private fun AccountList.isSortedByBalance(): Boolean = sortType == TokensSortType.BALANCE + + private fun AccountList.isGroupedByNetwork(): Boolean = groupType == TokensGroupType.NETWORK + + private fun Raise.sortAccountList( + accountList: AccountList, + sortedTokensIdsByAccount: AccountCryptoCurrencies, + isSortedByBalance: Boolean, + isGroupedByNetwork: Boolean, + ): Ior { + val errors = mutableMapOf() + + val updatedAccountList = AccountList( + userWalletId = accountList.userWalletId, + accounts = accountList.accounts.sortTokens(sortedTokensIdsByAccount, errors), + totalAccounts = accountList.totalAccounts, + sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE, + groupType = if (isGroupedByNetwork) TokensGroupType.NETWORK else TokensGroupType.NONE, + ).getOrElse { + raise(TokenListSortingError.UnableToSortTokenList) + } + + return if (errors.isEmpty()) { + updatedAccountList.rightIor() + } else if (errors.size == sortedTokensIdsByAccount.keys.size) { + errors.leftIor() + } else { + (errors to updatedAccountList).bothIor() + } + } + + private fun List.sortTokens( + sortedTokensIdsByAccount: AccountCryptoCurrencies, + errors: MutableMap, + ): List { + return map { account -> + if (account !is Account.CryptoPortfolio) return@map account + + val sortedCurrencies = sortedTokensIdsByAccount[account] ?: return@map account + + val accountCurrencies = account.cryptoCurrencies.toList() + .sortTokens(sortedCurrencies = sortedCurrencies) + .getOrElse { + errors[account.accountId] = it + return@map account + } + .toSet() + + account.copy(cryptoCurrencies = accountCurrencies) + } + } + + private fun List.sortTokens( + sortedCurrencies: List, + ): Either> = either { + val nonEmptySortedTokensIds = ensureNotNull(sortedCurrencies.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + + val sortedTokens = sortedMapOf() + + distinct().forEach { currency -> + val index = nonEmptySortedTokensIds.indexOfFirst { sortedCurrency -> + sortedCurrency.id == currency.id + } + + if (index >= 0) { + sortedTokens[index] = currency + } else { + raise(TokenListSortingError.UnableToSortTokenList) + } + } + + ensureNotNull(sortedTokens.values.toNonEmptyListOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + } + + private suspend fun Raise.applySorting(accountList: AccountList) { + coroutineScope { + val results = awaitAll( + async { + Either.catch { accountsCRUDRepository.saveAccountsLocally(accountList) } + }, + async { + Either.catch { accountsCRUDRepository.syncTokens(accountList.userWalletId) } + }, + ) + + ensure(results.none { it.isLeft() }) { + val message = results.mapNotNull { it.leftOrNull() }.joinToString() + raise(TokenListSortingError.DataError(IllegalStateException(message))) + } + } + } + + private fun Ior.toEitherNeg(): Either { + return when (this) { + is Ior.Left -> this.value.left() + is Ior.Right -> this.value.right() + is Ior.Both -> this.leftValue.left() + } + } + + private suspend fun Raise.catchOrDataError(block: suspend () -> A): A { + return catch( + block = { block() }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt new file mode 100644 index 0000000000..48026d148b --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -0,0 +1,375 @@ +package com.tangem.domain.account.status.usecase + +import arrow.core.left +import arrow.core.right +import arrow.core.some +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ApplyTokenListSortingUseCaseTest { + + private val accountsCRUDRepository = mockk(relaxUnitFun = true) + + private val useCase = ApplyTokenListSortingUseCaseV2( + accountsCRUDRepository = accountsCRUDRepository, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val userWalletId = UserWalletId(stringValue = "011") + + @AfterEach + fun tearDown() { + clearMocks(accountsCRUDRepository) + } + + @Test + fun `when tokens are empty then error should be received`() = runTest { + // Act + val actual = useCase( + sortedTokensIdsByAccount = emptyMap(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Assert + val expected = TokenListSortingError.TokenListIsEmpty.left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder(inverse = true) { + accountsCRUDRepository.getAccountListSync(userWalletId = any()) + accountsCRUDRepository.saveAccountsLocally(accountList = any()) + accountsCRUDRepository.syncTokens(userWalletId = any()) + } + } + + @Test + fun `when getAccountListSync throws exception then error should be received`() = runTest { + // Arrange + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + + val exception = IllegalStateException("No internet connection") + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } throws exception + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to emptyList()), + isGroupedByNetwork = true, + isSortedByBalance = false, + ) + + // Assert + val expected = TokenListSortingError.DataError(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) } + + coVerifyOrder(inverse = true) { + accountsCRUDRepository.saveAccountsLocally(accountList = any()) + accountsCRUDRepository.syncTokens(userWalletId = any()) + } + } + + @Test + fun `when saveAccountsLocally throws exception then error should be received`() = runTest { + // Arrange + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val exception = IllegalStateException("No internet connection") + coEvery { accountsCRUDRepository.saveAccountsLocally(accountList) } throws exception + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to emptyList()), + isGroupedByNetwork = true, + isSortedByBalance = false, + ).leftOrNull() as TokenListSortingError.DataError + + // Assert + val expected = TokenListSortingError.DataError(IllegalStateException(exception.toString())) + Truth.assertThat(actual.cause).isInstanceOf(expected.cause::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.cause.message) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(accountList) + accountsCRUDRepository.syncTokens(userWalletId = userWalletId) + } + } + + @Test + fun `when syncTokens throws exception then error should be received`() = runTest { + // Arrange + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val exception = IllegalStateException("No internet connection") + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to emptyList()), + isGroupedByNetwork = true, + isSortedByBalance = false, + ).leftOrNull() as TokenListSortingError.DataError + + // Assert + val expected = TokenListSortingError.DataError(IllegalStateException(exception.toString())) + Truth.assertThat(actual.cause).isInstanceOf(expected.cause::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.cause.message) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(accountList) + accountsCRUDRepository.syncTokens(userWalletId = userWalletId) + } + } + + @Test + fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { + // Arrange + val token1 = mockk(relaxed = true) + val token2 = mockk(relaxed = true) + val token3 = mockk(relaxed = true) + + val accountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = setOf(token1, token2, token3), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val sortedTokens = setOf(token2, token3, token1) + val updatedAccountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = sortedTokens, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ) + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to sortedTokens.toList()), + isGroupedByNetwork = true, + isSortedByBalance = true, + ) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(updatedAccountList) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { + // Arrange + val token1 = mockk(relaxed = true) + val token2 = mockk(relaxed = true) + val token3 = mockk(relaxed = true) + + val accountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = setOf(token1, token2, token3), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val sortedTokens = setOf(token2, token3, token1) + val updatedAccountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = sortedTokens, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NETWORK, + ) + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to sortedTokens.toList()), + isGroupedByNetwork = true, + isSortedByBalance = false, + ) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(updatedAccountList) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { + // Arrange + val token1 = mockk(relaxed = true) + val token2 = mockk(relaxed = true) + val token3 = mockk(relaxed = true) + + val accountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = setOf(token1, token2, token3), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val sortedTokens = setOf(token2, token3, token1) + val updatedAccountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = sortedTokens, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NONE, + ) + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to sortedTokens.toList()), + isGroupedByNetwork = false, + isSortedByBalance = true, + ) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(updatedAccountList) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { + // Arrange + val token1 = mockk(relaxed = true) + val token2 = mockk(relaxed = true) + val token3 = mockk(relaxed = true) + + val accountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = setOf(token1, token2, token3), + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ) + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val sortedTokens = setOf(token2, token3, token1) + val updatedAccountList = AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = sortedTokens, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + + // Act + val actual = useCase( + sortedTokensIdsByAccount = mapOf(accountList.mainAccount to sortedTokens.toList()), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(updatedAccountList) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest { + // Arrange + val token1 = mockk(relaxed = true) + val token2 = mockk(relaxed = true) + val token3 = mockk(relaxed = true) + + val customAccount = Account.CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(1).getOrNull()!!), + name = "Account 1", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + cryptoCurrencies = setOf(token1, token2, token3), + ) + .getOrNull()!! + + val accountList = (AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = setOf(token1, token2, token3), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + customAccount).getOrNull()!! + + coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() + + val sortedTokensIdsByAccount: Map> = mapOf( + accountList.mainAccount to listOf(token3, token2, token1), + customAccount to listOf(token2, token1), // missing token3 + ) + + val updatedAccountList = AccountList( + userWalletId = userWalletId, + accounts = listOf( + accountList.mainAccount.copy(cryptoCurrencies = setOf(token3, token2, token1)), + customAccount, // unchanged due to error + ), + totalAccounts = accountList.totalAccounts, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + .getOrNull()!! + + // Act + val actual = useCase( + sortedTokensIdsByAccount = sortedTokensIdsByAccount, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Assert + val expected = TokenListSortingError.UnableToSortAccounts( + accountIds = listOf(customAccount.accountId.value), + ).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + accountsCRUDRepository.getAccountListSync(userWalletId) + accountsCRUDRepository.saveAccountsLocally(updatedAccountList) + accountsCRUDRepository.syncTokens(userWalletId) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt index 847432134b..eb03e4aa5e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt @@ -8,5 +8,7 @@ sealed class TokenListSortingError { object UnableToSortTokenList : TokenListSortingError() + data class UnableToSortAccounts(val accountIds: List) : TokenListSortingError() + data class DataError(val cause: Throwable) : TokenListSortingError() } \ No newline at end of file