Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-14 14:32:52 +04:00
parent c6aa63a44f
commit 1f1f7adef4
5 changed files with 301 additions and 7 deletions

View file

@ -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<ArchivedAccount> = 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<List<ArchivedAccount>> = option {
listOf(
createMockArchivedAccount(userWalletId),
)
}
override fun getArchivedAccounts(userWalletId: UserWalletId): Flow<List<ArchivedAccount>> {
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,
)
}
}

View file

@ -10,10 +10,12 @@ tasks.withType<Test>().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)

View file

@ -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<ArchivedAccount>
/**
* 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<List<ArchivedAccount>>
/**
* 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<List<ArchivedAccount>>
/**
* 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
*

View file

@ -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<ArchivedAccount>
/**
* 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<Throwable, ArchivedAccountList> = 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<Throwable, ArchivedAccountList> {
return Either.catch {
crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse {
error("Archived accounts not found for user wallet: $userWalletId")
}
}
}
private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either<Throwable, Unit> {
return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) }
}
private suspend fun ProducerScope<Lce<Throwable, ArchivedAccountList>>.subscribeOnArchivedAccounts(
userWalletId: UserWalletId,
) {
crudRepository.getArchivedAccounts(userWalletId)
.distinctUntilChanged()
.retryWhen { cause, _ ->
send(cause.lceError())
delay(timeMillis = 2000)
true
}
.collectLatest { archivedAccounts ->
send(archivedAccounts.lceContent())
}
}
}

View file

@ -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<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
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<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
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<ArchivedAccount>(),
mockk<ArchivedAccount>(),
)
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 <T> TestScope.getEmittedValues(flow: Flow<T>): List<T> {
val values = mutableListOf<T>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
flow.toList(values)
}
return values
}
}