Updated on 2026-08-14
This commit is contained in:
parent
4df207e832
commit
d443530171
10 changed files with 197 additions and 5 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.onramp.*
|
||||
import com.tangem.domain.onramp.repositories.*
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -264,12 +265,14 @@ internal object OnrampDomainModule {
|
|||
onrampErrorResolver: OnrampErrorResolver,
|
||||
onrampTransactionRepository: OnrampTransactionRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
promoRepository: PromoRepository,
|
||||
): GetOnrampOffersUseCase {
|
||||
return GetOnrampOffersUseCase(
|
||||
onrampRepository = onrampRepository,
|
||||
errorResolver = onrampErrorResolver,
|
||||
onrampTransactionRepository = onrampTransactionRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
promoRepository = promoRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.promo.DefaultPromoBannerStore
|
||||
import com.tangem.datasource.local.promo.DefaultPromoStoriesStore
|
||||
import com.tangem.datasource.local.promo.PromoBannerStore
|
||||
import com.tangem.datasource.local.promo.PromoStoriesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -18,4 +21,10 @@ object PromoStoreModule {
|
|||
fun providePromoStoriesStore(): PromoStoriesStore {
|
||||
return DefaultPromoStoriesStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePromoBannerStore(): PromoBannerStore {
|
||||
return DefaultPromoBannerStore(dataStore = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.local.promo
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
|
||||
internal class DefaultPromoBannerStore(
|
||||
private val dataStore: RuntimeSharedStore<Map<String, PromoBannerResponse>>,
|
||||
) : PromoBannerStore {
|
||||
|
||||
override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? {
|
||||
return dataStore.getSyncOrNull()?.get(promoId)
|
||||
}
|
||||
|
||||
override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) {
|
||||
dataStore.update(emptyMap()) { current ->
|
||||
current + (promoId to promoBanner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.local.promo
|
||||
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
|
||||
|
||||
interface PromoBannerStore {
|
||||
|
||||
suspend fun getSyncOrNull(promoId: String): PromoBannerResponse?
|
||||
|
||||
suspend fun store(promoId: String, promoBanner: PromoBannerResponse)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStor
|
|||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.promo.PromoBannerStore
|
||||
import com.tangem.datasource.local.promo.PromoStoriesStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
|
|
@ -28,6 +29,7 @@ internal class DefaultPromoRepository(
|
|||
private val tangemApi: TangemTechApi,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val promoStoriesStore: PromoStoriesStore,
|
||||
private val promoBannerStore: PromoBannerStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val referralRepository: ReferralRepository,
|
||||
) : PromoRepository {
|
||||
|
|
@ -96,6 +98,18 @@ internal class DefaultPromoRepository(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun isMoonpayPromoActive(): Boolean {
|
||||
val banner = runCatching(dispatchers.io) {
|
||||
val response = promoBannerStore.getSyncOrNull(MOONPAY_NAME) ?: run {
|
||||
val apiResponse = tangemApi.getPromoBanner(MOONPAY_NAME).getOrThrow()
|
||||
promoBannerStore.store(MOONPAY_NAME, apiResponse)
|
||||
apiResponse
|
||||
}
|
||||
promoBannerConverter.convert(response)
|
||||
}.getOrNull()
|
||||
return banner?.isActive == true
|
||||
}
|
||||
|
||||
override fun getStoryById(id: String): Flow<StoryContent?> = isReadyToShowStories(id).mapLatest {
|
||||
getStoryByIdSync(id = id, refresh = false)
|
||||
}
|
||||
|
|
@ -166,6 +180,7 @@ internal class DefaultPromoRepository(
|
|||
const val SEPA_NAME = "sepa"
|
||||
const val VISA_NAME = "visa-waitlist"
|
||||
const val BLACK_FRIDAY_NAME = "black-friday"
|
||||
const val MOONPAY_NAME = "moonpay"
|
||||
const val STORIES_LOAD_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.promo.di
|
|||
import com.tangem.data.promo.DefaultPromoRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.promo.PromoBannerStore
|
||||
import com.tangem.datasource.local.promo.PromoStoriesStore
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
|
|
@ -23,6 +24,7 @@ internal object PromoDataModule {
|
|||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
promoStoriesStore: PromoStoriesStore,
|
||||
promoBannerStore: PromoBannerStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
referralRepository: ReferralRepository,
|
||||
): PromoRepository {
|
||||
|
|
@ -32,6 +34,7 @@ internal object PromoDataModule {
|
|||
promoStoriesStore = promoStoriesStore,
|
||||
dispatchers = dispatchers,
|
||||
referralRepository = referralRepository,
|
||||
promoBannerStore = promoBannerStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
api(projects.domain.core)
|
||||
api(projects.domain.settings)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(projects.domain.promo)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.coroutine)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import com.tangem.domain.onramp.repositories.OnrampRepository
|
|||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||
import com.tangem.domain.onramp.utils.calculateRateDif
|
||||
import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetOnrampOffersUseCase(
|
||||
|
|
@ -21,14 +23,16 @@ class GetOnrampOffersUseCase(
|
|||
private val onrampTransactionRepository: OnrampTransactionRepository,
|
||||
private val errorResolver: OnrampErrorResolver,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val promoRepository: PromoRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(): EitherFlow<OnrampError, List<OnrampOffersBlock>> {
|
||||
return combine(
|
||||
onrampRepository.getQuotes(),
|
||||
onrampTransactionRepository.getAllTransactions(),
|
||||
) { quotes, transactions ->
|
||||
processOffers(quotes, transactions)
|
||||
flow { emit(promoRepository.isMoonpayPromoActive()) },
|
||||
) { quotes, transactions, isMoonpayPromoActive ->
|
||||
processOffers(quotes, transactions, isMoonpayPromoActive)
|
||||
}
|
||||
.map { offers -> offers.right() }
|
||||
.catch { throwable -> errorResolver.resolve(throwable).left() }
|
||||
|
|
@ -37,6 +41,7 @@ class GetOnrampOffersUseCase(
|
|||
private suspend fun processOffers(
|
||||
quotes: List<OnrampQuote>,
|
||||
transactions: List<OnrampTransaction>,
|
||||
isMoonpayPromoActive: Boolean,
|
||||
): List<OnrampOffersBlock> {
|
||||
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
|
||||
if (validQuotes.isEmpty()) return emptyList()
|
||||
|
|
@ -57,7 +62,7 @@ class GetOnrampOffersUseCase(
|
|||
|
||||
val recentOffer = findRecentOffer(offers, transactions)
|
||||
val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable)
|
||||
val fastestOffer = findFastestOffer(offers, isGooglePayAvailable)
|
||||
val fastestOffer = findFastestOffer(offers, isGooglePayAvailable, isMoonpayPromoActive)
|
||||
|
||||
return buildOffersBlocks(
|
||||
recentOffer = recentOffer,
|
||||
|
|
@ -85,8 +90,24 @@ class GetOnrampOffersUseCase(
|
|||
return offers.maxWithOrNull(offerComparator(isGooglePayAvailable))
|
||||
}
|
||||
|
||||
private fun findFastestOffer(offers: List<OnrampOffer>, isGooglePayAvailable: Boolean): OnrampOffer? {
|
||||
val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() }
|
||||
private fun findFastestOffer(
|
||||
offers: List<OnrampOffer>,
|
||||
isGooglePayAvailable: Boolean,
|
||||
isMoonpayPromoActive: Boolean,
|
||||
): OnrampOffer? {
|
||||
val moonpayPromoOffers = if (isMoonpayPromoActive) {
|
||||
offers.filter {
|
||||
it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID &&
|
||||
it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val instantOffers = moonpayPromoOffers.ifEmpty {
|
||||
offers.filter { it.quote.paymentMethod.type.isInstant() }
|
||||
}
|
||||
|
||||
return if (instantOffers.isNotEmpty()) {
|
||||
instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable))
|
||||
} else {
|
||||
|
|
@ -287,4 +308,8 @@ class GetOnrampOffersUseCase(
|
|||
-> true
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MOONPAY_PROMO_PROVIDER_ID = "moonpay"
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
|
|||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
|
@ -24,6 +25,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true)
|
||||
private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true)
|
||||
private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true)
|
||||
private val promoRepository: PromoRepository = mockk(relaxUnitFun = true)
|
||||
|
||||
private lateinit var useCase: GetOnrampOffersUseCase
|
||||
|
||||
|
|
@ -35,6 +37,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
onrampTransactionRepository = onrampTransactionRepository,
|
||||
errorResolver = errorResolver,
|
||||
settingsRepository = settingsRepository,
|
||||
promoRepository = promoRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -225,6 +228,7 @@ class GetOnrampOffersUseCaseTest {
|
|||
|
||||
val transactions = emptyList<OnrampTransaction>()
|
||||
|
||||
coEvery { promoRepository.isMoonpayPromoActive() } returns false
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns false
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(
|
||||
|
|
@ -245,6 +249,107 @@ class GetOnrampOffersUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should fallback to standard instant offers when promo is active but no Moonpay offers exist`() =
|
||||
runTest {
|
||||
val instantMethod = createMockPaymentMethod("gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY)
|
||||
val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val provider = createMockProvider("other", "Other Provider")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockQuote(instantMethod, provider, BigDecimal("95.0")),
|
||||
createMockQuote(slowMethod, provider, BigDecimal("100.0")),
|
||||
)
|
||||
|
||||
val transactions = emptyList<OnrampTransaction>()
|
||||
|
||||
coEvery { promoRepository.isMoonpayPromoActive() } returns true
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns true
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions)
|
||||
|
||||
val result = useCase()
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
|
||||
Truth.assertThat(recommendedBlock).isNotNull()
|
||||
Truth.assertThat(recommendedBlock?.offers).hasSize(2)
|
||||
|
||||
val fastestOffer =
|
||||
recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest }
|
||||
Truth.assertThat(fastestOffer).isNotNull()
|
||||
|
||||
when (val quote = fastestOffer?.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
Truth.assertThat(quote.provider.id).isNotEqualTo("moonpay")
|
||||
Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY)
|
||||
}
|
||||
else -> Truth.assertThat(false).isTrue()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke should show Moonpay fastest offer when promo is active`() = runTest {
|
||||
val moonpayGooglePayMethod = createMockPaymentMethod("moonpay-gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY)
|
||||
val otherGooglePayMethod = createMockPaymentMethod(
|
||||
"other-gpay",
|
||||
"Other Google Pay",
|
||||
PaymentMethodType.GOOGLE_PAY,
|
||||
)
|
||||
val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD)
|
||||
val moonpayProvider = createMockProvider("moonpay", "Moonpay")
|
||||
val otherProvider = createMockProvider("other", "Other Provider")
|
||||
|
||||
val quotes = listOf(
|
||||
createMockQuote(moonpayGooglePayMethod, moonpayProvider, BigDecimal("100.0")),
|
||||
createMockQuote(otherGooglePayMethod, otherProvider, BigDecimal("95.0")),
|
||||
createMockQuote(slowMethod, otherProvider, BigDecimal("105.0")),
|
||||
)
|
||||
|
||||
val transactions = emptyList<OnrampTransaction>()
|
||||
|
||||
coEvery { promoRepository.isMoonpayPromoActive() } returns true
|
||||
coEvery { settingsRepository.isGooglePayAvailability() } returns true
|
||||
coEvery { onrampRepository.getQuotes() } returns flowOf(quotes)
|
||||
coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions)
|
||||
|
||||
val result = useCase()
|
||||
|
||||
result.collect { either ->
|
||||
Truth.assertThat(either.isRight()).isTrue()
|
||||
either.fold(
|
||||
ifLeft = { error -> Truth.assertThat(error).isNull() },
|
||||
ifRight = { offers ->
|
||||
Truth.assertThat(offers).hasSize(1)
|
||||
|
||||
val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended }
|
||||
Truth.assertThat(recommendedBlock).isNotNull()
|
||||
|
||||
val fastestOffer = recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest }
|
||||
Truth.assertThat(fastestOffer).isNotNull()
|
||||
|
||||
when (val quote = fastestOffer?.quote) {
|
||||
is OnrampQuote.Data -> {
|
||||
Truth.assertThat(quote.provider.id).isEqualTo("moonpay")
|
||||
Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY)
|
||||
Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0"))
|
||||
}
|
||||
else -> Truth.assertThat(false).isTrue()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockPaymentMethod(
|
||||
id: String,
|
||||
name: String,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface PromoRepository {
|
|||
fun isMarketsStakingNotificationHideClicked(): Flow<Boolean>
|
||||
|
||||
suspend fun setMarketsStakingNotificationHideClicked()
|
||||
|
||||
suspend fun isMoonpayPromoActive(): Boolean
|
||||
// endregion
|
||||
|
||||
// region Stories
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue