Updated on 2026-08-14
This commit is contained in:
parent
d128161e53
commit
4c80e2eaa2
16 changed files with 678 additions and 192 deletions
|
|
@ -18,26 +18,6 @@ enum class PaymentMethodType(val id: String?) {
|
|||
OTHER(id = null),
|
||||
;
|
||||
|
||||
// TODO will be removed in next request [REDACTED_TASK_KEY]
|
||||
@Suppress("MagicNumber")
|
||||
fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
|
||||
when (this) {
|
||||
GOOGLE_PAY -> 0
|
||||
CARD -> 1
|
||||
REVOLUT_PAY -> 2
|
||||
SEPA -> 3
|
||||
OTHER -> 4
|
||||
}
|
||||
} else {
|
||||
when (this) {
|
||||
CARD -> 2
|
||||
REVOLUT_PAY -> 1
|
||||
SEPA -> 2
|
||||
OTHER -> 3
|
||||
GOOGLE_PAY -> 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority regardless of real speed. By business logic.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ data class OnrampPaymentMethodGroup(
|
|||
val bestRateOffer: OnrampOffer?,
|
||||
val providerCount: Int,
|
||||
val isBestPaymentMethod: Boolean,
|
||||
) {
|
||||
val methodStatus: PaymentMethodStatus,
|
||||
)
|
||||
|
||||
val bestRateAmount: BigDecimal? = bestRateOffer?.let { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote.toAmount.value
|
||||
else -> BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
sealed interface PaymentMethodStatus {
|
||||
data object Available : PaymentMethodStatus
|
||||
data class Unavailable(val availableFrom: BigDecimal) : PaymentMethodStatus
|
||||
}
|
||||
|
|
@ -5,10 +5,7 @@ import arrow.core.right
|
|||
import com.tangem.domain.core.utils.EitherFlow
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampOffer
|
||||
import com.tangem.domain.onramp.model.OnrampOfferAdvantages
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethodGroup
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.*
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
|
|
@ -35,17 +32,73 @@ class GetOnrampAllOffersUseCase(
|
|||
}
|
||||
|
||||
private suspend fun processAllOffers(quotes: List<OnrampQuote>): List<OnrampPaymentMethodGroup> {
|
||||
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
|
||||
if (validQuotes.isEmpty()) return emptyList()
|
||||
val relevantQuotes = quotes.filter { it !is OnrampQuote.Error }
|
||||
if (relevantQuotes.isEmpty()) return emptyList()
|
||||
|
||||
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
|
||||
|
||||
val overallBestRateQuote = validQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
|
||||
val validQuotes = relevantQuotes.filterIsInstance<OnrampQuote.Data>()
|
||||
val overallBestRateQuote = findOverallBestRateQuote(validQuotes, isGooglePayAvailable)
|
||||
val bestRate = overallBestRateQuote?.toAmount?.value
|
||||
|
||||
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
|
||||
val offersByPaymentMethod = relevantQuotes.groupBy { it.paymentMethod }
|
||||
|
||||
return offersByPaymentMethod.map { (paymentMethod, methodQuotes) ->
|
||||
val methodOffers = methodQuotes.map { quote ->
|
||||
return offersByPaymentMethod
|
||||
.map { (paymentMethod, methodQuotes) ->
|
||||
createPaymentMethodGroup(
|
||||
paymentMethod = paymentMethod,
|
||||
methodQuotes = methodQuotes,
|
||||
overallBestRateQuote = overallBestRateQuote,
|
||||
bestRate = bestRate,
|
||||
isGooglePayAvailable = isGooglePayAvailable,
|
||||
)
|
||||
}
|
||||
.sortedBy { it.paymentMethod.type.getPriorityForMethod(isGooglePayAvailable) }
|
||||
}
|
||||
|
||||
private fun findOverallBestRateQuote(
|
||||
validQuotes: List<OnrampQuote.Data>,
|
||||
isGooglePayAvailable: Boolean,
|
||||
): OnrampQuote.Data? {
|
||||
return validQuotes.maxWithOrNull(
|
||||
compareOffersByRateSpeedAndPriority(
|
||||
isGooglePayAvailable = isGooglePayAvailable,
|
||||
isSepaPrioritized = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPaymentMethodGroup(
|
||||
paymentMethod: OnrampPaymentMethod,
|
||||
methodQuotes: List<OnrampQuote>,
|
||||
overallBestRateQuote: OnrampQuote.Data?,
|
||||
bestRate: BigDecimal?,
|
||||
isGooglePayAvailable: Boolean,
|
||||
): OnrampPaymentMethodGroup {
|
||||
val methodOffers = methodQuotes.mapNotNull { quote ->
|
||||
createOffer(quote, overallBestRateQuote, bestRate)
|
||||
}
|
||||
|
||||
val groupBestRateQuote = findGroupBestRateQuote(methodQuotes, isGooglePayAvailable)
|
||||
val groupBestRateOffer = findBestRateOffer(methodOffers, groupBestRateQuote)
|
||||
|
||||
return OnrampPaymentMethodGroup(
|
||||
paymentMethod = paymentMethod,
|
||||
offers = sortOffers(methodOffers),
|
||||
providerCount = countUniqueProviders(methodOffers),
|
||||
bestRateOffer = groupBestRateOffer,
|
||||
isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod,
|
||||
methodStatus = determineMethodStatus(methodQuotes),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createOffer(
|
||||
quote: OnrampQuote,
|
||||
overallBestRateQuote: OnrampQuote.Data?,
|
||||
bestRate: BigDecimal?,
|
||||
): OnrampOffer? {
|
||||
return when (quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
val advantages = if (quote == overallBestRateQuote) {
|
||||
OnrampOfferAdvantages.BestRate
|
||||
} else {
|
||||
|
|
@ -54,28 +107,70 @@ class GetOnrampAllOffersUseCase(
|
|||
val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
|
||||
OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages)
|
||||
}
|
||||
|
||||
val groupBestRateOfferData =
|
||||
methodQuotes.maxWithOrNull(compareOffersByRateSpeedAndPriority(isGooglePayAvailable))
|
||||
val groupBestRateOffer = methodOffers.find {
|
||||
when (val quote = it.quote) {
|
||||
is OnrampQuote.Data -> quote == groupBestRateOfferData
|
||||
else -> false
|
||||
}
|
||||
is OnrampQuote.AmountError -> {
|
||||
OnrampOffer(quote = quote, rateDif = null, advantages = OnrampOfferAdvantages.Default)
|
||||
}
|
||||
is OnrampQuote.Error -> null
|
||||
}
|
||||
}
|
||||
|
||||
OnrampPaymentMethodGroup(
|
||||
paymentMethod = paymentMethod,
|
||||
offers = methodOffers.sortedByDescending { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote.toAmount.value
|
||||
else -> BigDecimal.ZERO
|
||||
}
|
||||
},
|
||||
providerCount = methodOffers.map { it.quote.provider.id }.distinct().size,
|
||||
bestRateOffer = groupBestRateOffer,
|
||||
isBestPaymentMethod = overallBestRateQuote?.paymentMethod == paymentMethod,
|
||||
)
|
||||
}.sortedBy { it.paymentMethod.type.getPriority(isGooglePayAvailable) }
|
||||
private fun findGroupBestRateQuote(
|
||||
validMethodQuotes: List<OnrampQuote>,
|
||||
isGooglePayAvailable: Boolean,
|
||||
): OnrampQuote? {
|
||||
val dataQuotes = validMethodQuotes.filterIsInstance<OnrampQuote.Data>()
|
||||
val amountErrorQuotes = validMethodQuotes.filterIsInstance<OnrampQuote.AmountError>()
|
||||
|
||||
return when {
|
||||
dataQuotes.isNotEmpty() -> {
|
||||
dataQuotes.maxWithOrNull(
|
||||
compareOffersByRateSpeedAndPriority(
|
||||
isGooglePayAvailable = isGooglePayAvailable,
|
||||
isSepaPrioritized = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
amountErrorQuotes.isNotEmpty() -> {
|
||||
amountErrorQuotes.minByOrNull { it.error.requiredAmount }
|
||||
}
|
||||
else -> validMethodQuotes.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findBestRateOffer(methodOffers: List<OnrampOffer>, groupBestRateQuote: OnrampQuote?): OnrampOffer? {
|
||||
if (groupBestRateQuote == null) return null
|
||||
|
||||
return methodOffers.find { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote == groupBestRateQuote
|
||||
is OnrampQuote.AmountError -> quote == groupBestRateQuote
|
||||
is OnrampQuote.Error -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortOffers(methodOffers: List<OnrampOffer>): List<OnrampOffer> {
|
||||
return methodOffers.sortedByDescending { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote.toAmount.value
|
||||
else -> BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun countUniqueProviders(methodOffers: List<OnrampOffer>): Int {
|
||||
return methodOffers.map { it.quote.provider.id }.distinct().size
|
||||
}
|
||||
|
||||
private fun determineMethodStatus(methodQuotes: List<OnrampQuote>): PaymentMethodStatus {
|
||||
val hasAtLeastOneValidQuote = methodQuotes.any { it is OnrampQuote.Data }
|
||||
return if (hasAtLeastOneValidQuote) {
|
||||
PaymentMethodStatus.Available
|
||||
} else {
|
||||
val minSum = methodQuotes
|
||||
.filterIsInstance<OnrampQuote.AmountError>()
|
||||
.minOfOrNull { it.error.requiredAmount } ?: BigDecimal.ZERO
|
||||
PaymentMethodStatus.Unavailable(minSum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,8 +77,7 @@ class GetOnrampOffersUseCase(
|
|||
|
||||
return offers.find { offer ->
|
||||
offer.quote.provider.info.name == lastTransaction.providerName &&
|
||||
offer.quote.paymentMethod.name == lastTransaction.paymentMethod &&
|
||||
lastTransaction.status.isTerminal
|
||||
offer.quote.paymentMethod.name == lastTransaction.paymentMethod && isRecentUsed(lastTransaction.status)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,4 +248,23 @@ class GetOnrampOffersUseCase(
|
|||
return offer1.quote.provider.id == offer2.quote.provider.id &&
|
||||
offer1.quote.paymentMethod.id == offer2.quote.paymentMethod.id
|
||||
}
|
||||
|
||||
private fun isRecentUsed(onrampStatus: OnrampStatus.Status): Boolean {
|
||||
return when (onrampStatus) {
|
||||
OnrampStatus.Status.Created,
|
||||
OnrampStatus.Status.Expired,
|
||||
OnrampStatus.Status.Paused,
|
||||
OnrampStatus.Status.WaitingForPayment,
|
||||
OnrampStatus.Status.PaymentProcessing,
|
||||
OnrampStatus.Status.Verifying,
|
||||
OnrampStatus.Status.Paid,
|
||||
OnrampStatus.Status.Sending,
|
||||
OnrampStatus.Status.RefundInProgress,
|
||||
-> false
|
||||
OnrampStatus.Status.Failed,
|
||||
OnrampStatus.Status.Finished,
|
||||
OnrampStatus.Status.Refunded,
|
||||
-> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,14 @@ package com.tangem.domain.onramp
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.model.OnrampAmount
|
||||
import com.tangem.domain.onramp.model.OnrampOfferAdvantages
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.OnrampProvider
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
|
|
@ -40,7 +44,7 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return empty list when no valid quotes`() = runTest {
|
||||
fun `should return empty list when no quotes`() = runTest {
|
||||
val emptyQuotes = listOf<OnrampQuote>()
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
|
||||
|
||||
|
|
@ -57,16 +61,40 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should return grouped offers with best rate marked`() = runTest {
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card")
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer")
|
||||
fun `should return empty list when only Error quotes`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider = createMockProvider("provider1", "Provider 1")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockErrorQuote(paymentMethod, provider),
|
||||
createMockErrorQuote(paymentMethod, provider),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers -> Truth.assertThat(offers).isEmpty() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should group offers by payment method with best rate marked`() = runTest {
|
||||
val paymentMethod1 = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockQuote(paymentMethod1, provider1, BigDecimal("100.0")),
|
||||
createMockQuote(paymentMethod1, provider2, BigDecimal("95.0")),
|
||||
createMockQuote(paymentMethod2, provider1, BigDecimal("98.0")),
|
||||
createMockDataQuote(paymentMethod1, provider1, BigDecimal("100.0")),
|
||||
createMockDataQuote(paymentMethod1, provider2, BigDecimal("95.0")),
|
||||
createMockDataQuote(paymentMethod2, provider1, BigDecimal("98.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
|
|
@ -86,6 +114,7 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
Truth.assertThat(cardGroup?.offers).hasSize(2)
|
||||
Truth.assertThat(cardGroup?.providerCount).isEqualTo(2)
|
||||
Truth.assertThat(cardGroup?.isBestPaymentMethod).isTrue()
|
||||
Truth.assertThat(cardGroup?.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
|
||||
val bestRateOffer = cardGroup?.offers?.find { it.advantages == OnrampOfferAdvantages.BestRate }
|
||||
Truth.assertThat(bestRateOffer).isNotNull()
|
||||
|
|
@ -95,6 +124,7 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
Truth.assertThat(bankGroup?.offers).hasSize(1)
|
||||
Truth.assertThat(bankGroup?.providerCount).isEqualTo(1)
|
||||
Truth.assertThat(bankGroup?.isBestPaymentMethod).isFalse()
|
||||
Truth.assertThat(bankGroup?.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -104,14 +134,14 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should sort offers by toAmount descending`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card")
|
||||
fun `should sort offers by toAmount descending`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider = createMockProvider("provider1", "Provider 1")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockQuote(paymentMethod, provider, BigDecimal("90.0")),
|
||||
createMockQuote(paymentMethod, provider, BigDecimal("100.0")),
|
||||
createMockQuote(paymentMethod, provider, BigDecimal("95.0")),
|
||||
createMockDataQuote(paymentMethod, provider, BigDecimal("90.0")),
|
||||
createMockDataQuote(paymentMethod, provider, BigDecimal("100.0")),
|
||||
createMockDataQuote(paymentMethod, provider, BigDecimal("95.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
|
|
@ -144,13 +174,248 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createMockPaymentMethod(id: String, name: String): OnrampPaymentMethod {
|
||||
@Test
|
||||
fun `should include AmountError offers in group`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")),
|
||||
createMockAmountErrorQuote(paymentMethod, provider2),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(cardGroup.offers).hasSize(2)
|
||||
Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
|
||||
val dataOffer = cardGroup.offers.find { it.quote is OnrampQuote.Data }
|
||||
Truth.assertThat(dataOffer).isNotNull()
|
||||
Truth.assertThat(dataOffer?.rateDif).isNull() // Best rate offer has null rateDif
|
||||
|
||||
val amountErrorOffer = cardGroup.offers.find { it.quote is OnrampQuote.AmountError }
|
||||
Truth.assertThat(amountErrorOffer).isNotNull()
|
||||
Truth.assertThat(amountErrorOffer?.rateDif).isNull()
|
||||
Truth.assertThat(amountErrorOffer?.advantages).isEqualTo(OnrampOfferAdvantages.Default)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should mark payment method as Unavailable when only AmountError offers`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockAmountErrorQuote(paymentMethod, provider1, requiredAmount = BigDecimal("100.0")),
|
||||
createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("50.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(
|
||||
cardGroup.methodStatus,
|
||||
).isEqualTo(PaymentMethodStatus.Unavailable(BigDecimal("50.0")))
|
||||
Truth.assertThat(cardGroup.offers).hasSize(2)
|
||||
|
||||
Truth.assertThat(cardGroup.bestRateOffer).isNotNull()
|
||||
val bestQuote = cardGroup.bestRateOffer?.quote as? OnrampQuote.AmountError
|
||||
Truth.assertThat(bestQuote?.error?.requiredAmount).isEqualTo(BigDecimal("50.0"))
|
||||
|
||||
Truth.assertThat(cardGroup.isBestPaymentMethod).isFalse()
|
||||
|
||||
cardGroup.offers.forEach { offer ->
|
||||
Truth.assertThat(offer.quote).isInstanceOf(OnrampQuote.AmountError::class.java)
|
||||
Truth.assertThat(offer.rateDif).isNull()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not include Error quotes in groups`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")),
|
||||
createMockErrorQuote(paymentMethod, provider2),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(cardGroup.offers).hasSize(1)
|
||||
Truth.assertThat(cardGroup.offers.first().quote).isInstanceOf(OnrampQuote.Data::class.java)
|
||||
Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should prioritize Data over AmountError for best rate`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
val provider3 = createMockProvider("provider3", "Provider 3")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockDataQuote(paymentMethod, provider1, BigDecimal("90.0")),
|
||||
createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("10.0")),
|
||||
createMockDataQuote(paymentMethod, provider3, BigDecimal("100.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(cardGroup.offers).hasSize(3)
|
||||
Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
|
||||
Truth.assertThat(cardGroup.bestRateOffer).isNotNull()
|
||||
val bestQuote = cardGroup.bestRateOffer?.quote as? OnrampQuote.Data
|
||||
Truth.assertThat(bestQuote?.toAmount?.value).isEqualTo(BigDecimal("100.0"))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should sort AmountError offers to bottom`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
val provider3 = createMockProvider("provider3", "Provider 3")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockAmountErrorQuote(paymentMethod, provider1, requiredAmount = BigDecimal("50.0")),
|
||||
createMockDataQuote(paymentMethod, provider2, BigDecimal("95.0")),
|
||||
createMockDataQuote(paymentMethod, provider3, BigDecimal("100.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(cardGroup.offers).hasSize(3)
|
||||
|
||||
Truth.assertThat(cardGroup.offers[0].quote).isInstanceOf(OnrampQuote.Data::class.java)
|
||||
Truth.assertThat(cardGroup.offers[1].quote).isInstanceOf(OnrampQuote.Data::class.java)
|
||||
Truth.assertThat(cardGroup.offers[2].quote).isInstanceOf(OnrampQuote.AmountError::class.java)
|
||||
|
||||
val firstAmount = (cardGroup.offers[0].quote as OnrampQuote.Data).toAmount.value
|
||||
val secondAmount = (cardGroup.offers[1].quote as OnrampQuote.Data).toAmount.value
|
||||
Truth.assertThat(firstAmount).isEqualTo(BigDecimal("100.0"))
|
||||
Truth.assertThat(secondAmount).isEqualTo(BigDecimal("95.0"))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle mixed Data AmountError and Error quotes correctly`() = runTest {
|
||||
val paymentMethod = createMockPaymentMethod("card", "Card", PaymentMethodType.CARD)
|
||||
val provider1 = createMockProvider("provider1", "Provider 1")
|
||||
val provider2 = createMockProvider("provider2", "Provider 2")
|
||||
val provider3 = createMockProvider("provider3", "Provider 3")
|
||||
val provider4 = createMockProvider("provider4", "Provider 4")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockDataQuote(paymentMethod, provider1, BigDecimal("100.0")),
|
||||
createMockAmountErrorQuote(paymentMethod, provider2, requiredAmount = BigDecimal("50.0")),
|
||||
createMockErrorQuote(paymentMethod, provider3),
|
||||
createMockDataQuote(paymentMethod, provider4, BigDecimal("95.0")),
|
||||
)
|
||||
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
|
||||
val result = useCase(userWalletId, cryptoCurrencyId)
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val cardGroup = offers.first()
|
||||
Truth.assertThat(cardGroup.offers).hasSize(3)
|
||||
Truth.assertThat(cardGroup.methodStatus).isEqualTo(PaymentMethodStatus.Available)
|
||||
|
||||
val dataOffers = cardGroup.offers.filter { it.quote is OnrampQuote.Data }
|
||||
val amountErrorOffers = cardGroup.offers.filter { it.quote is OnrampQuote.AmountError }
|
||||
val errorOffers = cardGroup.offers.filter { it.quote is OnrampQuote.Error }
|
||||
|
||||
Truth.assertThat(dataOffers).hasSize(2)
|
||||
Truth.assertThat(amountErrorOffers).hasSize(1)
|
||||
Truth.assertThat(errorOffers).isEmpty()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockPaymentMethod(id: String, name: String, type: PaymentMethodType): OnrampPaymentMethod {
|
||||
return mockk<OnrampPaymentMethod> {
|
||||
every { this@mockk.id } returns id
|
||||
every { this@mockk.name } returns name
|
||||
every { this@mockk.type } returns mockk {
|
||||
every { getPriority(any()) } returns 1
|
||||
}
|
||||
every { this@mockk.type } returns type
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +426,7 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createMockQuote(
|
||||
private fun createMockDataQuote(
|
||||
paymentMethod: OnrampPaymentMethod,
|
||||
provider: OnrampProvider,
|
||||
toAmount: BigDecimal,
|
||||
|
|
@ -169,9 +434,44 @@ class GetOnrampAllOffersUseCaseTest {
|
|||
return mockk<OnrampQuote.Data> {
|
||||
every { this@mockk.paymentMethod } returns paymentMethod
|
||||
every { this@mockk.provider } returns provider
|
||||
every { this@mockk.toAmount } returns mockk {
|
||||
every { value } returns toAmount
|
||||
every { this@mockk.toAmount } returns createMockAmount(toAmount)
|
||||
every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0"))
|
||||
every { this@mockk.countryCode } returns "US"
|
||||
every { this@mockk.minFromAmount } returns null
|
||||
every { this@mockk.maxFromAmount } returns null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockAmountErrorQuote(
|
||||
paymentMethod: OnrampPaymentMethod,
|
||||
provider: OnrampProvider,
|
||||
requiredAmount: BigDecimal = BigDecimal("50.0"),
|
||||
): OnrampQuote.AmountError {
|
||||
return mockk<OnrampQuote.AmountError> {
|
||||
every { this@mockk.paymentMethod } returns paymentMethod
|
||||
every { this@mockk.provider } returns provider
|
||||
every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0"))
|
||||
every { this@mockk.countryCode } returns "US"
|
||||
every { this@mockk.error } returns mockk<OnrampError.AmountError> {
|
||||
every { this@mockk.requiredAmount } returns requiredAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockErrorQuote(paymentMethod: OnrampPaymentMethod, provider: OnrampProvider): OnrampQuote.Error {
|
||||
return mockk<OnrampQuote.Error> {
|
||||
every { this@mockk.paymentMethod } returns paymentMethod
|
||||
every { this@mockk.provider } returns provider
|
||||
every { this@mockk.fromAmount } returns createMockAmount(BigDecimal("10.0"))
|
||||
every { this@mockk.countryCode } returns "US"
|
||||
every { this@mockk.error } returns mockk<OnrampError>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockAmount(value: BigDecimal): OnrampAmount {
|
||||
return mockk<OnrampAmount> {
|
||||
every { this@mockk.value } returns value
|
||||
every { this@mockk.symbol } returns "USD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -285,7 +285,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
every { this@mockk.providerName } returns providerName
|
||||
every { this@mockk.paymentMethod } returns paymentMethod
|
||||
every { this@mockk.timestamp } returns timestamp
|
||||
every { this@mockk.status.isTerminal } returns true
|
||||
every { this@mockk.status } returns OnrampStatus.Status.Finished
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
|
||||
|
|
@ -14,6 +15,7 @@ import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
|||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -23,42 +25,64 @@ internal class AllOffersStateFactory(
|
|||
private val allOffersIntents: AllOffersIntents,
|
||||
) {
|
||||
|
||||
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>): AllOffersStateUM {
|
||||
fun getLoadedPaymentsState(methodGroups: List<OnrampPaymentMethodGroup>, currencyCode: String): AllOffersStateUM {
|
||||
return AllOffersStateUM.Content(
|
||||
methods = methodGroups.map { methodGroup ->
|
||||
AllOffersPaymentMethodUM(
|
||||
offers = mapOffersToUM(methodGroup.offers).toPersistentList(),
|
||||
methodConfig = OnrampPaymentMethodConfig(
|
||||
method = methodGroup.paymentMethod,
|
||||
onClick = { allOffersIntents.onPaymentMethodClicked(methodGroup.paymentMethod.id) },
|
||||
),
|
||||
diff = methodGroup
|
||||
.bestRateOffer
|
||||
?.rateDif
|
||||
?.takeIf { it > BigDecimal.ZERO }
|
||||
?.let { diff ->
|
||||
stringReference("$MINUS${diff.format { percent() }}")
|
||||
},
|
||||
rate = methodGroup.bestRateOffer?.let { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> quote.toAmount.value.format {
|
||||
crypto(
|
||||
symbol = quote.toAmount.symbol,
|
||||
decimals = quote.toAmount.decimals,
|
||||
)
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
} ?: "",
|
||||
providersCount = methodGroup.providerCount,
|
||||
isBestRate = methodGroup.isBestPaymentMethod,
|
||||
)
|
||||
methods = methodGroups.map { group ->
|
||||
createPaymentMethodUM(group, currencyCode)
|
||||
}.toPersistentList(),
|
||||
currentMethod = null,
|
||||
currentMethod = replaceOffersForCurrentMethod(methodGroups, currencyCode),
|
||||
onBackClicked = { allOffersIntents.onBackClicked() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPaymentMethodUM(
|
||||
methodGroup: OnrampPaymentMethodGroup,
|
||||
currencyCode: String,
|
||||
): AllOffersPaymentMethodUM {
|
||||
return AllOffersPaymentMethodUM(
|
||||
offers = mapOffersToUM(methodGroup.offers, currencyCode).toPersistentList(),
|
||||
methodConfig = createMethodConfig(methodGroup.paymentMethod),
|
||||
diff = formatRateDiff(methodGroup.bestRateOffer?.rateDif),
|
||||
rate = formatBestRate(methodGroup.bestRateOffer, currencyCode),
|
||||
providersCount = methodGroup.providerCount,
|
||||
isBestRate = methodGroup.isBestPaymentMethod,
|
||||
paymentMethodStatus = methodGroup.methodStatus,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMethodConfig(paymentMethod: OnrampPaymentMethod): OnrampPaymentMethodConfig {
|
||||
return OnrampPaymentMethodConfig(
|
||||
method = paymentMethod,
|
||||
onClick = { allOffersIntents.onPaymentMethodClicked(paymentMethod.id) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatRateDiff(rateDif: BigDecimal?) = rateDif
|
||||
?.takeIf { it > BigDecimal.ZERO }
|
||||
?.let { diff -> stringReference("$MINUS${diff.format { percent() }}") }
|
||||
|
||||
private fun formatBestRate(bestRateOffer: OnrampOffer?, currencyCode: String): String {
|
||||
return when (val quote = bestRateOffer?.quote) {
|
||||
is OnrampQuote.Data -> formatCryptoAmount(quote.toAmount)
|
||||
is OnrampQuote.AmountError -> formatRequiredAmount(quote, currencyCode)
|
||||
is OnrampQuote.Error,
|
||||
null,
|
||||
-> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatCryptoAmount(amount: OnrampAmount): String {
|
||||
return amount.value.format {
|
||||
crypto(symbol = amount.symbol, decimals = amount.decimals)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRequiredAmount(quote: OnrampQuote.AmountError, currencyCode: String): String {
|
||||
return quote.error.requiredAmount.format {
|
||||
fiat(fiatCurrencySymbol = quote.fromAmount.symbol, fiatCurrencyCode = currencyCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPaymentsState(): AllOffersStateUM {
|
||||
return when (val currentState = currentStateProvider.invoke()) {
|
||||
is AllOffersStateUM.Content -> {
|
||||
|
|
@ -72,34 +96,36 @@ internal class AllOffersStateFactory(
|
|||
}
|
||||
|
||||
fun getOnrampErrorState(onrampError: OnrampError): AllOffersStateUM {
|
||||
return when (onrampError) {
|
||||
is OnrampError.DataError -> getErrorState(
|
||||
errorCode = onrampError.code,
|
||||
onRefresh = allOffersIntents::onRefresh,
|
||||
)
|
||||
OnrampError.PairsNotFound,
|
||||
is OnrampError.DomainError,
|
||||
-> getErrorState(onRefresh = allOffersIntents::onRefresh)
|
||||
is OnrampError.AmountError.TooBigError,
|
||||
is OnrampError.AmountError.TooSmallError,
|
||||
OnrampError.RedirectError.VerificationFailed,
|
||||
OnrampError.RedirectError.WrongRequestId,
|
||||
-> currentStateProvider()
|
||||
return if (shouldShowErrorState(onrampError)) {
|
||||
createErrorState(errorCode = (onrampError as? OnrampError.DataError)?.code)
|
||||
} else {
|
||||
currentStateProvider()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): AllOffersStateUM {
|
||||
val state = currentStateProvider()
|
||||
return when (state) {
|
||||
private fun shouldShowErrorState(error: OnrampError): Boolean {
|
||||
return when (error) {
|
||||
is OnrampError.DataError,
|
||||
OnrampError.PairsNotFound,
|
||||
is OnrampError.DomainError,
|
||||
-> true
|
||||
is OnrampError.AmountError,
|
||||
is OnrampError.RedirectError,
|
||||
-> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createErrorState(errorCode: String? = null): AllOffersStateUM {
|
||||
return when (currentStateProvider()) {
|
||||
is AllOffersStateUM.Content,
|
||||
AllOffersStateUM.Loading,
|
||||
-> AllOffersStateUM.Error(
|
||||
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
|
||||
errorCode = errorCode,
|
||||
onRefresh = onRefresh,
|
||||
onRefresh = allOffersIntents::onRefresh,
|
||||
),
|
||||
)
|
||||
is AllOffersStateUM.Error -> state
|
||||
is AllOffersStateUM.Error -> currentStateProvider()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,52 +134,70 @@ internal class AllOffersStateFactory(
|
|||
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||
OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.Default
|
||||
OnrampOfferAdvantages.GreatRate -> OnrampOfferAdvantagesUM.GreatRate
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapOffersToUM(offers: List<OnrampOffer>): List<OnrampOfferUM> {
|
||||
return buildList {
|
||||
offers.forEach { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
add(
|
||||
OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
paymentMethod = quote.paymentMethod,
|
||||
providerName = quote.provider.info.name,
|
||||
rate = quote.toAmount.value.format {
|
||||
crypto(
|
||||
symbol = quote.toAmount.symbol,
|
||||
decimals = quote.toAmount.decimals,
|
||||
)
|
||||
},
|
||||
diff = offer
|
||||
.rateDif
|
||||
?.takeIf { it > BigDecimal.ZERO }
|
||||
?.let { diff ->
|
||||
stringReference("$MINUS${diff.format { percent() }}")
|
||||
},
|
||||
onBuyClicked = {
|
||||
allOffersIntents.onBuyClick(
|
||||
quote = OnrampProviderWithQuote.Data(
|
||||
provider = quote.provider,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
toAmount = quote.toAmount,
|
||||
fromAmount = quote.fromAmount,
|
||||
),
|
||||
onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is OnrampQuote.AmountError,
|
||||
is OnrampQuote.Error,
|
||||
-> Unit
|
||||
}
|
||||
private fun mapOffersToUM(offers: List<OnrampOffer>, currencyCode: String): List<OnrampOfferUM> {
|
||||
return offers.mapNotNull { offer ->
|
||||
when (val quote = offer.quote) {
|
||||
is OnrampQuote.Data -> createDataOfferUM(quote, offer)
|
||||
is OnrampQuote.AmountError -> createAmountErrorOfferUM(quote, offer, currencyCode)
|
||||
is OnrampQuote.Error -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDataOfferUM(quote: OnrampQuote.Data, offer: OnrampOffer): OnrampOfferUM {
|
||||
return OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
paymentMethod = quote.paymentMethod,
|
||||
providerName = quote.provider.info.name,
|
||||
rate = formatCryptoAmount(quote.toAmount),
|
||||
diff = formatRateDiff(offer.rateDif),
|
||||
onBuyClicked = {
|
||||
allOffersIntents.onBuyClick(
|
||||
quote = OnrampProviderWithQuote.Data(
|
||||
provider = quote.provider,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
toAmount = quote.toAmount,
|
||||
fromAmount = quote.fromAmount,
|
||||
),
|
||||
onrampOfferAdvantagesUM = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAmountErrorOfferUM(
|
||||
quote: OnrampQuote.AmountError,
|
||||
offer: OnrampOffer,
|
||||
currencyCode: String,
|
||||
): OnrampOfferUM {
|
||||
return OnrampOfferUM(
|
||||
category = OnrampOfferCategoryUM.Recommended,
|
||||
advantages = OnrampOfferAdvantagesUM.Unavailable,
|
||||
paymentMethod = quote.paymentMethod,
|
||||
providerName = quote.provider.info.name,
|
||||
rate = formatRequiredAmount(quote, currencyCode),
|
||||
diff = formatRateDiff(offer.rateDif),
|
||||
onBuyClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceOffersForCurrentMethod(
|
||||
methodGroups: List<OnrampPaymentMethodGroup>,
|
||||
currencyCode: String,
|
||||
): AllOffersPaymentMethodUM? {
|
||||
val currentMethod = (currentStateProvider() as? AllOffersStateUM.Content)?.currentMethod ?: return null
|
||||
|
||||
val updateForCurrentMethod = methodGroups.find { it.paymentMethod.id == currentMethod.methodConfig.method.id }
|
||||
|
||||
return updateForCurrentMethod?.let { updatedCurrentMethod ->
|
||||
currentMethod.copy(
|
||||
offers = mapOffersToUM(updatedCurrentMethod.offers, currencyCode).toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.onramp.alloffers.entity
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ internal data class AllOffersPaymentMethodUM(
|
|||
val rate: String,
|
||||
val providersCount: Int,
|
||||
val isBestRate: Boolean,
|
||||
val paymentMethodStatus: PaymentMethodStatus,
|
||||
)
|
||||
|
||||
internal data class OnrampPaymentMethodConfig(
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ internal class AllOffersModel @Inject constructor(
|
|||
providerName = quote.provider.info.name,
|
||||
paymentMethodName = quote.paymentMethod.name,
|
||||
)?.let { analyticsEventHandler::send }
|
||||
dismiss()
|
||||
params.openRedirectPage(quote)
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +96,12 @@ internal class AllOffersModel @Inject constructor(
|
|||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offersGroup ->
|
||||
_state.update { stateFactory.getLoadedPaymentsState(offersGroup) }
|
||||
_state.update {
|
||||
stateFactory.getLoadedPaymentsState(
|
||||
methodGroups = offersGroup,
|
||||
currencyCode = params.amountCurrencyCode,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM
|
||||
|
|
@ -237,6 +238,7 @@ private fun AllOffersContentSheetPaymentPreview() {
|
|||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
paymentMethodStatus = PaymentMethodStatus.Available,
|
||||
)
|
||||
|
||||
TangemThemePreview {
|
||||
|
|
@ -302,6 +304,7 @@ private fun AllOffersContentSheetOffersPreview() {
|
|||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
paymentMethodStatus = PaymentMethodStatus.Available,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags
|
||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||
import com.tangem.domain.onramp.model.PaymentMethodStatus
|
||||
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
|
||||
import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig
|
||||
|
|
@ -89,21 +90,50 @@ private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier
|
|||
SpacerW(12.dp)
|
||||
|
||||
Column {
|
||||
PaymentMethodInfoBlock(
|
||||
paymentMethodName = methodUM.methodConfig.method.name,
|
||||
rate = methodUM.rate,
|
||||
diff = methodUM.diff,
|
||||
isBestRate = methodUM.isBestRate,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
|
||||
SpacerW(8.dp)
|
||||
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
|
||||
when (methodUM.paymentMethodStatus) {
|
||||
PaymentMethodStatus.Available -> {
|
||||
PaymentMethodInfoBlock(
|
||||
paymentMethodName = methodUM.methodConfig.method.name,
|
||||
rate = methodUM.rate,
|
||||
diff = methodUM.diff,
|
||||
isBestRate = methodUM.isBestRate,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ProvidersCountBlockInfo(providersCount = methodUM.providersCount)
|
||||
SpacerW(8.dp)
|
||||
TimingBlockInfo(speed = methodUM.methodConfig.method.type.getProcessingSpeed())
|
||||
}
|
||||
}
|
||||
is PaymentMethodStatus.Unavailable -> {
|
||||
UnavailablePaymentMethodInfoBlock(
|
||||
paymentMethodName = methodUM.methodConfig.method.name,
|
||||
errorAmount = methodUM.rate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnavailablePaymentMethodInfoBlock(paymentMethodName: String, errorAmount: String) {
|
||||
Column(modifier = Modifier.padding(bottom = 14.dp)) {
|
||||
Text(
|
||||
text = paymentMethodName,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_NAME),
|
||||
)
|
||||
SpacerH(2.dp)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onramp_provider_min_amount, errorAmount),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.UP_TO_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaymentMethodInfoBlock(
|
||||
paymentMethodName: String,
|
||||
|
|
@ -267,6 +297,7 @@ private fun PaymentMethodsContentPreview() {
|
|||
rate = "0,0245334 BTC",
|
||||
providersCount = 2,
|
||||
isBestRate = true,
|
||||
paymentMethodStatus = PaymentMethodStatus.Available,
|
||||
)
|
||||
TangemThemePreview {
|
||||
PaymentMethodsContent(persistentListOf(method))
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
|||
cryptoCurrency = params.cryptoCurrency,
|
||||
country = config.country,
|
||||
launchSepa = false,
|
||||
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable()
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create(
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ internal class OnrampV2AmountStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getAmountSecondaryFieldUpdatedState(quotes: List<OnrampQuote>): OnrampV2MainComponentUM {
|
||||
fun getSecondaryFieldAmountErrorState(quotes: List<OnrampQuote>): OnrampV2MainComponentUM {
|
||||
val currentState = currentStateProvider()
|
||||
if (currentState !is OnrampV2MainComponentUM.Content) return currentState
|
||||
|
||||
|
|
@ -82,6 +82,7 @@ internal class OnrampV2AmountStateFactory(
|
|||
?: OnrampSecondaryFieldErrorUM.Empty,
|
||||
),
|
||||
errorNotification = null,
|
||||
offersBlockState = OnrampOffersBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,9 +229,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
maybeOffers.fold(
|
||||
ifLeft = ::handleOnrampError,
|
||||
ifRight = { offers ->
|
||||
if (offers.isEmpty()) {
|
||||
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
} else {
|
||||
if (offers.isNotEmpty()) {
|
||||
_state.update { onrampOffersStateFactory.getOffersState(offers) }
|
||||
}
|
||||
},
|
||||
|
|
@ -285,10 +283,16 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
|||
|
||||
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
|
||||
sendOnrampQuotesErrorAnalytic(quotes)
|
||||
if (quotes.all { it is OnrampQuote.AmountError }) {
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldUpdatedState(quotes) }
|
||||
} else {
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
|
||||
when {
|
||||
quotes.isEmpty() -> {
|
||||
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
|
||||
}
|
||||
quotes.all { it is OnrampQuote.AmountError } -> {
|
||||
_state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) }
|
||||
}
|
||||
else -> {
|
||||
_state.update { amountStateFactory.getAmountSecondaryFieldResetState() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,8 @@ private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifi
|
|||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
OnrampAmountContentLoading()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier)
|
|||
.size(TangemTheme.dimens.size40)
|
||||
.clip(TangemTheme.shapes.roundedCorners8)
|
||||
.background(TangemColorPalette.Light1)
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.padding(TangemTheme.dimens.spacing4)
|
||||
.testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(imageUrl)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue