From fa125539b411e4afd55e9b4d91e013b590ebe2d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Sep 2025 13:35:18 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../account/models/AccountStatusList.kt | 9 +- .../kotlin/com/tangem/domain/core/lce/Lce.kt | 5 + domain/models/build.gradle.kts | 3 +- .../domain/models/account/AccountStatus.kt | 8 +- .../tangem/domain/models/quote/PriceChange.kt | 6 +- .../operations/TotalFiatBalanceCalculator.kt | 34 ++++++ .../TotalFiatBalanceCalculatorTest.kt | 112 +++++++++++++++++- 7 files changed, 165 insertions(+), 12 deletions(-) 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 357cbf110c..2ab3ad738b 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 @@ -8,9 +8,10 @@ import kotlinx.serialization.Serializable /** * Represents a list of account statuses associated with a user wallet * - * @property userWallet the user wallet to which the account statuses belong - * @property accountStatuses a set of account statuses associated with the user wallet - * @property totalAccounts the total number of accounts + * @property userWallet the user wallet to which the account statuses belong + * @property accountStatuses a set of account statuses associated with the user wallet + * @property totalAccounts the total number of accounts (including archived ones) + * @property totalFiatBalance the total fiat balance across all accounts * [REDACTED_AUTHOR] */ @@ -19,7 +20,7 @@ data class AccountStatusList( val userWallet: UserWallet, val accountStatuses: Set, val totalAccounts: Int, - val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Failed, + val totalFiatBalance: TotalFiatBalance, ) { val mainAccount: AccountStatus diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt index 1eba8138a8..24f77bbc0c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -5,6 +5,7 @@ import com.tangem.domain.core.utils.flatMap import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import kotlinx.serialization.Serializable /** * A sealed class representing the three states of a data load operation: Loading, Content, and Error. @@ -12,6 +13,7 @@ import com.tangem.domain.core.utils.lceLoading * @param E The type of the error object. * @param C The type of the content object. */ +@Serializable sealed class Lce { /** @@ -19,6 +21,7 @@ sealed class Lce { * * @param partialContent The partial content that has been loaded so far, if any. */ + @Serializable data class Loading(val partialContent: C?) : Lce() /** @@ -26,6 +29,7 @@ sealed class Lce { * * @param content The loaded content. */ + @Serializable data class Content(val content: C) : Lce() /** @@ -33,6 +37,7 @@ sealed class Lce { * * @param error The error that occurred during loading. */ + @Serializable data class Error(val error: E) : Lce() /** diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index 6c9f6b1143..fa609f56f6 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -1,5 +1,3 @@ -import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants - plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) @@ -13,6 +11,7 @@ tasks.withType().configureEach { dependencies { api(projects.domain.visa.models) + api(projects.domain.core) api(projects.core.utils) implementation(tangemDeps.card.core) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index 73bc34e70e..1712913f2e 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -1,5 +1,7 @@ package com.tangem.domain.models.account +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList import kotlinx.serialization.Serializable @@ -17,12 +19,14 @@ sealed interface AccountStatus { /** * Represents the status of a crypto portfolio account * - * @property account the crypto portfolio account - * @property tokenList the list of tokens associated with the account + * @property account the crypto portfolio account + * @property tokenList the list of tokens associated with the account + * @property priceChangeLce the price change information wrapped in a Lce (Loading, Content, Error) state */ @Serializable data class CryptoPortfolio( override val account: Account.CryptoPortfolio, val tokenList: TokenList, + val priceChangeLce: Lce, ) : AccountStatus } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/quote/PriceChange.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/PriceChange.kt index b38d5196dd..83b2239dd2 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/quote/PriceChange.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/PriceChange.kt @@ -1,7 +1,8 @@ package com.tangem.domain.models.quote import com.tangem.domain.models.StatusSource -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable /** * Represents the price change of a cryptocurrency asset over a specific time period. @@ -11,7 +12,8 @@ import java.math.BigDecimal * [REDACTED_AUTHOR] */ +@Serializable data class PriceChange( - val value: BigDecimal, + val value: SerializedBigDecimal, val source: StatusSource, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt index bef4a4e3dc..47826a442d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt @@ -1,11 +1,13 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList +import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero @@ -37,6 +39,26 @@ object TotalFiatBalanceCalculator { } } + fun calculate(balances: List): TotalFiatBalance { + val nonEmptyBalances = balances.toNonEmptyListOrNull() + ?: return TokenList.Empty.totalFiatBalance + + val computationState = ComputationState.resolve(balances = nonEmptyBalances) + + return when (computationState) { + ComputationState.LOADING -> TotalFiatBalance.Loading + ComputationState.NON_COMPUTABLE -> TotalFiatBalance.Failed + ComputationState.COMPUTABLE -> { + val loaded = balances.filterIsInstance() + + TotalFiatBalance.Loaded( + amount = loaded.sumOf(TotalFiatBalance.Loaded::amount), + source = loaded.map(TotalFiatBalance.Loaded::source).getResultStatusSource(), + ) + } + } + } + private fun compute(statuses: NonEmptyList): TotalFiatBalance { var mutableBalance: TotalFiatBalance = TotalFiatBalance.Loading @@ -189,6 +211,18 @@ object TotalFiatBalanceCalculator { return COMPUTABLE } + + fun resolve(balances: List): ComputationState { + for (balance in balances) { + when (balance) { + TotalFiatBalance.Failed -> return NON_COMPUTABLE + TotalFiatBalance.Loading -> return LOADING + is TotalFiatBalance.Loaded -> continue + } + } + + return COMPUTABLE + } } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt index 12dba0e8bd..5c2a253292 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt @@ -31,7 +31,7 @@ class TotalFiatBalanceCalculatorTest { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class LoadingOrNonComputable { + inner class CalculateCurrenciesLoadingOrNonComputable { @Test fun `one token is Loading, total is Loading`() { @@ -117,7 +117,7 @@ class TotalFiatBalanceCalculatorTest { @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class Computable { + inner class CalculateCurrenciesComputable { @Test fun `Unreachable token isIncludeToBalanceOnError, total is Loaded`() { @@ -377,6 +377,114 @@ class TotalFiatBalanceCalculatorTest { } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CalculateBalancesLoadingOrNonComputable { + + @Test + fun `one balance is Loading, total is Loading`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Loading, + TotalFiatBalance.Failed, + ) + + // Act + val actual = TotalFiatBalanceCalculator.calculate(balances) + + // Assert + val expected = TotalFiatBalance.Loading + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `one balance is Failed, total is Failed`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Failed, + TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL), + ) + + // Act + val actual = TotalFiatBalanceCalculator.calculate(balances) + + // Assert + val expected = TotalFiatBalance.Failed + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CalculateBalancesComputable { + + @Test + fun `all balances are Loaded, total is Loaded`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL), + TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL), + ) + + // Act + val actual = TotalFiatBalanceCalculator.calculate(balances) + + // Assert + val expected = TotalFiatBalance.Loaded( + amount = BigDecimal(11), + source = StatusSource.ACTUAL, + ) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `StatusSource is Actual is all balances are Actual`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL), + TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ACTUAL), + ) + + // Act + val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source + + // Assert + val expected = StatusSource.ACTUAL + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `StatusSource is Cache is any balance is Cache`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL), + TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.CACHE), + ) + + // Act + val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source + + // Assert + val expected = StatusSource.CACHE + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `StatusSource is ONLY_CACHE is any balance is ONLY_CACHE`() { + // Arrange + val balances = nonEmptyListOf( + TotalFiatBalance.Loaded(amount = BigDecimal.TEN, source = StatusSource.ACTUAL), + TotalFiatBalance.Loaded(amount = BigDecimal.ONE, source = StatusSource.ONLY_CACHE), + ) + + // Act + val actual = (TotalFiatBalanceCalculator.calculate(balances) as TotalFiatBalance.Loaded).source + // Assert + val expected = StatusSource.ONLY_CACHE + Truth.assertThat(actual).isEqualTo(expected) + } + } + private fun createLoading(currency: CryptoCurrency): CryptoCurrencyStatus { return CryptoCurrencyStatus( currency = currency,