From 66e04bc0cd3ba8557cd110b88786159a5d68b626 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 5 Apr 2025 12:54:59 +0400 Subject: [PATCH] Updated on 2026-08-14 --- data/quotes/build.gradle.kts | 1 + .../single/DefaultSingleQuoteProducer.kt | 36 ++++ .../single/DefaultSingleQuoteProducerTest.kt | 175 ++++++++++++++++++ domain/quotes/.gitignore | 1 + domain/quotes/build.gradle.kts | 9 + .../quotes/single/SingleQuoteProducer.kt | 17 ++ .../quotes/single/SingleQuoteSupplier.kt | 17 ++ settings.gradle.kts | 1 + 8 files changed, 257 insertions(+) create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt create mode 100644 domain/quotes/.gitignore create mode 100644 domain/quotes/build.gradle.kts create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 56a4b96552..1da7f7a50e 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.quotes) implementation(projects.domain.wallets.models) implementation(deps.androidx.datastore) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt new file mode 100644 index 0000000000..346bce56ca --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt @@ -0,0 +1,36 @@ +package com.tangem.data.quotes.single + +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.tokens.model.Quote +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull + +/** + * Default implementation of [SingleQuoteProducer] + * + * @property params params + * @property quotesStore quotes store + */ +internal class DefaultSingleQuoteProducer @AssistedInject constructor( + @Assisted val params: SingleQuoteProducer.Params, + private val quotesStore: QuotesStoreV2, +) : SingleQuoteProducer { + + override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + + override fun produce(): Flow { + return quotesStore.get() + .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } + .distinctUntilChanged() + } + + @AssistedFactory + interface Factory : SingleQuoteProducer.Factory { + override fun create(params: SingleQuoteProducer.Params): DefaultSingleQuoteProducer + } +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt new file mode 100644 index 0000000000..655c97fad6 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt @@ -0,0 +1,175 @@ +package com.tangem.data.quotes.single + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.domain.models.StatusSource +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleQuoteProducerTest { + + private val params = SingleQuoteProducer.Params( + rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), + ) + + private val quotesStore = mockk() + + private val producer = DefaultSingleQuoteProducer( + params = params, + quotesStore = quotesStore, + ) + + @Test + fun `test that flow is mapped for network from params`() = runTest { + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val storeQuote = flowOf( + setOf( + status, + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ), + ) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produce() + + verify { quotesStore.get() } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(status)) + } + + @Test + fun `test that flow is updated if quote is updated`() = runTest { + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + // first emit + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + storeQuote.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + val updatedStatus = Quote.Value( + rawCurrencyId = params.rawCurrencyId, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ) + storeQuote.emit(value = setOf(updatedStatus)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus)) + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + // first emit + val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + storeQuote.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + storeQuote.emit(value = setOf(status)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val status = Quote.Value( + rawCurrencyId = params.rawCurrencyId, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ) + + val innerFlow = MutableStateFlow(value = false) + val storeQuote = flow { + if (innerFlow.value) { + emit(setOf(status)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { quotesStore.get() } returns storeQuote + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) + + innerFlow.emit(value = true) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow doesn't contain network from params`() = runTest { + val storeFlow = flowOf( + setOf( + Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + ), + ) + + every { quotesStore.get() } returns storeFlow + + val actual = producer.produceWithFallback() + + verify { quotesStore.get() } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(0) + } +} \ No newline at end of file diff --git a/domain/quotes/.gitignore b/domain/quotes/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/quotes/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/quotes/build.gradle.kts b/domain/quotes/build.gradle.kts new file mode 100644 index 0000000000..8f7c2b07d7 --- /dev/null +++ b/domain/quotes/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + api(projects.domain.core) + api(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt new file mode 100644 index 0000000000..7185ea9577 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote + +/** + * Producer of quote [CryptoCurrency.RawID] + * +[REDACTED_AUTHOR] + */ +interface SingleQuoteProducer : FlowProducer { + + data class Params(val rawCurrencyId: CryptoCurrency.RawID) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt new file mode 100644 index 0000000000..e65dbb492d --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.tokens.model.Quote + +/** + * Supplier of quote [SingleQuoteProducer.Params] + * + * @property factory factory for creating [SingleQuoteProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleQuoteSupplier( + override val factory: SingleQuoteProducer.Factory, + override val keyCreator: (SingleQuoteProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 76403e5965..68b2aa945c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -288,6 +288,7 @@ include(":domain:promo:models") include(":domain:nft") include(":domain:nft:models") include(":domain:networks") +include(":domain:quotes") // endregion Domain modules // region Data modules