Updated on 2026-08-14
This commit is contained in:
parent
a1bce4b52c
commit
08521b6c11
12 changed files with 230 additions and 90 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Error, Account.CryptoPortfolio> = 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<Error>.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<Error>.getAccountList(userWalletId: UserWalletId): Option<AccountList> {
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun Raise<Error>.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<Error>.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SingleAccountListFetcher>()
|
||||
private val mainAccountTokensMigration = mockk<MainAccountTokensMigration>()
|
||||
|
||||
private val useCase = AddCryptoPortfolioUseCase(
|
||||
crudRepository = crudRepository,
|
||||
singleAccountListFetcher = singleAccountListFetcher,
|
||||
mainAccountTokensMigration = mainAccountTokensMigration,
|
||||
)
|
||||
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue