Updated on 2026-08-14
This commit is contained in:
parent
934b6ed6c6
commit
2a044a5582
3 changed files with 247 additions and 8 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue