From 08521b6c110af63bb63d2f77eda71f96457f7c9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Sep 2025 22:23:18 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 14 +- .../data/account/di/AccountDataModule.kt | 15 ++ .../fetcher/DefaultMultiAccountListFetcher.kt | 3 +- .../DefaultSingleAccountListFetcher.kt | 3 +- .../DefaultMainAccountTokensMigration.kt | 5 - .../DefaultMainAccountTokensMigrationTest.kt | 10 +- .../domain/account/models/AccountList.kt | 12 +- .../usecase/AddCryptoPortfolioUseCase.kt | 50 +++--- .../domain/account/models/AccountListTest.kt | 2 +- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 158 +++++++++++++++--- .../createedit/AccountCreateEditModel.kt | 40 +++-- .../createedit/error/AccountFeatureError.kt | 8 +- 12 files changed, 230 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index d90cfcd00a..34f9a1101c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.usecase.* import dagger.Module import dagger.Provides @@ -15,8 +17,16 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase { - return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + fun provideAddCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + singleAccountListFetcher: SingleAccountListFetcher, + mainAccountTokensMigration: MainAccountTokensMigration, + ): AddCryptoPortfolioUseCase { + return AddCryptoPortfolioUseCase( + crudRepository = accountsCRUDRepository, + singleAccountListFetcher = singleAccountListFetcher, + mainAccountTokensMigration = mainAccountTokensMigration, + ) } @Provides 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 df7c1a3919..198b1291fd 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 @@ -7,13 +7,16 @@ 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.account.tokens.DefaultMainAccountTokensMigration 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.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -61,4 +64,16 @@ internal object AccountDataModule { @Provides @Singleton fun provideWalletAccountsSaver(impl: DefaultWalletAccountsFetcher): WalletAccountsSaver = impl + + @Provides + @Singleton + fun provideMainAccountTokensMigration( + accountsResponseStoreFactory: AccountsResponseStoreFactory, + userTokensSaver: UserTokensSaver, + ): MainAccountTokensMigration { + return DefaultMainAccountTokensMigration( + accountsResponseStoreFactory = accountsResponseStoreFactory, + userTokensSaver = userTokensSaver, + ) + } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt index ada7843d5c..2be1ac75dc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject /** * Implementation of [MultiAccountListFetcher] @@ -20,7 +21,7 @@ import java.util.concurrent.ConcurrentHashMap * [REDACTED_AUTHOR] */ -internal class DefaultMultiAccountListFetcher( +internal class DefaultMultiAccountListFetcher @Inject constructor( private val singleAccountListFetcher: SingleAccountListFetcher, private val userWalletsStore: UserWalletsStore, ) : MultiAccountListFetcher { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt index b10047e2ec..44d745e817 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt @@ -3,6 +3,7 @@ package com.tangem.data.account.fetcher import arrow.core.Either import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.domain.account.fetcher.SingleAccountListFetcher +import javax.inject.Inject /** * Implementation of [SingleAccountListFetcher] @@ -11,7 +12,7 @@ import com.tangem.domain.account.fetcher.SingleAccountListFetcher * [REDACTED_AUTHOR] */ -internal class DefaultSingleAccountListFetcher( +internal class DefaultSingleAccountListFetcher @Inject constructor( private val walletAccountsFetcher: WalletAccountsFetcher, ) : SingleAccountListFetcher { diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index acdc6e4578..90a1de9362 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -10,7 +10,6 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.utils.assignTokens import com.tangem.data.account.utils.toUserTokensResponse -import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse @@ -28,14 +27,12 @@ import timber.log.Timber * * @property accountsResponseStoreFactory Factory for creating stores to access cached account responses. * @property userTokensSaver Saver for updating user tokens in persistent storage. - * @property walletAccountsSaver Saver for updating wallet accounts in persistent storage. * [REDACTED_AUTHOR] */ internal class DefaultMainAccountTokensMigration( private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val userTokensSaver: UserTokensSaver, - private val walletAccountsSaver: WalletAccountsSaver, ) : MainAccountTokensMigration { override suspend fun migrate( @@ -81,8 +78,6 @@ internal class DefaultMainAccountTokensMigration( }, ) - walletAccountsSaver.store(userWalletId = userWalletId, response = updatedResponse) - userTokensSaver.push( userWalletId = userWalletId, response = updatedResponse.toUserTokensResponse(), diff --git a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt index 685272f7ac..37c3d84862 100644 --- a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt @@ -8,7 +8,6 @@ import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration import com.tangem.data.account.utils.toUserTokensResponse -import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse @@ -32,11 +31,9 @@ class DefaultMainAccountTokensMigrationTest { private val accountsResponseStoreFlow = MutableStateFlow(value = null) private val userTokensSaver = mockk(relaxed = true) - private val walletAccountsSaver = mockk(relaxed = true) private val migration = DefaultMainAccountTokensMigration( accountsResponseStoreFactory = accountsResponseStoreFactory, userTokensSaver = userTokensSaver, - walletAccountsSaver = walletAccountsSaver, ) private val userWalletId = UserWalletId("011") @@ -50,7 +47,7 @@ class DefaultMainAccountTokensMigrationTest { @AfterEach fun tearDown() { - clearMocks(accountsResponseStoreFactory, accountsResponseStore, userTokensSaver, walletAccountsSaver) + clearMocks(accountsResponseStoreFactory, accountsResponseStore, userTokensSaver) accountsResponseStoreFlow.value = null } @@ -65,7 +62,6 @@ class DefaultMainAccountTokensMigrationTest { coVerify(inverse = true) { accountsResponseStoreFactory.create(any()) accountsResponseStore.data - walletAccountsSaver.store(userWalletId = any(), response = any()) userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any()) } } @@ -85,7 +81,6 @@ class DefaultMainAccountTokensMigrationTest { } coVerify(inverse = true) { - walletAccountsSaver.store(userWalletId = any(), response = any()) userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any()) } } @@ -115,7 +110,6 @@ class DefaultMainAccountTokensMigrationTest { } coVerify(inverse = true) { - walletAccountsSaver.store(userWalletId = any(), response = any()) userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any()) } } @@ -151,7 +145,6 @@ class DefaultMainAccountTokensMigrationTest { } coVerify(inverse = true) { - walletAccountsSaver.store(userWalletId = any(), response = any()) userTokensSaver.push(userWalletId = any(), response = any(), onFailSend = any()) } } @@ -206,7 +199,6 @@ class DefaultMainAccountTokensMigrationTest { coVerifySequence { accountsResponseStoreFactory.create(userWalletId) accountsResponseStore.data - walletAccountsSaver.store(userWalletId = userWalletId, response = migratedResponse) userTokensSaver.push( userWalletId = userWalletId, response = migratedResponse.toUserTokensResponse(), diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 0a5b08f87b..c81e865d8e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -120,8 +120,8 @@ data class AccountList private constructor( } @Serializable - data class DuplicateAccountNames(val message: String) : Error { - override fun toString(): String = "$tag: Account list contains duplicate account names. $message" + data object DuplicateAccountNames : Error { + override fun toString(): String = "$tag: Account list contains duplicate account names" } } @@ -161,17 +161,11 @@ data class AccountList private constructor( val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } - val defaultMainNameCount = accounts.count { it.accountName is AccountName.DefaultMain } - val customNames = accounts.mapNotNull { (it.accountName as? AccountName.Custom)?.value } val uniqueCustomNameCount = customNames.distinct().size - ensure(defaultMainNameCount == 0 || defaultMainNameCount == 1) { - Error.DuplicateAccountNames("Only one account can have the default main name.") - } - ensure(customNames.size == uniqueCustomNameCount) { - Error.DuplicateAccountNames("Custom account names must be unique.") + Error.DuplicateAccountNames } AccountList( diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 6f233d2da7..8085ff2eb1 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -1,15 +1,14 @@ package com.tangem.domain.account.usecase import arrow.core.Either -import arrow.core.Option import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.account.fetcher.SingleAccountListFetcher 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.account.tokens.MainAccountTokensMigration import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWalletId @@ -17,11 +16,15 @@ import com.tangem.domain.models.wallet.UserWalletId * Use case for adding a new crypto portfolio account * * @property crudRepository the repository used for performing CRUD operations on accounts + * @property singleAccountListFetcher fetches the list of accounts for a single user wallet + * @property mainAccountTokensMigration handles the migration of tokens from the main account to the new * [REDACTED_AUTHOR] */ class AddCryptoPortfolioUseCase( private val crudRepository: AccountsCRUDRepository, + private val singleAccountListFetcher: SingleAccountListFetcher, + private val mainAccountTokensMigration: MainAccountTokensMigration, ) { /** @@ -40,21 +43,29 @@ class AddCryptoPortfolioUseCase( icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, ): Either = either { - val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) + fetchAccountList(userWalletId) - val accountList = getAccountList(userWalletId = userWalletId).getOrElse { - createNewAccountList(userWalletId = userWalletId) - } + val accountList = getAccountList(userWalletId = userWalletId) + + val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) val updatedAccounts = (accountList + newAccount).getOrElse { raise(Error.AccountListRequirementsNotMet(it)) } - saveAccounts(updatedAccounts) + saveAccounts(accountList = updatedAccounts) + + mainAccountTokensMigration.migrate(userWalletId, derivationIndex) newAccount } + private suspend fun Raise.fetchAccountList(userWalletId: UserWalletId) { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId)).onLeft { + raise(Error.DataOperationFailed(cause = it)) + } + } + private fun createAccount( userWalletId: UserWalletId, accountName: AccountName, @@ -70,26 +81,14 @@ class AddCryptoPortfolioUseCase( ) } - private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - } - - private fun Raise.createNewAccountList(userWalletId: UserWalletId): AccountList { - val userWallet = catch( - block = { crudRepository.getUserWallet(userWalletId = userWalletId) }, - catch = { raise(Error.DataOperationFailed(cause = it)) }, - ) - - // TODO: [REDACTED_JIRA] - return AccountList.empty( - userWallet = userWallet, - cryptoCurrencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ) + .getOrElse { + raise(Error.DataOperationFailed(message = "Account list not found for wallet $userWalletId")) + } } private suspend fun Raise.saveAccounts(accountList: AccountList) { @@ -115,7 +114,8 @@ class AddCryptoPortfolioUseCase( /** Error indicating that a data operation failed */ data class DataOperationFailed(val cause: Throwable) : Error { - override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + + constructor(message: String) : this(cause = IllegalStateException(message)) } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 4ed41500f0..0c43198fa0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -157,7 +157,7 @@ class AccountListTest { createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 0), createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 1), ), - expected = AccountList.Error.DuplicateAccountNames("Custom account names must be unique.").left(), + expected = AccountList.Error.DuplicateAccountNames.left(), ), ) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index f677187f17..8d3ab663f7 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -5,8 +5,11 @@ import arrow.core.left import arrow.core.right import arrow.core.toOption import com.google.common.truth.Truth +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.tokens.MainAccountTokensMigration +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account @@ -23,13 +26,20 @@ import org.junit.jupiter.api.TestInstance class AddCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = AddCryptoPortfolioUseCase(crudRepository) + private val singleAccountListFetcher = mockk() + private val mainAccountTokensMigration = mockk() + + private val useCase = AddCryptoPortfolioUseCase( + crudRepository = crudRepository, + singleAccountListFetcher = singleAccountListFetcher, + mainAccountTokensMigration = mainAccountTokensMigration, + ) private val userWallet = mockk() @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) + clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration, userWallet) every { userWallet.walletId } returns userWalletId } @@ -41,7 +51,13 @@ class AddCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { + mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) + } returns Unit.right() // Act val actual = useCase( @@ -55,22 +71,23 @@ class AddCryptoPortfolioUseCaseTest { val expected = newAccount.right() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) + mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) } - - coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } } @Test - fun `invoke should create new account list if none exists`() = runTest { + fun `invoke should return error if fetch is failed`() = runTest { // Arrange val newAccount = createNewAccount() - val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!! + val exception = Exception("Fetch error") - coEvery { crudRepository.getAccountListSync(userWalletId) } returns None - coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns exception.left() // Act val actual = useCase( @@ -81,13 +98,54 @@ class AddCryptoPortfolioUseCaseTest { ) // Assert - val expected = newAccount.right() - Truth.assertThat(actual).isEqualTo(expected) + val expected = exception + Truth.assertThat((actual.leftOrNull() as Error.DataOperationFailed).cause).isInstanceOf(expected::class.java) + Truth.assertThat((actual.leftOrNull() as Error.DataOperationFailed).cause).hasMessageThat() + .isEqualTo(expected.message) - coVerifyOrder { + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } + + coVerify(inverse = true) { + crudRepository.getAccountListSync(any()) + crudRepository.saveAccounts(any()) + mainAccountTokensMigration.migrate(any(), any()) + } + } + + @Test + fun `invoke should return error if account list none exists`() = runTest { + // Arrange + val newAccount = createNewAccount() + + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns None + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.accountName, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = IllegalStateException("Accounts for $userWalletId are not created") + Truth.assertThat((actual.leftOrNull() as Error.DataOperationFailed).cause).isInstanceOf(expected::class.java) + Truth.assertThat((actual.leftOrNull() as Error.DataOperationFailed).cause).hasMessageThat() + .isEqualTo(expected.message) + + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) - crudRepository.getUserWallet(userWalletId) - crudRepository.saveAccounts(newAccountList) + } + + coVerify(inverse = true) { + crudRepository.saveAccounts(any()) + mainAccountTokensMigration.migrate(any(), any()) } } @@ -102,6 +160,9 @@ class AddCryptoPortfolioUseCaseTest { val newAccount = createNewAccount(derivationIndex = 21) + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() // Act @@ -113,17 +174,20 @@ class AddCryptoPortfolioUseCaseTest { ) // Assert - val expected = AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet( + val expected = Error.AccountListRequirementsNotMet( cause = AccountList.Error.ExceedsMaxAccountsCount, ).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) + } coVerify(inverse = true) { - crudRepository.getUserWallet(any()) crudRepository.saveAccounts(any()) + mainAccountTokensMigration.migrate(any(), any()) } } @@ -133,6 +197,9 @@ class AddCryptoPortfolioUseCaseTest { val newAccount = createNewAccount() val exception = IllegalStateException("Test error") + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act @@ -144,14 +211,17 @@ class AddCryptoPortfolioUseCaseTest { ) // Assert - val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + val expected = Error.DataOperationFailed(cause = exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) + } coVerify(inverse = true) { - crudRepository.getUserWallet(any()) crudRepository.saveAccounts(any()) + mainAccountTokensMigration.migrate(any(), any()) } } @@ -164,6 +234,9 @@ class AddCryptoPortfolioUseCaseTest { val exception = IllegalStateException("Test error") + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception @@ -176,15 +249,54 @@ class AddCryptoPortfolioUseCaseTest { ) // Assert - val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + val expected = Error.DataOperationFailed(cause = exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifyOrder { + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.saveAccounts(updatedAccountList) } - coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + coVerify(inverse = true) { + mainAccountTokensMigration.migrate(any(), any()) + } + } + + @Test + fun `invoke should return new account if migrate returns error`() = runTest { + // Arrange + val newAccount = createNewAccount() + val accountList = AccountList.empty(userWallet) + val updatedAccountList = (accountList + newAccount).getOrNull()!! + + val exception = Exception("Migration error") + coEvery { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } returns Unit.right() + coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() + coEvery { + mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) + } returns exception.left() + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.accountName, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) + } } private companion object { diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 1bf1ceac7d..3b8cfadab3 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit +import androidx.annotation.StringRes import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.account.toDomain -import androidx.annotation.StringRes import com.tangem.common.ui.account.toUM import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent @@ -18,6 +18,7 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase @@ -110,14 +111,30 @@ internal class AccountCreateEditModel @Inject constructor( derivationIndex = derivationIndex, ) uiState.value = uiState.value.toggleProgress(showProgress = false) + result - .onLeft { showMessage(it.toString()) } + .onLeft(::handleAddAccountError) .onRight { showMessage(R.string.account_create_success_message) router.pop() } } + private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) { + if (error is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet && + error.cause is AccountList.Error.DuplicateAccountNames + ) { + // TODO("account") show alert that the account name already exists + return + } + + // TODO: show alert https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=38882-113775&t=vk6TCy4MkYol1cPb-4 + logError( + error = AccountFeatureError.CreateAccount.FailedToCreateAccount(cause = error), + + ) + } + private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) { val state = uiState.value val name = state.account.name.toDomain().getOrNull() ?: return @@ -203,33 +220,30 @@ internal class AccountCreateEditModel @Inject constructor( } } .onLeft { cause -> - handleError( - error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, - message = cause.toString(), + val error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex(cause) + + logError( + error = error, params = mapOf( "userWalletId" to userWalletId.stringValue, "cause" to cause.toString(), ), ) + messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) + return@launch } } } - private fun handleError( - error: AccountFeatureError, - message: String? = null, - params: Map = mapOf(), - ) { - val exception = IllegalStateException("$error. Cause: $message") + private fun logError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) Timber.e(exception) analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent(exception = exception, params = params), ) - - messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt index 9ab8549fc7..cb762654a4 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt @@ -1,6 +1,8 @@ package com.tangem.features.account.createedit.error import com.tangem.core.error.UniversalError +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase sealed interface AccountFeatureError : UniversalError { @@ -14,9 +16,13 @@ sealed interface AccountFeatureError : UniversalError { override val subsystemCode: String get() = "001" - data object UnableToGetDerivationIndex : CreateAccount { + data class UnableToGetDerivationIndex(val cause: GetUnoccupiedAccountIndexUseCase.Error) : CreateAccount { override val specificErrorCode: String = "001" } + + data class FailedToCreateAccount(val cause: AddCryptoPortfolioUseCase.Error) : CreateAccount { + override val specificErrorCode: String = "002" + } } sealed interface EditAccount : AccountFeatureError {