diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt new file mode 100644 index 0000000000..15d4280a28 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse + +/** + * Store of app currency data model [CurrenciesResponse.Currency] + * +[REDACTED_AUTHOR] + */ +interface AppCurrencyResponseStore { + + suspend fun getSyncOrNull(): CurrenciesResponse.Currency? +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt new file mode 100644 index 0000000000..6d1b36f2fd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull + +/** + * Default implementation of [AppCurrencyResponseStore] + * + * @property appPreferencesStore app preferences store + */ +internal class DefaultAppCurrencyResponseStore( + private val appPreferencesStore: AppPreferencesStore, +) : AppCurrencyResponseStore { + + override suspend fun getSyncOrNull(): CurrenciesResponse.Currency? { + return appPreferencesStore.getObjectSyncOrNull( + PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt index fade673123..70fb61dbea 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.di +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.datasource.appcurrency.DefaultAppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,4 +21,10 @@ internal object AppCurrencyDataModule { fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore { return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun provideAppCurrencyResponseStore(appPreferencesStore: AppPreferencesStore): AppCurrencyResponseStore { + return DefaultAppCurrencyResponseStore(appPreferencesStore) + } } \ No newline at end of file diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 1da7f7a50e..9005f39528 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -13,12 +13,17 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.data.common) + implementation(projects.data.tokens) + + implementation(projects.domain.appCurrency.models) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.quotes) implementation(projects.domain.wallets.models) implementation(deps.androidx.datastore) + implementation(deps.timber) implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt new file mode 100644 index 0000000000..5fc337e08c --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt @@ -0,0 +1,62 @@ +package com.tangem.data.quotes.multi + +import arrow.core.Either +import com.tangem.data.common.api.safeApiCallWithTimeout +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber + +/** + * Default implementation of [MultiQuoteFetcher] + * + * @property tangemTechApi tangemTech api + * @property appCurrencyResponseStore app currency response store + * @property quotesStore quotes store + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteFetcher( + private val tangemTechApi: TangemTechApi, + private val appCurrencyResponseStore: AppCurrencyResponseStore, + private val quotesStore: QuotesStoreV2, +) : MultiQuoteFetcher { + + private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() + + override suspend fun invoke(params: MultiQuoteFetcher.Params): Either = Either.catch { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + currenciesIds = params.currenciesIds.mapTo( + destination = hashSetOf(), + transform = CryptoCurrency.RawID::value, + ), + ) + + val appCurrency = appCurrencyResponseStore.getSyncOrNull() + ?: error(message = "Unable to get AppCurrency for updating quotes") + + safeApiCallWithTimeout( + call = { + val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") + val response = tangemTechApi.getQuotes(currencyId = appCurrency.id, coinIds = coinIds).bind() + + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response = response, + filteredIds = replacementIdsResult.idsFiltered, + ) + + quotesStore.storeActual(values = updatedResponse.quotes) + }, + onError = { error -> throw error }, + ) + } + .onLeft { + Timber.e(it) + quotesStore.storeError(currenciesIds = params.currenciesIds) + } +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt new file mode 100644 index 0000000000..b35a5c6f06 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt @@ -0,0 +1,143 @@ +package com.tangem.data.quotes.multi + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiQuoteFetcherTest { + + private val tangemTechApi = mockk(relaxed = true) + private val appCurrencyResponseStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) + + private val fetcher = DefaultMultiQuoteFetcher( + tangemTechApi = tangemTechApi, + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStore = quotesStore, + ) + + @Test + fun `fetch quotes successfully`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency + + val coinIds = "BTC,ETH" + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + appCurrencyResponseStore.getSyncOrNull() + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeActual(values = successResponse.quotes) + } + + coVerify(inverse = true) { + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch quotes failure because api request failed`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency + + val coinIds = "BTC,ETH" + + @Suppress("UNCHECKED_CAST") + val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse + coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + + appCurrencyResponseStore.getSyncOrNull() + + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + @Test + fun `fetch quotes failure because app currency not found`() = runTest { + val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null + + val actual = fetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = params.currenciesIds) + appCurrencyResponseStore.getSyncOrNull() + quotesStore.storeError(currenciesIds = params.currenciesIds) + } + + coVerify(inverse = true) { + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + private companion object { + + val currenciesIds = setOf( + CryptoCurrency.RawID(value = "BTC"), + CryptoCurrency.RawID(value = "ETH"), + ) + + val usdAppCurrency = CurrenciesResponse.Currency( + id = "USD".lowercase(), + code = "USD", + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ) + + val successResponse = QuotesResponse( + quotes = mapOf( + "BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + "ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN), + ), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index 66f0ea8b64..bf7779378a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse /** * Adapter to replace unsupported currencies for quotes request if it necessary */ -internal class QuotesUnsupportedCurrenciesIdAdapter { +class QuotesUnsupportedCurrenciesIdAdapter { /** * Replaces unsupported currencies id to it replacements for request diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt new file mode 100644 index 0000000000..4520ad1fd6 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.quotes.multi + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Fetcher of quotes + * +[REDACTED_AUTHOR] + */ +interface MultiQuoteFetcher : FlowFetcher { + + data class Params(val currenciesIds: Set) +} \ No newline at end of file