diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt index 54da8c1d75..6e3d4faf1d 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.raise.ensure import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId @@ -28,6 +29,10 @@ class GetUnoccupiedAccountIndexUseCase( suspend operator fun invoke(userWalletId: UserWalletId): Either = either { val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId) + ensure(totalAccountsCount != 0) { + Error.DataOperationFailed("DerivationIndex cannot be zero because it is reserved for the main account") + } + DerivationIndex(value = totalAccountsCount).getOrElse { raise(Error.InvalidDerivationIndex(it)) } @@ -60,7 +65,8 @@ class GetUnoccupiedAccountIndexUseCase( /** Error indicating that a data operation failed */ data class DataOperationFailed(val cause: Throwable) : Error { - override fun toString(): String = "$tag: 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/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index 2145e7ed15..9b0487ea55 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -38,20 +38,20 @@ class UpdateCryptoPortfolioUseCase( accountName: AccountName? = null, icon: CryptoPortfolioIcon? = null, ): Either = either { - ensure(accountName != null || icon != null) { Error.NothingToUpdate } + validate(accountName, icon) val accountList = getAccountList(userWalletId = accountId.userWalletId) val account = accountList.accounts .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio - ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + ?: raise(Error.DataOperationFailed(message = "Account not found: $accountId")) val updatedAccount = account .setName(name = accountName) .setIcon(icon = icon) val updatedAccounts = (accountList + updatedAccount).getOrElse { - raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) + raise(Error.AccountListRequirementsNotMet(it)) } saveAccounts(updatedAccounts) @@ -59,12 +59,18 @@ class UpdateCryptoPortfolioUseCase( updatedAccount } + private fun Raise.validate(accountName: AccountName?, icon: CryptoPortfolioIcon?) { + ensure(accountName != null || icon != null) { Error.NothingToUpdate } + } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + .getOrElse { + raise(Error.DataOperationFailed(message = "Account list not found for wallet $userWalletId")) + } } private suspend fun Raise.saveAccounts(accountList: AccountList) { @@ -92,39 +98,14 @@ class UpdateCryptoPortfolioUseCase( override fun toString(): String = "Nothing to update: both account name and icon are null" } - /** 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"}" + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "Account list requirements not met: $cause" } - /** - * Represents critical technical errors that can occur during the update operation. - * These errors are a consequence of an inconsistent state. - */ - sealed interface CriticalTechError : Error { + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { - /** - - * - * @property userWalletId the unique identifier of the user wallet - */ - data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { - override fun toString(): String = "Accounts for $userWalletId are not created" - } - - /** Error indicating that the account with [accountId] was not found */ - data class AccountNotFound(val accountId: AccountId) : CriticalTechError { - override fun toString(): String = "Account with ID $accountId not found" - } - - /** - * Error indicating that the account list requirements were not met. - * - * @property cause the underlying cause of the error - */ - data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { - override fun toString(): String = "Account list requirements not met: $cause" - } + 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/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index 8d3ab663f7..4808e416a1 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 @@ -130,13 +130,12 @@ class AddCryptoPortfolioUseCaseTest { accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, - ) + ).leftOrNull() as Error.DataOperationFailed // 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) + val expected = IllegalStateException("Account list not found for wallet $userWalletId") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt index 5f85fe95ef..c469708e18 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -4,6 +4,7 @@ import arrow.core.left import arrow.core.toOption import com.google.common.truth.Truth import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase.Error import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import io.mockk.clearMocks @@ -27,6 +28,23 @@ class GetUnoccupiedAccountIndexUseCaseTest { clearMocks(crudRepository) } + @Test + fun `invoke should return error if repository returns 0`() = runTest { + // Arrange + coEvery { crudRepository.getTotalAccountsCountSync(userWalletId) } returns 0.toOption() + + // Act + val actual = useCase(userWalletId = userWalletId).leftOrNull() as Error.DataOperationFailed + + // Assert + val expected = + IllegalStateException("DerivationIndex cannot be zero because it is reserved for the main account") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) + + coVerify { crudRepository.getTotalAccountsCountSync(userWalletId) } + } + @Test fun `invoke should return next unoccupied index when repository returns count`() = runTest { // Arrange @@ -36,7 +54,7 @@ class GetUnoccupiedAccountIndexUseCaseTest { val actual = useCase(userWalletId = userWalletId) // Assert - val expected = DerivationIndex(4) + val expected = DerivationIndex(3) Truth.assertThat(actual).isEqualTo(expected) coVerify { crudRepository.getTotalAccountsCountSync(userWalletId) } @@ -52,7 +70,7 @@ class GetUnoccupiedAccountIndexUseCaseTest { val actual = useCase(userWalletId = userWalletId) // Assert - val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left() + val expected = Error.DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) coVerify { crudRepository.getTotalAccountsCountSync(userWalletId) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index 5d6625fbe0..3d904d3117 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -178,11 +178,12 @@ class UpdateCryptoPortfolioUseCaseTest { coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList // Act - val actual = useCase(accountId = accountId, accountName = newAccountName) + val actual = useCase(accountId, newAccountName).leftOrNull() as Error.DataOperationFailed // Assert - val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left() - Truth.assertThat(actual).isEqualTo(expected) + val expected = IllegalStateException("Account list not found for wallet $userWalletId") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } @@ -202,11 +203,12 @@ class UpdateCryptoPortfolioUseCaseTest { coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() // Act - val actual = useCase(accountId = accountId, accountName = newAccountName) + val actual = useCase(accountId, newAccountName).leftOrNull() as Error.DataOperationFailed // Assert - val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left() - Truth.assertThat(actual).isEqualTo(expected) + val expected = IllegalStateException("Account not found: $accountId") + Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) + Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) coVerifyOrder { crudRepository.getAccountListSync(userWalletId = userWalletId) } coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } 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 3b8cfadab3..8a050693bf 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 @@ -13,7 +13,6 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage @@ -131,7 +130,6 @@ internal class AccountCreateEditModel @Inject constructor( // 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), - ) } @@ -141,6 +139,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = state.account.portfolioIcon.toDomain() val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon + uiState.value = uiState.value.toggleProgress(showProgress = true) val result = updateCryptoPortfolioUseCase( icon = if (isNewIcon) icon else null, @@ -148,23 +147,34 @@ internal class AccountCreateEditModel @Inject constructor( accountId = params.account.accountId, ) uiState.value = uiState.value.toggleProgress(showProgress = false) + result - .onLeft { showMessage(it.toString()) } + .onLeft(::handleEditAccountError) .onRight { showMessage(R.string.account_edit_success_message) router.pop() } } + private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) { + if (error is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet && + error.cause is AccountList.Error.DuplicateAccountNames + ) { + // TODO("account") show alert that the account name already exists + return + } + + // TODO: show alert like in create account flow + logError( + error = AccountFeatureError.EditAccount.FailedToEditAccount(cause = error), + ) + } + private fun showMessage(@StringRes id: Int) { val message = resourceReference(id) messageSender.send(ToastMessage(message = message)) } - private fun showMessage(text: String) { - messageSender.send(ToastMessage(message = stringReference(text))) - } - private fun onCloseClick() = unsaveChangeDialog() private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { 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 cb762654a4..dc12ce1e49 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 @@ -3,6 +3,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 +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase sealed interface AccountFeatureError : UniversalError { @@ -29,7 +31,16 @@ sealed interface AccountFeatureError : UniversalError { override val subsystemCode: String get() = "002" - data object RequiredCryptoPortfolio : EditAccount { + data class FailedToEditAccount(val cause: UpdateCryptoPortfolioUseCase.Error) : EditAccount { + override val specificErrorCode: String = "001" + } + } + + sealed interface ArchivedAccountList : AccountFeatureError { + + override val subsystemCode: String get() = "003" + + data class FailedToRecoverAccount(val cause: RecoverCryptoPortfolioUseCase.Error) : ArchivedAccountList { override val specificErrorCode: String = "001" } }