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 5c85ec5a96..ba9858ba48 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 @@ -27,6 +27,7 @@ import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.utils.sendOnrampErrorEvent +import com.tangem.features.onramp.utils.sendProviderCalculatedEvent import com.tangem.features.onramp.utils.showDemoModeWarningIfNeeded import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -330,6 +331,10 @@ internal class OnrampMainComponentModel @Inject constructor( state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } } else -> { + analyticsEventHandler.sendProviderCalculatedEvent( + quotes = quotes, + tokenSymbol = params.cryptoCurrency.symbol, + ) state.update { prevState -> val resetState = amountStateFactory.getAmountSecondaryFieldResetState() if (prevState is OnrampMainComponentUM.Content && diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt new file mode 100644 index 0000000000..cd0f9f8480 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampQuote + +internal fun AnalyticsEventHandler.sendProviderCalculatedEvent(quotes: List, tokenSymbol: String) { + val quote = quotes.findBestRateQuote() ?: return + + send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = quote.provider.info.name, + tokenSymbol = tokenSymbol, + paymentMethod = quote.paymentMethod.name, + ), + ) +} + +private fun List.findBestRateQuote(): OnrampQuote.Data? { + return filterIsInstance().maxByOrNull { it.toAmount.value } +} \ No newline at end of file diff --git a/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt new file mode 100644 index 0000000000..1e1cc15ee8 --- /dev/null +++ b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt @@ -0,0 +1,123 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.test.core.ProvideTestModels +import io.mockk.Called +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class OnrampProviderCalculatedAnalyticsSenderTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) + + @BeforeEach + fun resetMocks() { + clearMocks(analyticsEventHandler) + } + + @ParameterizedTest + @ProvideTestModels + fun `GIVEN quotes WHEN send THEN provider calculated sent for best-rate provider`(model: SelectionModel) { + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = model.quotes, tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify(exactly = 1) { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = model.expectedProviderName, + tokenSymbol = TOKEN_SYMBOL, + paymentMethod = PAYMENT_METHOD, + ), + ) + } + } + + @Test + fun `GIVEN no quotes WHEN send THEN no event sent`() { + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = emptyList(), tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify { analyticsEventHandler wasNot Called } + } + + @Test + fun `GIVEN only non-loaded quotes WHEN send THEN no event sent`() { + // Arrange + val quotes = listOf(mockk(), mockk()) + + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = quotes, tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify { analyticsEventHandler wasNot Called } + } + + private fun provideTestModels() = listOf( + SelectionModel( + name = "highest-rate quote among several is selected", + quotes = listOf( + createQuote(providerName = "Low", rate = BigDecimal("100")), + createQuote(providerName = "High", rate = BigDecimal("120")), + createQuote(providerName = "Mid", rate = BigDecimal("90")), + ), + expectedProviderName = "High", + ), + SelectionModel( + name = "SEPA quote with lower rate is NOT prioritized, higher-rate quote wins", + quotes = listOf( + createQuote(providerName = "SepaLowerRate", rate = BigDecimal("100")), + createQuote(providerName = "CardHigherRate", rate = BigDecimal("105")), + ), + expectedProviderName = "CardHigherRate", + ), + SelectionModel( + name = "single loaded quote is selected", + quotes = listOf( + createQuote(providerName = "Single", rate = BigDecimal("100")), + ), + expectedProviderName = "Single", + ), + SelectionModel( + name = "best-rate loaded quote is selected even when error quotes are present", + quotes = listOf( + mockk(), + createQuote(providerName = "Loaded", rate = BigDecimal("100")), + mockk(), + ), + expectedProviderName = "Loaded", + ), + ) + + private fun createQuote(providerName: String, rate: BigDecimal): OnrampQuote.Data { + return mockk { + every { provider.info.name } returns providerName + every { paymentMethod.name } returns PAYMENT_METHOD + every { toAmount.value } returns rate + } + } + + internal data class SelectionModel( + val name: String, + val quotes: List, + val expectedProviderName: String, + ) { + override fun toString(): String = name + } + + private companion object { + const val TOKEN_SYMBOL = "BTC" + const val PAYMENT_METHOD = "Card" + } +} \ No newline at end of file