Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-02 15:50:42 +02:00
parent ab6072e12d
commit d34a83ced5
8 changed files with 814 additions and 2 deletions

View file

@ -4,6 +4,10 @@ plugins {
id("configuration")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Core modules */
implementation(projects.core.analytics.models)
@ -15,4 +19,11 @@ dependencies {
api(projects.domain.core)
api(projects.domain.settings)
implementation(deps.kotlin.serialization)
/** Tests */
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.onramp.model
data class OnrampOffersBlock(
val category: OnrampOfferCategory,
val offers: List<OnrampOffer>,
val isVisible: Boolean = offers.isNotEmpty(),
)
data class OnrampOffer(
val quote: OnrampQuote,
val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default,
)
enum class OnrampOfferAdvantages {
Default, BestRate, Fastest,
}
enum class OnrampOfferCategory {
Recent, Recommended,
}

View file

@ -13,27 +13,59 @@ data class OnrampPaymentMethod(
enum class PaymentMethodType(val id: String?) {
GOOGLE_PAY(id = "google-pay"),
CARD(id = "card"),
REVOLUT_PAY(id = "invoice-revolut-pay"),
SEPA(id = "sepa"),
OTHER(id = null),
;
@Suppress("MagicNumber")
fun getPriority(isGooglePayEnabled: Boolean): Int = if (isGooglePayEnabled) {
when (this) {
GOOGLE_PAY -> 0
CARD -> 1
OTHER -> 2
SEPA -> 2
REVOLUT_PAY -> 3
OTHER -> 4
}
} else {
when (this) {
CARD -> 0
GOOGLE_PAY -> 1
OTHER -> 2
SEPA -> 2
REVOLUT_PAY -> 3
OTHER -> 4
}
}
/**
* BE AWARE. HARDCODED. Returns the speed of transaction for payment method type.
*/
fun getProcessingSpeed(): PaymentSpeed = when (this) {
REVOLUT_PAY,
GOOGLE_PAY,
-> PaymentSpeed.Instant
CARD -> PaymentSpeed.FewMin
SEPA -> PaymentSpeed.FewDays
OTHER -> PaymentSpeed.PlentyDays
}
/**
* @param speed - the lower the value, the faster the speed.
*/
@Suppress("MagicNumber")
enum class PaymentSpeed(val speed: Int) {
Instant(0), FewMin(1), FewDays(2), PlentyDays(3), Unknown(4)
}
fun isInstant(): Boolean = getProcessingSpeed() == PaymentSpeed.Instant
companion object {
fun getType(id: String): PaymentMethodType = when (id) {
GOOGLE_PAY.id -> GOOGLE_PAY
CARD.id -> CARD
REVOLUT_PAY.id -> REVOLUT_PAY
SEPA.id -> SEPA
else -> OTHER
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.onramp.model
import java.math.BigDecimal
data class OnrampPaymentMethodGroup(
val paymentMethod: OnrampPaymentMethod,
val offers: List<OnrampOffer>,
val bestRateOffer: OnrampOffer?,
val providerCount: Int,
val isBestPaymentMethod: Boolean,
) {
val bestRateAmount: BigDecimal? = bestRateOffer?.let { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
}

View file

@ -0,0 +1,76 @@
package com.tangem.domain.onramp
import arrow.core.left
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.error.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampAllOffersUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
private val settingsRepository: SettingsRepository,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): EitherFlow<OnrampError, List<OnrampPaymentMethodGroup>> {
return onrampRepository.getQuotes()
.map { quotes -> processAllOffers(quotes).right() }
.catch { throwable -> errorResolver.resolve(throwable).left() }
}
private suspend fun processAllOffers(quotes: List<OnrampQuote>): List<OnrampPaymentMethodGroup> {
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList()
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
return offersByPaymentMethod.map { (paymentMethod, methodQuotes) ->
val methodOffers = methodQuotes.map { quote ->
val advantages = if (quote == overallBestRateQuote) {
OnrampOfferAdvantages.BestRate
} else {
OnrampOfferAdvantages.Default
}
OnrampOffer(quote = quote, advantages = advantages)
}
val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value }
val groupBestRateOffer = methodOffers.find {
when (val quote = it.quote) {
is OnrampQuote.Data -> quote == groupBestRateOfferData
else -> false
}
}
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) }
}
}

View file

@ -0,0 +1,197 @@
package com.tangem.domain.onramp
import arrow.core.left
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.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
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.onramp.repositories.OnrampTransactionRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
class GetOnrampOffersUseCase(
private val onrampRepository: OnrampRepository,
private val onrampTransactionRepository: OnrampTransactionRepository,
private val errorResolver: OnrampErrorResolver,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): EitherFlow<OnrampError, List<OnrampOffersBlock>> {
return combine(
onrampRepository.getQuotes(),
onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId),
) { quotes, transactions ->
processOffers(quotes, transactions)
}
.map { offers -> offers.right() }
.catch { throwable -> errorResolver.resolve(throwable).left() }
}
private fun processOffers(
quotes: List<OnrampQuote>,
transactions: List<OnrampTransaction>,
): List<OnrampOffersBlock> {
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
if (validQuotes.isEmpty()) return emptyList()
val offers = validQuotes.map { quote ->
OnrampOffer(quote = quote)
}
val recentOffer = findRecentOffer(offers, transactions)
val bestRateOffer = findBestRateOffer(offers)
val fastestOffer = findFastestOffer(offers)
return buildOffersBlocks(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
allOffers = offers,
)
}
private fun findRecentOffer(offers: List<OnrampOffer>, transactions: List<OnrampTransaction>): OnrampOffer? {
val lastTransaction = transactions.maxByOrNull { it.timestamp } ?: return null
return offers.find { offer ->
offer.quote.provider.id == lastTransaction.providerType &&
offer.quote.paymentMethod.id == lastTransaction.paymentMethod
}
}
private fun findBestRateOffer(offers: List<OnrampOffer>): OnrampOffer? {
return offers.maxByOrNull { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
}
private fun findFastestOffer(offers: List<OnrampOffer>): OnrampOffer? {
val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() }
return if (instantOffers.isNotEmpty()) {
instantOffers.maxByOrNull { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
} else {
val offersBySpeed = offers.groupBy { offer ->
offer.quote.paymentMethod.type.getProcessingSpeed().speed
}
val fastestSpeed = offersBySpeed.keys.minOrNull() ?: return null
val fastestOffers = offersBySpeed[fastestSpeed] ?: return null
fastestOffers.maxByOrNull { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
}
}
private fun buildOffersBlocks(
recentOffer: OnrampOffer?,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
allOffers: List<OnrampOffer>,
): List<OnrampOffersBlock> {
return buildList {
if (recentOffer != null) {
add(
OnrampOffersBlock(
category = OnrampOfferCategory.Recent,
offers = listOf(
recentOffer.copy(
advantages = determineAdvantages(
recentOffer,
bestRateOffer,
fastestOffer,
),
),
),
),
)
}
val recommendedOffers = buildRecommendedOffers(
recentOffer = recentOffer,
bestRateOffer = bestRateOffer,
fastestOffer = fastestOffer,
)
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
add(
OnrampOffersBlock(
category = OnrampOfferCategory.Recommended,
offers = recommendedOffers,
),
)
}
}
}
private fun determineAdvantages(
recentOffer: OnrampOffer,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
): OnrampOfferAdvantages {
if (isSameOffer(recentOffer, bestRateOffer) && isSameOffer(recentOffer, fastestOffer)) {
return OnrampOfferAdvantages.BestRate
}
if (isSameOffer(recentOffer, bestRateOffer)) {
return OnrampOfferAdvantages.BestRate
}
if (isSameOffer(recentOffer, fastestOffer)) {
return OnrampOfferAdvantages.Fastest
}
return OnrampOfferAdvantages.Default
}
private fun buildRecommendedOffers(
recentOffer: OnrampOffer?,
bestRateOffer: OnrampOffer?,
fastestOffer: OnrampOffer?,
): List<OnrampOffer> {
return buildList {
if (isSameOffer(bestRateOffer, fastestOffer)) {
bestRateOffer?.let { offer ->
add(offer.copy(advantages = OnrampOfferAdvantages.BestRate))
}
} else {
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
add(bestRateOffer.copy(advantages = OnrampOfferAdvantages.BestRate))
}
if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) &&
!isSameOffer(fastestOffer, bestRateOffer)
) {
add(fastestOffer.copy(advantages = OnrampOfferAdvantages.Fastest))
}
}
}
}
private fun hasOnlyOneMethodAndProvider(offers: List<OnrampOffer>): Boolean {
val uniquePaymentMethods = offers.map { it.quote.paymentMethod.id }.distinct()
val uniqueProviders = offers.map { it.quote.provider.id }.distinct()
return uniquePaymentMethods.size == 1 && uniqueProviders.size == 1
}
private fun isSameOffer(offer1: OnrampOffer?, offer2: OnrampOffer?): Boolean {
if (offer1 == null || offer2 == null) return false
return offer1.quote.provider.id == offer2.quote.provider.id &&
offer1.quote.paymentMethod.id == offer2.quote.paymentMethod.id
}
}

View file

@ -0,0 +1,177 @@
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.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.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetOnrampAllOffersUseCaseTest {
private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true)
private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true)
private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true)
private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
private val userWalletId: UserWalletId = mockk(relaxUnitFun = true)
private lateinit var useCase: GetOnrampAllOffersUseCase
@BeforeEach
fun setup() {
clearMocks(onrampRepository, errorResolver, settingsRepository, cryptoCurrencyId)
useCase = GetOnrampAllOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = errorResolver,
settingsRepository = settingsRepository,
)
}
@Test
fun `invoke should return empty list when no valid quotes`() = runTest {
val emptyQuotes = listOf<OnrampQuote>()
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
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() },
)
}
coVerify { onrampRepository.getQuotes() }
}
@Test
fun `invoke should return grouped offers with best rate marked`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card")
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer")
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")),
)
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(2)
val cardGroup = offers.find { it.paymentMethod.id == "card" }
Truth.assertThat(cardGroup).isNotNull()
Truth.assertThat(cardGroup?.offers).hasSize(2)
Truth.assertThat(cardGroup?.providerCount).isEqualTo(2)
Truth.assertThat(cardGroup?.isBestPaymentMethod).isTrue()
val bestRateOffer = cardGroup?.offers?.find { it.advantages == OnrampOfferAdvantages.BestRate }
Truth.assertThat(bestRateOffer).isNotNull()
val bankGroup = offers.find { it.paymentMethod.id == "bank" }
Truth.assertThat(bankGroup).isNotNull()
Truth.assertThat(bankGroup?.offers).hasSize(1)
Truth.assertThat(bankGroup?.providerCount).isEqualTo(1)
Truth.assertThat(bankGroup?.isBestPaymentMethod).isFalse()
},
)
}
coVerify { onrampRepository.getQuotes() }
coVerify { settingsRepository.isGooglePayAvailability() }
}
@Test
fun `invoke should sort offers by toAmount descending`() = runTest {
val paymentMethod = createMockPaymentMethod("card", "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")),
)
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 group = offers.first()
Truth.assertThat(group.offers).hasSize(3)
val amounts = group.offers.map { offer ->
when (val quote = offer.quote) {
is OnrampQuote.Data -> quote.toAmount.value
else -> BigDecimal.ZERO
}
}
Truth.assertThat(amounts).containsExactly(
BigDecimal("100.0"),
BigDecimal("95.0"),
BigDecimal("90.0"),
).inOrder()
},
)
}
}
private fun createMockPaymentMethod(id: String, name: String): 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
}
}
}
private fun createMockProvider(id: String, name: String): OnrampProvider {
return mockk<OnrampProvider> {
every { this@mockk.id } returns id
every { this@mockk.info.name } returns name
}
}
private fun createMockQuote(
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
toAmount: BigDecimal,
): OnrampQuote.Data {
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
}
}
}
}

View file

@ -0,0 +1,280 @@
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.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GetOnrampOffersUseCaseTest {
private val onrampRepository: OnrampRepository = mockk(relaxUnitFun = true)
private val onrampTransactionRepository: OnrampTransactionRepository = mockk(relaxUnitFun = true)
private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true)
private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
private val userWalletId: UserWalletId = mockk(relaxUnitFun = true)
private lateinit var useCase: GetOnrampOffersUseCase
@BeforeEach
fun setup() {
clearMocks(onrampRepository, onrampTransactionRepository, errorResolver, cryptoCurrencyId)
useCase = GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
onrampTransactionRepository = onrampTransactionRepository,
errorResolver = errorResolver,
)
}
@Test
fun `invoke should return empty list when no valid quotes`() = runTest {
val emptyQuotes = listOf<OnrampQuote>()
val emptyTransactions = listOf<OnrampTransaction>()
coEvery { onrampRepository.getQuotes() } returns flowOf(emptyQuotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
emptyTransactions,
)
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() },
)
}
coVerify { onrampRepository.getQuotes() }
coVerify { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) }
}
@Test
fun `invoke should return offers blocks with recent and recommended categories`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = true)
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")),
createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")),
)
val transactions = listOf(
createMockTransaction("provider1", "card", 1000L),
)
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
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(2)
val recentBlock = offers.find { it.category == OnrampOfferCategory.Recent }
Truth.assertThat(recentBlock).isNotNull()
Truth.assertThat(recentBlock?.offers).hasSize(1)
Truth.assertThat(recentBlock?.offers?.first()?.advantages).isEqualTo(OnrampOfferAdvantages.Fastest)
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
Truth.assertThat(recommendedBlock?.offers?.first()?.advantages)
.isEqualTo(OnrampOfferAdvantages.BestRate)
},
)
}
}
@Test
fun `invoke should find best rate offer correctly`() = runTest {
val paymentMethod1 = createMockPaymentMethod("card", "Card", isInstant = false)
val paymentMethod2 = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(paymentMethod1, provider1, BigDecimal("90.0")),
createMockQuote(paymentMethod2, provider2, BigDecimal("100.0")),
createMockQuote(paymentMethod1, provider1, BigDecimal("95.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
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 recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(1)
val bestRateOffer = recommendedBlock?.offers?.first()
Truth.assertThat(bestRateOffer?.advantages).isEqualTo(OnrampOfferAdvantages.BestRate)
when (val quote = bestRateOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
else -> Truth.assertThat(false).isTrue()
}
},
)
}
}
@Test
fun `invoke should find fastest offer correctly`() = runTest {
val instantPaymentMethod = createMockPaymentMethod("card", "Card", isInstant = true)
val slowPaymentMethod = createMockPaymentMethod("bank", "Bank Transfer", isInstant = false)
val provider1 = createMockProvider("provider1", "Provider 1")
val provider2 = createMockProvider("provider2", "Provider 2")
val quotes = listOf(
createMockQuote(instantPaymentMethod, provider1, BigDecimal("90.0")),
createMockQuote(slowPaymentMethod, provider2, BigDecimal("100.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
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 recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
Truth.assertThat(recommendedBlock).isNotNull()
Truth.assertThat(recommendedBlock?.offers).hasSize(2)
val bestRateOffer = recommendedBlock
?.offers
?.find { it.advantages == OnrampOfferAdvantages.BestRate }
val fastestOffer = recommendedBlock
?.offers
?.find { it.advantages == OnrampOfferAdvantages.Fastest }
Truth.assertThat(bestRateOffer).isNotNull()
Truth.assertThat(fastestOffer).isNotNull()
when (val quote = bestRateOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
else -> Truth.assertThat(false).isTrue()
}
when (val quote = fastestOffer?.quote) {
is OnrampQuote.Data -> Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("90.0"))
else -> Truth.assertThat(false).isTrue()
}
},
)
}
}
@Test
fun `invoke should not show recommended block when only one method and provider`() = runTest {
val paymentMethod = createMockPaymentMethod("card", "Card", isInstant = false)
val provider = createMockProvider("provider1", "Provider 1")
val quotes = listOf(
createMockQuote(paymentMethod, provider, BigDecimal("100.0")),
)
val transactions = emptyList<OnrampTransaction>()
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
coEvery { onrampTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId) } returns flowOf(
transactions,
)
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()
},
)
}
}
private fun createMockPaymentMethod(id: String, name: String, isInstant: Boolean): OnrampPaymentMethod {
return mockk<OnrampPaymentMethod> {
every { this@mockk.id } returns id
every { this@mockk.name } returns name
every { this@mockk.type } returns mockk {
every { isInstant() } returns isInstant
every { getProcessingSpeed() } returns mockk {
every { speed } returns if (isInstant) 1 else 3
}
}
}
}
private fun createMockProvider(id: String, name: String): OnrampProvider {
return mockk<OnrampProvider> {
every { this@mockk.id } returns id
every { this@mockk.info.name } returns name
}
}
private fun createMockQuote(
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
toAmount: BigDecimal,
): OnrampQuote.Data {
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
}
}
}
private fun createMockTransaction(providerType: String, paymentMethod: String, timestamp: Long): OnrampTransaction {
return mockk<OnrampTransaction> {
every { this@mockk.providerType } returns providerType
every { this@mockk.paymentMethod } returns paymentMethod
every { this@mockk.timestamp } returns timestamp
}
}
}