Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-11 13:43:39 +04:00
parent 934b6ed6c6
commit 2a044a5582
3 changed files with 247 additions and 8 deletions

View file

@ -31,8 +31,10 @@ internal object AccountDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideArchiveCryptoPortfolioUseCase(): ArchiveCryptoPortfolioUseCase { fun provideArchiveCryptoPortfolioUseCase(
return ArchiveCryptoPortfolioUseCase() accountsCRUDRepository: AccountsCRUDRepository,
): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
} }
@Provides @Provides

View file

@ -1,22 +1,102 @@
package com.tangem.domain.account.usecase package com.tangem.domain.account.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either import arrow.core.raise.either
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
/** /**
* Use case for archiving a crypto portfolio.
* This class provides functionality to archive a specific account within a user's crypto portfolio.
* It ensures that the account exists and meets the necessary requirements before performing the operation.
*
* @property crudRepository repository for performing CRUD operations on accounts
*
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class ArchiveCryptoPortfolioUseCase { class ArchiveCryptoPortfolioUseCase(
private val crudRepository: AccountsCRUDRepository,
) {
/** Archives the specified account by its [accountId] */
suspend operator fun invoke(accountId: AccountId): Either<Error, Unit> = either { suspend operator fun invoke(accountId: AccountId): Either<Error, Unit> = either {
// TODO: [REDACTED_JIRA] val accountList = getAccountList(userWalletId = accountId.userWalletId)
// Remove the account from the list of active accounts
// Add the account to the list of archived accounts val archivingAccount = accountList.accounts
// Save to backend .firstOrNull { it.accountId == accountId }
?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
val updatedAccounts = (accountList - archivingAccount).getOrElse {
raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it))
}
saveAccounts(updatedAccounts)
} }
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>.saveAccounts(accountList: AccountList) {
catch(
block = { crudRepository.saveAccounts(accountList) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
}
/**
* Represents possible errors that can occur during the archiving process
*/
sealed interface Error { sealed interface Error {
data object DataOperationFailed : Error
/** Error indicating that a data operation failed */
data class DataOperationFailed(val cause: Throwable) : Error {
override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}"
}
/**
* Represents critical technical errors that can occur during the update operation.
* These errors are a consequence of an inconsistent state.
*/
sealed interface CriticalTechError : Error {
/**
*
* @property userWalletId the unique identifier of the user wallet
*/
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
override fun toString(): String {
return "${this.javaClass.simpleName}: 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 = "${this.javaClass.simpleName}: 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 {
return "${this.javaClass.simpleName}: Account list requirements not met: $cause"
}
}
}
} }
} }

View file

@ -0,0 +1,157 @@
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.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.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
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ArchiveCryptoPortfolioUseCaseTest {
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository)
private val userWallet = mockk<UserWallet>()
@BeforeEach
fun resetMocks() {
clearMocks(crudRepository, userWallet)
every { userWallet.walletId } returns userWalletId
}
@Test
fun `invoke should archive existing crypto portfolio account`() = runTest {
// Arrange
val account = createAccount(userWalletId)
val accountList = (AccountList.empty(userWallet) + account).getOrNull()!!
val accountId = account.accountId
val archivedAccount = account.copy(isArchived = true)
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
// Assert
val expected = Unit.right()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
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.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.saveAccounts(any()) }
}
@Test
fun `invoke should return error if account not found`() = runTest {
// Arrange
val accountList = AccountList.empty(userWallet)
val accountId = AccountId.forCryptoPortfolio(
userWalletId = userWalletId,
derivationIndex = DerivationIndex(1).getOrNull()!!,
)
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.CriticalTechError.AccountNotFound(accountId).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder { crudRepository.getAccounts(userWalletId) }
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) + account).getOrNull()!!
val accountId = account.accountId
val archivedAccount = account.copy(isArchived = true)
val updatedAccountList = (accountList - archivedAccount).getOrNull()!!
val exception = IllegalStateException("Save failed")
coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption()
coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception
// Act
val actual = useCase(accountId)
// Assert
val expected = Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerifyOrder {
crudRepository.getAccounts(userWalletId)
crudRepository.saveAccounts(updatedAccountList)
}
}
private companion object {
val userWalletId = UserWalletId("011")
}
}