Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-20 18:10:16 +04:00
parent f3f1d88d1b
commit f57fc16ff6
29 changed files with 107 additions and 264 deletions

View file

@ -3,20 +3,14 @@ package com.tangem.common.test.data.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.utils.extensions.orZero
fun QuotesResponse.Quote.toDomain(
rawCurrencyId: String,
source: StatusSource = StatusSource.ACTUAL,
fiatCurrency: FiatCurrency = FiatCurrency.Default,
): QuoteStatus {
fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
return QuoteStatus(
rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId),
value = QuoteStatus.Data(
source = source,
fiatCurrency = fiatCurrency,
fiatRate = price.orZero(),
priceChange = priceChange24h.orZero().movePointLeft(2),
fiatRateUSD = priceUsd.orZero(),
@ -24,9 +18,6 @@ fun QuotesResponse.Quote.toDomain(
)
}
fun Pair<String, QuotesResponse.Quote>.toDomain(
source: StatusSource = StatusSource.ACTUAL,
fiatCurrency: FiatCurrency = FiatCurrency.Default,
): QuoteStatus {
return second.toDomain(rawCurrencyId = first, source = source, fiatCurrency = fiatCurrency)
fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
return second.toDomain(rawCurrencyId = first, source = source)
}

View file

@ -8,7 +8,6 @@ import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
@ -95,7 +94,6 @@ internal class HotCryptoCurrencyConverter(
QuoteStatus(
rawCurrencyId = rawCurrencyId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default, // hot crypto rates are quoted in USD
fiatRate = fiatRate,
fiatRateUSD = BigDecimal.ZERO,
priceChange = priceChange.movePointLeft(2),

View file

@ -1,20 +0,0 @@
package com.tangem.data.quotes.converter
import com.tangem.data.quotes.store.QuoteStatusDM
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.utils.converter.TwoWayConverter
/**
* Two-way converter between domain [FiatCurrency] and persisted [QuoteStatusDM.FiatCurrency].
*
* - [convert] domain DM (for persistence).
* - [convertBack] DM domain (for restore on cold start).
*/
internal object FiatCurrencyConverter : TwoWayConverter<FiatCurrency, QuoteStatusDM.FiatCurrency> {
override fun convert(value: FiatCurrency): QuoteStatusDM.FiatCurrency =
QuoteStatusDM.FiatCurrency(code = value.code, symbol = value.symbol)
override fun convertBack(value: QuoteStatusDM.FiatCurrency): FiatCurrency =
FiatCurrency(code = value.code, symbol = value.symbol)
}

View file

@ -3,7 +3,6 @@ package com.tangem.data.quotes.converter
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
@ -12,13 +11,11 @@ import com.tangem.utils.extensions.orZero
* Converter from [QuotesResponse.Quote] to [QuoteStatus]
*
* @property source status source
* @property fiatCurrency fiat currency in which the quote is expressed
*
[REDACTED_AUTHOR]
*/
internal class QuoteStatusConverter(
private val source: StatusSource,
private val fiatCurrency: FiatCurrency,
) : Converter<Map.Entry<String, QuotesResponse.Quote>, QuoteStatus> {
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): QuoteStatus {
@ -28,7 +25,6 @@ internal class QuoteStatusConverter(
rawCurrencyId = CryptoCurrency.RawID(currencyId),
value = QuoteStatus.Data(
source = source,
fiatCurrency = fiatCurrency,
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
fiatRateUSD = quote.priceUsd.orZero(),

View file

@ -4,16 +4,16 @@ import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater
import com.tangem.data.quotes.repository.DefaultQuotesRepository
import com.tangem.data.quotes.store.DefaultQuotesStatusesStore
import com.tangem.data.quotes.store.QuoteStatusDM
import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
@ -30,7 +30,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object QuotesDataModule {
@OptIn(ExperimentalStdlibApi::class)
@Singleton
@Provides
fun provideQuotesStoreV2(
@ -42,13 +41,13 @@ internal object QuotesDataModule {
runtimeStore = RuntimeSharedStore(),
persistenceDataStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
defaultValue = QuoteStatusDM.Empty,
adapter = moshi.adapter<QuoteStatusDM>(),
moshi = moshi,
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "quotes_v2") },
produceFile = { context.dataStoreFile(fileName = "quotes") },
scope = appScope,
),
legacyCacheFile = context.dataStoreFile(fileName = "quotes"),
scope = appScope,
)
}

View file

@ -8,11 +8,9 @@ import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.data.quotes.store.setSourceAsCache
import com.tangem.data.quotes.store.setSourceAsOnlyCache
import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
@ -52,10 +50,10 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
),
)
val (fiatCurrencyId, fiatCurrency) = resolveFiatCurrency()
val appCurrencyId = getAppCurrencyId(params = params)
val response = quotesFetcher.fetch(
fiatCurrencyId = fiatCurrencyId,
fiatCurrencyId = appCurrencyId,
currenciesIds = replacementIdsResult.idsForRequest,
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H, Field.PRICE_USD),
)
@ -66,27 +64,24 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
filteredIds = replacementIdsResult.idsFiltered,
)
quotesStatusesStore.store(values = updatedResponse.quotes, fiatCurrency = fiatCurrency)
quotesStatusesStore.store(values = updatedResponse.quotes)
}
.onLeft { throwable ->
TangemLogger.e("Error", throwable)
quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
}
private suspend fun resolveFiatCurrency(): Pair<String, FiatCurrency> {
val stored = appCurrencyResponseStore.getSyncOrNull() ?: failOnMissingAppCurrency()
if (stored.id.isBlank()) failOnMissingAppCurrency()
private suspend fun getAppCurrencyId(params: MultiQuoteStatusFetcher.Params): String {
val appCurrencyId = params.appCurrencyId
?: appCurrencyResponseStore.getSyncOrNull()?.id
return stored.id to stored.toFiatCurrency()
}
private fun failOnMissingAppCurrency(): Nothing {
if (appCurrencyId.isNullOrBlank()) {
val exception = IllegalStateException("Unable to get AppCurrency for updating quotes")
TangemLogger.e("Error", exception)
throw exception
}
private fun CurrenciesResponse.Currency.toFiatCurrency(): FiatCurrency {
return FiatCurrency(code = code, symbol = unit)
return appCurrencyId
}
}

View file

@ -1,13 +1,11 @@
package com.tangem.data.quotes.store
import androidx.datastore.core.DataStore
import com.tangem.data.quotes.converter.FiatCurrencyConverter
import com.tangem.data.quotes.converter.QuoteStatusConverter
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.extensions.addOrReplace
@ -16,7 +14,6 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import java.io.File
internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
@ -24,46 +21,27 @@ internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
* Default implementation of [QuotesStatusesStore]
*
* @property runtimeStore runtime store
* @property persistenceDataStore persistence store (keeps quotes together with the fiat currency they're expressed in)
* @property legacyCacheFile pre-v2 cache file kept on disk; deleted once on init
* @param scope app coroutine scope
* @property persistenceDataStore persistence store
* @param dispatchers dispatchers
*/
internal class DefaultQuotesStatusesStore(
private val runtimeStore: RuntimeSharedStore<Set<QuoteStatus>>,
private val persistenceDataStore: DataStore<QuoteStatusDM>,
private val legacyCacheFile: File,
private val persistenceDataStore: DataStore<CurrencyIdWithQuote>,
private val scope: AppCoroutineScope,
) : QuotesStatusesStore {
init {
scope.launch {
deleteLegacyCacheFile()
val cachedStatuses = persistenceDataStore.data.firstOrNull()
val cached = persistenceDataStore.data.firstOrNull() ?: return@launch
val fiatCurrency = cached.fiatCurrency?.let(FiatCurrencyConverter::convertBack) ?: return@launch
if (cached.quotes.isEmpty()) return@launch
if (cachedStatuses.isNullOrEmpty()) return@launch
runtimeStore.store(
value = QuoteStatusConverter(source = StatusSource.CACHE, fiatCurrency = fiatCurrency)
.convertSet(input = cached.quotes.entries),
value = QuoteStatusConverter(source = StatusSource.CACHE).convertSet(input = cachedStatuses.entries),
)
}
}
private fun deleteLegacyCacheFile() {
if (!legacyCacheFile.exists()) return
runCatching { legacyCacheFile.delete() }
.onSuccess { deleted ->
if (deleted) {
TangemLogger.i("Deleted legacy quotes cache file: ${legacyCacheFile.name}")
} else {
TangemLogger.e("Could not delete legacy quotes cache file: ${legacyCacheFile.name}")
}
}
.onFailure { TangemLogger.e("Failed to delete legacy quotes cache file", it) }
}
override fun get(): Flow<Set<QuoteStatus>> = runtimeStore.get()
override suspend fun getAllSyncOrNull(): Set<QuoteStatus>? = runtimeStore.getSyncOrNull()
@ -103,33 +81,24 @@ internal class DefaultQuotesStatusesStore(
}
}
override suspend fun store(values: CurrencyIdWithQuote, fiatCurrency: FiatCurrency) {
override suspend fun store(values: CurrencyIdWithQuote) {
if (values.isEmpty()) return
coroutineScope {
launch { storeInRuntime(values = values, fiatCurrency = fiatCurrency) }
launch { storeInPersistence(values = values, fiatCurrency = fiatCurrency) }
launch { storeInRuntime(values = values) }
launch { storeInPersistence(values = values) }
}
}
private suspend fun storeInRuntime(values: CurrencyIdWithQuote, fiatCurrency: FiatCurrency) {
val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL, fiatCurrency = fiatCurrency)
.convertSet(input = values.entries)
private suspend fun storeInRuntime(values: CurrencyIdWithQuote) {
val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL).convertSet(input = values.entries)
runtimeStore.update(default = emptySet()) { saved ->
saved.addOrReplace(items = quotes) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId }
}
}
private suspend fun storeInPersistence(values: CurrencyIdWithQuote, fiatCurrency: FiatCurrency) {
persistenceDataStore.updateData { stored ->
val isSameCurrency = stored.fiatCurrency?.code == fiatCurrency.code
val mergedQuotes = if (isSameCurrency) stored.quotes + values else values
QuoteStatusDM(
fiatCurrency = FiatCurrencyConverter.convert(fiatCurrency),
quotes = mergedQuotes,
)
}
private suspend fun storeInPersistence(values: CurrencyIdWithQuote) {
persistenceDataStore.updateData { storedQuotes -> storedQuotes + values }
}
}

View file

@ -1,28 +0,0 @@
package com.tangem.data.quotes.store
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
/**
* Persisted form of [com.tangem.domain.models.quote.QuoteStatus] cache. Carries the fiat currency
* the quotes are expressed in, so it can be restored on cold start.
*
* @property fiatCurrency fiat currency the [quotes] are expressed in; `null` for an empty default cache
* @property quotes map of currency id to its quote
*/
@JsonClass(generateAdapter = true)
internal data class QuoteStatusDM(
val fiatCurrency: FiatCurrency?,
val quotes: Map<String, QuotesResponse.Quote>,
) {
@JsonClass(generateAdapter = true)
internal data class FiatCurrency(
val code: String,
val symbol: String,
)
companion object {
val Empty = QuoteStatusDM(fiatCurrency = null, quotes = emptyMap())
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.data.quotes.store
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import kotlinx.coroutines.flow.Flow
@ -40,9 +39,8 @@ internal interface QuotesStatusesStore {
* Store quotes statuses
*
* @param values map of currency ids and quotes
* @param fiatCurrency fiat currency the [values] are expressed in
*
* See complex methods in `QuotesStatusesStoreExt`.
*/
suspend fun store(values: Map<String, QuotesResponse.Quote>, fiatCurrency: FiatCurrency)
suspend fun store(values: Map<String, QuotesResponse.Quote>)
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.test.data.quote.toDomain
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.TestInstance
@ -25,8 +24,7 @@ internal class QuoteStatusConverterTest {
@ProvideTestModels
fun convert(model: ConvertTestModel) {
// Act
val actual = QuoteStatusConverter(source = model.source, fiatCurrency = FiatCurrency.Default)
.convert(value = model.value)
val actual = QuoteStatusConverter(source = model.source).convert(value = model.value)
// Assert
val expected = model.expected
@ -64,7 +62,6 @@ internal class QuoteStatusConverterTest {
rawCurrencyId = CryptoCurrency.RawID("ETH"),
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal("0.00"),
@ -86,7 +83,6 @@ internal class QuoteStatusConverterTest {
rawCurrencyId = CryptoCurrency.RawID("ETH"),
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal("0.00"),
@ -108,7 +104,6 @@ internal class QuoteStatusConverterTest {
rawCurrencyId = CryptoCurrency.RawID("ETH"),
value = QuoteStatus.Data(
source = StatusSource.ACTUAL,
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal.ONE,
fiatRateUSD = BigDecimal.ONE,
priceChange = BigDecimal("0.01"),

View file

@ -69,7 +69,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
appCurrencyResponseStore.getSyncOrNull()
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
quotesStore.store(values = successResponse.quotes, fiatCurrency = any())
quotesStore.store(values = successResponse.quotes)
}
coVerify(inverse = true) {
@ -93,21 +93,21 @@ internal class DefaultMultiQuoteStatusFetcherTest {
quotesStore.setSourceAsCache(currenciesIds = any())
appCurrencyResponseStore.getSyncOrNull()
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any(), fiatCurrency = any())
quotesStore.store(values = any())
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
}
@Test
fun `fetch ignores params appCurrencyId and uses stored app currency`() = runTest {
// Arrange: params.appCurrencyId is set but different from stored — fetcher must still use stored
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = "eur")
fun `fetch successfully if appCurrencyId from params is not null`() = runTest {
// Arrange
val appCurrencyId = "usd"
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
val currenciesIds = setOf("BTC", "ETH")
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
coEvery {
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
} returns successResponse.right()
// Act
@ -119,16 +119,42 @@ internal class DefaultMultiQuoteStatusFetcherTest {
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
appCurrencyResponseStore.getSyncOrNull()
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
quotesStore.store(values = successResponse.quotes, fiatCurrency = any())
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
quotesStore.store(values = successResponse.quotes)
}
coVerify(inverse = true) {
appCurrencyResponseStore.getSyncOrNull()
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
}
}
@Test
fun `fetch failure because appCurrencyId from params is blank`() = runTest {
// Arrange
val appCurrencyId = ""
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
// Act
val actual = fetcher(params)
// Assert
val expected = IllegalStateException("Unable to get AppCurrency for updating quotes").left()
assertEither(actual, expected)
coVerifyOrder {
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
}
coVerify(inverse = true) {
appCurrencyResponseStore.getSyncOrNull()
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any())
}
}
@Test
fun `fetch failure because api request failed`() = runTest {
// Arrange
@ -160,7 +186,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
}
coVerify(inverse = true) {
quotesStore.store(values = any(), fiatCurrency = any())
quotesStore.store(values = any())
}
}
@ -187,7 +213,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
coVerify(inverse = true) {
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
quotesStore.store(values = any(), fiatCurrency = any())
quotesStore.store(values = any())
}
}

View file

@ -4,7 +4,6 @@ import com.google.common.truth.Truth
import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.test.core.ProvideTestModels
@ -42,7 +41,6 @@ internal class DefaultQuotesRepositoryTest {
private val ethQuote = QuoteStatus(
rawCurrencyId = ethRawId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
@ -113,7 +111,6 @@ internal class DefaultQuotesRepositoryTest {
private val ethQuote = QuoteStatus(
rawCurrencyId = ethRawId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,

View file

@ -5,7 +5,6 @@ import com.tangem.data.quotes.store.QuotesStatusesStore
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
import com.tangem.test.core.getEmittedValues
@ -82,7 +81,6 @@ internal class DefaultSingleQuoteStatusProducerTest {
val updatedStatus = QuoteStatus(
rawCurrencyId = params.rawCurrencyId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
fiatRateUSD = BigDecimal.ZERO,
@ -131,7 +129,6 @@ internal class DefaultSingleQuoteStatusProducerTest {
val status = QuoteStatus(
rawCurrencyId = params.rawCurrencyId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal.ONE,
fiatRateUSD = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,

View file

@ -25,7 +25,7 @@ import kotlin.properties.Delegates
internal class QuotesStatusesStoreExtTest {
private var runtimeStore: RuntimeSharedStore<Set<QuoteStatus>> by Delegates.notNull()
private var persistenceStore: MockStateDataStore<QuoteStatusDM> by Delegates.notNull()
private var persistenceStore: MockStateDataStore<CurrencyIdWithQuote> by Delegates.notNull()
private var store: DefaultQuotesStatusesStore by Delegates.notNull()
private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
@ -34,12 +34,11 @@ internal class QuotesStatusesStoreExtTest {
@BeforeEach
fun resetMocks() {
runtimeStore = RuntimeSharedStore()
persistenceStore = MockStateDataStore(default = QuoteStatusDM.Empty)
persistenceStore = MockStateDataStore(default = emptyMap())
store = DefaultQuotesStatusesStore(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
scope = TestAppCoroutineScope(),
)
}

View file

@ -6,11 +6,9 @@ import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
import com.tangem.common.test.data.quote.toDomain
import com.tangem.common.test.datastore.MockStateDataStore
import com.tangem.data.quotes.converter.FiatCurrencyConverter
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.test.core.ProvideTestModels
import com.tangem.test.core.getEmittedValues
@ -33,7 +31,7 @@ import kotlin.properties.Delegates
internal class QuotesStatusesStoreTest {
private var runtimeStore: RuntimeSharedStore<Set<QuoteStatus>> by Delegates.notNull()
private var persistenceStore: MockStateDataStore<QuoteStatusDM> by Delegates.notNull()
private var persistenceStore: MockStateDataStore<CurrencyIdWithQuote> by Delegates.notNull()
private var store: DefaultQuotesStatusesStore by Delegates.notNull()
// region Data models
@ -50,12 +48,11 @@ internal class QuotesStatusesStoreTest {
@BeforeEach
fun resetMocks() {
runtimeStore = RuntimeSharedStore()
persistenceStore = MockStateDataStore(default = QuoteStatusDM.Empty)
persistenceStore = MockStateDataStore(default = emptyMap())
store = DefaultQuotesStatusesStore(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
scope = TestAppCoroutineScope(),
)
}
@ -68,7 +65,7 @@ internal class QuotesStatusesStoreTest {
fun `initialization if cache store is empty`() = runTest {
// Arrange
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore: DataStore<QuoteStatusDM> = mockk()
val persistenceStore: DataStore<CurrencyIdWithQuote> = mockk()
every { persistenceStore.data } returns emptyFlow()
@ -76,7 +73,6 @@ internal class QuotesStatusesStoreTest {
DefaultQuotesStatusesStore(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
scope = TestAppCoroutineScope(),
)
@ -91,13 +87,12 @@ internal class QuotesStatusesStoreTest {
fun `initialization if cache store contains empty map`() = runTest {
// Arrange
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore = MockStateDataStore<QuoteStatusDM>(default = QuoteStatusDM.Empty)
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
// Act
DefaultQuotesStatusesStore(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
scope = TestAppCoroutineScope(),
)
@ -112,20 +107,19 @@ internal class QuotesStatusesStoreTest {
fun `initialization if cache store is not empty`() = runTest {
// Arrange
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
val persistenceStore = MockStateDataStore<QuoteStatusDM>(default = QuoteStatusDM.Empty)
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
persistenceStore.updateData {
QuoteStatusDM(
fiatCurrency = FiatCurrencyConverter.convert(FiatCurrency.Default),
quotes = mapOf(btcQuoteDM, ethQuoteDM),
)
it.toMutableMap().apply {
this += btcQuoteDM
this += ethQuoteDM
}
}
// Act
DefaultQuotesStatusesStore(
runtimeStore = runtimeStore,
persistenceDataStore = persistenceStore,
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
scope = TestAppCoroutineScope(),
)
@ -477,7 +471,7 @@ internal class QuotesStatusesStoreTest {
}
// Act
store.store(values = model.values, fiatCurrency = FiatCurrency.Default)
store.store(values = model.values)
val runtimeActual = runtimeStore.getSyncOrNull()
val persistenceActual = getEmittedValues(persistenceStore.data)
@ -497,14 +491,14 @@ internal class QuotesStatusesStoreTest {
initialPersistence = null,
values = emptyMap(),
runtimeExpected = null,
persistenceExpected = QuoteStatusDM.Empty,
persistenceExpected = emptyMap(),
),
StoreTestModel(
initialRuntime = null,
initialPersistence = null,
values = mapOf(btcQuoteDM),
runtimeExpected = setOf(btcQuote),
persistenceExpected = persistedDefault(btcQuoteDM),
persistenceExpected = mapOf(btcQuoteDM),
),
// endregion
@ -514,48 +508,48 @@ internal class QuotesStatusesStoreTest {
initialPersistence = null,
values = emptyMap(),
runtimeExpected = setOf(btcQuote),
persistenceExpected = QuoteStatusDM.Empty,
persistenceExpected = emptyMap(),
),
StoreTestModel(
initialRuntime = setOf(ethQuote),
initialPersistence = null,
values = mapOf(btcQuoteDM),
runtimeExpected = setOf(ethQuote, btcQuote),
persistenceExpected = persistedDefault(btcQuoteDM),
persistenceExpected = mapOf(btcQuoteDM),
),
// endregion
// region runtime store is null
StoreTestModel(
initialRuntime = null,
initialPersistence = persistedDefault(btcQuoteDM),
initialPersistence = mapOf(btcQuoteDM),
values = emptyMap(),
runtimeExpected = null,
persistenceExpected = persistedDefault(btcQuoteDM),
persistenceExpected = mapOf(btcQuoteDM),
),
StoreTestModel(
initialRuntime = null,
initialPersistence = persistedDefault(ethQuoteDM),
initialPersistence = mapOf(ethQuoteDM),
values = mapOf(btcQuoteDM),
runtimeExpected = setOf(btcQuote),
persistenceExpected = persistedDefault(ethQuoteDM, btcQuoteDM),
persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM),
),
// endregion
// region stores contain data
StoreTestModel(
initialRuntime = setOf(btcQuote),
initialPersistence = persistedDefault(btcQuoteDM),
initialPersistence = mapOf(btcQuoteDM),
values = emptyMap(),
runtimeExpected = setOf(btcQuote),
persistenceExpected = persistedDefault(btcQuoteDM),
persistenceExpected = mapOf(btcQuoteDM),
),
StoreTestModel(
initialRuntime = setOf(btcQuote),
initialPersistence = persistedDefault(btcQuoteDM),
initialPersistence = mapOf(btcQuoteDM),
values = mapOf(ethQuoteDM),
runtimeExpected = setOf(ethQuote, btcQuote),
persistenceExpected = persistedDefault(ethQuoteDM, btcQuoteDM),
persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM),
),
// endregion
)
@ -563,18 +557,9 @@ internal class QuotesStatusesStoreTest {
data class StoreTestModel(
val initialRuntime: Set<QuoteStatus>?,
val initialPersistence: QuoteStatusDM?,
val initialPersistence: CurrencyIdWithQuote?,
val values: CurrencyIdWithQuote,
val persistenceExpected: QuoteStatusDM?,
val persistenceExpected: CurrencyIdWithQuote?,
val runtimeExpected: Set<QuoteStatus>?,
)
companion object {
private fun persistedDefault(
vararg quotes: Pair<String, com.tangem.datasource.api.tangemTech.models.QuotesResponse.Quote>,
): QuoteStatusDM = QuoteStatusDM(
fiatCurrency = FiatCurrencyConverter.convert(FiatCurrency.Default),
quotes = mapOf(*quotes),
)
}
}

View file

@ -8,6 +8,5 @@ dependencies {
/** Project - Domain */
implementation(projects.core.utils)
implementation(projects.domain.core)
implementation(projects.domain.models)
implementation(projects.domain.appCurrency.models)
}

View file

@ -3,7 +3,6 @@ package com.tangem.domain.appcurrency.extenstions
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.FiatCurrency
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
@ -15,5 +14,3 @@ suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency {
.firstOrNull()
?: AppCurrency.Default
}
fun AppCurrency.toFiatCurrency(): FiatCurrency = FiatCurrency(code = code, symbol = symbol)

View file

@ -1,21 +0,0 @@
package com.tangem.domain.models.currency
import kotlinx.serialization.Serializable
/**
* Fiat currency in which a [com.tangem.domain.models.quote.QuoteStatus] is expressed. Plain business
* entity without UI metadata (icons, localized name) those live in `AppCurrency`.
*
* @property code ISO code, e.g. "USD"
* @property symbol display symbol, e.g. "$"
*/
@Serializable
data class FiatCurrency(
val code: String,
val symbol: String,
) {
companion object {
val Default = FiatCurrency(code = "USD", symbol = "$")
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.domain.models.quote
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import java.math.BigDecimal
/**
@ -40,14 +39,12 @@ data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value
* price change.
*
* @property source status source
* @property fiatCurrency fiat currency in which [fiatRate] is expressed
* @property fiatRate the current fiat exchange rate for the cryptocurrency
* @property fiatRateUSD the current fiat exchange rate in USD for the cryptocurrency
* @property priceChange the price change for the cryptocurrency
*/
data class Data(
override val source: StatusSource,
val fiatCurrency: FiatCurrency,
val fiatRate: BigDecimal,
val fiatRateUSD: BigDecimal,
val priceChange: BigDecimal,

View file

@ -3,7 +3,6 @@ package com.tangem.domain.tokens.mock
import arrow.core.nonEmptySetOf
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import java.math.BigDecimal
@ -13,7 +12,6 @@ internal object MockQuotes {
val quote1 = QuoteStatus(
rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("1.23"),
fiatRateUSD = BigDecimal("1.23"),
priceChange = BigDecimal("0.01"),
@ -24,7 +22,6 @@ internal object MockQuotes {
val quote2 = QuoteStatus(
rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("2.34"),
fiatRateUSD = BigDecimal("2.34"),
priceChange = BigDecimal("-0.02"),
@ -35,7 +32,6 @@ internal object MockQuotes {
val quote3 = QuoteStatus(
rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("3.45"),
fiatRateUSD = BigDecimal("3.45"),
priceChange = BigDecimal("0.03"),
@ -46,7 +42,6 @@ internal object MockQuotes {
val quote4 = QuoteStatus(
rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("4.56"),
fiatRateUSD = BigDecimal("4.56"),
priceChange = BigDecimal("-0.04"),
@ -57,7 +52,6 @@ internal object MockQuotes {
val quote5 = QuoteStatus(
rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("5.67"),
fiatRateUSD = BigDecimal("5.67"),
priceChange = BigDecimal("0.05"),
@ -68,7 +62,6 @@ internal object MockQuotes {
val quote6 = QuoteStatus(
rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("6.78"),
fiatRateUSD = BigDecimal("6.78"),
priceChange = BigDecimal("-0.06"),
@ -79,7 +72,6 @@ internal object MockQuotes {
val quote7 = QuoteStatus(
rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("7.89"),
fiatRateUSD = BigDecimal("7.89"),
priceChange = BigDecimal("0.07"),
@ -90,7 +82,6 @@ internal object MockQuotes {
val quote8 = QuoteStatus(
rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("8.90"),
fiatRateUSD = BigDecimal("8.90"),
priceChange = BigDecimal("-0.08"),
@ -101,7 +92,6 @@ internal object MockQuotes {
val quote9 = QuoteStatus(
rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("9.01"),
fiatRateUSD = BigDecimal("9.01"),
priceChange = BigDecimal("0.09"),
@ -112,7 +102,6 @@ internal object MockQuotes {
val quote10 = QuoteStatus(
rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = BigDecimal("10.12"),
fiatRateUSD = BigDecimal("10.12"),
priceChange = BigDecimal("-0.10"),

View file

@ -7,7 +7,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.network.NetworkStatus
@ -44,7 +43,6 @@ class CryptoCurrencyStatusFactoryTest {
)
private val fullQuote = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
fiatRate = 1800.0.toBigDecimal(),
fiatRateUSD = 1800.0.toBigDecimal(),
priceChange = (-2.5).toBigDecimal(),

View file

@ -7,7 +7,6 @@ import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.quote.PriceChange
import com.tangem.domain.tokens.mock.MockTokens

View file

@ -8,7 +8,6 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.BalanceType

View file

@ -8,7 +8,6 @@ import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
@ -82,7 +81,6 @@ class YieldSupplyMinAmountUseCaseTest {
QuoteStatus(
rawCurrencyId = CryptoCurrency.RawID("polygon-ecosystem-token"),
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,

View file

@ -4,7 +4,6 @@ import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -8,7 +8,6 @@ import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
@ -78,7 +77,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,
@ -131,7 +129,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = nativeFiatRate,
fiatRateUSD = nativeFiatRate,
@ -259,7 +256,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
QuoteStatus(
rawCurrencyId = nativeCoin.id.rawCurrencyId!!,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ZERO, // non-positive
fiatRateUSD = BigDecimal.ZERO,

View file

@ -4,7 +4,6 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import org.junit.jupiter.api.Test

View file

@ -8,7 +8,6 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.yield.supply.YieldSupplyRepository

View file

@ -11,7 +11,6 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.FiatCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapCurrencyStatus
@ -87,7 +86,6 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
QuoteStatus(
rawCurrencyId = rawId,
value = QuoteStatus.Data(
fiatCurrency = FiatCurrency.Default,
source = StatusSource.ACTUAL,
fiatRate = BigDecimal.ONE,
fiatRateUSD = BigDecimal.ONE,