Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-05 12:54:59 +04:00
parent 5e83a20d1a
commit 66e04bc0cd
8 changed files with 257 additions and 0 deletions

View file

@ -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)

View file

@ -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<Quote> {
return quotesStore.get()
.mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } }
.distinctUntilChanged()
}
@AssistedFactory
interface Factory : SingleQuoteProducer.Factory {
override fun create(params: SingleQuoteProducer.Params): DefaultSingleQuoteProducer
}
}

View file

@ -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<QuotesStoreV2>()
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<Set<Quote>>(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<Set<Quote>>(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)
}
}

1
domain/quotes/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
api(projects.domain.core)
api(projects.domain.tokens.models)
}

View file

@ -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<Quote> {
data class Params(val rawCurrencyId: CryptoCurrency.RawID)
interface Factory : FlowProducer.Factory<Params, SingleQuoteProducer>
}

View file

@ -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<SingleQuoteProducer, SingleQuoteProducer.Params, Quote>()

View file

@ -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