Updated on 2026-08-14
This commit is contained in:
parent
50c633e873
commit
939dcf8405
4 changed files with 367 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
|
|||
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.account.usecase.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -86,4 +87,16 @@ internal object AccountDomainModule {
|
|||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApplyAccountListSortingUseCase(
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ApplyAccountListSortingUseCase {
|
||||
return ApplyAccountListSortingUseCase(
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.*
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* Use case to apply a specific sorting order to a list of accounts within a user's wallet.
|
||||
*
|
||||
* @property accountsCRUDRepository Repository for CRUD operations on accounts.
|
||||
* @property dispatchers Coroutine dispatcher provider for managing threading.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ApplyAccountListSortingUseCase(
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Applies the sorting order of the provided list of accounts to the account list
|
||||
* associated with the user wallet ID of the first account in the list.
|
||||
*
|
||||
* @param accountIds List of AccountId representing the desired order.
|
||||
* @return Either an Error or Unit on successful completion.
|
||||
*/
|
||||
suspend operator fun invoke(accountIds: List<AccountId>): Either<Error, Unit> =
|
||||
eitherOn(dispatcher = dispatchers.default) {
|
||||
val nonEmptyIds = accountIds.toNonEmptyListOrNull()
|
||||
ensureNotNull(nonEmptyIds) { Error.EmptyList }
|
||||
ensure(nonEmptyIds.size > 1) { Error.UnableToSortSingleAccount }
|
||||
|
||||
val userWalletId = nonEmptyIds.first().userWalletId
|
||||
|
||||
ensureNotNull(userWalletId) {
|
||||
Error.DataOperationFailed("UserWalletId is null in the accounts list")
|
||||
}
|
||||
|
||||
val accountList = getAccountList(userWalletId)
|
||||
|
||||
ensure(accountList.activeAccounts > 1) { Error.UnableToSortSingleAccount }
|
||||
|
||||
val positionByAccountId = nonEmptyIds.withIndex().associate { it.value to it.index }
|
||||
val sortedAccounts = accountList.accounts.sortedBy {
|
||||
positionByAccountId[it.accountId] ?: raise(Error.SomeAccountsNotFound)
|
||||
}
|
||||
|
||||
val updatedAccountList = withError(
|
||||
transform = { Error.DataOperationFailed("Unable to create AccountList: $it") },
|
||||
) {
|
||||
AccountList(
|
||||
userWalletId = accountList.userWalletId,
|
||||
accounts = sortedAccounts,
|
||||
totalAccounts = accountList.totalAccounts,
|
||||
sortType = accountList.sortType,
|
||||
groupType = accountList.groupType,
|
||||
)
|
||||
.bind()
|
||||
}
|
||||
|
||||
if (updatedAccountList == accountList) {
|
||||
return@eitherOn
|
||||
}
|
||||
|
||||
accountsCRUDRepository.saveAccounts(accountList = updatedAccountList)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
|
||||
return catch(
|
||||
block = { accountsCRUDRepository.getAccountListSync(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse {
|
||||
raise(Error.DataOperationFailed("Account list not found for wallet $userWalletId"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface representing possible errors that can occur during the application
|
||||
* of account list sorting.
|
||||
*/
|
||||
sealed interface Error {
|
||||
|
||||
data object EmptyList : Error
|
||||
|
||||
data object UnableToSortSingleAccount : Error
|
||||
|
||||
data object SomeAccountsNotFound : Error
|
||||
|
||||
data class DataOperationFailed(val cause: Throwable) : Error {
|
||||
|
||||
constructor(message: String) : this(cause = IllegalStateException(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.domain.account.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.some
|
||||
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.ApplyAccountListSortingUseCase.Error
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ApplyAccountListSortingUseCaseTest {
|
||||
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = ApplyAccountListSortingUseCase(
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(accountsCRUDRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns EmptyList when accountIds is empty`() = runTest {
|
||||
// Act
|
||||
val actual = useCase(accountIds = emptyList())
|
||||
|
||||
// Assert
|
||||
val expected = Error.EmptyList.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
accountsCRUDRepository.getAccountListSync(any())
|
||||
accountsCRUDRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns UnableToSortSingleAccount when accountIds contain one ID`() = runTest {
|
||||
// Act
|
||||
val actual = useCase(accountIds = listOf(mockk(relaxed = true)))
|
||||
|
||||
// Assert
|
||||
val expected = Error.UnableToSortSingleAccount.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerify(inverse = true) {
|
||||
accountsCRUDRepository.getAccountListSync(any())
|
||||
accountsCRUDRepository.saveAccounts(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns DataOperationFailed when getAccountList returns error`() = runTest {
|
||||
// Arrange
|
||||
val accountList = createAccountList()
|
||||
val accountIds = accountList.toAccountIds()
|
||||
|
||||
val exception = Exception("Test error")
|
||||
coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountIds)
|
||||
|
||||
// Assert
|
||||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) { accountsCRUDRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns UnableToSortSingleAccount when saved accountList contain one account`() = runTest {
|
||||
// Arrange
|
||||
val accountIds = createAccountList().toAccountIds()
|
||||
|
||||
coEvery {
|
||||
accountsCRUDRepository.getAccountListSync(userWalletId)
|
||||
} returns AccountList.empty(userWalletId).some()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountIds)
|
||||
|
||||
// Assert
|
||||
val expected = Error.UnableToSortSingleAccount.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) { accountsCRUDRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns SomeAccountsNotFound when saved AccountList doesn't contain unknown ID`() = runTest {
|
||||
// Arrange
|
||||
val unknownAccountId = AccountId.forMainCryptoPortfolio(
|
||||
userWalletId = UserWalletId("012"),
|
||||
)
|
||||
|
||||
val accountList = createAccountList()
|
||||
val accountIds = listOf(accountList.mainAccount.accountId, unknownAccountId)
|
||||
|
||||
coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountIds)
|
||||
|
||||
// Assert
|
||||
val expected = Error.SomeAccountsNotFound.left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) { accountsCRUDRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns Right without accounts saving when nothing to change`() = runTest {
|
||||
// Arrange
|
||||
val accountList = createAccountList()
|
||||
val accountIds = accountList.toAccountIds()
|
||||
|
||||
coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountIds)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) { accountsCRUDRepository.saveAccounts(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns Right`() = runTest {
|
||||
// Arrange
|
||||
val accountList = createAccountList()
|
||||
val accountIds = accountList.toAccountIds().reversed()
|
||||
|
||||
val updatedAccountList = AccountList(
|
||||
userWalletId = accountList.userWalletId,
|
||||
accounts = accountList.accounts.reversed(),
|
||||
totalAccounts = accountList.totalAccounts,
|
||||
sortType = accountList.sortType,
|
||||
groupType = accountList.groupType,
|
||||
).getOrNull()!!
|
||||
|
||||
coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some()
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountIds)
|
||||
|
||||
// Assert
|
||||
val expected = Unit.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
accountsCRUDRepository.getAccountListSync(userWalletId)
|
||||
accountsCRUDRepository.saveAccounts(updatedAccountList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccountList(): AccountList {
|
||||
val accountList = AccountList.empty(userWalletId)
|
||||
|
||||
val account1 = Account.CryptoPortfolio(
|
||||
accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(1).getOrNull()!!),
|
||||
name = "Account 1",
|
||||
icon = CryptoPortfolioIcon.ofDefaultCustomAccount(),
|
||||
derivationIndex = 1,
|
||||
)
|
||||
.getOrNull()!!
|
||||
|
||||
return (accountList + account1).getOrNull()!!
|
||||
}
|
||||
|
||||
private fun AccountList.toAccountIds(): List<AccountId> {
|
||||
return this.accounts.map { it.accountId }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.feature.walletsettings.utils
|
||||
|
||||
import com.tangem.domain.account.usecase.ApplyAccountListSortingUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Saves the sorting order of accounts with a debounce to prevent frequent updates.
|
||||
*
|
||||
* @property applyAccountListSortingUseCase Use case to apply the account list sorting.
|
||||
* @param dispatchers Coroutine dispatcher provider for managing threading.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Singleton
|
||||
@OptIn(FlowPreview::class)
|
||||
internal class AccountListSortingSaver @Inject constructor(
|
||||
private val applyAccountListSortingUseCase: ApplyAccountListSortingUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io)
|
||||
private val applySortingFlow = MutableStateFlow<List<AccountId>?>(value = null)
|
||||
|
||||
init {
|
||||
applySortingFlow
|
||||
.filterNotNull()
|
||||
.debounce { 3.seconds }
|
||||
.onEach { accountIds ->
|
||||
applyAccountListSortingUseCase.invoke(accountIds).onLeft {
|
||||
Timber.e("Error while saving account list sorting: $it")
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided list of account IDs to apply the sorting order.
|
||||
*
|
||||
* @param accountIds List of AccountId representing the desired order.
|
||||
*/
|
||||
fun save(accountIds: List<AccountId>) {
|
||||
applySortingFlow.value = accountIds
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue