From 1f1f7adef44f1904db8249ba25557a646c83ca56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:32:52 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultAccountsCRUDRepository.kt | 39 ++++- domain/account/build.gradle.kts | 2 + .../repository/AccountsCRUDRepository.kt | 23 +++ .../usecase/GetArchivedAccountsUseCase.kt | 86 ++++++++++ .../usecase/GetArchivedAccountsUseCaseTest.kt | 158 ++++++++++++++++++ 5 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a46b5d096a..a9fba30f57 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -13,6 +13,8 @@ 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 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** [REDACTED_AUTHOR] @@ -37,16 +39,23 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun getArchivedAccount(accountId: AccountId): Option = option { - ArchivedAccount( - accountId = accountId, - name = AccountName("Archived Account").getOrNull()!!, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = DerivationIndex(value = 1000).getOrNull()!!, - tokensCount = 2, - networksCount = 1, + createMockArchivedAccount(userWalletId = accountId.userWalletId) + } + + override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { + listOf( + createMockArchivedAccount(userWalletId), ) } + override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { + return flow { + getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() + } + } + + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + override suspend fun saveAccounts(accountList: AccountList) { runtimeStore.update(emptyList()) { it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } @@ -62,4 +71,20 @@ internal class DefaultAccountsCRUDRepository( override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } + + private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { + val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + tokensCount = 2, + networksCount = 1, + ) + } } \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index cf1bc96831..75db105c86 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -10,10 +10,12 @@ tasks.withType().configureEach { dependencies { + api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) implementation(deps.kotlin.serialization) testImplementation(deps.test.coroutine) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a05f267633..ac6921e167 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Repository interface for performing CRUD operations on accounts @@ -38,6 +39,28 @@ interface AccountsCRUDRepository { */ suspend fun getArchivedAccount(accountId: AccountId): Option + /** + * Retrieves a list of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not + */ + suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + + /** + * Provides a flow of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + fun getArchivedAccounts(userWalletId: UserWalletId): Flow> + + /** + * Fetches archived accounts for a specific user wallet and updates the repository + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + /** * Saves a list of accounts to the repository * diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt new file mode 100644 index 0000000000..cbcfb13168 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch + +typealias ArchivedAccountList = List + +/** + * Use case for retrieving archived accounts for a specific user wallet + * + * @property crudRepository the repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetArchivedAccountsUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Executes the use case to retrieve archived accounts for the given user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + operator fun invoke(userWalletId: UserWalletId): LceFlow = channelFlow { + val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId) + + archivedAccounts + .onRight { send(it.lceContent()) } + .onLeft { + send(lceLoading()) + + launch { + fetchArchivedAccounts(userWalletId).getOrElse { + send(it.lceError()) + } + } + } + + subscribeOnArchivedAccounts(userWalletId) + } + .distinctUntilChanged() + + private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { + crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + error("Archived accounts not found for user wallet: $userWalletId") + } + } + } + + private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) } + } + + private suspend fun ProducerScope>.subscribeOnArchivedAccounts( + userWalletId: UserWalletId, + ) { + crudRepository.getArchivedAccounts(userWalletId) + .distinctUntilChanged() + .retryWhen { cause, _ -> + send(cause.lceError()) + + delay(timeMillis = 2000) + + true + } + .collectLatest { archivedAccounts -> + send(archivedAccounts.lceContent()) + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt new file mode 100644 index 0000000000..eb0019f93c --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetArchivedAccountsUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetArchivedAccountsUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should emit archived accounts when repository returns data`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf(archivedAccounts.lceContent()) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + + coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) } + } + + @Test + fun `invoke should emit loading and fetch when accounts not found`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Fetch error") + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() + coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + exception.lceError(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun TestScope.getEmittedValues(flow: Flow): List { + val values = mutableListOf() + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) + } + + return values + } +} \ No newline at end of file