From 704e5812a639cf49e4aaa1070f8fab4c8937847c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Sep 2025 13:33:40 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../account/di/AccountListFetcherModule.kt | 24 +++ .../fetcher/DefaultMultiAccountListFetcher.kt | 66 ++++++++ .../DefaultSingleAccountListFetcher.kt | 21 +++ .../DefaultMultiAccountListFetcherTest.kt | 153 ++++++++++++++++++ .../DefaultSingleAccountListFetcherTest.kt | 62 +++++++ .../fetcher/MultiAccountListFetcher.kt | 20 +++ .../fetcher/SingleAccountListFetcher.kt | 14 ++ 7 files changed, 360 insertions(+) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/di/AccountListFetcherModule.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcherTest.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/fetcher/MultiAccountListFetcher.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/fetcher/SingleAccountListFetcher.kt diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountListFetcherModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountListFetcherModule.kt new file mode 100644 index 0000000000..39484b01f6 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountListFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.account.di + +import com.tangem.data.account.fetcher.DefaultMultiAccountListFetcher +import com.tangem.data.account.fetcher.DefaultSingleAccountListFetcher +import com.tangem.domain.account.fetcher.MultiAccountListFetcher +import com.tangem.domain.account.fetcher.SingleAccountListFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountListFetcherModule { + + @Binds + @Singleton + fun bindSingleAccountListFetcher(impl: DefaultSingleAccountListFetcher): SingleAccountListFetcher + + @Binds + @Singleton + fun bindMultiAccountListFetcher(impl: DefaultMultiAccountListFetcher): MultiAccountListFetcher +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt new file mode 100644 index 0000000000..ada7843d5c --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.fetcher + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.fetcher.MultiAccountListFetcher +import com.tangem.domain.account.fetcher.SingleAccountListFetcher +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap + +/** + * Implementation of [MultiAccountListFetcher] + * + * @property singleAccountListFetcher instance of [SingleAccountListFetcher] to fetch accounts for a single wallet + * @property userWalletsStore instance of [UserWalletsStore] to get all user wallets + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiAccountListFetcher( + private val singleAccountListFetcher: SingleAccountListFetcher, + private val userWalletsStore: UserWalletsStore, +) : MultiAccountListFetcher { + + override suspend fun invoke(params: MultiAccountListFetcher.Params): Either = either { + when (params) { + is MultiAccountListFetcher.Params.Set -> { + if (params.ids.isEmpty()) { + Timber.d("No wallet ids provided to fetch accounts.") + return@either + } + + val errors = ConcurrentHashMap() + + coroutineScope { + params.ids.forEach { userWalletId -> + launch { + singleAccountListFetcher( + params = SingleAccountListFetcher.Params(userWalletId), + ) + .onLeft { error -> errors[userWalletId] = error } + } + } + } + + if (errors.isNotEmpty()) { + val exception = IllegalStateException( + "Failed to fetch accounts for wallets:\n${errors.entries.joinToString(separator = "\n")}", + ) + + Timber.e(exception) + + raise(exception) + } + } + MultiAccountListFetcher.Params.All -> { + val userWalletsIds = userWalletsStore.userWalletsSync.map(UserWallet::walletId).toSet() + + invoke(params = MultiAccountListFetcher.Params.Set(ids = userWalletsIds)).bind() + } + } + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt new file mode 100644 index 0000000000..b10047e2ec --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcher.kt @@ -0,0 +1,21 @@ +package com.tangem.data.account.fetcher + +import arrow.core.Either +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.domain.account.fetcher.SingleAccountListFetcher + +/** + * Implementation of [SingleAccountListFetcher] + * + * @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a single wallet + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleAccountListFetcher( + private val walletAccountsFetcher: WalletAccountsFetcher, +) : SingleAccountListFetcher { + + override suspend fun invoke(params: SingleAccountListFetcher.Params): Either = Either.catch { + walletAccountsFetcher.fetch(userWalletId = params.userWalletId) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt new file mode 100644 index 0000000000..0ded63586a --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt @@ -0,0 +1,153 @@ +package com.tangem.data.account.fetcher + +import arrow.core.left +import arrow.core.right +import com.tangem.common.test.utils.assertEitherLeft +import com.tangem.common.test.utils.assertEitherRight +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.fetcher.MultiAccountListFetcher +import com.tangem.domain.account.fetcher.SingleAccountListFetcher +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.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultMultiAccountListFetcherTest { + + private val singleAccountListFetcher: SingleAccountListFetcher = mockk() + private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + + private val fetcher = DefaultMultiAccountListFetcher(singleAccountListFetcher, userWalletsStore) + + private val userWalletId1 = UserWalletId("011") + private val userWalletId2 = UserWalletId("012") + + private val userWalletIds = setOf(userWalletId1, userWalletId2) + private val userWallets = userWalletIds.map { + mockk { + every { this@mockk.walletId } returns it + } + } + + @AfterEach + fun tearDown() { + clearMocks(singleAccountListFetcher, userWalletsStore) + } + + @Test + fun `invoke returns Right when fetch succeeds for Set`() = runTest { + // Arrange + val params = MultiAccountListFetcher.Params.Set(ids = userWalletIds) + + userWalletIds.onEach { + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(it)) + } returns Unit.right() + } + + // Act + val actual = fetcher.invoke(params) + + // Assert + assertEitherRight(actual) + + coVerify(ordering = Ordering.SEQUENCE) { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) + } + } + + @Test + fun `invoke returns Left when fetch throws exception for Set`() = runTest { + // Arrange + val params = MultiAccountListFetcher.Params.Set(ids = userWalletIds) + + val exception = Exception("Fetch failed") + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + } returns exception.left() + + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) + } returns Unit.right() + + // Act + val actual = fetcher.invoke(params) + + // Assert + val expected = IllegalStateException( + """ + Failed to fetch accounts for wallets: + UserWalletId(011...011)=java.lang.Exception: Fetch failed + """.trimIndent(), + ) + assertEitherLeft(actual, expected) + + coVerify(ordering = Ordering.SEQUENCE) { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) + } + } + + @Test + fun `invoke returns Right when fetch succeeds for All`() = runTest { + // Arrange + val params = MultiAccountListFetcher.Params.All + + every { userWalletsStore.userWalletsSync } returns listOf(userWallets.first()) + + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + } returns Unit.right() + + // Act + val actual = fetcher.invoke(params) + + // Assert + assertEitherRight(actual) + + coVerify(ordering = Ordering.SEQUENCE) { + userWalletsStore.userWalletsSync + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + } + } + + @Test + fun `invoke returns Left when fetch throws exception for All`() = runTest { + // Arrange + val params = MultiAccountListFetcher.Params.All + + every { userWalletsStore.userWalletsSync } returns userWallets + + val exception = Exception("Fetch failed") + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + } returns exception.left() + + coEvery { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) + } returns Unit.right() + + // Act + val actual = fetcher.invoke(params) + + // Assert + val expected = IllegalStateException( + """ + Failed to fetch accounts for wallets: + UserWalletId(011...011)=java.lang.Exception: Fetch failed + """.trimIndent(), + ) + assertEitherLeft(actual, expected) + + coVerify(ordering = Ordering.SEQUENCE) { + userWalletsStore.userWalletsSync + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) + } + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcherTest.kt new file mode 100644 index 0000000000..a7d7628c9f --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultSingleAccountListFetcherTest.kt @@ -0,0 +1,62 @@ +package com.tangem.data.account.fetcher + +import com.tangem.common.test.utils.assertEitherLeft +import com.tangem.common.test.utils.assertEitherRight +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.domain.account.fetcher.SingleAccountListFetcher +import com.tangem.domain.models.wallet.UserWalletId +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 + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultSingleAccountListFetcherTest { + + private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true) + private val fetcher = DefaultSingleAccountListFetcher(walletAccountsFetcher) + + private val userWalletId = UserWalletId("011") + + @AfterEach + fun tearDown() { + clearMocks(walletAccountsFetcher) + } + + @Test + fun `invoke returns Right when fetch succeeds`() = runTest { + // Arrange + val params = SingleAccountListFetcher.Params(userWalletId = userWalletId) + + // Act + val actual = fetcher.invoke(params) + + // Assert + assertEitherRight(actual) + + coVerify(ordering = Ordering.SEQUENCE) { + walletAccountsFetcher.fetch(userWalletId) + } + } + + @Test + fun `invoke returns Left when fetch throws exception`() = runTest { + // Arrange + val params = SingleAccountListFetcher.Params(userWalletId = userWalletId) + + val exception = Exception("Fetch failed") + coEvery { walletAccountsFetcher.fetch(userWalletId) } throws exception + + // Act + val actual = fetcher.invoke(params) + + // Assert + val expected = exception + assertEitherLeft(actual, expected) + + coVerify(ordering = Ordering.SEQUENCE) { + walletAccountsFetcher.fetch(userWalletId) + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/fetcher/MultiAccountListFetcher.kt b/domain/account/src/main/java/com/tangem/domain/account/fetcher/MultiAccountListFetcher.kt new file mode 100644 index 0000000000..bff4dd124e --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/fetcher/MultiAccountListFetcher.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.account.fetcher + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Component that fetches a list of accounts for multiple wallets by a set of [UserWalletId]s or + * all accounts if no set is provided + * +[REDACTED_AUTHOR] + */ +interface MultiAccountListFetcher : FlowFetcher { + + sealed interface Params { + + data class Set(val ids: kotlin.collections.Set) : Params + + data object All : Params + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/fetcher/SingleAccountListFetcher.kt b/domain/account/src/main/java/com/tangem/domain/account/fetcher/SingleAccountListFetcher.kt new file mode 100644 index 0000000000..d282f20949 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/fetcher/SingleAccountListFetcher.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.account.fetcher + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Component that fetches a list of accounts for a single wallet by [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface SingleAccountListFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file