Updated on 2026-08-14
This commit is contained in:
parent
d83dda4f3a
commit
947c5e44e6
6 changed files with 374 additions and 11 deletions
|
|
@ -39,7 +39,9 @@ internal object AccountDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRecoverCryptoPortfolioUseCase(): RecoverCryptoPortfolioUseCase {
|
||||
return RecoverCryptoPortfolioUseCase()
|
||||
fun provideRecoverCryptoPortfolioUseCase(
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
): RecoverCryptoPortfolioUseCase {
|
||||
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ package com.tangem.data.account.repository
|
|||
import arrow.core.Option
|
||||
import arrow.core.Option.Companion.catch
|
||||
import arrow.core.none
|
||||
import arrow.core.raise.option
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.*
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
|
|
@ -35,6 +36,17 @@ internal class DefaultAccountsCRUDRepository(
|
|||
?: return none()
|
||||
}
|
||||
|
||||
override suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount> = option {
|
||||
ArchivedAccount(
|
||||
accountId = accountId,
|
||||
name = AccountName("Archived Account").getOrNull()!!,
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = DerivationIndex(value = 1000).getOrNull()!!,
|
||||
tokensCount = 2,
|
||||
networksCount = 1,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveAccounts(accountList: AccountList) {
|
||||
runtimeStore.update(emptyList()) {
|
||||
it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.account.models
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an archived crypto portfolio account
|
||||
*
|
||||
* @property accountId the unique identifier of the archived account
|
||||
* @property name the name of the archived account
|
||||
* @property icon the icon representing the archived account
|
||||
* @property derivationIndex the derivation index for the archived account
|
||||
* @property tokensCount the number of tokens in the archived account
|
||||
* @property networksCount the number of networks associated with the archived account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Serializable
|
||||
data class ArchivedAccount(
|
||||
val accountId: AccountId,
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: DerivationIndex,
|
||||
val tokensCount: Int,
|
||||
val networksCount: Int,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.account.repository
|
|||
|
||||
import arrow.core.Option
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -30,6 +31,13 @@ interface AccountsCRUDRepository {
|
|||
*/
|
||||
suspend fun getAccount(accountId: AccountId): Option<Account.CryptoPortfolio>
|
||||
|
||||
/**
|
||||
* Retrieves a archived account by its unique identifier
|
||||
*
|
||||
* @param accountId the unique identifier of the account
|
||||
*/
|
||||
suspend fun getArchivedAccount(accountId: AccountId): Option<ArchivedAccount>
|
||||
|
||||
/**
|
||||
* Saves a list of accounts to the repository
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,25 +1,129 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
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.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Use case for recovering a crypto portfolio account from archived accounts
|
||||
*
|
||||
* @property crudRepository repository for performing CRUD operations on accounts
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RecoverCryptoPortfolioUseCase {
|
||||
class RecoverCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Recovers a crypto portfolio account by moving it from archived accounts to active accounts
|
||||
*
|
||||
* @param accountId the unique identifier of the account to recover
|
||||
*/
|
||||
suspend operator fun invoke(accountId: AccountId): Either<Error, Account.CryptoPortfolio> = either {
|
||||
raise(Error.DataOperationFailed)
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
val archivedAccount = getArchivedAccount(accountId = accountId)
|
||||
|
||||
// TODO: [REDACTED_JIRA]
|
||||
// Remove the account from the list of archived accounts
|
||||
// Add the account to the list of active accounts
|
||||
// Save to backend
|
||||
val recoveredAccount = archivedAccount.recover()
|
||||
|
||||
val updatedAccountList = (accountList + recoveredAccount)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) }
|
||||
|
||||
saveAccounts(updatedAccountList)
|
||||
|
||||
recoveredAccount
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { crudRepository.getAccounts(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getArchivedAccount(accountId: AccountId): ArchivedAccount {
|
||||
return catch(
|
||||
block = { crudRepository.getArchivedAccount(accountId = accountId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse {
|
||||
raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchivedAccount.recover(): Account.CryptoPortfolio {
|
||||
return Account.CryptoPortfolio(
|
||||
accountId = this.accountId,
|
||||
accountName = this.name,
|
||||
accountIcon = this.icon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
isArchived = false,
|
||||
cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList(
|
||||
currencies = emptySet(),
|
||||
sortType = TokensSortType.NONE,
|
||||
groupType = TokensGroupType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveAccounts(accountList: AccountList) {
|
||||
catch(
|
||||
block = { crudRepository.saveAccounts(accountList) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible errors that can occur during the add operation
|
||||
*/
|
||||
sealed interface Error {
|
||||
data object DataOperationFailed : Error
|
||||
|
||||
val tag: String
|
||||
get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error"
|
||||
|
||||
/**
|
||||
* Critical technical errors that can occur during the recovery operation
|
||||
*/
|
||||
sealed interface CriticalTechError : Error {
|
||||
|
||||
/**
|
||||
|
||||
*
|
||||
* @property userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
|
||||
override fun toString(): String = "$tag: 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 = "$tag: 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) : Error {
|
||||
override fun toString(): String = "$tag: Account list requirements not met: $cause"
|
||||
}
|
||||
}
|
||||
|
||||
/** 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"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.None
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class RecoverCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(crudRepository)
|
||||
private val userWallet = mockk<UserWallet>()
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(crudRepository, userWallet)
|
||||
every { userWallet.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should recover archived crypto portfolio account`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.name,
|
||||
icon = account.icon,
|
||||
derivationIndex = account.derivationIndex,
|
||||
tokensCount = 1,
|
||||
networksCount = 1,
|
||||
)
|
||||
|
||||
val recoveredAccount = account.copy(isArchived = false)
|
||||
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = recoveredAccount.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts returns None`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccount(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
)
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccount(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getArchivedAccount throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val exception = IllegalStateException("Test error")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
}
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if getArchivedAccount returns null`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
}
|
||||
coVerify(inverse = true) { crudRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return error if saveAccounts throws exception`() = runTest {
|
||||
// Arrange
|
||||
val account = createAccount(userWalletId)
|
||||
val accountList = AccountList.empty(userWallet)
|
||||
val archivedAccount = ArchivedAccount(
|
||||
accountId = account.accountId,
|
||||
name = account.name,
|
||||
icon = account.icon,
|
||||
derivationIndex = account.derivationIndex,
|
||||
tokensCount = 1,
|
||||
networksCount = 1,
|
||||
)
|
||||
|
||||
val recoveredAccount = account.copy(isArchived = false)
|
||||
val updatedAccountList = (accountList + recoveredAccount).getOrNull()!!
|
||||
val exception = IllegalStateException("Save failed")
|
||||
|
||||
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption()
|
||||
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
crudRepository.getAccounts(userWalletId)
|
||||
crudRepository.getArchivedAccount(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue