Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-30 17:36:17 +04:00
parent 9cc51216bc
commit c8c122b848
34 changed files with 383 additions and 303 deletions

View file

@ -3,7 +3,7 @@ package com.tangem.data.quotes
import com.tangem.data.quotes.store.QuotesStoreV2
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import javax.inject.Inject
/**
@ -17,9 +17,10 @@ internal class DefaultQuotesRepositoryV2 @Inject constructor(
private val quotesStore: QuotesStoreV2,
) : QuotesRepositoryV2 {
override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>? {
override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<QuoteStatus>? {
return quotesStore.getAllSyncOrNull()?.mapTo(hashSetOf()) {
it.takeIf { it.rawCurrencyId in currenciesIds } ?: Quote.Empty(it.rawCurrencyId)
it.takeIf { it.rawCurrencyId in currenciesIds }
?: QuoteStatus(rawCurrencyId = it.rawCurrencyId)
}
}
}

View file

@ -7,7 +7,7 @@ import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -61,7 +61,7 @@ internal class DefaultMultiQuoteUpdater @Inject constructor(
.filterNotNull()
.mapLatest { appCurrency ->
val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty()
.mapTo(destination = hashSetOf(), transform = Quote::rawCurrencyId)
.mapTo(destination = hashSetOf(), transform = QuoteStatus::rawCurrencyId)
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id),

View file

@ -2,7 +2,7 @@ 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 com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -24,9 +24,9 @@ internal class DefaultSingleQuoteProducer @AssistedInject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : SingleQuoteProducer {
override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId)
override val fallback: QuoteStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
override fun produce(): Flow<Quote> {
override fun produce(): Flow<QuoteStatus> {
return quotesStore.get()
.mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } }
.distinctUntilChanged()

View file

@ -3,10 +3,10 @@ package com.tangem.data.quotes.store
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.datasource.local.quote.converter.QuoteStatusConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.CoroutineScope
@ -26,7 +26,7 @@ internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
* @param dispatchers dispatchers
*/
internal class DefaultQuotesStoreV2(
private val runtimeStore: RuntimeSharedStore<Set<Quote>>,
private val runtimeStore: RuntimeSharedStore<Set<QuoteStatus>>,
private val persistenceDataStore: DataStore<CurrencyIdWithQuote>,
dispatchers: CoroutineDispatcherProvider,
) : QuotesStoreV2 {
@ -40,14 +40,14 @@ internal class DefaultQuotesStoreV2(
if (cachedStatuses.isNullOrEmpty()) return@launch
runtimeStore.store(
value = QuoteConverter(isCached = true).convertSet(input = cachedStatuses.entries),
value = QuoteStatusConverter(isCached = true).convertSet(input = cachedStatuses.entries),
)
}
}
override fun get(): Flow<Set<Quote>> = runtimeStore.get()
override fun get(): Flow<Set<QuoteStatus>> = runtimeStore.get()
override suspend fun getAllSyncOrNull(): Set<Quote>? = runtimeStore.getSyncOrNull()
override suspend fun getAllSyncOrNull(): Set<QuoteStatus>? = runtimeStore.getSyncOrNull()
override suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>) {
updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE)
@ -56,7 +56,7 @@ internal class DefaultQuotesStoreV2(
override suspend fun storeActual(values: Map<String, QuotesResponse.Quote>) {
coroutineScope {
launch {
val quotes = QuoteConverter(isCached = false).convertSet(input = values.entries)
val quotes = QuoteStatusConverter(isCached = false).convertSet(input = values.entries)
storeInRuntimeStore(values = quotes)
}
launch { storeInPersistenceStore(values = values) }
@ -66,14 +66,14 @@ internal class DefaultQuotesStoreV2(
override suspend fun storeError(currenciesIds: Set<CryptoCurrency.RawID>) {
updateStatusSourceInRuntime(
currenciesIds = currenciesIds,
ifNotFound = Quote::Empty,
ifNotFound = ::QuoteStatus,
source = StatusSource.ONLY_CACHE,
)
}
private suspend fun updateStatusSourceInRuntime(
currenciesIds: Set<CryptoCurrency.RawID>,
ifNotFound: (CryptoCurrency.RawID) -> Quote? = { null },
ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null },
source: StatusSource,
) {
runtimeStore.update(default = emptySet()) { stored ->
@ -82,14 +82,14 @@ internal class DefaultQuotesStoreV2(
?: ifNotFound(id)
?: return@mapNotNullTo null
quote.copySealed(source = source)
quote.copy(value = quote.value.copySealed(source = source))
}
stored.addOrReplace(items = updatedQuotes) { old, new -> old.rawCurrencyId == new.rawCurrencyId }
}
}
private suspend fun storeInRuntimeStore(values: Set<Quote>) {
private suspend fun storeInRuntimeStore(values: Set<QuoteStatus>) {
runtimeStore.update(default = emptySet()) { saved ->
saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId }
}

View file

@ -2,17 +2,17 @@ package com.tangem.data.quotes.store
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import kotlinx.coroutines.flow.Flow
/** Store of [Quote]'es set */
/** Store of [QuoteStatus]'es set */
internal interface QuotesStoreV2 {
/** Get flow of quotes */
fun get(): Flow<Set<Quote>>
fun get(): Flow<Set<QuoteStatus>>
/** Get all quotes synchronously or null */
suspend fun getAllSyncOrNull(): Set<Quote>?
suspend fun getAllSyncOrNull(): Set<QuoteStatus>?
/** Refresh status of [currenciesIds] */
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)

View file

@ -23,7 +23,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMultiQuoteFetcherTest {
internal class DefaultMultiQuoteStatusFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)

View file

@ -16,7 +16,7 @@ import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultMultiQuoteUpdaterTest {
internal class DefaultMultiQuoteStatusUpdaterTest {
private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk()
private val quotesStore: QuotesStoreV2 = mockk()

View file

@ -21,7 +21,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
internal class DefaultSingleQuoteFetcherTest {
internal class DefaultSingleQuoteStatusFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)

View file

@ -6,7 +6,7 @@ import com.tangem.data.quotes.store.QuotesStoreV2
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.quotes.single.SingleQuoteProducer
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
@ -19,7 +19,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
internal class DefaultSingleQuoteProducerTest {
internal class DefaultSingleQuoteStatusProducerTest {
private val params = SingleQuoteProducer.Params(
rawCurrencyId = CryptoCurrency.RawID(value = "BTC"),
@ -35,11 +35,11 @@ internal class DefaultSingleQuoteProducerTest {
@Test
fun `test that flow is mapped for network from params`() = runTest {
val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId)
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
val storeQuote = flowOf(
setOf(
status,
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
),
)
@ -57,7 +57,7 @@ internal class DefaultSingleQuoteProducerTest {
@Test
fun `test that flow is updated if quote is updated`() = runTest {
val storeQuote = MutableSharedFlow<Set<Quote>>(replay = 2, extraBufferCapacity = 1)
val storeQuote = MutableSharedFlow<Set<QuoteStatus>>(replay = 2, extraBufferCapacity = 1)
every { quotesStore.get() } returns storeQuote
@ -66,7 +66,7 @@ internal class DefaultSingleQuoteProducerTest {
verify { quotesStore.get() }
// first emit
val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId)
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
storeQuote.emit(value = setOf(status))
val values1 = getEmittedValues(flow = actual)
@ -75,11 +75,13 @@ internal class DefaultSingleQuoteProducerTest {
Truth.assertThat(values1).isEqualTo(listOf(status))
// second emit
val updatedStatus = Quote.Value(
val updatedStatus = QuoteStatus(
rawCurrencyId = params.rawCurrencyId,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
value = QuoteStatus.Data(
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
),
)
storeQuote.emit(value = setOf(updatedStatus))
@ -91,7 +93,7 @@ internal class DefaultSingleQuoteProducerTest {
@Test
fun `test that flow is filtered the same status`() = runTest {
val storeQuote = MutableSharedFlow<Set<Quote>>(replay = 2, extraBufferCapacity = 1)
val storeQuote = MutableSharedFlow<Set<QuoteStatus>>(replay = 2, extraBufferCapacity = 1)
every { quotesStore.get() } returns storeQuote
@ -100,7 +102,7 @@ internal class DefaultSingleQuoteProducerTest {
verify { quotesStore.get() }
// first emit
val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId)
val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
storeQuote.emit(value = setOf(status))
val values1 = getEmittedValues(flow = actual)
@ -120,11 +122,13 @@ internal class DefaultSingleQuoteProducerTest {
@Test
fun `test if flow throws exception`() = runTest {
val exception = IllegalStateException()
val status = Quote.Value(
val status = QuoteStatus(
rawCurrencyId = params.rawCurrencyId,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
value = QuoteStatus.Data(
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
source = StatusSource.ACTUAL,
),
)
val innerFlow = MutableStateFlow(value = false)
@ -146,7 +150,7 @@ internal class DefaultSingleQuoteProducerTest {
val values1 = getEmittedValues(flow = actual)
Truth.assertThat(values1.size).isEqualTo(1)
val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId)
val fallbackStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId)
Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus))
innerFlow.emit(value = true)
@ -160,7 +164,7 @@ internal class DefaultSingleQuoteProducerTest {
fun `test if flow doesn't contain network from params`() = runTest {
val storeFlow = flowOf(
setOf(
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
),
)

View file

@ -6,7 +6,7 @@ import com.tangem.common.test.data.quote.toDomain
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -17,7 +17,7 @@ import java.math.BigDecimal
*/
internal class QuotesStoreGetMethodTest {
private val runtimeStore = RuntimeSharedStore<Set<Quote>>()
private val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
private val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
private val store = DefaultQuotesStoreV2(
@ -32,7 +32,7 @@ internal class QuotesStoreGetMethodTest {
val values = getEmittedValues(flow = actual)
Truth.assertThat(values).isEqualTo(emptyList<Set<Quote>>())
Truth.assertThat(values).isEqualTo(emptyList<Set<QuoteStatus>>())
}
@Test
@ -43,7 +43,7 @@ internal class QuotesStoreGetMethodTest {
val values = getEmittedValues(flow = actual)
Truth.assertThat(values).isEqualTo(listOf(emptySet<Quote>()))
Truth.assertThat(values).isEqualTo(listOf(emptySet<QuoteStatus>()))
}
@Test
@ -74,7 +74,7 @@ internal class QuotesStoreGetMethodTest {
val actual = store.getAllSyncOrNull()
Truth.assertThat(actual).isEqualTo(emptySet<Quote>())
Truth.assertThat(actual).isEqualTo(emptySet<QuoteStatus>())
}
@Test

View file

@ -7,7 +7,7 @@ import com.tangem.common.test.data.quote.toDomain
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every
import io.mockk.mockk
@ -23,7 +23,7 @@ internal class QuotesStoreInitializationTest {
@Test
fun `test initialization if cache store is empty`() = runTest {
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore: DataStore<CurrencyIdWithQuote> = mockk()
every { persistenceStore.data } returns emptyFlow()
@ -39,7 +39,7 @@ internal class QuotesStoreInitializationTest {
@Test
fun `test initialization if cache store contains empty map`() = runTest {
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
DefaultQuotesStoreV2(
@ -53,7 +53,7 @@ internal class QuotesStoreInitializationTest {
@Test
fun `test initialization if cache store is not empty`() = runTest {
val runtimeStore = RuntimeSharedStore<Set<Quote>>()
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)

View file

@ -7,7 +7,7 @@ import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.QuoteStatus
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.test.runTest
@ -19,7 +19,7 @@ import java.math.BigDecimal
*/
internal class QuotesStoreUpdateMethodsTest {
private val runtimeStore = RuntimeSharedStore<Set<Quote>>()
private val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
private val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
private val store = DefaultQuotesStoreV2(
@ -37,8 +37,8 @@ internal class QuotesStoreUpdateMethodsTest {
store.refresh(currenciesIds = currenciesIds)
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet<Quote>())
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet<QuoteStatus>())
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<QuoteStatus>>())
}
@Test
@ -50,10 +50,12 @@ internal class QuotesStoreUpdateMethodsTest {
store.refresh(currenciesIds = setOf(quote.rawCurrencyId))
val runtimeExpected = setOf(quote.copySealed(source = StatusSource.CACHE))
val runtimeExpected = setOf(
quote.copy(value = quote.value.copySealed(source = StatusSource.CACHE)),
)
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<QuoteStatus>>())
}
@Test
@ -94,10 +96,12 @@ internal class QuotesStoreUpdateMethodsTest {
store.storeError(currenciesIds = currenciesIds)
val runtimeExpected = currenciesIds.map(Quote::Empty).toSet()
val runtimeExpected = currenciesIds
.map { QuoteStatus(rawCurrencyId = it, value = QuoteStatus.Empty) }
.toSet()
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<QuoteStatus>>())
}
@Test
@ -107,7 +111,7 @@ internal class QuotesStoreUpdateMethodsTest {
runtimeStore.store(
value = setOf(
status.toDomain(rawCurrencyId = "BTC", source = StatusSource.CACHE),
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty),
),
)
@ -120,10 +124,10 @@ internal class QuotesStoreUpdateMethodsTest {
val runtimeExpected = setOf(
status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE),
Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")),
QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH"), value = QuoteStatus.Empty),
)
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<Quote>>())
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<QuoteStatus>>())
}
}