diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt index 0bb1bbecda..f604f71009 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/error/OnrampQuotesErrorConverter.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.domain.onramp.model.OnrampAmount import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero internal class OnrampQuotesErrorConverter( private val jsonAdapter: JsonAdapter, @@ -35,7 +36,12 @@ internal class OnrampQuotesErrorConverter( return OnrampQuote.Error.AmountTooSmallError( paymentMethod = input.paymentMethod, provider = input.provider, - amount = createFromAmountWithOffset( + fromAmount = OnrampAmount( + symbol = input.amount.currencySymbol, + value = input.amount.value.orZero(), + decimals = input.amount.decimals, + ), + requiredAmount = createFromAmountWithOffset( amountWithOffset = minAmount, decimals = decimals, symbol = input.amount.currencySymbol, @@ -53,7 +59,12 @@ internal class OnrampQuotesErrorConverter( return OnrampQuote.Error.AmountTooBigError( paymentMethod = input.paymentMethod, provider = input.provider, - amount = createFromAmountWithOffset( + fromAmount = OnrampAmount( + symbol = input.amount.currencySymbol, + value = input.amount.value.orZero(), + decimals = input.amount.decimals, + ), + requiredAmount = createFromAmountWithOffset( amountWithOffset = maxAmount, decimals = decimals, symbol = input.amount.currencySymbol, diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt index 4499df29bd..15738335be 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -21,7 +21,16 @@ sealed interface OnrampProviderWithQuote { val availablePaymentMethods: List, ) : Unavailable - data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable - data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable + data class AvailableFrom( + override val provider: OnrampProvider, + val fromAmount: OnrampAmount, + val requiredAmount: OnrampAmount, + ) : Unavailable + + data class AvailableUpTo( + override val provider: OnrampProvider, + val fromAmount: OnrampAmount, + val requiredAmount: OnrampAmount, + ) : Unavailable } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index 9c2ab7307f..8a4619bbf8 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -19,13 +19,15 @@ sealed class OnrampQuote { data class AmountTooSmallError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: OnrampAmount, + val fromAmount: OnrampAmount, + val requiredAmount: OnrampAmount, ) : Error() data class AmountTooBigError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: OnrampAmount, + val fromAmount: OnrampAmount, + val requiredAmount: OnrampAmount, ) : Error() } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt index e1de3cdca5..55867bd7de 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -54,11 +54,13 @@ class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository when (amountError) { is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo( provider = amountError.provider, - amount = amountError.amount, + fromAmount = amountError.fromAmount, + requiredAmount = amountError.requiredAmount, ) is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom( provider = amountError.provider, - amount = amountError.amount, + fromAmount = amountError.fromAmount, + requiredAmount = amountError.requiredAmount, ) } } diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt index bc37384338..0b0dee9fce 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt @@ -4,12 +4,12 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map -import java.math.BigDecimal class GetOnrampQuotesUseCase( private val settingsRepository: SettingsRepository, @@ -24,15 +24,29 @@ class GetOnrampQuotesUseCase( quotes.groupBy { it.paymentMethod.type } .asSequence() .sortedBy { it.key.getPriority(isGooglePayAvailable) } - .map { grouped -> - grouped.value.sortedByDescending { - (it as? OnrampQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO - } - } + .sortByRate() .toList() .flatten() .right() } .catch { emit(it.left()) } } + + /** + * Sorting providers by rule: + * + * 1. Highest rate + * 2. Smallest difference between entered amount and required min/max amount + */ + private fun Sequence>>.sortByRate() = map { grouped -> + grouped.value.sortedByDescending { + when (it) { + is OnrampQuote.Data -> it.toAmount.value + + // negative difference to sort both when data and unavailable is present + is OnrampQuote.Error.AmountTooSmallError -> it.fromAmount.value - it.requiredAmount.value + is OnrampQuote.Error.AmountTooBigError -> it.requiredAmount.value - it.fromAmount.value + } + } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 0c2753785f..4c1add554e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -60,7 +60,7 @@ internal class OnrampAmountStateFactory( ) } - fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM { + fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM { val currentState = currentStateProvider() if (currentState !is OnrampMainComponentUM.Content) return currentState @@ -71,7 +71,7 @@ internal class OnrampAmountStateFactory( amountBlockState = amountState.copy( secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState), ), - providerBlockState = quote.toProviderBlockState(), + providerBlockState = quote.toProviderBlockState(isBestRate), buyButtonConfig = currentState.buyButtonConfig.copy( enabled = quote is OnrampQuote.Data, onClick = { @@ -118,11 +118,11 @@ internal class OnrampAmountStateFactory( ) } - private fun OnrampQuote.toProviderBlockState(): OnrampProviderBlockUM { + private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM { return OnrampProviderBlockUM.Content( paymentMethod = paymentMethod, providerName = provider.info.name, - isBestRate = true, + isBestRate = isBestRate, onClick = onrampIntents::openProviders, ) } @@ -136,7 +136,7 @@ internal class OnrampAmountStateFactory( OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) } is OnrampQuote.Error.AmountTooBigError -> { - val amount = this.amount.value.format { + val amount = requiredAmount.value.format { fiat( fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, @@ -150,7 +150,7 @@ internal class OnrampAmountStateFactory( ) } is OnrampQuote.Error.AmountTooSmallError -> { - val amount = this.amount.value.format { + val amount = requiredAmount.value.format { fiat( fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index ec0fbf7399..f3e4f8ab61 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -177,7 +177,9 @@ internal class OnrampMainComponentModel @Inject constructor( private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .onEach { maybeQuotes -> - val quote = maybeQuotes.getOrNull()?.firstOrNull() ?: return@onEach + val quotes = maybeQuotes.getOrNull() ?: return@onEach + val isNoQuotes = quotes.none { it is OnrampQuote.Data } + val quote = quotes.firstOrNull() ?: return@onEach if (quote is OnrampQuote.Data && lastAmount.value != quote.fromAmount.value) { lastAmount.value = quote.fromAmount.value analyticsEventHandler.send( @@ -188,7 +190,7 @@ internal class OnrampMainComponentModel @Inject constructor( ), ) } - _state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote) } + _state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote, isNoQuotes) } } .launchIn(modelScope) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index d02e063697..bcdbd2fb04 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -32,7 +32,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList") @@ -125,11 +124,11 @@ internal class SelectProviderModel @Inject constructor( } private fun List.toProvidersListItems(): ImmutableList { - val sortedByRate = sortedByDescending { - (it as? OnrampProviderWithQuote.Data)?.toAmount?.value ?: BigDecimal.ZERO - } - val bestProvider = sortedByRate.firstOrNull() - return sortedByRate.mapIndexed { index, quote -> + val sorted = sortByRate() + + val isQuoteExist = any { it is OnrampProviderWithQuote.Data } + val bestProvider = sorted.firstOrNull().takeIf { isQuoteExist } + return sorted.mapIndexed { index, quote -> when (quote) { is OnrampProviderWithQuote.Data -> { val rate = quote.toAmount.value.format { @@ -154,8 +153,8 @@ internal class SelectProviderModel @Inject constructor( ) } is OnrampProviderWithQuote.Unavailable.AvailableFrom -> { - val amount = quote.amount.value.format { - crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + val amount = quote.requiredAmount.value.format { + crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals) } ProviderListItemUM.Unavailable( providerId = quote.provider.id, @@ -165,8 +164,8 @@ internal class SelectProviderModel @Inject constructor( ) } is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> { - val amount = quote.amount.value.format { - crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + val amount = quote.requiredAmount.value.format { + crypto(symbol = quote.requiredAmount.symbol, decimals = quote.requiredAmount.decimals) } ProviderListItemUM.Unavailable( providerId = quote.provider.id, @@ -188,4 +187,21 @@ internal class SelectProviderModel @Inject constructor( } }.toImmutableList() } + + /** + * Sorting providers by rule: + * + * 1. Highest rate + * 2. Smallest difference between entered amount and required min/max amount + */ + private fun List.sortByRate() = sortedByDescending { + when (it) { + is OnrampProviderWithQuote.Data -> it.toAmount.value + + // negative difference to sort both when data and unavailable is present + is OnrampProviderWithQuote.Unavailable.AvailableFrom -> it.fromAmount.value - it.requiredAmount.value + is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> it.requiredAmount.value - it.fromAmount.value + is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> null + } + } } \ No newline at end of file