From 7cb4b57fe9cc37446675bd780d75c0b252514c4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Nov 2025 12:21:14 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 11 +- .../account/models/AccountStatusList.kt | 11 + .../ArchiveCryptoPortfolioUseCaseTest.kt | 152 --------- domain/account/status/build.gradle.kts | 3 +- .../status/di/AccountStatusUseCaseModule.kt | 4 +- .../usecase/ArchiveCryptoPortfolioUseCase.kt | 76 ++++- .../GetAccountCurrencyByAddressUseCase.kt | 1 + .../usecase/ManageCryptoCurrenciesUseCase.kt | 85 +++-- .../ArchiveCryptoPortfolioUseCaseTest.kt | 307 ++++++++++++++++++ .../GetAccountCurrencyByAddressUseCaseTest.kt | 24 +- .../tangem/domain/core/flow/FlowSupplier.kt | 13 +- domain/referral/.gitignore | 1 + domain/referral/build.gradle.kts | 9 + .../domain/referral/ReferralRepository.kt | 28 ++ .../account/details/AccountDetailsModel.kt | 7 +- features/referral/data/build.gradle.kts | 1 + .../referral/data/detekt-baseline-debug.xml | 7 - .../referral/converters/ReferralConverter.kt | 6 +- .../referral/data/ReferralRepositoryImpl.kt | 20 +- .../referral/di/ReferralRepositoryModule.kt | 19 ++ settings.gradle.kts | 1 + 21 files changed, 556 insertions(+), 230 deletions(-) delete mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt rename domain/account/{src/main/java/com/tangem/domain/account => status/src/main/java/com/tangem/domain/account/status}/usecase/ArchiveCryptoPortfolioUseCase.kt (51%) create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt create mode 100644 domain/referral/.gitignore create mode 100644 domain/referral/build.gradle.kts create mode 100644 domain/referral/src/main/java/com/tangem/domain/referral/ReferralRepository.kt delete mode 100644 features/referral/data/detekt-baseline-debug.xml diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 90f6777934..ce95153840 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -3,10 +3,13 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase 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.feature.referral.data.ExternalReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -43,9 +46,15 @@ internal object AccountDomainModule { @Provides @Singleton fun provideArchiveCryptoPortfolioUseCase( + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, accountsCRUDRepository: AccountsCRUDRepository, + referralRepository: ExternalReferralRepository, ): ArchiveCryptoPortfolioUseCase { - return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + return ArchiveCryptoPortfolioUseCase( + singleAccountStatusListSupplier = singleAccountStatusListSupplier, + crudRepository = accountsCRUDRepository, + referralRepository = referralRepository, + ) } @Provides diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index c817094f5a..f7c9a12dc5 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -1,5 +1,6 @@ package com.tangem.domain.account.models +import arrow.core.Either import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance @@ -40,4 +41,14 @@ data class AccountStatusList( fun flattenCurrencies(): List = accountStatuses .map { accountStatus -> accountStatus.flattenCurrencies() } .flatten() + + fun toAccountList(): Either { + return AccountList( + userWalletId = userWalletId, + accounts = accountStatuses.map(AccountStatus::account), + totalAccounts = totalAccounts, + sortType = sortType, + groupType = groupType, + ) + } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt deleted file mode 100644 index 461bd17267..0000000000 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.domain.account.usecase - -import arrow.core.None -import arrow.core.left -import arrow.core.right -import arrow.core.toOption -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.ArchiveCryptoPortfolioUseCase.Error -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.test.mock.MockAccounts.createAccount -import io.mockk.* -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ArchiveCryptoPortfolioUseCaseTest { - - private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository) - - @BeforeEach - fun resetMocks() { - clearMocks(crudRepository) - } - - @Test - fun `invoke should archive existing crypto portfolio account`() = runTest { - // Arrange - val account = createAccount(derivationIndex = 1) - val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! - val accountId = account.accountId - - val updatedAccountList = (accountList - account).getOrNull()!! - - coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() - - // Act - val actual = useCase(accountId) - - // Assert - val expected = Unit.right() - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { - crudRepository.getAccountListSync(userWalletId) - crudRepository.saveAccounts(updatedAccountList) - } - } - - @Test - fun `invoke should return error if getAccounts returns None`() = runTest { - // Arrange - val accountId = AccountId.forCryptoPortfolio( - userWalletId = userWalletId, - derivationIndex = DerivationIndex.Main, - ) - - coEvery { crudRepository.getAccountListSync(userWalletId) } returns None - - // Act - val actual = useCase(accountId) - - // Assert - val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } - coVerify(inverse = true) { crudRepository.saveAccounts(any()) } - } - - @Test - fun `invoke should return error if getAccounts throws exception`() = runTest { - // Arrange - val accountId = AccountId.forCryptoPortfolio( - userWalletId = userWalletId, - derivationIndex = DerivationIndex.Main, - ) - - val exception = IllegalStateException("Test error") - - coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception - - // Act - val actual = useCase(accountId) - - // Assert - val expected = Error.DataOperationFailed(exception).left() - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } - coVerify(inverse = true) { crudRepository.saveAccounts(any()) } - } - - @Test - fun `invoke should return error if account not found`() = runTest { - // Arrange - val accountList = AccountList.empty(userWalletId) - val accountId = AccountId.forCryptoPortfolio( - userWalletId = userWalletId, - derivationIndex = DerivationIndex(1).getOrNull()!!, - ) - - coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() - - // Act - val actual = useCase(accountId) - - // Assert - val expected = Error.CriticalTechError.AccountNotFound(accountId).left() - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { crudRepository.getAccountListSync(userWalletId) } - coVerify(inverse = true) { crudRepository.saveAccounts(any()) } - } - - @Test - fun `invoke should return error if saveAccounts throws exception`() = runTest { - // Arrange - val account = createAccount(derivationIndex = 1) - val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! - val accountId = account.accountId - - val updatedAccountList = (accountList - account).getOrNull()!! - - val exception = IllegalStateException("Save failed") - - coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception - - // Act - val actual = useCase(accountId) - - // Assert - val expected = Error.DataOperationFailed(exception).left() - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { - crudRepository.getAccountListSync(userWalletId) - crudRepository.saveAccounts(updatedAccountList) - } - } - - private companion object { - val userWalletId = UserWalletId("011") - } -} \ No newline at end of file diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index ae69207870..7e7cfa8521 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { api(projects.domain.quotes) api(projects.domain.models) api(projects.domain.networks) + api(projects.domain.referral) api(projects.domain.staking) api(projects.domain.tokens) api(projects.domain.wallets) @@ -42,7 +43,7 @@ dependencies { // end testRuntimeOnly(deps.test.junit5.engine) - testImplementation(tangemDeps.blockchain) testImplementation(projects.common.test) testImplementation(projects.test.core) + testImplementation(projects.test.mock) } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index caf0fb6122..70d95e70cd 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -91,7 +91,7 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton fun provideManageCryptoCurrenciesUseCase( - singleAccountListSupplier: SingleAccountListSupplier, + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, accountsCRUDRepository: AccountsCRUDRepository, currenciesRepository: CurrenciesRepository, derivationsRepository: DerivationsRepository, @@ -103,7 +103,7 @@ internal object AccountStatusUseCaseModule { dispatchers: CoroutineDispatcherProvider, ): ManageCryptoCurrenciesUseCase { return ManageCryptoCurrenciesUseCase( - singleAccountListSupplier = singleAccountListSupplier, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, accountsCRUDRepository = accountsCRUDRepository, currenciesRepository = currenciesRepository, derivationsRepository = derivationsRepository, diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt similarity index 51% rename from domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt index 75056d6eb0..7a0c57b6ac 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -1,31 +1,48 @@ -package com.tangem.domain.account.usecase +package com.tangem.domain.account.status.usecase import arrow.core.Either import arrow.core.getOrElse -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either +import arrow.core.raise.* import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.referral.ReferralRepository /** * Use case for archiving a crypto portfolio. * This class provides functionality to archive a specific account within a user's crypto portfolio. * It ensures that the account exists and meets the necessary requirements before performing the operation. * + * @property singleAccountStatusListSupplier supplier to get the list of account statuses * @property crudRepository repository for performing CRUD operations on accounts + * @property referralRepository repository for handling referral status checks * [REDACTED_AUTHOR] */ class ArchiveCryptoPortfolioUseCase( + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val crudRepository: AccountsCRUDRepository, + private val referralRepository: ReferralRepository, ) { /** Archives the specified account by its [accountId] */ suspend operator fun invoke(accountId: AccountId): Either = either { - val accountList = getAccountList(userWalletId = accountId.userWalletId) + val accountStatusList = getAccountStatusList(userWalletId = accountId.userWalletId) + val accountList = accountStatusList.toAccountList().getOrElse { + raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) + } + + ensure(accountList.accounts.any { it.accountId == accountId }) { + Error.CriticalTechError.AccountNotFound(accountId = accountId) + } + + checkReferralStatus(accountStatusList = accountStatusList, accountId = accountId) val archivingAccount = accountList.accounts .firstOrNull { it.accountId == accountId } @@ -38,16 +55,48 @@ class ArchiveCryptoPortfolioUseCase( saveAccounts(updatedAccounts) } - private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { - return catch( - block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, + private suspend fun Raise.checkReferralStatus(accountStatusList: AccountStatusList, accountId: AccountId) { + val referralStatus = catch( + block = { referralRepository.getReferralStatus(userWalletId = accountStatusList.userWalletId.stringValue) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + + val referralToken = referralStatus.token + val address = referralStatus.address + + if (!referralStatus.isActive || referralToken == null || address == null) return + + val account = accountStatusList.accountStatuses + .asSequence() + .filterIsInstance() + .firstOrNull { it.accountId == accountId } + + ensureNotNull(account) { + Error.CriticalTechError.AccountNotFound(accountId = accountId) + } + + val statuses = account.flattenCurrencies() + + val hasNotReferralToken = statuses.none { status -> + val currency = status.currency + + currency.network.backendId == referralToken.networkId && + (currency as? CryptoCurrency.Token)?.contractAddress == referralToken.contractAddress && + status.value.networkAddress?.availableAddresses?.any { it.value == address } == true + } + + ensure(hasNotReferralToken) { Error.ActiveReferralStatus } + } + + private suspend fun Raise.getAccountStatusList(userWalletId: UserWalletId): AccountStatusList { + return singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId = userWalletId), + ) + ?: raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } private suspend fun Raise.saveAccounts(accountList: AccountList) { - catch( + arrow.core.raise.catch( block = { crudRepository.saveAccounts(accountList) }, catch = { raise(Error.DataOperationFailed(cause = it)) }, ) @@ -58,9 +107,14 @@ class ArchiveCryptoPortfolioUseCase( */ sealed interface Error { + val tag: String + get() = this.javaClass.simpleName + + data object ActiveReferralStatus : Error + /** Error indicating that a data operation failed */ data class DataOperationFailed(val cause: Throwable) : Error { - override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}" + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" } /** diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt index b29d088ba7..31de28f37e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt @@ -82,6 +82,7 @@ class GetAccountCurrencyByAddressUseCase( for (id in userWalletIds) { val networkStatus = multiNetworkStatusSupplier.getSyncOrNull( params = MultiNetworkStatusProducer.Params(userWalletId = id), + timeMillis = 1000L, ) ?.firstOrNull { it.getAddress() == address } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index ca59ae9ed8..9277ab4837 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -3,16 +3,18 @@ package com.tangem.domain.account.status.usecase import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch -import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher -import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.utils.NetworksCleaner @@ -27,7 +29,7 @@ import timber.log.Timber /** * Use case for saving crypto currencies to a specific account. * - * @property singleAccountListSupplier Supplier to get account details. + * @property singleAccountStatusListSupplier Supplier for fetching the status of a single account. * @property accountsCRUDRepository Repository for performing CRUD operations on accounts. * @property currenciesRepository Repository for managing currencies. * @property derivationsRepository Repository for deriving public keys. @@ -43,7 +45,7 @@ import timber.log.Timber */ @Suppress("LongParameterList") class ManageCryptoCurrenciesUseCase( - private val singleAccountListSupplier: SingleAccountListSupplier, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val accountsCRUDRepository: AccountsCRUDRepository, private val currenciesRepository: CurrenciesRepository, private val derivationsRepository: DerivationsRepository, @@ -76,12 +78,13 @@ class ManageCryptoCurrenciesUseCase( val userWalletId = accountId.userWalletId withContext(NonCancellable) { - val account = getAccount(accountId = accountId) + val accountStatus = getAccountStatus(accountId = accountId) - val modifiedCurrencyList = account.cryptoCurrencies.modify(add = add, remove = remove) + val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + .modify(add = add, remove = remove) saveAccount( - account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), + account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), ) val isDerivingFailed = derivePublicKeys( @@ -117,10 +120,10 @@ class ManageCryptoCurrenciesUseCase( val userWalletId = accountId.userWalletId withContext(NonCancellable) { - val account = getAccount(accountId = accountId) + val accountStatus = getAccountStatus(accountId = accountId) - val foundToken = account.cryptoCurrencies - .filterIsInstance() + val foundToken = accountStatus.tokenList.flattenCurrencies() + .mapNotNull { it.currency as? CryptoCurrency.Token } .firstOrNull { it.network.backendId == networkId && !it.isCustom && @@ -131,9 +134,10 @@ class ManageCryptoCurrenciesUseCase( val tokenToAdd = findToken(userWalletId, contractAddress, networkId) - val modifiedCurrencyList = account.cryptoCurrencies.modify(add = listOf(tokenToAdd)) + val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + .modify(add = listOf(tokenToAdd)) - saveAccount(account = account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) + saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) parallelUpdatingScope.launch { cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) @@ -144,26 +148,31 @@ class ManageCryptoCurrenciesUseCase( } } - private suspend fun Raise.getAccount(accountId: AccountId): Account.CryptoPortfolio { - val accountList = singleAccountListSupplier.getSyncOrNull( - params = SingleAccountListProducer.Params(userWalletId = accountId.userWalletId), + private suspend fun Raise.getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio { + val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull( + params = SingleAccountStatusListProducer.Params(userWalletId = accountId.userWalletId), ) ?: raise(IllegalStateException("No accounts for wallet ${accountId.userWalletId}")) - return accountList.accounts.firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + return accountStatusList.accountStatuses + .firstOrNull { it.accountId == accountId } as? AccountStatus.CryptoPortfolio ?: raise(IllegalStateException("No account with id $accountId")) } - private fun Set.modify( + private fun List.modify( add: List, remove: List = emptyList(), ): ModifiedCurrencyList { - val mutableCurrencies = this.toMutableList() + val mutableCurrencies = this.map(CryptoCurrencyStatus::currency).toMutableList() val added = mutableListOf() val removed = mutableListOf() - val existingCurrenciesById = mutableCurrencies.associateBy(::TempID) + val existingCurrenciesById = this.associateBy(::TempID) - add.groupByNetwork { !existingCurrenciesById.containsKey(it) } + add.groupByNetwork { tempID -> + val found = existingCurrenciesById[tempID] ?: return@groupByNetwork true + + found.value is CryptoCurrencyStatus.MissedDerivation + } .forEach { (network, currenciesById) -> val coinTempId = TempID(network) @@ -204,20 +213,6 @@ class ManageCryptoCurrenciesUseCase( return ModifiedCurrencyList(added = added, removed = removed, total = mutableCurrencies) } - private suspend fun Raise.saveAccount(account: Account.CryptoPortfolio) { - catch( - block = { accountsCRUDRepository.saveAccount(account) }, - catch = ::raise, - ) - } - - private suspend fun derivePublicKeys( - userWalletId: UserWalletId, - currencies: List, - ): Either = Either.catch { - derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies) - } - private fun List.groupByNetwork( valuePredicate: (TempID) -> Boolean, ): LinkedHashMap> { @@ -230,13 +225,27 @@ class ManageCryptoCurrenciesUseCase( val id = TempID(currency) if (valuePredicate(id)) { - mutableMap.put(id, currency) + mutableMap[id] = currency } } return destination } + private suspend fun Raise.saveAccount(account: Account.CryptoPortfolio) { + catch( + block = { accountsCRUDRepository.saveAccount(account) }, + catch = ::raise, + ) + } + + private suspend fun derivePublicKeys( + userWalletId: UserWalletId, + currencies: List, + ): Either = Either.catch { + derivationsRepository.derivePublicKeys(userWalletId = userWalletId, currencies = currencies) + } + private suspend fun Raise.findToken( userWalletId: UserWalletId, networkId: String, @@ -307,6 +316,12 @@ class ManageCryptoCurrenciesUseCase( derivationPath = currency.network.derivationPath, contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) + + constructor(status: CryptoCurrencyStatus) : this( + networkId = status.currency.network.backendId, + derivationPath = status.currency.network.derivationPath, + contractAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress, + ) } private data class ModifiedCurrencyList( diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..109fe9d65d --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,307 @@ +package com.tangem.domain.account.status.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase.Error +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.referral.ReferralRepository +import com.tangem.domain.referral.ReferralStatus +import com.tangem.test.mock.MockAccounts.createAccountList +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchiveCryptoPortfolioUseCaseTest { + + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val referralRepository: ReferralRepository = mockk(relaxUnitFun = true) + + private val useCase = ArchiveCryptoPortfolioUseCase( + singleAccountStatusListSupplier = singleAccountStatusListSupplier, + crudRepository = crudRepository, + referralRepository = referralRepository, + ) + + @BeforeEach + fun resetMocks() { + clearMocks(singleAccountStatusListSupplier, referralRepository, crudRepository) + } + + @Test + fun `invoke should archive existing crypto portfolio account`() = runTest { + // Arrange + val accountList = createAccountList(activeAccounts = 2) + val archivingAccount = accountList.accounts.last() + val updatedAccountList = (accountList - archivingAccount).getOrNull()!! + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus() + + val referralStatus = ReferralStatus(isActive = false, token = null, address = null) + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + // Act + val actual = useCase(accountId = archivingAccount.accountId) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + singleAccountStatusListSupplier.getSyncOrNull(params) + referralRepository.getReferralStatus(userWalletId.stringValue) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should archive if referral status is active but token is null`() = runTest { + // Arrange + val accountList = createAccountList(activeAccounts = 2) + val archivingAccount = accountList.accounts.last() + val updatedAccountList = (accountList - archivingAccount).getOrNull()!! + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus() + + val referralStatus = ReferralStatus(isActive = true, token = null, address = null) + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + // Act + val actual = useCase(accountId = archivingAccount.accountId) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + singleAccountStatusListSupplier.getSyncOrNull(params) + referralRepository.getReferralStatus(userWalletId.stringValue) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should archive if referral status is active but token is absent`() = runTest { + // Arrange + val accountList = createAccountList(activeAccounts = 2) + val archivingAccount = accountList.accounts.last() + val updatedAccountList = (accountList - archivingAccount).getOrNull()!! + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus() + + val token = ReferralStatus.Token( + networkId = "ethereum", + contractAddress = "0x1", + ) + val referralStatus = ReferralStatus(isActive = true, token = token, address = "0xABC") + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + // Act + val actual = useCase(accountId = archivingAccount.accountId) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + singleAccountStatusListSupplier.getSyncOrNull(params) + referralRepository.getReferralStatus(userWalletId.stringValue) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke returns error if account contains referral token `() = runTest { + // Arrange + val token = ReferralStatus.Token( + networkId = "ethereum", + contractAddress = "0x1", + ) + val defaultAddress = "0xABC" + + val cryptoCurrency = mockk { + every { this@mockk.network.backendId } returns token.networkId + every { this@mockk.contractAddress } returns token.contractAddress!! + } + + val networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = defaultAddress, + type = NetworkAddress.Address.Type.Primary, + ), + ) + + val cryptoCurrencyStatus = mockk { + every { this@mockk.currency } returns cryptoCurrency + every { this@mockk.value.networkAddress } returns networkAddress + } + + val accountList = createAccountList(activeAccounts = 2) + val archivingAccount = accountList.accounts.last() + val archivingAccountStatus = AccountStatus.CryptoPortfolio( + account = archivingAccount as Account.CryptoPortfolio, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Failed, + sortedBy = TokensSortType.NONE, + currencies = listOf(cryptoCurrencyStatus), + ), + priceChangeLce = Unit.lceError(), + ) + val accountListStatus = with(accountList.toStatus()) { + copy( + accountStatuses = accountStatuses.filter { it.accountId != archivingAccount.accountId } + + archivingAccountStatus, + ) + } + + (accountList - archivingAccount).getOrNull()!! + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountListStatus + + val referralStatus = ReferralStatus(isActive = true, token = token, address = defaultAddress) + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + // Act + val actual = useCase(accountId = archivingAccount.accountId) + + // Assert + val expected = Error.ActiveReferralStatus.left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + singleAccountStatusListSupplier.getSyncOrNull(params) + referralRepository.getReferralStatus(userWalletId.stringValue) + } + + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if supplier returns null`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(1).getOrNull()!!, + ) + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns null + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { singleAccountStatusListSupplier.getSyncOrNull(params) } + coVerify(inverse = true) { + referralRepository.getReferralStatus(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if account not found`() = runTest { + // Arrange + val accountList = AccountList.empty(userWalletId) + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Companion(1).getOrNull()!!, + ) + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus() + + val referralStatus = ReferralStatus(isActive = false, token = null, address = null) + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { singleAccountStatusListSupplier.getSyncOrNull(params) } + + coVerify(inverse = true) { + referralRepository.getReferralStatus(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + val accountList = createAccountList(activeAccounts = 2) + val archivingAccount = accountList.accounts.last() + val updatedAccountList = (accountList - archivingAccount).getOrNull()!! + + val params = SingleAccountStatusListProducer.Params(userWalletId) + coEvery { singleAccountStatusListSupplier.getSyncOrNull(params) } returns accountList.toStatus() + + val referralStatus = ReferralStatus(isActive = false, token = null, address = null) + coEvery { referralRepository.getReferralStatus(userWalletId.stringValue) } returns referralStatus + + val exception = IllegalStateException("Save failed") + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId = archivingAccount.accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + singleAccountStatusListSupplier.getSyncOrNull(params) + referralRepository.getReferralStatus(userWalletId.stringValue) + crudRepository.saveAccounts(updatedAccountList) + } + } + + private fun AccountList.toStatus(): AccountStatusList { + return AccountStatusList( + userWalletId = Companion.userWalletId, + accountStatuses = accounts.map { + AccountStatus.CryptoPortfolio( + account = it as Account.CryptoPortfolio, + tokenList = TokenList.Empty, + priceChangeLce = Unit.lceError(), + ) + }, + totalAccounts = totalAccounts, + totalFiatBalance = TotalFiatBalance.Loading, + sortType = sortType, + groupType = groupType, + ) + } + + private companion object { + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index 8be24389d9..ba62fe222b 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -113,7 +113,7 @@ class GetAccountCurrencyByAddressUseCaseTest { // Arrange every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns null // Act @@ -124,7 +124,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -133,7 +133,7 @@ class GetAccountCurrencyByAddressUseCaseTest { // Arrange every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns emptySet() // Act @@ -144,7 +144,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -158,7 +158,7 @@ class GetAccountCurrencyByAddressUseCaseTest { every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) // Act @@ -169,7 +169,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -185,7 +185,7 @@ class GetAccountCurrencyByAddressUseCaseTest { every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) coEvery { singleAccountListSupplier.getSyncOrNull( @@ -201,7 +201,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), ) @@ -222,7 +222,7 @@ class GetAccountCurrencyByAddressUseCaseTest { every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) coEvery { singleAccountListSupplier.getSyncOrNull( @@ -238,7 +238,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), ) @@ -257,7 +257,7 @@ class GetAccountCurrencyByAddressUseCaseTest { every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) coEvery { - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) coEvery { singleAccountListSupplier.getSyncOrNull( @@ -274,7 +274,7 @@ class GetAccountCurrencyByAddressUseCaseTest { coVerifySequence { accountsCRUDRepository.getUserWalletsSync() - multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId)) + multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), ) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt index ab1c15002a..63fb3dae08 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt @@ -2,6 +2,7 @@ package com.tangem.domain.core.flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withTimeoutOrNull /** * [Flow] supplier @@ -16,8 +17,14 @@ interface FlowSupplier { /** Supply [Flow] by [params] */ operator fun invoke(params: Params): Flow - /** Get first [Data] by [params] or null if [Flow] is empty */ - suspend fun getSyncOrNull(params: Params): Data? { - return invoke(params).firstOrNull() + /** Synchronously get first value or null from [Flow] within [timeMillis] */ + suspend fun getSyncOrNull(params: Params, timeMillis: Long? = null): Data? { + val block = suspend { invoke(params).firstOrNull() } + + return if (timeMillis == null) { + block() + } else { + withTimeoutOrNull(timeMillis) { block() } + } } } \ No newline at end of file diff --git a/domain/referral/.gitignore b/domain/referral/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/referral/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/referral/build.gradle.kts b/domain/referral/build.gradle.kts new file mode 100644 index 0000000000..ed80a19c56 --- /dev/null +++ b/domain/referral/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + api(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/referral/src/main/java/com/tangem/domain/referral/ReferralRepository.kt b/domain/referral/src/main/java/com/tangem/domain/referral/ReferralRepository.kt new file mode 100644 index 0000000000..cbbf048329 --- /dev/null +++ b/domain/referral/src/main/java/com/tangem/domain/referral/ReferralRepository.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.referral + +import kotlinx.serialization.Serializable + +/** + * Repository for handling referral-related operations. + * +[REDACTED_AUTHOR] + */ +interface ReferralRepository { + + /** Retrieves the referral status for a given [userWalletId] */ + suspend fun getReferralStatus(userWalletId: String): ReferralStatus +} + +@Serializable +data class ReferralStatus( + val isActive: Boolean, + val token: Token?, + val address: String?, +) { + + @Serializable + data class Token( + val networkId: String, + val contractAddress: String?, + ) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index d8b27533c3..36c4655927 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.producer.SingleAccountProducer +import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.isMultiCurrency @@ -104,13 +104,14 @@ internal class AccountDetailsModel @Inject constructor( } private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) { - // todo account referral case val titleRes = when (error) { is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, -> R.string.common_something_went_wrong + is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, + -> R.string.account_could_not_archive_referral_program_title } val messageRes = when (error) { is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, @@ -118,6 +119,8 @@ internal class AccountDetailsModel @Inject constructor( is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, -> R.string.account_could_not_archive + is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, + -> R.string.account_could_not_archive_referral_program_message } val dialogMessage = DialogMessage( diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index bbe186db0f..85cd526d9f 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.referral) implementation(projects.features.referral.domain) /** Libs */ diff --git a/features/referral/data/detekt-baseline-debug.xml b/features/referral/data/detekt-baseline-debug.xml deleted file mode 100644 index c6103ddaf9..0000000000 --- a/features/referral/data/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - MultilineLambdaItParameter:ReferralConverter.kt$ExpectedAwardsConverter${ ExpectedAward( paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), amount = "${it.amount} ${it.currency}", ) } - - diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt index 921f57b107..6bedb6bbed 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -70,10 +70,10 @@ private class ExpectedAwardsConverter : Converter ExpectedAward( - paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), - amount = "${it.amount} ${it.currency}", + paymentDate = awardItem.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), + amount = "${awardItem.amount} ${awardItem.currency}", ) }, ) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 6db3d31efa..9bc5ac4c5d 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.referral.ReferralStatus import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.models.ReferralData @@ -21,13 +22,15 @@ import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject +typealias ExternalReferralRepository = com.tangem.domain.referral.ReferralRepository + internal class ReferralRepositoryImpl @Inject constructor( private val referralApi: TangemTechApi, private val referralConverter: ReferralConverter, private val coroutineDispatcher: CoroutineDispatcherProvider, private val userWalletsStore: UserWalletsStore, excludedBlockchains: ExcludedBlockchains, -) : ReferralRepository { +) : ReferralRepository, ExternalReferralRepository { private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) @@ -47,6 +50,21 @@ internal class ReferralRepositoryImpl @Inject constructor( } } + override suspend fun getReferralStatus(userWalletId: String): ReferralStatus { + val data = getReferralData(userWalletId) + + return ReferralStatus( + isActive = data is ReferralData.ParticipantData, + token = data.tokens.firstOrNull()?.let { tokenData -> + ReferralStatus.Token( + networkId = tokenData.networkId, + contractAddress = tokenData.contractAddress, + ) + }, + address = (data as? ReferralData.ParticipantData)?.referral?.address, + ) + } + override suspend fun isReferralParticipant(userWalletId: UserWalletId): Boolean { val storedReferralData = referralStatus[userWalletId.stringValue] ?: getReferralData(userWalletId.stringValue) return storedReferralData is ReferralData.ParticipantData diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 153d375e6b..8d2c4cdfd4 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.data.ExternalReferralRepository import com.tangem.feature.referral.data.ReferralRepositoryImpl import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -34,4 +35,22 @@ class ReferralRepositoryModule { excludedBlockchains = excludedBlockchains, ) } + + @Provides + @Singleton + fun provideExternalReferralRepository( + tangemTechApi: TangemTechApi, + referralConverter: ReferralConverter, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + userWalletsStore: UserWalletsStore, + excludedBlockchains: ExcludedBlockchains, + ): ExternalReferralRepository { + return ReferralRepositoryImpl( + referralApi = tangemTechApi, + referralConverter = referralConverter, + coroutineDispatcher = coroutineDispatcherProvider, + userWalletsStore = userWalletsStore, + excludedBlockchains = excludedBlockchains, + ) + } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9d61983167..60632a9860 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -360,6 +360,7 @@ include(":domain:notifications") include(":domain:notifications:models") include(":domain:express") include(":domain:express:models") +include(":domain:referral") include(":domain:swap") include(":domain:swap:models") include(":domain:wallet-manager")