From 2a044a55823b996aec6856d295fa5b9f26f7d5e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 13:43:39 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 6 +- .../usecase/ArchiveCryptoPortfolioUseCase.kt | 92 +++++++++- .../ArchiveCryptoPortfolioUseCaseTest.kt | 157 ++++++++++++++++++ 3 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index c4f9660136..41d100f879 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -31,8 +31,10 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideArchiveCryptoPortfolioUseCase(): ArchiveCryptoPortfolioUseCase { - return ArchiveCryptoPortfolioUseCase() + fun provideArchiveCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): ArchiveCryptoPortfolioUseCase { + return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt index 8e473b507d..0611b106fb 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -1,22 +1,102 @@ 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.repository.AccountsCRUDRepository 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] */ -class ArchiveCryptoPortfolioUseCase { +class ArchiveCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** Archives the specified account by its [accountId] */ suspend operator fun invoke(accountId: AccountId): Either = either { - // TODO: [REDACTED_JIRA] - // Remove the account from the list of active accounts - // Add the account to the list of archived accounts - // Save to backend + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val archivingAccount = accountList.accounts + .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.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.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 { - 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" + } + } + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..aaea0a5379 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -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() + + @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") + } +} \ No newline at end of file