Updated on 2026-08-14
This commit is contained in:
parent
c4e357a6bc
commit
851d82631e
29 changed files with 264 additions and 107 deletions
|
|
@ -8,6 +8,7 @@ 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
|
||||
|
|
@ -94,6 +95,7 @@ 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),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
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)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
|
@ -10,12 +11,14 @@ import com.tangem.utils.extensions.orZero
|
|||
/**
|
||||
* Converter from [QuotesResponse.Quote] to [QuoteStatus]
|
||||
*
|
||||
* @property source status source
|
||||
* @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 {
|
||||
|
|
@ -25,6 +28,7 @@ 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(),
|
||||
|
|
|
|||
|
|
@ -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,6 +30,7 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
internal object QuotesDataModule {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideQuotesStoreV2(
|
||||
|
|
@ -41,13 +42,13 @@ internal object QuotesDataModule {
|
|||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceDataStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes<QuotesResponse.Quote>(),
|
||||
defaultValue = emptyMap(),
|
||||
defaultValue = QuoteStatusDM.Empty,
|
||||
adapter = moshi.adapter<QuoteStatusDM>(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes") },
|
||||
produceFile = { context.dataStoreFile(fileName = "quotes_v2") },
|
||||
scope = appScope,
|
||||
),
|
||||
legacyCacheFile = context.dataStoreFile(fileName = "quotes"),
|
||||
scope = appScope,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ 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
|
||||
|
|
@ -50,10 +52,10 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
val appCurrencyId = getAppCurrencyId(params = params)
|
||||
val (fiatCurrencyId, fiatCurrency) = resolveFiatCurrency()
|
||||
|
||||
val response = quotesFetcher.fetch(
|
||||
fiatCurrencyId = appCurrencyId,
|
||||
fiatCurrencyId = fiatCurrencyId,
|
||||
currenciesIds = replacementIdsResult.idsForRequest,
|
||||
fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H, Field.PRICE_USD),
|
||||
)
|
||||
|
|
@ -64,24 +66,27 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor(
|
|||
filteredIds = replacementIdsResult.idsFiltered,
|
||||
)
|
||||
|
||||
quotesStatusesStore.store(values = updatedResponse.quotes)
|
||||
quotesStatusesStore.store(values = updatedResponse.quotes, fiatCurrency = fiatCurrency)
|
||||
}
|
||||
.onLeft { throwable ->
|
||||
TangemLogger.e("Error", throwable)
|
||||
quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds)
|
||||
}
|
||||
|
||||
private suspend fun getAppCurrencyId(params: MultiQuoteStatusFetcher.Params): String {
|
||||
val appCurrencyId = params.appCurrencyId
|
||||
?: appCurrencyResponseStore.getSyncOrNull()?.id
|
||||
private suspend fun resolveFiatCurrency(): Pair<String, FiatCurrency> {
|
||||
val stored = appCurrencyResponseStore.getSyncOrNull() ?: failOnMissingAppCurrency()
|
||||
if (stored.id.isBlank()) failOnMissingAppCurrency()
|
||||
|
||||
if (appCurrencyId.isNullOrBlank()) {
|
||||
val exception = IllegalStateException("Unable to get AppCurrency for updating quotes")
|
||||
TangemLogger.e("Error", exception)
|
||||
return stored.id to stored.toFiatCurrency()
|
||||
}
|
||||
|
||||
throw exception
|
||||
}
|
||||
private fun failOnMissingAppCurrency(): Nothing {
|
||||
val exception = IllegalStateException("Unable to get AppCurrency for updating quotes")
|
||||
TangemLogger.e("Error", exception)
|
||||
throw exception
|
||||
}
|
||||
|
||||
return appCurrencyId
|
||||
private fun CurrenciesResponse.Currency.toFiatCurrency(): FiatCurrency {
|
||||
return FiatCurrency(code = code, symbol = unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
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
|
||||
|
|
@ -14,6 +16,7 @@ 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>
|
||||
|
||||
|
|
@ -21,27 +24,46 @@ internal typealias CurrencyIdWithQuote = Map<String, QuotesResponse.Quote>
|
|||
* Default implementation of [QuotesStatusesStore]
|
||||
*
|
||||
* @property runtimeStore runtime store
|
||||
* @property persistenceDataStore persistence store
|
||||
* @param dispatchers dispatchers
|
||||
* @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
|
||||
*/
|
||||
internal class DefaultQuotesStatusesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<Set<QuoteStatus>>,
|
||||
private val persistenceDataStore: DataStore<CurrencyIdWithQuote>,
|
||||
private val persistenceDataStore: DataStore<QuoteStatusDM>,
|
||||
private val legacyCacheFile: File,
|
||||
private val scope: AppCoroutineScope,
|
||||
) : QuotesStatusesStore {
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
val cachedStatuses = persistenceDataStore.data.firstOrNull()
|
||||
deleteLegacyCacheFile()
|
||||
|
||||
if (cachedStatuses.isNullOrEmpty()) return@launch
|
||||
val cached = persistenceDataStore.data.firstOrNull() ?: return@launch
|
||||
val fiatCurrency = cached.fiatCurrency?.let(FiatCurrencyConverter::convertBack) ?: return@launch
|
||||
|
||||
if (cached.quotes.isEmpty()) return@launch
|
||||
|
||||
runtimeStore.store(
|
||||
value = QuoteStatusConverter(source = StatusSource.CACHE).convertSet(input = cachedStatuses.entries),
|
||||
value = QuoteStatusConverter(source = StatusSource.CACHE, fiatCurrency = fiatCurrency)
|
||||
.convertSet(input = cached.quotes.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()
|
||||
|
|
@ -81,24 +103,33 @@ internal class DefaultQuotesStatusesStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun store(values: CurrencyIdWithQuote) {
|
||||
override suspend fun store(values: CurrencyIdWithQuote, fiatCurrency: FiatCurrency) {
|
||||
if (values.isEmpty()) return
|
||||
|
||||
coroutineScope {
|
||||
launch { storeInRuntime(values = values) }
|
||||
launch { storeInPersistence(values = values) }
|
||||
launch { storeInRuntime(values = values, fiatCurrency = fiatCurrency) }
|
||||
launch { storeInPersistence(values = values, fiatCurrency = fiatCurrency) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntime(values: CurrencyIdWithQuote) {
|
||||
val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL).convertSet(input = values.entries)
|
||||
private suspend fun storeInRuntime(values: CurrencyIdWithQuote, fiatCurrency: FiatCurrency) {
|
||||
val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL, fiatCurrency = fiatCurrency)
|
||||
.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) {
|
||||
persistenceDataStore.updateData { storedQuotes -> storedQuotes + values }
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
||||
|
|
@ -38,9 +39,10 @@ internal interface QuotesStatusesStore {
|
|||
/**
|
||||
* Store quotes statuses
|
||||
*
|
||||
* @param values map of currency ids and quotes
|
||||
* @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>)
|
||||
suspend fun store(values: Map<String, QuotesResponse.Quote>, fiatCurrency: FiatCurrency)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
|
@ -24,7 +25,8 @@ internal class QuoteStatusConverterTest {
|
|||
@ProvideTestModels
|
||||
fun convert(model: ConvertTestModel) {
|
||||
// Act
|
||||
val actual = QuoteStatusConverter(source = model.source).convert(value = model.value)
|
||||
val actual = QuoteStatusConverter(source = model.source, fiatCurrency = FiatCurrency.Default)
|
||||
.convert(value = model.value)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
|
|
@ -62,6 +64,7 @@ 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"),
|
||||
|
|
@ -83,6 +86,7 @@ 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"),
|
||||
|
|
@ -104,6 +108,7 @@ 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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
quotesStore.store(values = successResponse.quotes, fiatCurrency = any())
|
||||
}
|
||||
|
||||
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())
|
||||
quotesStore.store(values = any(), fiatCurrency = any())
|
||||
quotesStore.setSourceAsOnlyCache(currenciesIds = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch successfully if appCurrencyId from params is not null`() = runTest {
|
||||
// Arrange
|
||||
val appCurrencyId = "usd"
|
||||
val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId)
|
||||
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")
|
||||
|
||||
val currenciesIds = setOf("BTC", "ETH")
|
||||
|
||||
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
} returns successResponse.right()
|
||||
|
||||
// Act
|
||||
|
|
@ -119,42 +119,16 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
|
||||
coVerifyOrder {
|
||||
quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds)
|
||||
quotesFetcher.fetch(fiatCurrencyId = appCurrencyId, currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.store(values = successResponse.quotes)
|
||||
appCurrencyResponseStore.getSyncOrNull()
|
||||
quotesFetcher.fetch(fiatCurrencyId = "usd", currenciesIds = currenciesIds, fields = fields)
|
||||
quotesStore.store(values = successResponse.quotes, fiatCurrency = any())
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -186,7 +160,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
quotesStore.store(values = any())
|
||||
quotesStore.store(values = any(), fiatCurrency = any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +187,7 @@ internal class DefaultMultiQuoteStatusFetcherTest {
|
|||
|
||||
coVerify(inverse = true) {
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currenciesIds = any(), fields = any())
|
||||
quotesStore.store(values = any())
|
||||
quotesStore.store(values = any(), fiatCurrency = any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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
|
||||
|
|
@ -41,6 +42,7 @@ internal class DefaultQuotesRepositoryTest {
|
|||
private val ethQuote = QuoteStatus(
|
||||
rawCurrencyId = ethRawId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatCurrency = FiatCurrency.Default,
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
|
|
@ -111,6 +113,7 @@ internal class DefaultQuotesRepositoryTest {
|
|||
private val ethQuote = QuoteStatus(
|
||||
rawCurrencyId = ethRawId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatCurrency = FiatCurrency.Default,
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatRate = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
|
@ -81,6 +82,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
val updatedStatus = QuoteStatus(
|
||||
rawCurrencyId = params.rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatCurrency = FiatCurrency.Default,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
|
|
@ -129,6 +131,7 @@ internal class DefaultSingleQuoteStatusProducerTest {
|
|||
val status = QuoteStatus(
|
||||
rawCurrencyId = params.rawCurrencyId,
|
||||
value = QuoteStatus.Data(
|
||||
fiatCurrency = FiatCurrency.Default,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
fiatRateUSD = BigDecimal.ZERO,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import kotlin.properties.Delegates
|
|||
internal class QuotesStatusesStoreExtTest {
|
||||
|
||||
private var runtimeStore: RuntimeSharedStore<Set<QuoteStatus>> by Delegates.notNull()
|
||||
private var persistenceStore: MockStateDataStore<CurrencyIdWithQuote> by Delegates.notNull()
|
||||
private var persistenceStore: MockStateDataStore<QuoteStatusDM> by Delegates.notNull()
|
||||
private var store: DefaultQuotesStatusesStore by Delegates.notNull()
|
||||
|
||||
private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO)
|
||||
|
|
@ -34,11 +34,12 @@ internal class QuotesStatusesStoreExtTest {
|
|||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
runtimeStore = RuntimeSharedStore()
|
||||
persistenceStore = MockStateDataStore(default = emptyMap())
|
||||
persistenceStore = MockStateDataStore(default = QuoteStatusDM.Empty)
|
||||
|
||||
store = DefaultQuotesStatusesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceDataStore = persistenceStore,
|
||||
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
|
||||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ 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
|
||||
|
|
@ -31,7 +33,7 @@ import kotlin.properties.Delegates
|
|||
internal class QuotesStatusesStoreTest {
|
||||
|
||||
private var runtimeStore: RuntimeSharedStore<Set<QuoteStatus>> by Delegates.notNull()
|
||||
private var persistenceStore: MockStateDataStore<CurrencyIdWithQuote> by Delegates.notNull()
|
||||
private var persistenceStore: MockStateDataStore<QuoteStatusDM> by Delegates.notNull()
|
||||
private var store: DefaultQuotesStatusesStore by Delegates.notNull()
|
||||
|
||||
// region Data models
|
||||
|
|
@ -48,11 +50,12 @@ internal class QuotesStatusesStoreTest {
|
|||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
runtimeStore = RuntimeSharedStore()
|
||||
persistenceStore = MockStateDataStore(default = emptyMap())
|
||||
persistenceStore = MockStateDataStore(default = QuoteStatusDM.Empty)
|
||||
|
||||
store = DefaultQuotesStatusesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceDataStore = persistenceStore,
|
||||
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
|
||||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
}
|
||||
|
|
@ -65,7 +68,7 @@ internal class QuotesStatusesStoreTest {
|
|||
fun `initialization if cache store is empty`() = runTest {
|
||||
// Arrange
|
||||
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
|
||||
val persistenceStore: DataStore<CurrencyIdWithQuote> = mockk()
|
||||
val persistenceStore: DataStore<QuoteStatusDM> = mockk()
|
||||
|
||||
every { persistenceStore.data } returns emptyFlow()
|
||||
|
||||
|
|
@ -73,6 +76,7 @@ internal class QuotesStatusesStoreTest {
|
|||
DefaultQuotesStatusesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceDataStore = persistenceStore,
|
||||
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
|
||||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
|
|
@ -87,12 +91,13 @@ internal class QuotesStatusesStoreTest {
|
|||
fun `initialization if cache store contains empty map`() = runTest {
|
||||
// Arrange
|
||||
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
|
||||
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||
val persistenceStore = MockStateDataStore<QuoteStatusDM>(default = QuoteStatusDM.Empty)
|
||||
|
||||
// Act
|
||||
DefaultQuotesStatusesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceDataStore = persistenceStore,
|
||||
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
|
||||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
|
|
@ -107,19 +112,20 @@ internal class QuotesStatusesStoreTest {
|
|||
fun `initialization if cache store is not empty`() = runTest {
|
||||
// Arrange
|
||||
val runtimeStore = RuntimeSharedStore<Set<QuoteStatus>>()
|
||||
val persistenceStore = MockStateDataStore<CurrencyIdWithQuote>(default = emptyMap())
|
||||
val persistenceStore = MockStateDataStore<QuoteStatusDM>(default = QuoteStatusDM.Empty)
|
||||
|
||||
persistenceStore.updateData {
|
||||
it.toMutableMap().apply {
|
||||
this += btcQuoteDM
|
||||
this += ethQuoteDM
|
||||
}
|
||||
QuoteStatusDM(
|
||||
fiatCurrency = FiatCurrencyConverter.convert(FiatCurrency.Default),
|
||||
quotes = mapOf(btcQuoteDM, ethQuoteDM),
|
||||
)
|
||||
}
|
||||
|
||||
// Act
|
||||
DefaultQuotesStatusesStore(
|
||||
runtimeStore = runtimeStore,
|
||||
persistenceDataStore = persistenceStore,
|
||||
legacyCacheFile = java.io.File("legacy-quotes-cache-noop"),
|
||||
scope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
|
|
@ -471,7 +477,7 @@ internal class QuotesStatusesStoreTest {
|
|||
}
|
||||
|
||||
// Act
|
||||
store.store(values = model.values)
|
||||
store.store(values = model.values, fiatCurrency = FiatCurrency.Default)
|
||||
|
||||
val runtimeActual = runtimeStore.getSyncOrNull()
|
||||
val persistenceActual = getEmittedValues(persistenceStore.data)
|
||||
|
|
@ -491,14 +497,14 @@ internal class QuotesStatusesStoreTest {
|
|||
initialPersistence = null,
|
||||
values = emptyMap(),
|
||||
runtimeExpected = null,
|
||||
persistenceExpected = emptyMap(),
|
||||
persistenceExpected = QuoteStatusDM.Empty,
|
||||
),
|
||||
StoreTestModel(
|
||||
initialRuntime = null,
|
||||
initialPersistence = null,
|
||||
values = mapOf(btcQuoteDM),
|
||||
runtimeExpected = setOf(btcQuote),
|
||||
persistenceExpected = mapOf(btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(btcQuoteDM),
|
||||
),
|
||||
// endregion
|
||||
|
||||
|
|
@ -508,48 +514,48 @@ internal class QuotesStatusesStoreTest {
|
|||
initialPersistence = null,
|
||||
values = emptyMap(),
|
||||
runtimeExpected = setOf(btcQuote),
|
||||
persistenceExpected = emptyMap(),
|
||||
persistenceExpected = QuoteStatusDM.Empty,
|
||||
),
|
||||
StoreTestModel(
|
||||
initialRuntime = setOf(ethQuote),
|
||||
initialPersistence = null,
|
||||
values = mapOf(btcQuoteDM),
|
||||
runtimeExpected = setOf(ethQuote, btcQuote),
|
||||
persistenceExpected = mapOf(btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(btcQuoteDM),
|
||||
),
|
||||
// endregion
|
||||
|
||||
// region runtime store is null
|
||||
StoreTestModel(
|
||||
initialRuntime = null,
|
||||
initialPersistence = mapOf(btcQuoteDM),
|
||||
initialPersistence = persistedDefault(btcQuoteDM),
|
||||
values = emptyMap(),
|
||||
runtimeExpected = null,
|
||||
persistenceExpected = mapOf(btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(btcQuoteDM),
|
||||
),
|
||||
StoreTestModel(
|
||||
initialRuntime = null,
|
||||
initialPersistence = mapOf(ethQuoteDM),
|
||||
initialPersistence = persistedDefault(ethQuoteDM),
|
||||
values = mapOf(btcQuoteDM),
|
||||
runtimeExpected = setOf(btcQuote),
|
||||
persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(ethQuoteDM, btcQuoteDM),
|
||||
),
|
||||
// endregion
|
||||
|
||||
// region stores contain data
|
||||
StoreTestModel(
|
||||
initialRuntime = setOf(btcQuote),
|
||||
initialPersistence = mapOf(btcQuoteDM),
|
||||
initialPersistence = persistedDefault(btcQuoteDM),
|
||||
values = emptyMap(),
|
||||
runtimeExpected = setOf(btcQuote),
|
||||
persistenceExpected = mapOf(btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(btcQuoteDM),
|
||||
),
|
||||
StoreTestModel(
|
||||
initialRuntime = setOf(btcQuote),
|
||||
initialPersistence = mapOf(btcQuoteDM),
|
||||
initialPersistence = persistedDefault(btcQuoteDM),
|
||||
values = mapOf(ethQuoteDM),
|
||||
runtimeExpected = setOf(ethQuote, btcQuote),
|
||||
persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM),
|
||||
persistenceExpected = persistedDefault(ethQuoteDM, btcQuoteDM),
|
||||
),
|
||||
// endregion
|
||||
)
|
||||
|
|
@ -557,9 +563,18 @@ internal class QuotesStatusesStoreTest {
|
|||
|
||||
data class StoreTestModel(
|
||||
val initialRuntime: Set<QuoteStatus>?,
|
||||
val initialPersistence: CurrencyIdWithQuote?,
|
||||
val initialPersistence: QuoteStatusDM?,
|
||||
val values: CurrencyIdWithQuote,
|
||||
val persistenceExpected: CurrencyIdWithQuote?,
|
||||
val persistenceExpected: QuoteStatusDM?,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue