Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-11 16:16:44 +04:00
parent 61f2412cb5
commit e3ec61bc4b
11 changed files with 509 additions and 8 deletions

View file

@ -73,7 +73,14 @@ interface AccountsCRUDRepository {
*
* @param userWalletId the unique identifier of the user wallet
*/
suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Option<Int>
suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option<Int>
/**
* Provides a flow of the total count of accounts associated with a specific user wallet including archived accounts
*
* @param userWalletId the unique identifier of the user wallet
*/
fun getTotalAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>>
/**
* Retrieves a user wallet by its unique identifier
@ -82,4 +89,10 @@ interface AccountsCRUDRepository {
* @return the [UserWallet] associated with the given identifier
*/
fun getUserWallet(userWalletId: UserWalletId): UserWallet
/** Provides a flow of all user wallets */
fun getUserWallets(): Flow<List<UserWallet>>
/** Synchronously retrieves all user wallets */
fun getUserWalletsSync(): List<UserWallet>
}

View file

@ -35,7 +35,7 @@ class GetUnoccupiedAccountIndexUseCase(
private suspend fun Raise<Error>.getTotalAccountsCount(userWalletId: UserWalletId): Int {
return catch(
block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) },
block = { crudRepository.getTotalAccountsCountSync(userWalletId = userWalletId) },
catch = { raise(Error.DataOperationFailed(cause = it)) },
)
.getOrElse { raise(Error.DataNotFound) }

View file

@ -0,0 +1,66 @@
package com.tangem.domain.account.usecase
import arrow.core.Option
import arrow.core.getOrElse
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
/**
* Use case to determine if the accounts mode is enabled.
* Accounts mode is considered enabled if there are at least two accounts in any of the user wallets that support
* multiple currencies.
*
* @property crudRepository repository to interact with user wallets and their accounts
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val crudRepository: AccountsCRUDRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<Boolean> {
if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(value = false)
return crudRepository.getUserWallets()
.flatMapLatest { userWallets ->
val totalAccountsCountList = getTotalAccountsCountList(userWallets)
combine(flows = totalAccountsCountList) { it.toList().isModeEnabled() }
}
.onEmpty { emit(false) }
}
suspend fun invokeSync(): Boolean {
if (!accountsFeatureToggles.isFeatureEnabled) return false
return crudRepository.getUserWalletsSync()
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map 0
crudRepository.getTotalAccountsCountSync(userWalletId = userWallet.walletId).getOrZero()
}
.isModeEnabled()
}
private fun getTotalAccountsCountList(userWallets: List<UserWallet>): List<Flow<Int>> {
return userWallets
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map flowOf(0)
crudRepository.getTotalAccountsCount(userWalletId = userWallet.walletId)
.map { maybeCount -> maybeCount.getOrZero() }
}
}
private fun Option<Int>.getOrZero(): Int = getOrElse { 0 }
private fun List<Int>.isModeEnabled(): Boolean = any { it >= 2 }
}

View file

@ -30,7 +30,7 @@ class GetUnoccupiedAccountIndexUseCaseTest {
@Test
fun `invoke should return next unoccupied index when repository returns count`() = runTest {
// Arrange
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3.toOption()
coEvery { crudRepository.getTotalAccountsCountSync(userWalletId) } returns 3.toOption()
// Act
val actual = useCase(userWalletId = userWalletId)
@ -39,14 +39,14 @@ class GetUnoccupiedAccountIndexUseCaseTest {
val expected = DerivationIndex(4)
Truth.assertThat(actual).isEqualTo(expected)
coVerify { crudRepository.getTotalAccountsCount(userWalletId) }
coVerify { crudRepository.getTotalAccountsCountSync(userWalletId) }
}
@Test
fun `invoke should return error if repository throws exception`() = runTest {
// Arrange
val exception = IllegalStateException("Test error")
coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception
coEvery { crudRepository.getTotalAccountsCountSync(userWalletId) } throws exception
// Act
val actual = useCase(userWalletId = userWalletId)
@ -55,6 +55,6 @@ class GetUnoccupiedAccountIndexUseCaseTest {
val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left()
Truth.assertThat(actual).isEqualTo(expected)
coVerify { crudRepository.getTotalAccountsCount(userWalletId) }
coVerify { crudRepository.getTotalAccountsCountSync(userWalletId) }
}
}

View file

@ -0,0 +1,301 @@
package com.tangem.domain.account.usecase
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import io.mockk.*
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IsAccountsModeEnabledUseCaseTest {
private val accountsCRUDRepository: AccountsCRUDRepository = mockk()
private val featureToggles: AccountsFeatureToggles = mockk()
private val useCase = IsAccountsModeEnabledUseCase(accountsCRUDRepository, featureToggles)
@AfterEach
fun tearDown() {
clearMocks(accountsCRUDRepository, featureToggles)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Invoke {
@Test
fun `returns false when feature is disabled`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns false
// Act
val actual = useCase.invoke().firstOrNull()
// Assert
Truth.assertThat(actual).isFalse()
verify(exactly = 1) { featureToggles.isFeatureEnabled }
verify(inverse = true) { accountsCRUDRepository.getUserWallets() }
}
@Test
fun `returns false when getUserWallets emits empty flow`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWallets() } returns emptyFlow()
// Act
val actual = useCase.invoke().firstOrNull()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWallets()
}
verify(inverse = true) { accountsCRUDRepository.getTotalAccountsCount(any()) }
}
@Test
fun `returns false when getUserWallets emits one wallet with isMultiCurrency false`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWallets()
}
verify(inverse = true) { accountsCRUDRepository.getTotalAccountsCount(any()) }
}
@Test
fun `returns true when getUserWallets emits one wallet with isMultiCurrency true`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet))
every { accountsCRUDRepository.getTotalAccountsCount(wallet.walletId) } returns flowOf(2.some())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWallets()
accountsCRUDRepository.getTotalAccountsCount(wallet.walletId)
}
}
@Test
fun `returns false when getUserWallets emits one wallet with isMultiCurrency true and None counts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet))
every { accountsCRUDRepository.getTotalAccountsCount(wallet.walletId) } returns flowOf(none())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWallets()
accountsCRUDRepository.getTotalAccountsCount(wallet.walletId)
}
}
@Test
fun `returns true when getUserWallets emits two wallets, one isMultiCurrency false, one true`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet1, wallet2))
every { accountsCRUDRepository.getTotalAccountsCount(wallet2.walletId) } returns flowOf(2.some())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWallets()
accountsCRUDRepository.getTotalAccountsCount(wallet2.walletId)
}
verify(inverse = true) { accountsCRUDRepository.getTotalAccountsCount(wallet1.walletId) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class InvokeSync {
@Test
fun `returns false when feature is disabled`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns false
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verify(exactly = 1) { featureToggles.isFeatureEnabled }
verify(inverse = true) { accountsCRUDRepository.getUserWalletsSync() }
}
@Test
fun `returns false when getUserWalletsSync returns empty list`() = runTest {
// Arrange
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWalletsSync() } returns emptyList()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWalletsSync()
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalAccountsCountSync(any()) }
}
@Test
fun `returns false when getUserWalletsSync returns one wallet with isMultiCurrency false`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWalletsSync()
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalAccountsCountSync(any()) }
}
@Test
fun `returns true when getUserWalletsSync returns one wallet with isMultiCurrency true`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalAccountsCountSync(wallet.walletId) } returns 2.some()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWalletsSync()
accountsCRUDRepository.getTotalAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns false when getUserWalletsSync returns multi wallet with None counts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalAccountsCountSync(wallet.walletId) } returns none()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWalletsSync()
accountsCRUDRepository.getTotalAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns true when getUserWalletsSync returns multi and single wallets`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { featureToggles.isFeatureEnabled } returns true
every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet1, wallet2)
coEvery { accountsCRUDRepository.getTotalAccountsCountSync(wallet2.walletId) } returns 2.some()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
featureToggles.isFeatureEnabled
accountsCRUDRepository.getUserWalletsSync()
accountsCRUDRepository.getTotalAccountsCountSync(wallet2.walletId)
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalAccountsCountSync(wallet1.walletId) }
}
}
private fun createUserWallet(isMultiCurrency: Boolean): UserWallet = mockk {
every { this@mockk.walletId } returns UserWalletId(stringValue = "011")
every { this@mockk.isMultiCurrency } returns isMultiCurrency
}
}