Updated on 2026-08-14
This commit is contained in:
parent
fb9bfa5258
commit
5c576d559f
7 changed files with 83 additions and 56 deletions
|
|
@ -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<Error, DerivationIndex> = 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,20 +38,20 @@ class UpdateCryptoPortfolioUseCase(
|
|||
accountName: AccountName? = null,
|
||||
icon: CryptoPortfolioIcon? = null,
|
||||
): Either<Error, Account.CryptoPortfolio> = 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<Error>.validate(accountName: AccountName?, icon: CryptoPortfolioIcon?) {
|
||||
ensure(accountName != null || icon != null) { Error.NothingToUpdate }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.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<Error>.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue