From ad17ee6d6d950821f3fbb890c2471358f130292b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Apr 2026 20:29:29 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../SwapAmountSelectQuoteTransformer.kt | 14 +- .../SwapAmountSelectQuoteTransformerTest.kt | 185 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformerTest.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 19a5e22946..271054c6a7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -61,8 +61,10 @@ internal class SwapAmountSelectQuoteTransformer( secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, ) + val hasInsufficientFundsForFixed = hasInsufficientFundsForFixed(prevState, quoteContent) + return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content && !hasInsufficientFundsForFixed, selectedQuote = quoteUM, isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = newPrimaryAmount, @@ -71,6 +73,16 @@ internal class SwapAmountSelectQuoteTransformer( ) } + private fun hasInsufficientFundsForFixed( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + ): Boolean { + if (prevState.selectedAmountType != SwapAmountType.To) return false + val fromAmount = quoteContent?.fromAmount ?: return false + val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount ?: return false + return fromAmount > primaryBalance + } + private fun getPrimaryAmount( prevState: SwapAmountUM.Content, quoteContent: SwapQuoteUM.Content?, diff --git a/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformerTest.kt b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformerTest.kt new file mode 100644 index 0000000000..b1776a7f6c --- /dev/null +++ b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformerTest.kt @@ -0,0 +1,185 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import io.mockk.every +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.Test +import java.math.BigDecimal + +internal class SwapAmountSelectQuoteTransformerTest { + + private val provider = ExpressProvider( + providerId = "test-provider", + name = "Test Provider", + type = ExpressProviderType.CEX, + imageLarge = "", + termsOfUse = null, + privacyPolicy = null, + slippage = null, + ) + + @Test + fun `GIVEN fixed mode quote with fromAmount exceeding primary balance WHEN transform THEN isPrimaryButtonEnabled is false`() { + // GIVEN + val prevState = buildContentState( + selectedAmountType = SwapAmountType.To, + primaryBalance = BigDecimal("10"), + ) + val quote = buildContentQuote(fromAmount = BigDecimal("20"), toAmount = BigDecimal("1")) + val transformer = buildTransformer(quoteUM = quote) + + // WHEN + val result = transformer.transform(prevState) + + // THEN + val content = result as SwapAmountUM.Content + assertThat(content.isPrimaryButtonEnabled).isFalse() + assertThat(content.selectedQuote).isEqualTo(quote) + } + + @Test + fun `GIVEN fixed mode quote with fromAmount within primary balance WHEN transform THEN isPrimaryButtonEnabled is true`() { + // GIVEN + val prevState = buildContentState( + selectedAmountType = SwapAmountType.To, + primaryBalance = BigDecimal("100"), + ) + val quote = buildContentQuote(fromAmount = BigDecimal("20"), toAmount = BigDecimal("1")) + val transformer = buildTransformer(quoteUM = quote) + + // WHEN + val result = transformer.transform(prevState) + + // THEN + val content = result as SwapAmountUM.Content + assertThat(content.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `GIVEN float mode with selectedAmountType From WHEN transform THEN isPrimaryButtonEnabled is true regardless of fromAmount`() { + // GIVEN + val prevState = buildContentState( + selectedAmountType = SwapAmountType.From, + primaryBalance = BigDecimal("10"), + ) + // fromAmount > balance, but we're in From-mode so the check must not fire + val quote = buildContentQuote(fromAmount = BigDecimal("20"), toAmount = BigDecimal("1")) + val transformer = buildTransformer(quoteUM = quote) + + // WHEN + val result = transformer.transform(prevState) + + // THEN + val content = result as SwapAmountUM.Content + assertThat(content.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `GIVEN quote is SwapQuoteUM Error WHEN transform THEN isPrimaryButtonEnabled is false`() { + // GIVEN + val prevState = buildContentState( + selectedAmountType = SwapAmountType.To, + primaryBalance = BigDecimal("100"), + ) + val errorQuote = SwapQuoteUM.Error( + provider = provider, + expressError = ExpressError.InternalError(code = 500), + ) + val transformer = buildTransformer(quoteUM = errorQuote) + + // WHEN + val result = transformer.transform(prevState) + + // THEN + val content = result as SwapAmountUM.Content + assertThat(content.isPrimaryButtonEnabled).isFalse() + assertThat(content.selectedQuote).isEqualTo(errorQuote) + } + + @Test + fun `GIVEN prevState is SwapAmountUM Empty WHEN transform THEN returns the same state`() { + // GIVEN + val prevState = SwapAmountUM.Empty(swapDirection = SwapDirection.Direct) + val quote = buildContentQuote(fromAmount = BigDecimal("20"), toAmount = BigDecimal("1")) + val transformer = buildTransformer(quoteUM = quote) + + // WHEN + val result = transformer.transform(prevState) + + // THEN + assertThat(result).isEqualTo(prevState) + } + + private fun buildTransformer(quoteUM: SwapQuoteUM): SwapAmountSelectQuoteTransformer { + return SwapAmountSelectQuoteTransformer( + quoteUM = quoteUM, + secondaryMaximumAmountBoundary = null, + secondaryMinimumAmountBoundary = null, + isNeedApplyFCARestrictions = false, + isBalanceHidden = false, + primaryMaximumAmountBoundary = null, + primaryMinimumAmountBoundary = null, + primaryFiatRateUSD = null, + secondaryFiatRateUSD = null, + ) + } + + private fun buildContentQuote(fromAmount: BigDecimal, toAmount: BigDecimal): SwapQuoteUM.Content { + return SwapQuoteUM.Content( + provider = provider, + toAmount = toAmount, + fromAmount = fromAmount, + toAmountValue = TextReference.EMPTY, + fromAmountValue = TextReference.EMPTY, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSingleProvider = true, + rate = TextReference.EMPTY, + quoteId = null, + ) + } + + private fun buildContentState( + selectedAmountType: SwapAmountType, + primaryBalance: BigDecimal?, + ): SwapAmountUM.Content { + val primaryStatus = mockk(relaxed = true).also { status -> + every { status.value.amount } returns primaryBalance + every { status.value.fiatRate } returns null + every { status.value.fiatAmount } returns null + every { status.currency.symbol } returns "BTC" + every { status.currency.decimals } returns 8 + } + return SwapAmountUM.Content( + isPrimaryButtonEnabled = false, + swapDirection = SwapDirection.Direct, + selectedAmountType = selectedAmountType, + primaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.From), + secondaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.To), + primaryCryptoCurrencyStatus = primaryStatus, + secondaryCryptoCurrencyStatus = null, + swapRateType = ExpressRateType.Fixed, + swapRateMode = SwapRateMode.FIXED_ONLY, + priceImpact = null, + swapCurrencies = SwapCurrencies.EMPTY, + swapQuotes = persistentListOf(), + selectedQuote = SwapQuoteUM.Empty, + isShowFCAWarning = false, + appCurrency = null, + isShowBestRateAnimation = false, + ) + } +} \ No newline at end of file