diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 374af96dd0..52533057f0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -333,10 +333,14 @@ internal object WalletsDomainModule { fun providesUpdateRemoteWalletsInfoUseCase( walletsRepository: WalletsRepository, userWalletsSyncDelegate: UserWalletsSyncDelegate, + userWalletsListRepository: UserWalletsListRepository, + generateWalletNameUseCase: GenerateWalletNameUseCase, ): UpdateRemoteWalletsInfoUseCase { return UpdateRemoteWalletsInfoUseCase( walletsRepository = walletsRepository, userWalletsSyncDelegate = userWalletsSyncDelegate, + generateWalletNameUseCase = generateWalletNameUseCase, + userWalletsListRepository = userWalletsListRepository, ) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index e0ae509ee6..7b8ddc3674 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -75,7 +75,7 @@ class UserTokensSaver( val conversionData = appsFlyerConversionStore.get() val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet.name, + walletName = userWallet.name.takeIf { it.isNotBlank() }, walletType = WalletType.from(userWallet), refcode = conversionData?.refcode, campaign = conversionData?.campaign, diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 64fe198777..74edd4593f 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -96,7 +96,7 @@ class UserTokensSaverTest { val userWalletId = UserWalletId("1234567890abcdef") val userWallet = mockk { every { this@mockk.walletId } returns userWalletId - every { this@mockk.name } returns "Wallet" + every { this@mockk.name } returns "" } val response = UserTokensResponse( @@ -104,7 +104,7 @@ class UserTokensSaverTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, tokens = emptyList(), - walletName = userWallet.name, + walletName = null, walletType = WalletType.COLD, ) val enrichedResponse = UserTokensResponse( @@ -112,12 +112,13 @@ class UserTokensSaverTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.MANUAL, tokens = emptyList(), - walletName = userWallet.name, + walletName = null, walletType = WalletType.COLD, ) val error = ApiResponseError.UnknownException(Exception("API Error")) var onFailSendCalled = false + every { accountsFeatureToggles.isFeatureEnabled } returns true coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt index a59becb20f..ee19f03a29 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt @@ -2,7 +2,7 @@ package com.tangem.domain.wallets.models import com.tangem.domain.models.wallet.UserWalletId -class UserWalletRemoteInfo( +data class UserWalletRemoteInfo( val walletId: UserWalletId, val name: String, val isNotificationsEnabled: Boolean, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt index 8ae0a158b0..3249c1a229 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt @@ -1,6 +1,10 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.repository.WalletsRepository @@ -8,10 +12,34 @@ import com.tangem.domain.wallets.repository.WalletsRepository class UpdateRemoteWalletsInfoUseCase( private val walletsRepository: WalletsRepository, private val userWalletsSyncDelegate: UserWalletsSyncDelegate, + private val userWalletsListRepository: UserWalletsListRepository, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, ) { suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { - val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value) + val userWalletsMap = userWalletsListRepository.userWallets.value?.associateBy { it.walletId }.orEmpty() + val remoteData = walletsRepository.getWalletsInfo(applicationId.value) + + val walletsInfo = remoteData.map { walletRemoteInfo -> + if (walletRemoteInfo.name.isBlank()) { + walletRemoteInfo.copy(name = generateName(walletRemoteInfo.walletId, userWalletsMap)) + } else { + walletRemoteInfo + } + }.filter { it.name.isNotBlank() } + userWalletsSyncDelegate.syncWallets(walletsInfo) } + + private fun generateName(userWalletId: UserWalletId, userWalletsMap: Map): String { + val userWallet = userWalletsMap[userWalletId] ?: return "" + return when (userWallet) { + is UserWallet.Hot -> generateWalletNameUseCase.invokeForHot() + is UserWallet.Cold -> generateWalletNameUseCase( + userWallet.scanResponse.productType, + card = userWallet.scanResponse.card, + isStartToCoin = userWallet.scanResponse.cardTypesResolver.isStart2Coin(), + ) + } + } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCaseTest.kt new file mode 100644 index 0000000000..6f2846aaca --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCaseTest.kt @@ -0,0 +1,245 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate +import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.domain.wallets.repository.WalletsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class UpdateRemoteWalletsInfoUseCaseTest { + + private lateinit var useCase: UpdateRemoteWalletsInfoUseCase + private lateinit var walletsRepository: WalletsRepository + private lateinit var userWalletsSyncDelegate: UserWalletsSyncDelegate + private lateinit var userWalletListRepository: UserWalletsListRepository + private lateinit var generateWalletNameUseCase: GenerateWalletNameUseCase + + @Before + fun setup() { + walletsRepository = mockk() + userWalletsSyncDelegate = mockk() + userWalletListRepository = mockk { + every { userWallets } returns MutableStateFlow?>(emptyList()) + } + generateWalletNameUseCase = mockk() + useCase = UpdateRemoteWalletsInfoUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + userWalletsListRepository = userWalletListRepository, + generateWalletNameUseCase = generateWalletNameUseCase, + ) + } + + @Test + fun `GIVEN wallets with non-empty names WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletsInfo = listOf( + UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true), + UserWalletRemoteInfo(UserWalletId("0E0F1011"), "Wallet 2", true), + ) + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo + coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { walletsRepository.getWalletsInfo(applicationId.value) } + coVerify { userWalletsSyncDelegate.syncWallets(walletsInfo) } + } + + @Test + fun `GIVEN wallets with empty names WHEN invoke THEN filter out wallets with empty names`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletWithName = UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true) + val walletWithEmptyName = UserWalletRemoteInfo(UserWalletId("0E0F1011"), "", true) + val walletsInfo = listOf(walletWithName, walletWithEmptyName) + val filteredWallets = listOf(walletWithName) + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo + coEvery { userWalletsSyncDelegate.syncWallets(filteredWallets) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { userWalletsSyncDelegate.syncWallets(filteredWallets) } + } + + @Test + fun `GIVEN empty wallets list WHEN invoke THEN sync with empty list`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletsInfo = emptyList() + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo + coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { userWalletsSyncDelegate.syncWallets(emptyList()) } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Left with Throwable`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val exception = RuntimeException("Network error") + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } throws exception + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + } + + @Test + fun `GIVEN syncWallets throws exception WHEN invoke THEN return Left with Throwable`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletsInfo = listOf( + UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "Wallet 1", true), + ) + val exception = RuntimeException("Sync error") + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo + coEvery { userWalletsSyncDelegate.syncWallets(walletsInfo) } throws exception + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + } + + @Test + fun `GIVEN all wallets have empty names WHEN invoke THEN sync with empty list`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletsInfo = listOf( + UserWalletRemoteInfo(UserWalletId("0A0B0C0D"), "", true), + UserWalletRemoteInfo(UserWalletId("0E0F1011"), "", true), + ) + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns walletsInfo + coEvery { userWalletsSyncDelegate.syncWallets(emptyList()) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { userWalletsSyncDelegate.syncWallets(emptyList()) } + } + + @Test + fun `GIVEN hot wallet with blank name WHEN invoke THEN generate name using invokeForHot`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletId = UserWalletId("0A0B0C0D") + val hotWallet = mockk { + every { this@mockk.walletId } returns walletId + } + every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(hotWallet)) + + val remoteWalletWithBlankName = UserWalletRemoteInfo(walletId, "", true) + val generatedName = "Wallet" + every { generateWalletNameUseCase.invokeForHot() } returns generatedName + + val expectedWallet = UserWalletRemoteInfo(walletId, generatedName, true) + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(remoteWalletWithBlankName) + coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { generateWalletNameUseCase.invokeForHot() } + coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } + } + + @Test + fun `GIVEN cold wallet with blank name WHEN invoke THEN generate name using productType`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val walletId = UserWalletId("0A0B0C0D") + val mockScanResponse = mockk(relaxed = true) { + every { productType } returns ProductType.Wallet2 + every { card } returns mockk(relaxed = true) + } + val coldWallet = mockk { + every { this@mockk.walletId } returns walletId + every { scanResponse } returns mockScanResponse + } + every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(coldWallet)) + + val remoteWalletWithBlankName = UserWalletRemoteInfo(walletId, "", true) + val generatedName = "Wallet" + every { generateWalletNameUseCase.invoke(any(), any(), any()) } returns generatedName + + val expectedWallet = UserWalletRemoteInfo(walletId, generatedName, true) + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(remoteWalletWithBlankName) + coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { generateWalletNameUseCase.invoke(any(), any(), any()) } + coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } + } + + @Test + fun `GIVEN wallet with blank name not in repository WHEN invoke THEN filter it out`() = runTest { + // GIVEN + val applicationId = ApplicationId("test-app-id") + val knownWalletId = UserWalletId("0A0B0C0D") + val unknownWalletId = UserWalletId("0E0F1011") + + val hotWallet = mockk { + every { this@mockk.walletId } returns knownWalletId + } + every { userWalletListRepository.userWallets } returns MutableStateFlow(listOf(hotWallet)) + every { generateWalletNameUseCase.invokeForHot() } returns "Wallet" + + val knownWalletRemote = UserWalletRemoteInfo(knownWalletId, "", true) + val unknownWalletRemote = UserWalletRemoteInfo(unknownWalletId, "", true) + val expectedWallet = UserWalletRemoteInfo(knownWalletId, "Wallet", true) + + coEvery { walletsRepository.getWalletsInfo(applicationId.value) } returns listOf(knownWalletRemote, unknownWalletRemote) + coEvery { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } returns Unit.right() + + // WHEN + val result = useCase(applicationId) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify { userWalletsSyncDelegate.syncWallets(listOf(expectedWallet)) } + } +} \ No newline at end of file