Updated on 2026-08-14
This commit is contained in:
commit
06a915a0f4
2186 changed files with 101417 additions and 36593 deletions
|
|
@ -23,63 +23,87 @@ android {
|
|||
}
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.core.error.ext)
|
||||
implementation(projects.core.security)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.wallets)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.virtualAccount)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.quotes)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.virtualAccounts.details.api)
|
||||
|
||||
|
||||
/** Project - Utils */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
|
||||
/** Project - Libs */
|
||||
implementation(projects.libs.visa)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.androidx.datastore)
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region Other libraries
|
||||
api(deps.moshi)
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.arrow.fx)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.okio)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
||||
/** Libs - Tangem */
|
||||
// region Libs - Tangem
|
||||
api(tangemDeps.hot.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.hot.core)
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
// AndroidSecureStorageV2 for TangemPay secure token storage (see com.tangem.data.pay.store).
|
||||
implementation(tangemDeps.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
// endregion
|
||||
|
||||
/** DI */
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Test */
|
||||
// region Project - Core
|
||||
api(projects.core.analytics)
|
||||
api(projects.core.datasource)
|
||||
api(projects.core.security)
|
||||
api(projects.core.utils)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.core.error.ext)
|
||||
implementation(projects.core.pagination)
|
||||
// endregion
|
||||
|
||||
// region Project - Data
|
||||
api(projects.data.common)
|
||||
implementation(projects.data.wallets)
|
||||
// endregion
|
||||
|
||||
// region Project - Domain
|
||||
api(projects.domain.common)
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.networks)
|
||||
api(projects.domain.quotes)
|
||||
api(projects.domain.virtualAccount)
|
||||
api(projects.domain.visa)
|
||||
api(projects.domain.wallets)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
runtimeOnly(projects.domain.tokens)
|
||||
// endregion
|
||||
|
||||
// region Project - Domain models
|
||||
api(projects.domain.visa.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
// endregion
|
||||
|
||||
// region Project - Features
|
||||
api(projects.features.swap.domain)
|
||||
api(projects.features.virtualAccounts.details.api)
|
||||
api(projects.features.tangempay.details.api)
|
||||
// endregion
|
||||
|
||||
// region Project - Libs
|
||||
api(projects.libs.blockchainSdk)
|
||||
api(projects.libs.tangemSdkApi)
|
||||
implementation(projects.libs.visa)
|
||||
// endregion
|
||||
|
||||
// region Test
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -143,6 +143,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
private fun TangemPayEntryPoint.toEligibilityType(): TangemPayEligibilityType = when (this) {
|
||||
TangemPayEntryPoint.BANNER -> TangemPayEligibilityType.BANNER
|
||||
TangemPayEntryPoint.DETAILS -> TangemPayEligibilityType.DETAILS
|
||||
TangemPayEntryPoint.DEEPLINK -> TangemPayEligibilityType.DEEPLINK
|
||||
}
|
||||
|
||||
private data class UserWalletData(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayUserWalletDataCleaner @Inject constructor(
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
) : UserWalletDataCleaner {
|
||||
|
||||
override suspend fun clear(userWalletIds: List<UserWalletId>) {
|
||||
paymentAccountStatusesStore.remove(userWalletIds)
|
||||
txHistoryItemsStore.remove(userWalletIds.map { it.stringValue })
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
|||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.pay.*
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -51,6 +52,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
adminDailyLimit = card.limit?.adminCardLimit?.amount,
|
||||
frozenState = card.frozenState.toString(),
|
||||
lastDigits = card.lastDigits,
|
||||
images = card.images.map { image ->
|
||||
PaymentAccountStatusValueDM.ImageDM(
|
||||
type = image.type.name,
|
||||
url = image.url,
|
||||
)
|
||||
},
|
||||
state = card.state.toString(),
|
||||
)
|
||||
},
|
||||
|
|
@ -68,6 +75,8 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
// Transient statuses are not persisted
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.AwaitingPlanSelection,
|
||||
is PaymentAccountStatusValue.Inactive,
|
||||
is PaymentAccountStatusValue.Error.ExposedDevice,
|
||||
is PaymentAccountStatusValue.Error.Unavailable,
|
||||
is PaymentAccountStatusValue.Error.NotSynced,
|
||||
|
|
@ -90,11 +99,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
source = StatusSource.CACHE,
|
||||
customerId = value.customerId,
|
||||
depositAddress = value.depositAddress,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = value.availableForWithdrawal,
|
||||
),
|
||||
balance = value.getBalance(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = value.fiatRate,
|
||||
cards = value.cards.map { card ->
|
||||
|
|
@ -114,11 +119,13 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
),
|
||||
frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
|
||||
lastDigits = card.lastDigits,
|
||||
images = card.getImages(),
|
||||
state = TangemPayCardState.fromString(card.state),
|
||||
)
|
||||
},
|
||||
error = null,
|
||||
virtualAccount = null,
|
||||
tariffPlan = null,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
|
|
@ -158,6 +165,14 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun PaymentAccountStatusValueDM.ActiveAccount.getBalance(): PaymentAccountStatusValue.Balance {
|
||||
return PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance.toDomain(),
|
||||
cryptoBalance = cryptoBalance.toDomain(),
|
||||
availableForWithdrawal = availableForWithdrawal,
|
||||
)
|
||||
}
|
||||
|
||||
private fun PaymentAccountStatusValueDM.FiatBalanceDM.toDomain(): PaymentAccountStatusValue.FiatBalance {
|
||||
return PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = availableBalance,
|
||||
|
|
@ -174,4 +189,13 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
balance = balance,
|
||||
)
|
||||
}
|
||||
|
||||
private fun PaymentAccountStatusValueDM.TangemPayCard.getImages(): List<TangemPayTariffPlan.Image> {
|
||||
return images.map { image ->
|
||||
TangemPayTariffPlan.Image(
|
||||
type = TangemPayTariffPlan.Image.Type.fromString(image.type),
|
||||
url = image.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import java.util.Locale
|
||||
|
||||
internal object TangemPayTariffPlanConverter {
|
||||
|
||||
fun convert(value: CustomerMeResponse.TariffPlan?): TangemPayTariffPlan? {
|
||||
val id = value?.id ?: return null
|
||||
val name = value.name ?: return null
|
||||
val programName = value.programName ?: return null
|
||||
|
||||
// We cannot base our logic on knowledge what exact tier type is it.
|
||||
// Use it only as identifier to get data from other responses
|
||||
val tierId = value.type ?: return null
|
||||
|
||||
// Basic tier is a default tier. We can base some features on it.
|
||||
// Other tiers are adjusted from admin panel. It is not guaranteed to have it in future
|
||||
val isBasicTier = tierId.uppercase(Locale.US) == "BASIC"
|
||||
|
||||
return TangemPayTariffPlan(
|
||||
id = id,
|
||||
tierId = tierId,
|
||||
isBasicTier = isBasicTier,
|
||||
name = name,
|
||||
programName = programName,
|
||||
descriptionItems = value.descriptionItems.orEmpty().mapNotNull(::convertDescriptionItem),
|
||||
images = value.images.orEmpty().mapNotNull(::convertImage),
|
||||
fees = value.fees.orEmpty().mapNotNull(::convertFee),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertFee(fee: CustomerMeResponse.Fee): TangemPayTariffPlan.Fee? {
|
||||
val amount = fee.amount ?: return null
|
||||
return TangemPayTariffPlan.Fee(
|
||||
type = TangemPayTariffPlan.Fee.Type.fromString(fee.type),
|
||||
amount = amount,
|
||||
currency = fee.currency.orEmpty(),
|
||||
description = fee.description.orEmpty(),
|
||||
period = fee.period?.let(TangemPayTariffPlan.Fee.Period::fromString),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertDescriptionItem(item: CustomerMeResponse.DescriptionItem): TangemPayTariffPlan.DescriptionItem? {
|
||||
val title = item.title ?: return null
|
||||
return TangemPayTariffPlan.DescriptionItem(
|
||||
section = TangemPayTariffPlan.Section.fromString(item.type),
|
||||
order = item.order ?: 0,
|
||||
title = title,
|
||||
body = item.body.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertImage(image: CustomerMeResponse.Image): TangemPayTariffPlan.Image? {
|
||||
val url = image.url ?: return null
|
||||
return TangemPayTariffPlan.Image(
|
||||
type = TangemPayTariffPlan.Image.Type.fromString(image.type),
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
|||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.TangemPayUserWalletDataCleaner
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||
import com.tangem.data.pay.entity.DefaultTangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
|
||||
|
|
@ -21,6 +22,7 @@ import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
|||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.domain.common.wallets.UserWalletDataCleaner
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
|
|
@ -41,6 +43,7 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -117,8 +120,59 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
fun bindTangemPayUserWalletDataCleaner(impl: TangemPayUserWalletDataCleaner): UserWalletDataCleaner
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTariffPlanTransitionsRepository(
|
||||
repository: DefaultTariffPlanTransitionsRepository,
|
||||
): TangemPayTariffPlanTransitionsRepository
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
fun provideGetTangemPayTariffPlanTransitionsUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
): GetTangemPayTariffPlanTransitionsUseCase {
|
||||
return GetTangemPayTariffPlanTransitionsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSetTariffPlanPendingTransitionUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): SetTariffPlanPendingTransitionUseCase {
|
||||
return SetTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCancelTariffPlanPendingTransitionUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CancelTariffPlanPendingTransitionUseCase {
|
||||
return CancelTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetTangemPayTariffPlanStateUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
getTangemPayTariffPlanTransitionsUseCase: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
): GetTangemPayTariffPlanStateUseCase {
|
||||
return GetTangemPayTariffPlanStateUseCase(
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
getTariffPlanTransitions = getTangemPayTariffPlanTransitionsUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePaymentAccountStatusesStore(
|
||||
|
|
@ -247,6 +301,25 @@ internal interface TangemPayDataModule {
|
|||
return GetCustomerOffersUseCase(customerOffersRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetCashbackSummaryUseCase(cashbackRepository: CashbackRepository): GetCashbackSummaryUseCase {
|
||||
return GetCashbackSummaryUseCase(cashbackRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetCashbackDeactivationDismissedUseCase(
|
||||
cashbackRepository: CashbackRepository,
|
||||
): GetCashbackDeactivationDismissedUseCase {
|
||||
return GetCashbackDeactivationDismissedUseCase(cashbackRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSetCashbackDeactivationDismissedUseCase(
|
||||
cashbackRepository: CashbackRepository,
|
||||
): SetCashbackDeactivationDismissedUseCase {
|
||||
return SetCashbackDeactivationDismissedUseCase(cashbackRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCheckOrderConflictUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
|
|
@ -308,5 +381,73 @@ internal interface TangemPayDataModule {
|
|||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCancelTangemPayOrderUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
issueCardRepository: TangemPayIssueCardRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
): CancelTangemPayOrderUseCase {
|
||||
return CancelTangemPayOrderUseCase(
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCreateTariffPlanTransitionOrderUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
issueCardRepository: TangemPayIssueCardRepository,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CreateTariffPlanTransitionOrderUseCase {
|
||||
return CreateTariffPlanTransitionOrderUseCase(
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSubmitTariffTransitionUseCase(
|
||||
createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
|
||||
setPendingTransition: SetTariffPlanPendingTransitionUseCase,
|
||||
): SubmitTariffTransitionUseCase {
|
||||
return SubmitTariffTransitionUseCase(
|
||||
createTransitionOrder = createTransitionOrder,
|
||||
setPendingTransition = setPendingTransition,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCancelTariffTransitionUseCase(
|
||||
cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase,
|
||||
getTariffTransitionUseCase: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
submitTariffTransitionUseCase: SubmitTariffTransitionUseCase,
|
||||
getCurrentTariffUseCase: GetCurrentTariffUseCase,
|
||||
): CancelTariffTransitionUseCase {
|
||||
return CancelTariffTransitionUseCase(
|
||||
cancelTangemPayOrderUseCase = cancelTangemPayOrderUseCase,
|
||||
getTariffTransitionUseCase = getTariffTransitionUseCase,
|
||||
submitTariffTransitionUseCase = submitTariffTransitionUseCase,
|
||||
getCurrentTariffUseCase = getCurrentTariffUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetCurrentTariffUseCase(
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
): GetCurrentTariffUseCase {
|
||||
return GetCurrentTariffUseCase(
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,9 +18,11 @@ import com.tangem.domain.pay.model.OrderData
|
|||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanStateUseCase
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
|
|
@ -63,6 +65,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val issueCardRepository: TangemPayIssueCardRepository,
|
||||
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val getTangemPayTariffPlanStateUseCase: GetTangemPayTariffPlanStateUseCase,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
|
@ -176,17 +180,63 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
},
|
||||
ifRight = { customerInfo ->
|
||||
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
|
||||
val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(account.userWalletId)
|
||||
.onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") }
|
||||
}
|
||||
status
|
||||
resolveFinalStatus(
|
||||
account = account,
|
||||
customerInfo = customerInfo,
|
||||
status = customerInfo.mapToPaymentAccountStatus(account.userWalletId),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun resolveFinalStatus(
|
||||
account: Account.Payment,
|
||||
customerInfo: CustomerInfo,
|
||||
status: PaymentAccountStatusValue,
|
||||
): PaymentAccountStatusValue {
|
||||
val isIssuing = status is PaymentAccountStatusValue.IssuingCard
|
||||
val isApproved = customerInfo.kycStatus == KycStatus.APPROVED
|
||||
val userWalletId = account.userWalletId
|
||||
val tariffPlan = customerInfo.tariffPlan
|
||||
|
||||
if (!tangemPayFeatureToggles.isTiersPlusPlanEnabled) {
|
||||
if (isIssuing && isApproved) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(userWalletId)
|
||||
.onLeft { logger.e("createOrder failed: $it") }
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
if (!isIssuing || !isApproved) {
|
||||
return status
|
||||
}
|
||||
|
||||
if (tariffPlan == null) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
val hasActiveIssueOrder = issueCardRepository.getIssueOrderIds(userWalletId).isNotEmpty()
|
||||
return if (hasActiveIssueOrder) {
|
||||
PaymentAccountStatusValue.Inactive(
|
||||
source = StatusSource.ACTUAL,
|
||||
tariffPlan = getTangemPayTariffPlanStateUseCase(
|
||||
userWalletId = userWalletId,
|
||||
tariff = tariffPlan,
|
||||
),
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.ZERO,
|
||||
currency = tariffPlan.plan.feeCurrencyOrDefault(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
PaymentAccountStatusValue.AwaitingPlanSelection(
|
||||
source = StatusSource.ACTUAL,
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue {
|
||||
// Step 1: Check KYC status first
|
||||
val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold(
|
||||
|
|
@ -288,24 +338,26 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
params = SingleQuoteStatusProducer.Params(rawCurrencyId = TangemPayCurrencyFactory.TOKEN_ID),
|
||||
)?.value as? QuoteStatus.Data
|
||||
|
||||
val customerId = customerId
|
||||
val isDeactivated = productInstance?.status == CustomerInfo.ProductInstance.Status.DEACTIVATED
|
||||
val isFormer = state == CustomerInfo.State.FORMER
|
||||
val fiatBalance = fiatBalance
|
||||
val cryptoBalance = cryptoBalance
|
||||
|
||||
val hasCardData = cards.isNotEmpty() && productInstances.isNotEmpty()
|
||||
val isTiersPlusPlanEnabled = tangemPayFeatureToggles.isTiersPlusPlanEnabled
|
||||
return when {
|
||||
kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty() -> {
|
||||
PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
}
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() &&
|
||||
(isDeactivated || isFormer) -> {
|
||||
customerId.isNullOrEmpty() -> PaymentAccountStatusValue.IssuingCard(
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
kycStatus != KycStatus.APPROVED -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = customerId,
|
||||
)
|
||||
fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) ->
|
||||
PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
customerId = customerId,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
|
|
@ -315,15 +367,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
fiatRate = quotesData?.fiatRate,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
cards.isNotEmpty() && productInstances.isNotEmpty() &&
|
||||
fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
fiatBalance != null && cryptoBalance != null && (hasCardData || isTiersPlusPlanEnabled) ->
|
||||
convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
fiatRate = quotesData?.fiatRate,
|
||||
customerId = customerId,
|
||||
)
|
||||
else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
}
|
||||
|
|
@ -362,11 +413,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
productInstance.frozenState
|
||||
},
|
||||
lastDigits = cardInfo.lastFourDigits,
|
||||
images = cardInfo.images,
|
||||
state = getCardState(cardId, userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
if (tangemPayCards.isEmpty()) return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
if (!tangemPayFeatureToggles.isTiersPlusPlanEnabled && tangemPayCards.isEmpty()) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
// Additional-card issuance: the backend omits the new card until it is provisioned, so surface a
|
||||
// placeholder for every locally tracked in-flight issuance order alongside the real cards.
|
||||
|
|
@ -377,6 +431,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
// the previously shown order and append newly seen cards at the end.
|
||||
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
||||
|
||||
val allCards = orderedCards + issuingCards
|
||||
|
||||
if (allCards.isEmpty()) {
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
val virtualAccount = resolveVirtualAccountOnramp(userWalletId)
|
||||
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
|
|
@ -385,7 +445,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
depositAddress = cryptoBalance.depositAddress,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = fiatRate,
|
||||
cards = orderedCards + issuingCards,
|
||||
cards = allCards,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
|
|
@ -393,6 +453,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
),
|
||||
error = null,
|
||||
virtualAccount = virtualAccount,
|
||||
tariffPlan = tariffPlan?.let { tariff ->
|
||||
getTangemPayTariffPlanStateUseCase(
|
||||
userWalletId = userWalletId,
|
||||
tariff = tariff,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -494,7 +560,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
return if (closingOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isTerminal) {
|
||||
closeCardRepository.setCloseOrderId(cardId, null)
|
||||
closeCardRepository.removeCloseOrderId(cardId)
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Closing
|
||||
|
|
@ -502,6 +568,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
} else if (reissueOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isTerminal) {
|
||||
reissueCardRepository.removeReissueOrderId(cardId)
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Reissuing
|
||||
|
|
@ -539,6 +606,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
limit = null,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
lastDigits = "",
|
||||
images = emptyList(),
|
||||
state = TangemPayCardState.Issuing,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.data.pay.util.CashbackAccrualDocsConverter
|
||||
import com.tangem.data.pay.util.CashbackHistoryConverter
|
||||
import com.tangem.data.pay.util.CashbackPromotionsConverter
|
||||
import com.tangem.data.pay.util.CashbackSummaryConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CashbackDocument
|
||||
import com.tangem.domain.pay.model.CashbackHistory
|
||||
import com.tangem.domain.pay.model.CashbackPromotions
|
||||
import com.tangem.domain.pay.model.CashbackSummary
|
||||
import com.tangem.domain.pay.repository.CashbackRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultCashbackRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val storage: TangemPayStorage,
|
||||
) : CashbackRepository {
|
||||
|
||||
override suspend fun getCashbackSummary(userWalletId: UserWalletId): Either<VisaApiError, CashbackSummary> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCashbackSummary(authHeader = authHeader)
|
||||
}.map(CashbackSummaryConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackPromotions(userWalletId: UserWalletId): Either<VisaApiError, CashbackPromotions> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCashbackPromotions(authHeader = authHeader)
|
||||
}.map(CashbackPromotionsConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackAccrualDocs(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, List<CashbackDocument>> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCashbackAccrualDocs(authHeader = authHeader)
|
||||
}.map(CashbackAccrualDocsConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackHistory(
|
||||
userWalletId: UserWalletId,
|
||||
months: Int,
|
||||
): Either<VisaApiError, CashbackHistory> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCashbackHistory(authHeader = authHeader, months = months)
|
||||
}.map(CashbackHistoryConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean {
|
||||
val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
return storage.getCashbackDeactivationDismissed(customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun setDeactivationBannerDismissed(userWalletId: UserWalletId) {
|
||||
val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
storage.storeCashbackDeactivationDismissed(customerWalletAddress = customerWalletAddress, isDismissed = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,14 +37,21 @@ internal class DefaultCloseCardRepository @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit> =
|
||||
override suspend fun storeCloseOrderId(cardId: String, orderId: String): Either<UniversalError, Unit> =
|
||||
runSuspendCatching {
|
||||
tangemPayCloseCardStore.setCloseOrderId(cardId, orderId)
|
||||
tangemPayCloseCardStore.storeCloseOrderId(cardId, orderId)
|
||||
}.fold(
|
||||
onSuccess = { Unit.right() },
|
||||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun removeCloseOrderId(cardId: String): Either<UniversalError, Unit> = runSuspendCatching {
|
||||
tangemPayCloseCardStore.removeCloseOrderId(cardId)
|
||||
}.fold(
|
||||
onSuccess = { Unit.right() },
|
||||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> =
|
||||
either {
|
||||
runSuspendCatching { tangemPayCloseCardStore.getOrderId(cardId) }.getOrNull()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerOffersResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.Offer
|
||||
import com.tangem.domain.pay.model.OrderType
|
||||
|
|
@ -18,18 +16,11 @@ internal class DefaultCustomerOffersRepository @Inject constructor(
|
|||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : CustomerOffersRepository {
|
||||
|
||||
private val offersCache = RuntimeSharedStore<Map<String, List<Offer>>>()
|
||||
|
||||
override suspend fun getOffers(userWalletId: UserWalletId): Either<VisaApiError, List<Offer>> {
|
||||
val key = userWalletId.stringValue
|
||||
offersCache.getSyncOrNull()?.get(key)?.let { return it.right() }
|
||||
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCustomerOffers(authHeader = authHeader)
|
||||
}.map { response ->
|
||||
response.result.map { it.toDomain() }
|
||||
}.onRight { offers ->
|
||||
offersCache.update(default = emptyMap()) { it + (key to offers) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.data.pay.util.OrderConverter
|
|||
import com.tangem.data.pay.util.OrderStatusConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.OrderRequest
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.Order
|
||||
import com.tangem.domain.pay.model.OrderData
|
||||
|
|
@ -53,8 +54,10 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
|
|||
override suspend fun createOrder(
|
||||
userWalletId: UserWalletId,
|
||||
type: OrderType,
|
||||
specificationName: String,
|
||||
specificationName: String?,
|
||||
idempotencyKey: String,
|
||||
targetTariffPlanId: String?,
|
||||
transitionType: TangemPayTariffPlanTransition.Type?,
|
||||
): Either<VisaApiError, Order> {
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
|
|
@ -65,6 +68,8 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
|
|||
customerWalletAddress = walletAddress,
|
||||
specificationName = specificationName,
|
||||
type = type.wireValue,
|
||||
targetTariffPlanId = targetTariffPlanId,
|
||||
tariffPlanTransitionType = transitionType?.name,
|
||||
),
|
||||
idempotencyKey = idempotencyKey,
|
||||
),
|
||||
|
|
@ -74,4 +79,10 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
|
|||
OrderConverter.convert(result)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun cancelOrder(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, Unit> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.cancelOrder(authHeader = authHeader, orderId = orderId)
|
||||
}.map {}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,8 @@ import com.tangem.datasource.api.pay.models.request.VirtualAccountOrderRequest
|
|||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
|
|
@ -28,6 +29,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderType
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
|
@ -50,6 +52,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val paymentAccountStatusStore: PaymentAccountStatusesStore,
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
|
||||
) : OnboardingRepository {
|
||||
|
||||
// Save data for a session
|
||||
|
|
@ -159,7 +162,11 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
val data = OrderRequest.Data(customerWalletAddress = walletAddress)
|
||||
val data = OrderRequest.Data(
|
||||
customerWalletAddress = walletAddress,
|
||||
specificationName = "SP_000004",
|
||||
type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC.wireValue,
|
||||
)
|
||||
tangemPayApi.createOrder(
|
||||
authHeader = authHeader,
|
||||
body = OrderRequest(data = data, idempotencyKey = UUID.randomUUID().toString()),
|
||||
|
|
@ -311,6 +318,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}.map {
|
||||
val address = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
tangemPayStorage.clearAll(userWalletId = userWalletId, customerWalletAddress = address)
|
||||
txHistoryItemsStore.remove(userWalletId.stringValue)
|
||||
setHideMainOnboardingBanner(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ internal class DefaultReissueCardRepository @Inject constructor(
|
|||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun removeReissueOrderId(cardId: String): Either<UniversalError, Unit> = runSuspendCatching {
|
||||
tangemPayReissueCardStore.removeReissueOrderId(cardId)
|
||||
}.fold(
|
||||
onSuccess = { Unit.right() },
|
||||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getReissueOrderId(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import com.tangem.datasource.api.pay.models.request.SetPinRequest
|
|||
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
|||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.BatchListSource
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.pagination.fetcher.BatchFetcher
|
|||
import com.tangem.pagination.fetcher.CursorBatchFetcher
|
||||
import com.tangem.pagination.toBatchFlow
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val INITIAL_CURSOR = "initial_cursor_key"
|
||||
|
|
@ -76,22 +79,39 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
cursor: String?,
|
||||
limit: Int,
|
||||
): List<TangemPayTxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(userWalletId = userWalletId, cursor = cursor),
|
||||
skipCache = config.shouldRefresh,
|
||||
block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) },
|
||||
)
|
||||
val storeKey = userWalletId.stringValue
|
||||
val storeCursor = cursor ?: INITIAL_CURSOR
|
||||
|
||||
return txHistoryItemsStore.getSyncOrNull(
|
||||
key = userWalletId.stringValue,
|
||||
cursor = cursor ?: INITIAL_CURSOR,
|
||||
).orEmpty()
|
||||
val fetchResult = runSuspendCatching {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(userWalletId = userWalletId, cursor = cursor),
|
||||
skipCache = config.shouldRefresh,
|
||||
block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) },
|
||||
)
|
||||
}
|
||||
val storedPage = txHistoryItemsStore.getSyncOrNull(key = storeKey, cursor = storeCursor)
|
||||
|
||||
return fetchResult.fold(
|
||||
onSuccess = { storedPage.orEmpty() },
|
||||
onFailure = { e -> storedPage?.takeIf { it.isNotEmpty() } ?: throw e },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String {
|
||||
return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}"
|
||||
}
|
||||
|
||||
override suspend fun getTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
transactionId: String,
|
||||
): Either<VisaApiError, TangemPayTxHistoryItem?> {
|
||||
return requestPerformer.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
visaApi.getCustomerTransaction(authHeader = authHeader, transactionId = transactionId)
|
||||
}.map { response ->
|
||||
txHistoryItemConverter.convert(response.result)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) {
|
||||
requestPerformer.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.datasource.api.pay.TangemPayApi
|
|||
import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest
|
||||
import com.tangem.datasource.api.pay.models.request.WithdrawRequest
|
||||
import com.tangem.datasource.api.pay.models.response.WithdrawResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTariffPlanTransitionsRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : TangemPayTariffPlanTransitionsRepository {
|
||||
|
||||
override suspend fun getTransitions(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, List<TangemPayTariffPlanTransition>> = either {
|
||||
val response = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getTariffPlanTransitions(authHeader = authHeader)
|
||||
}.bind()
|
||||
|
||||
response.result.orEmpty().mapNotNull { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun setPendingTransition(
|
||||
userWalletId: UserWalletId,
|
||||
pendingTariffPlanId: String,
|
||||
): Either<VisaApiError, Unit> = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.setPendingTariffPlanTransition(
|
||||
authHeader = authHeader,
|
||||
body = SetPendingTariffPlanTransitionRequest(pendingTariffPlanId = pendingTariffPlanId),
|
||||
)
|
||||
}.map {}
|
||||
|
||||
override suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either<VisaApiError, Unit> =
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(authHeader = authHeader)
|
||||
}.map {}
|
||||
|
||||
private fun TariffPlanTransitionResponse.toDomain(): TangemPayTariffPlanTransition? {
|
||||
val plan = TangemPayTariffPlanConverter.convert(tariffPlan) ?: return null
|
||||
return TangemPayTariffPlanTransition(
|
||||
type = TangemPayTariffPlanTransition.Type.fromString(type),
|
||||
plan = plan,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
|
|||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessTokenRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
|
|
|
|||
|
|
@ -0,0 +1,317 @@
|
|||
package com.tangem.data.pay.store
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.data.pay.entity.WithdrawStoreData
|
||||
import com.tangem.data.pay.util.WithdrawStateConverter
|
||||
import com.tangem.data.pay.util.WithdrawStoreDataConverter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val AUTH_TOKENS_DEFAULT_KEY = "tangem_pay_default_key"
|
||||
private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key"
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
internal class DefaultTangemPayStorage @Inject constructor(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : TangemPayStorage {
|
||||
|
||||
private val secureStorage by lazy {
|
||||
AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "tangem_pay_storage",
|
||||
)
|
||||
}
|
||||
|
||||
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
|
||||
private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() }
|
||||
private val withdrawStateConverter by lazy { WithdrawStateConverter() }
|
||||
|
||||
private val listType by lazy {
|
||||
Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
|
||||
}
|
||||
private val mapType by lazy {
|
||||
Types.newParameterizedType(Map::class.java, String::class.java, listType)
|
||||
}
|
||||
private val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> by lazy {
|
||||
appPreferencesStore.moshi.adapter(mapType)
|
||||
}
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
appPreferencesStore
|
||||
.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId),
|
||||
)
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
secureStorage.store(
|
||||
json.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
createAuthTokensKey(customerWalletAddress),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val authTokens = secureStorage.get(createAuthTokensKey(customerWalletAddress))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(tokensAdapter::fromJson)
|
||||
|
||||
authTokens?.let { tokens ->
|
||||
if (tokens.idempotencyKey == null) {
|
||||
val newAuthTokens = tokens.copy(idempotencyKey = UUID.randomUUID().toString())
|
||||
storeAuthTokens(customerWalletAddress, newAuthTokens)
|
||||
newAuthTokens
|
||||
} else {
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearAuthTokens(customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), orderId)
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(key = PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress))
|
||||
.takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), isDone)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackDeactivationDismissed(customerWalletAddress: String): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayCashbackDeactivationDismissedKey(customerWalletAddress),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeCashbackDeactivationDismissed(customerWalletAddress: String, isDismissed: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.getTangemPayCashbackDeactivationDismissedKey(customerWalletAddress),
|
||||
value = isDismissed,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeVirtualAccountOrderId(customerWalletAddress: String, vaOrderId: String) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress),
|
||||
value = vaOrderId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getVirtualAccountOrderId(customerWalletAddress: String): String? {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress),
|
||||
).takeIf { !it.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun clearVirtualAccountOrderId(customerWalletAddress: String) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayVirtualAccountOrderIdKey(customerWalletAddress), "")
|
||||
}
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId),
|
||||
isPaeraCustomer,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
|
||||
}
|
||||
|
||||
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val newItem = withdrawStoreDataConverter.convert(data)
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size + 1) {
|
||||
for (item in currentList) { if (item.orderId != newItem.orderId) add(item) }
|
||||
add(newItem)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] =
|
||||
adapter.toJson(currentMap + (walletKey to updatedList))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
|
||||
val orders = appPreferencesStore.getObjectMapSync<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
|
||||
val map = appPreferencesStore.data.firstOrNull()
|
||||
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
|
||||
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val orders = mutablePreferences.getObjectMap<String>(
|
||||
PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.minus(createWithdrawOrderIdKey(userWalletId))
|
||||
mutablePreferences.setObjectMap(
|
||||
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
|
||||
value = orders,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val currentList = currentMap[walletKey].orEmpty()
|
||||
val updatedList = buildList(currentList.size) {
|
||||
for (item in currentList) {
|
||||
if (item.orderId != orderId) add(item)
|
||||
}
|
||||
}
|
||||
val updatedMap = if (updatedList.isEmpty()) {
|
||||
currentMap - walletKey
|
||||
} else {
|
||||
currentMap + (walletKey to updatedList)
|
||||
}
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide)
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return appPreferencesStore.getSyncOrNull(
|
||||
key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId),
|
||||
) == true
|
||||
}
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
value = eligibility,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Set<String> {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.TANGEM_PAY_ELIGIBILITY_KEY,
|
||||
default = emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true)
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||
val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId)
|
||||
return appPreferencesStore.getSyncOrNull(key) == true
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createAuthTokensKey(customerWalletAddress))
|
||||
}
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "")
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false)
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false)
|
||||
// Clear the withdraw order hints together with the rest of the cache.
|
||||
deleteActiveWithdrawOrder(userWalletId)
|
||||
clearWithdrawOrders(userWalletId)
|
||||
}
|
||||
|
||||
private suspend fun clearWithdrawOrders(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
val walletKey = createWithdrawOrderIdKey(userWalletId)
|
||||
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
val updatedMap = currentMap - walletKey
|
||||
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address"
|
||||
|
||||
private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId"
|
||||
}
|
||||
|
|
@ -124,6 +124,20 @@ internal class PaymentAccountStatusesStore(
|
|||
return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue)
|
||||
}
|
||||
|
||||
suspend fun remove(userWalletId: UserWalletId) {
|
||||
logger.i("remove($userWalletId)")
|
||||
remove(userWalletIds = listOf(userWalletId))
|
||||
}
|
||||
|
||||
suspend fun remove(userWalletIds: List<UserWalletId>) {
|
||||
logger.i("remove($userWalletIds)")
|
||||
val keys = userWalletIds.map { it.stringValue }.toSet()
|
||||
coroutineScope {
|
||||
launch { runtimeStore.update(default = emptyMap()) { it - keys } }
|
||||
launch { persistenceDataStore.updateData { it - keys } }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) {
|
||||
runtimeStore.update(default = emptyMap()) { stored ->
|
||||
stored.toMutableMap().apply {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.data.pay.store
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
|
||||
|
||||
suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens)
|
||||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens?
|
||||
suspend fun clearAuthTokens(customerWalletAddress: String)
|
||||
|
||||
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
|
||||
|
||||
suspend fun getOrderId(customerWalletAddress: String): String?
|
||||
|
||||
suspend fun clearOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun storeVirtualAccountOrderId(customerWalletAddress: String, vaOrderId: String)
|
||||
|
||||
suspend fun getVirtualAccountOrderId(customerWalletAddress: String): String?
|
||||
|
||||
suspend fun clearVirtualAccountOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
|
||||
|
||||
suspend fun getCashbackDeactivationDismissed(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeCashbackDeactivationDismissed(customerWalletAddress: String, isDismissed: Boolean)
|
||||
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean)
|
||||
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
|
||||
|
||||
/** Called after creating withdraw order, active order id */
|
||||
suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String)
|
||||
|
||||
/** Called after creating withdraw order, saves order data */
|
||||
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState)
|
||||
|
||||
/** Returns single active order id. Once the order is completed, deletes id from storage.
|
||||
* Only one active order allowed for a wallet */
|
||||
suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String?
|
||||
|
||||
/** Returns all withdraw orders saved.
|
||||
* Once we get tx hash for an order, it gets deleted from this storage */
|
||||
suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>?
|
||||
|
||||
/** Deletes active withdraw order. Called after order is completed */
|
||||
suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId)
|
||||
|
||||
/** Deletes withdraw order data. Called after getting its tx hash */
|
||||
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String)
|
||||
|
||||
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
|
||||
|
||||
suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean)
|
||||
suspend fun storeTangemPayEligibility(eligibility: Set<String>)
|
||||
suspend fun getTangemPayEligibility(): Set<String>
|
||||
|
||||
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
|
||||
suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId)
|
||||
suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse
|
||||
import com.tangem.domain.pay.model.CashbackDocument
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/** Maps [CashbackAccrualDocsResponse] (BFF) to domain [CashbackDocument]s, dropping malformed entries. */
|
||||
internal object CashbackAccrualDocsConverter : Converter<CashbackAccrualDocsResponse, List<CashbackDocument>> {
|
||||
|
||||
override fun convert(value: CashbackAccrualDocsResponse): List<CashbackDocument> {
|
||||
return value.docs.orEmpty().mapNotNull(::convertDoc)
|
||||
}
|
||||
|
||||
private fun convertDoc(doc: CashbackAccrualDocsResponse.Doc): CashbackDocument? {
|
||||
return CashbackDocument(
|
||||
id = doc.id ?: return null,
|
||||
title = doc.title ?: return null,
|
||||
url = doc.url ?: return null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackHistoryResponse
|
||||
import com.tangem.domain.pay.model.CashbackHistory
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Maps [CashbackHistoryResponse] (BFF) to the domain [CashbackHistory]. */
|
||||
internal object CashbackHistoryConverter : Converter<CashbackHistoryResponse, CashbackHistory> {
|
||||
|
||||
override fun convert(value: CashbackHistoryResponse): CashbackHistory {
|
||||
return CashbackHistory(
|
||||
currency = value.currency.orEmpty(),
|
||||
months = value.items.orEmpty().map { item ->
|
||||
CashbackHistory.MonthlyCashback(
|
||||
year = item.year,
|
||||
month = item.month,
|
||||
confirmedAmount = item.confirmedAmount ?: BigDecimal.ZERO,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse
|
||||
import com.tangem.domain.pay.model.CashbackPromotions
|
||||
import com.tangem.utils.converter.Converter
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/** Maps [CashbackPromotionsResponse] (BFF) to the domain [CashbackPromotions]. */
|
||||
internal object CashbackPromotionsConverter : Converter<CashbackPromotionsResponse, CashbackPromotions> {
|
||||
|
||||
override fun convert(value: CashbackPromotionsResponse): CashbackPromotions {
|
||||
return CashbackPromotions(
|
||||
cardTiers = value.cashbackOnCards?.tiers.orEmpty().map(::convertTier),
|
||||
additionalCashback = value.additionalCashback.orEmpty().map(::convertAdditional),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertTier(tier: CashbackPromotionsResponse.CardTier): CashbackPromotions.CardTier {
|
||||
return CashbackPromotions.CardTier(
|
||||
tier = tier.tier.orEmpty(),
|
||||
label = tier.label.orEmpty(),
|
||||
scope = tier.scope.orEmpty(),
|
||||
minTransactionAmount = tier.minTransactionAmount,
|
||||
monthlyCapAmount = tier.tierMonthlyCapAmount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertAdditional(
|
||||
promo: CashbackPromotionsResponse.AdditionalCashback,
|
||||
): CashbackPromotions.AdditionalCashback {
|
||||
val endDate = promo.endDate?.let { runCatching { DateTime.parse(it) }.getOrNull() }
|
||||
return CashbackPromotions.AdditionalCashback(
|
||||
id = promo.id.orEmpty(),
|
||||
name = promo.name.orEmpty(),
|
||||
description = promo.description.orEmpty(),
|
||||
isPermanent = promo.isPermanent ?: (endDate == null),
|
||||
endDate = endDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackSummaryResponse
|
||||
import com.tangem.domain.pay.model.CashbackDisplayMode
|
||||
import com.tangem.domain.pay.model.CashbackProgramStatus
|
||||
import com.tangem.domain.pay.model.CashbackSummary
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import com.tangem.utils.converter.Converter
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Maps [CashbackSummaryResponse] (BFF) to the domain [CashbackSummary]. */
|
||||
internal object CashbackSummaryConverter : Converter<CashbackSummaryResponse, CashbackSummary> {
|
||||
|
||||
override fun convert(value: CashbackSummaryResponse): CashbackSummary {
|
||||
return when (CashbackProgramStatus.fromString(value.cashbackProgramStatus)) {
|
||||
CashbackProgramStatus.ENABLED -> toEnabled(value)
|
||||
CashbackProgramStatus.DEACTIVATED -> CashbackSummary.Deactivated
|
||||
CashbackProgramStatus.DISABLED -> CashbackSummary.Disabled
|
||||
CashbackProgramStatus.UNKNOWN -> CashbackSummary.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun toEnabled(value: CashbackSummaryResponse): CashbackSummary {
|
||||
val period = value.period ?: return CashbackSummary.Unknown
|
||||
val payoutStart = period.payoutStartDate?.let(DateTime::parse) ?: return CashbackSummary.Unknown
|
||||
val payoutEnd = period.payoutEndDate?.let(DateTime::parse) ?: return CashbackSummary.Unknown
|
||||
|
||||
return CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.fromString(value.cashbackDisplayMode),
|
||||
cashback = TangemPayCashback(
|
||||
confirmedAmount = value.confirmedAmount ?: BigDecimal.ZERO,
|
||||
pendingAmount = value.pendingAmount ?: BigDecimal.ZERO,
|
||||
currency = value.currency.orEmpty(),
|
||||
payoutCurrency = value.payoutCurrency.orEmpty(),
|
||||
payoutNetwork = value.payoutNetwork.orEmpty(),
|
||||
period = TangemPayCashback.Period(
|
||||
year = period.year,
|
||||
month = period.month,
|
||||
payoutStart = payoutStart,
|
||||
payoutEnd = payoutEnd,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
|
||||
import com.tangem.datasource.api.pay.models.response.CryptoBalance
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.api.pay.models.response.FiatBalance
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
|
|
@ -18,6 +21,7 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDat
|
|||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, CustomerInfo> {
|
||||
override fun convert(value: CustomerMeResponse.Result): CustomerInfo {
|
||||
|
|
@ -45,9 +49,25 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
fiatBalance = fiatBalance?.toDomain(),
|
||||
cryptoBalance = cryptoBalance?.toDomain(),
|
||||
availableForWithdrawal = value.balance?.availableForWithdrawal?.amount.orZero(),
|
||||
tariffPlan = value.customerTariffPlan?.toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.CustomerTariffPlan.toDomain(): TangemPayCustomerTariffPlan? {
|
||||
val plan = tariffPlan?.toDomain() ?: return null
|
||||
return TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.fromString(status),
|
||||
source = TangemPayCustomerTariffPlan.Source.fromString(source),
|
||||
plan = plan,
|
||||
nextBillingAt = nextBillingAt.toDateTimeOrNull(),
|
||||
pendingPlan = pendingTariffPlan?.toDomain(),
|
||||
pendingTransitionAt = pendingTransitionAt.toDateTimeOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.TariffPlan.toDomain(): TangemPayTariffPlan? =
|
||||
TangemPayTariffPlanConverter.convert(this)
|
||||
|
||||
private fun CustomerMeResponse.ProductInstance.toDomain(): ProductInstance {
|
||||
val status = status.toDomain()
|
||||
val cardFrozenState = when (status) {
|
||||
|
|
@ -73,6 +93,15 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
cardStatus = TangemPayCard.Status.fromString(card.cardStatus),
|
||||
lastFourDigits = card.cardNumberEnd,
|
||||
isPinSet = card.isPinSet == true,
|
||||
images = card.images.orEmpty().mapNotNull(::convertCardImage),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertCardImage(image: CustomerMeResponse.Image): TangemPayTariffPlan.Image? {
|
||||
val url = image.url ?: return null
|
||||
return TangemPayTariffPlan.Image(
|
||||
type = TangemPayTariffPlan.Image.Type.fromString(image.type),
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -116,4 +145,6 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
CustomerMeResponse.ProductInstance.SpecificationDataType.ACCOUNT -> SpecificationDataType.ACCOUNT
|
||||
CustomerMeResponse.ProductInstance.SpecificationDataType.CARD -> SpecificationDataType.CARD
|
||||
}
|
||||
|
||||
private fun String?.toDateTimeOrNull(): DateTime? = this?.let { runCatching { DateTime.parse(it) }.getOrNull() }
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.pay.util
|
|||
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse
|
||||
import com.tangem.domain.pay.model.Order
|
||||
import com.tangem.domain.pay.model.OrderStep
|
||||
import com.tangem.domain.pay.model.OrderType
|
||||
|
||||
/** Maps a wire `OrderResponse.Result` into the domain [Order] model. */
|
||||
|
|
@ -15,11 +16,12 @@ internal object OrderConverter {
|
|||
customerId = value.customerId,
|
||||
type = type,
|
||||
status = status,
|
||||
step = value.step,
|
||||
step = OrderStep.fromString(value.step),
|
||||
stepChangeCode = value.stepChangeCode,
|
||||
productInstanceId = value.data.productInstanceId,
|
||||
paymentAccountId = value.data.paymentAccountId,
|
||||
cardId = null, // Card id not in v1 response shape; resolved via productInstanceId.
|
||||
toTariffPlanId = value.data.targetTariffPlanId,
|
||||
withdrawTxHash = value.data.transactionHash?.ifEmpty { null },
|
||||
createdAt = value.createdAt,
|
||||
updatedAt = value.updatedAt,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ internal class VisaLibLoader @Inject constructor(
|
|||
val config = getOrLoadConfig()
|
||||
|
||||
provider = VisaContractInfoProvider.Builder(
|
||||
useTestnetRpc = VisaConstants.USE_TEST_ENV,
|
||||
isTestnetRpcEnabled = VisaConstants.USE_TEST_ENV,
|
||||
bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) {
|
||||
config.testnet.bridgeProcessor
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) :
|
|||
status = TangemPayTxHistoryItemStatusConverter.convert(spend.status),
|
||||
enrichedMerchantIconUrl = spend.enrichedMerchantIcon,
|
||||
declinedReason = spend.declinedReason,
|
||||
cardName = spend.cardDisplayName,
|
||||
cardNumberLast4 = spend.cardNumberEnd,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.MockAwareCashbackRepository
|
||||
import com.tangem.data.pay.repository.MockAwareOnboardingRepository
|
||||
import com.tangem.data.pay.repository.MockAwareTangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.CashbackRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import dagger.Binds
|
||||
|
|
@ -21,4 +23,8 @@ internal interface TangemPayDataMockedModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindCardDetailsRepository(repository: MockAwareTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCashbackRepository(repository: MockAwareCashbackRepository): CashbackRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CashbackDisplayMode
|
||||
import com.tangem.domain.pay.model.CashbackDocument
|
||||
import com.tangem.domain.pay.model.CashbackHistory
|
||||
import com.tangem.domain.pay.model.CashbackPromotions
|
||||
import com.tangem.domain.pay.model.CashbackSummary
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import com.tangem.domain.pay.repository.CashbackRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** In MOCK env returns canned cashback data; otherwise delegates to [DefaultCashbackRepository]. */
|
||||
@Singleton
|
||||
internal class MockAwareCashbackRepository @Inject constructor(
|
||||
private val real: DefaultCashbackRepository,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : CashbackRepository {
|
||||
|
||||
private val isMockMode: Boolean
|
||||
get() = apiConfigsManager
|
||||
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
|
||||
.environment == ApiEnvironment.MOCK
|
||||
|
||||
override suspend fun getCashbackSummary(userWalletId: UserWalletId): Either<VisaApiError, CashbackSummary> {
|
||||
if (isMockMode) return MOCK_SUMMARY.right()
|
||||
return real.getCashbackSummary(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackPromotions(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, CashbackPromotions> {
|
||||
if (isMockMode) return MOCK_PROMOTIONS.right()
|
||||
return real.getCashbackPromotions(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackAccrualDocs(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, List<CashbackDocument>> {
|
||||
if (isMockMode) return MOCK_DOCS.right()
|
||||
return real.getCashbackAccrualDocs(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getCashbackHistory(
|
||||
userWalletId: UserWalletId,
|
||||
months: Int,
|
||||
): Either<VisaApiError, CashbackHistory> {
|
||||
if (isMockMode) return MOCK_HISTORY.copy(months = MOCK_HISTORY.months.takeLast(months)).right()
|
||||
return real.getCashbackHistory(userWalletId, months)
|
||||
}
|
||||
|
||||
override suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean =
|
||||
real.isDeactivationBannerDismissed(userWalletId)
|
||||
|
||||
override suspend fun setDeactivationBannerDismissed(userWalletId: UserWalletId) =
|
||||
real.setDeactivationBannerDismissed(userWalletId)
|
||||
|
||||
private companion object {
|
||||
val MOCK_SUMMARY = CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.FULL,
|
||||
cashback = TangemPayCashback(
|
||||
confirmedAmount = BigDecimal("22.54"),
|
||||
pendingAmount = BigDecimal("13.65"),
|
||||
currency = "USD",
|
||||
payoutCurrency = "USDC",
|
||||
payoutNetwork = "Polygon",
|
||||
period = TangemPayCashback.Period(
|
||||
year = 2026,
|
||||
month = 6,
|
||||
payoutStart = DateTime.parse("2026-07-02"),
|
||||
payoutEnd = DateTime.parse("2026-07-05"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val MOCK_PROMOTIONS = CashbackPromotions(
|
||||
cardTiers = listOf(
|
||||
CashbackPromotions.CardTier(
|
||||
tier = "basic",
|
||||
label = "Basic cards",
|
||||
scope = "All purchases",
|
||||
minTransactionAmount = BigDecimal("30"),
|
||||
monthlyCapAmount = BigDecimal("100"),
|
||||
),
|
||||
CashbackPromotions.CardTier(
|
||||
tier = "plus",
|
||||
label = "Plus cards",
|
||||
scope = "All purchases",
|
||||
minTransactionAmount = BigDecimal("30"),
|
||||
monthlyCapAmount = BigDecimal("300"),
|
||||
),
|
||||
),
|
||||
additionalCashback = listOf(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-permanent",
|
||||
name = "Groceries increase",
|
||||
description = "+1% cashback for groceries stores",
|
||||
isPermanent = true,
|
||||
endDate = null,
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-groceries-2026",
|
||||
name = "Groceries increase",
|
||||
description = "+1% cashback for groceries stores. Max \$10/month",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-cashback-2026",
|
||||
name = "Cashback increase",
|
||||
description = "+2% cashback for groceries stores. Max \$10/month",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val MOCK_DOCS = listOf(
|
||||
CashbackDocument(
|
||||
id = "excluded",
|
||||
title = "All categories without cashback",
|
||||
url = "https://tangem.com/docs/en/tangem-pay-cashback-excluded-mccs.pdf",
|
||||
),
|
||||
CashbackDocument(
|
||||
id = "terms",
|
||||
title = "Full terms of cashback program",
|
||||
url = "https://tangem.com/docs/en/tangem-pay-cashback-terms.pdf",
|
||||
),
|
||||
)
|
||||
|
||||
val MOCK_HISTORY = CashbackHistory(
|
||||
currency = "USD",
|
||||
months = listOf(
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 2, confirmedAmount = BigDecimal("12.02")),
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 3, confirmedAmount = BigDecimal("44.22")),
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 4, confirmedAmount = BigDecimal("38.52")),
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 5, confirmedAmount = BigDecimal("26.10")),
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,16 @@ import java.util.concurrent.ConcurrentHashMap
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** In MOCK env skips local-storage / signing enrollment; server calls go to WireMock. */
|
||||
/**
|
||||
* In MOCK env only skips the local-storage / NFC-signing enrollment steps (order ids, initial data). The
|
||||
* customer-facing state — whether a wallet has Tangem Pay ([hasTangemPayInWallet]), KYC status, ACTIVE /
|
||||
* INACTIVE, balances ([getCustomerInfo]) — is driven by the WireMock test scenario (authenticated with the
|
||||
* synthetic tokens from [com.tangem.data.pay.store.MockAwareTangemPayStorage]) rather than hardcoded:
|
||||
* - [hasTangemPayInWallet] delegates to the real repo, so the "existing customer" gate follows the
|
||||
* checkCustomerWalletId mock (the `tangem_pay_eligibility` scenario: `Started` → 404/NotPaeraCustomer →
|
||||
* no Payment account, `PaeraCustomer` → 200 → Payment account);
|
||||
* - [getCustomerInfo] delegates to the real repo (WireMock), so KYC / customer-state scenarios take effect.
|
||||
*/
|
||||
@Singleton
|
||||
internal class MockAwareOnboardingRepository @Inject constructor(
|
||||
private val real: DefaultOnboardingRepository,
|
||||
|
|
@ -46,6 +55,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
real.produceInitialData(userWalletId)
|
||||
}
|
||||
|
||||
// Delegates to WireMock (via the real repo + synthetic storage tokens) so the customer state — KYC status,
|
||||
// ACTIVE/INACTIVE, balances — follows the test scenario instead of a hardcoded "always active" customer.
|
||||
// Note: this may be invoked even when `hasTangemPayInWallet` is false (e.g., onboarding/deeplink flows),
|
||||
// so tests must provide the corresponding WireMock mappings.
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
|
||||
real.getCustomerInfo(userWalletId)
|
||||
|
||||
|
|
@ -108,6 +121,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
real.clearVirtualAccountOrderId(userWalletId)
|
||||
}
|
||||
|
||||
// The "existing Tangem Pay customer" gate (decides whether an active Payment account — and accounts mode —
|
||||
// appears). Delegates to WireMock's checkCustomerWalletId via the real repo (static token, no signing), so it
|
||||
// is driven by the `tangem_pay_eligibility` scenario: `Started` (default) → 404/NotPaeraCustomer → no account;
|
||||
// `PaeraCustomer` → 200 → account. Generic UI tests never set the scenario, so they stay Payment-free.
|
||||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
||||
real.hasTangemPayInWallet(userWalletId)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.data.pay.store
|
||||
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val MOCK_CUSTOMER_WALLET_ADDRESS = "0x0000000000000000000000000000000000000002"
|
||||
private const val MOCK_ACCESS_TOKEN = "mock-access-token"
|
||||
private const val MOCK_REFRESH_TOKEN = "mock-refresh-token"
|
||||
private const val MOCK_IDEMPOTENCY_KEY = "mock-idempotency-key"
|
||||
private const val MOCK_TOKEN_EXPIRES_AT = 9_999_999_999L
|
||||
|
||||
/** In MOCK env returns synthetic auth tokens + customer wallet address; otherwise delegates. */
|
||||
@Singleton
|
||||
internal class MockAwareTangemPayStorage @Inject constructor(
|
||||
private val real: DefaultTangemPayStorage,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
) : TangemPayStorage {
|
||||
|
||||
private val isMockMode: Boolean
|
||||
get() = apiConfigsManager
|
||||
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
|
||||
.environment == ApiEnvironment.MOCK
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
if (isMockMode) return
|
||||
real.storeCustomerWalletAddress(userWalletId, customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
|
||||
if (isMockMode) return MOCK_CUSTOMER_WALLET_ADDRESS
|
||||
return real.getCustomerWalletAddress(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
|
||||
if (isMockMode) return
|
||||
real.clearCustomerWalletAddress(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) {
|
||||
if (isMockMode) return
|
||||
real.storeAuthTokens(customerWalletAddress, tokens)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
|
||||
if (isMockMode) {
|
||||
return TangemPayAuthTokens(
|
||||
accessToken = MOCK_ACCESS_TOKEN,
|
||||
expiresAt = MOCK_TOKEN_EXPIRES_AT,
|
||||
refreshToken = MOCK_REFRESH_TOKEN,
|
||||
refreshExpiresAt = MOCK_TOKEN_EXPIRES_AT,
|
||||
idempotencyKey = MOCK_IDEMPOTENCY_KEY,
|
||||
)
|
||||
}
|
||||
return real.getAuthTokens(customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun clearAuthTokens(customerWalletAddress: String) {
|
||||
if (isMockMode) return
|
||||
real.clearAuthTokens(customerWalletAddress)
|
||||
}
|
||||
|
||||
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) =
|
||||
real.storeOrderId(customerWalletAddress, orderId)
|
||||
|
||||
override suspend fun getOrderId(customerWalletAddress: String): String? =
|
||||
real.getOrderId(customerWalletAddress)
|
||||
|
||||
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean =
|
||||
real.getAddToWalletDone(customerWalletAddress)
|
||||
|
||||
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) =
|
||||
real.storeAddToWalletDone(customerWalletAddress, isDone)
|
||||
|
||||
override suspend fun getCashbackDeactivationDismissed(customerWalletAddress: String): Boolean =
|
||||
real.getCashbackDeactivationDismissed(customerWalletAddress)
|
||||
|
||||
override suspend fun storeCashbackDeactivationDismissed(customerWalletAddress: String, isDismissed: Boolean) =
|
||||
real.storeCashbackDeactivationDismissed(customerWalletAddress, isDismissed)
|
||||
|
||||
override suspend fun clearOrderId(customerWalletAddress: String) =
|
||||
real.clearOrderId(customerWalletAddress)
|
||||
|
||||
override suspend fun storeVirtualAccountOrderId(customerWalletAddress: String, vaOrderId: String) =
|
||||
real.storeVirtualAccountOrderId(customerWalletAddress, vaOrderId)
|
||||
|
||||
override suspend fun getVirtualAccountOrderId(customerWalletAddress: String): String? =
|
||||
real.getVirtualAccountOrderId(customerWalletAddress)
|
||||
|
||||
override suspend fun clearVirtualAccountOrderId(customerWalletAddress: String) =
|
||||
real.clearVirtualAccountOrderId(customerWalletAddress)
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) =
|
||||
real.storeCheckCustomerWalletResult(userWalletId, isPaeraCustomer)
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? =
|
||||
real.checkCustomerWalletResult(userWalletId)
|
||||
|
||||
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) =
|
||||
real.storeActiveWithdrawOrderId(userWalletId, orderId)
|
||||
|
||||
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) =
|
||||
real.storeWithdrawOrder(userWalletId, data)
|
||||
|
||||
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? =
|
||||
real.getActiveWithdrawOrderId(userWalletId)
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>? =
|
||||
real.getWithdrawOrders(userWalletId)
|
||||
|
||||
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) =
|
||||
real.deleteActiveWithdrawOrder(userWalletId)
|
||||
|
||||
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) =
|
||||
real.deleteWithdrawOrder(userWalletId, orderId)
|
||||
|
||||
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) =
|
||||
real.storeHideOnboardingBanner(userWalletId, hide)
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean =
|
||||
real.getHideMainOnboardingBanner(userWalletId)
|
||||
|
||||
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) =
|
||||
real.storeTangemPayEligibility(eligibility)
|
||||
|
||||
override suspend fun getTangemPayEligibility(): Set<String> = real.getTangemPayEligibility()
|
||||
|
||||
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) =
|
||||
real.storeIsTangemPayDeactivated(userWalletId)
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean =
|
||||
real.isTangemPayDeactivated(userWalletId)
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
|
||||
real.clearAll(userWalletId, customerWalletAddress)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.pay.store.di
|
||||
|
||||
import com.tangem.data.pay.store.MockAwareTangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface TangemPayStorageMockedModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayStorage(impl: MockAwareTangemPayStorage): TangemPayStorage
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.DefaultCashbackRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.CashbackRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import dagger.Binds
|
||||
|
|
@ -21,4 +23,8 @@ internal interface TangemPayDataProductionModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCashbackRepository(repository: DefaultCashbackRepository): CashbackRepository
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.pay.store.di
|
||||
|
||||
import com.tangem.data.pay.store.DefaultTangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface TangemPayStorageProductionModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.pay
|
||||
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.Runs
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayUserWalletDataCleanerTest {
|
||||
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore = mockk()
|
||||
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore = mockk()
|
||||
|
||||
private val cleaner = TangemPayUserWalletDataCleaner(
|
||||
paymentAccountStatusesStore = paymentAccountStatusesStore,
|
||||
txHistoryItemsStore = txHistoryItemsStore,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(paymentAccountStatusesStore, txHistoryItemsStore)
|
||||
coEvery { paymentAccountStatusesStore.remove(any<List<UserWalletId>>()) } just Runs
|
||||
coEvery { txHistoryItemsStore.remove(any<List<String>>()) } just Runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallets WHEN clear THEN each store is cleared once with all ids in a single call`() = runTest {
|
||||
// Act
|
||||
cleaner.clear(listOf(WALLET_A, WALLET_B))
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { paymentAccountStatusesStore.remove(listOf(WALLET_A, WALLET_B)) }
|
||||
coVerify(exactly = 1) { txHistoryItemsStore.remove(listOf(WALLET_A.stringValue, WALLET_B.stringValue)) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val WALLET_A = UserWalletId("011")
|
||||
val WALLET_B = UserWalletId("022")
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,40 @@ internal class PaymentAccountStatusValueDMConverterTest {
|
|||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain AwaitingPlanSelection WHEN convert THEN returns null (transient, not persisted)`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.AwaitingPlanSelection(
|
||||
source = StatusSource.ACTUAL,
|
||||
tariffPlan = mockk(),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Inactive WHEN convert THEN returns null (transient, not persisted)`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.Inactive(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal("100"),
|
||||
currency = "USD",
|
||||
),
|
||||
tariffPlan = mockk(),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Error Unavailable WHEN convert THEN returns null (transient, not persisted)`() {
|
||||
// GIVEN
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TangemPayTariffPlanConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN null value WHEN convert THEN returns null`() {
|
||||
assertThat(TangemPayTariffPlanConverter.convert(null)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing id WHEN convert THEN returns null`() {
|
||||
val value = tariffPlan(id = null)
|
||||
|
||||
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing name WHEN convert THEN returns null`() {
|
||||
val value = tariffPlan(name = null)
|
||||
|
||||
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing type WHEN convert THEN returns null`() {
|
||||
val value = tariffPlan(type = null)
|
||||
|
||||
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing programName WHEN convert THEN returns null`() {
|
||||
val value = tariffPlan(programName = null)
|
||||
|
||||
assertThat(TangemPayTariffPlanConverter.convert(value)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN basic tier in mixed case WHEN convert THEN isBasicTier is true`() {
|
||||
val value = tariffPlan(type = "Basic")
|
||||
|
||||
val result = TangemPayTariffPlanConverter.convert(value)
|
||||
|
||||
assertThat(result?.tierId).isEqualTo("Basic")
|
||||
assertThat(result?.isBasicTier).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN full valid plan WHEN convert THEN maps all fields`() {
|
||||
// GIVEN
|
||||
val value = tariffPlan(
|
||||
type = "PLUS",
|
||||
descriptionItems = listOf(
|
||||
CustomerMeResponse.DescriptionItem(
|
||||
type = "PLAN_RELATED",
|
||||
order = 2,
|
||||
title = "Title",
|
||||
body = "Body",
|
||||
),
|
||||
),
|
||||
images = listOf(CustomerMeResponse.Image(type = "MAIN", url = "https://img")),
|
||||
fees = listOf(
|
||||
CustomerMeResponse.Fee(
|
||||
type = "RECURRING",
|
||||
amount = BigDecimal("9.99"),
|
||||
currency = "USD",
|
||||
description = "Monthly",
|
||||
period = "MONTH",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = TangemPayTariffPlanConverter.convert(value)
|
||||
|
||||
// THEN
|
||||
val expected = TangemPayTariffPlan(
|
||||
id = PLAN_ID,
|
||||
tierId = "PLUS",
|
||||
isBasicTier = false,
|
||||
name = PLAN_NAME,
|
||||
programName = PROGRAM_NAME,
|
||||
descriptionItems = listOf(
|
||||
TangemPayTariffPlan.DescriptionItem(
|
||||
section = TangemPayTariffPlan.Section.PLAN_RELATED,
|
||||
order = 2,
|
||||
title = "Title",
|
||||
body = "Body",
|
||||
),
|
||||
),
|
||||
images = listOf(
|
||||
TangemPayTariffPlan.Image(type = TangemPayTariffPlan.Image.Type.MAIN, url = "https://img"),
|
||||
),
|
||||
fees = listOf(
|
||||
TangemPayTariffPlan.Fee(
|
||||
type = TangemPayTariffPlan.Fee.Type.RECURRING,
|
||||
amount = BigDecimal("9.99"),
|
||||
currency = "USD",
|
||||
description = "Monthly",
|
||||
period = TangemPayTariffPlan.Fee.Period.MONTH,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown enum strings and null optional fields WHEN convert THEN falls back to defaults`() {
|
||||
// GIVEN
|
||||
val value = tariffPlan(
|
||||
type = "SOMETHING_NEW",
|
||||
descriptionItems = listOf(
|
||||
CustomerMeResponse.DescriptionItem(type = "wat", order = null, title = "Title", body = null),
|
||||
),
|
||||
images = listOf(CustomerMeResponse.Image(type = null, url = "https://img")),
|
||||
fees = listOf(
|
||||
CustomerMeResponse.Fee(
|
||||
type = null,
|
||||
amount = BigDecimal.ONE,
|
||||
currency = null,
|
||||
description = null,
|
||||
period = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = TangemPayTariffPlanConverter.convert(value)
|
||||
|
||||
// THEN
|
||||
val expected = TangemPayTariffPlan(
|
||||
id = PLAN_ID,
|
||||
tierId = "SOMETHING_NEW",
|
||||
isBasicTier = false,
|
||||
name = PLAN_NAME,
|
||||
programName = PROGRAM_NAME,
|
||||
descriptionItems = listOf(
|
||||
TangemPayTariffPlan.DescriptionItem(
|
||||
section = TangemPayTariffPlan.Section.UNKNOWN,
|
||||
order = 0,
|
||||
title = "Title",
|
||||
body = "",
|
||||
),
|
||||
),
|
||||
images = listOf(
|
||||
TangemPayTariffPlan.Image(type = TangemPayTariffPlan.Image.Type.UNKNOWN, url = "https://img"),
|
||||
),
|
||||
fees = listOf(
|
||||
TangemPayTariffPlan.Fee(
|
||||
type = TangemPayTariffPlan.Fee.Type.UNKNOWN,
|
||||
amount = BigDecimal.ONE,
|
||||
currency = "",
|
||||
description = "",
|
||||
period = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN nested items with missing required fields WHEN convert THEN filters them out`() {
|
||||
// GIVEN
|
||||
val value = tariffPlan(
|
||||
descriptionItems = listOf(
|
||||
CustomerMeResponse.DescriptionItem(type = "PLAN_RELATED", order = 1, title = null, body = "Body"),
|
||||
),
|
||||
images = listOf(CustomerMeResponse.Image(type = "MAIN", url = null)),
|
||||
fees = listOf(
|
||||
CustomerMeResponse.Fee(
|
||||
type = "FREE",
|
||||
amount = null,
|
||||
currency = "USD",
|
||||
description = "d",
|
||||
period = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = TangemPayTariffPlanConverter.convert(value)
|
||||
|
||||
// THEN
|
||||
assertThat(result?.descriptionItems).isEmpty()
|
||||
assertThat(result?.images).isEmpty()
|
||||
assertThat(result?.fees).isEmpty()
|
||||
}
|
||||
|
||||
private fun tariffPlan(
|
||||
id: String? = PLAN_ID,
|
||||
type: String? = "BASIC",
|
||||
name: String? = PLAN_NAME,
|
||||
programName: String? = PROGRAM_NAME,
|
||||
descriptionItems: List<CustomerMeResponse.DescriptionItem>? = null,
|
||||
images: List<CustomerMeResponse.Image>? = null,
|
||||
fees: List<CustomerMeResponse.Fee>? = null,
|
||||
) = CustomerMeResponse.TariffPlan(
|
||||
id = id,
|
||||
type = type,
|
||||
name = name,
|
||||
programName = programName,
|
||||
descriptionItems = descriptionItems,
|
||||
images = images,
|
||||
fees = fees,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val PLAN_ID = "plan-1"
|
||||
const val PLAN_NAME = "Plus"
|
||||
const val PROGRAM_NAME = "program-1"
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,15 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.BankCredentials
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanState
|
||||
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -28,8 +32,10 @@ import com.tangem.domain.pay.model.CustomerInfo
|
|||
import com.tangem.domain.pay.model.OrderData
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanStateUseCase
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
|
|
@ -59,6 +65,8 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk()
|
||||
private val issueCardRepository: TangemPayIssueCardRepository = mockk()
|
||||
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles = mockk()
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles = mockk()
|
||||
private val getTangemPayTariffPlanStateUseCase: GetTangemPayTariffPlanStateUseCase = mockk()
|
||||
|
||||
private val fetcher = DefaultPaymentAccountStatusFetcher(
|
||||
paymentAccountStatusesStore = paymentAccountStatusesStore,
|
||||
|
|
@ -74,6 +82,8 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardDetailsRepository = cardDetailsRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
virtualAccountFeatureToggles = virtualAccountFeatureToggles,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
getTangemPayTariffPlanStateUseCase = getTangemPayTariffPlanStateUseCase,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
|
@ -116,28 +126,54 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
lastFourDigits = "1234",
|
||||
isPinSet = true,
|
||||
images = emptyList(),
|
||||
)
|
||||
|
||||
private fun buildCustomerInfo(productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance)) =
|
||||
CustomerInfo(
|
||||
customerId = "cust_1",
|
||||
kycStatus = KycStatus.APPROVED,
|
||||
state = CustomerInfo.State.ACTIVE,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.TEN,
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "usdc",
|
||||
chainId = 137L,
|
||||
depositAddress = "0xdeposit",
|
||||
tokenContractAddress = "0xcontract",
|
||||
balance = BigDecimal.TEN,
|
||||
),
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
cards = listOf(cardInfo),
|
||||
productInstances = productInstances,
|
||||
)
|
||||
private val basicPlan = TangemPayTariffPlan(
|
||||
id = "plan_basic",
|
||||
tierId = "BASIC",
|
||||
isBasicTier = true,
|
||||
name = "Basic",
|
||||
programName = "program_basic",
|
||||
descriptionItems = emptyList(),
|
||||
images = emptyList(),
|
||||
fees = emptyList(),
|
||||
)
|
||||
|
||||
private val customerTariffPlan = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = TangemPayCustomerTariffPlan.Source.CUSTOMER,
|
||||
plan = basicPlan,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
pendingTransitionAt = null,
|
||||
)
|
||||
|
||||
private fun buildCustomerInfo(
|
||||
productInstances: List<CustomerInfo.ProductInstance> = listOf(cardProductInstance),
|
||||
fiatBalance: PaymentAccountStatusValue.FiatBalance? = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal.TEN,
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance: PaymentAccountStatusValue.CryptoBalance? = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "usdc",
|
||||
chainId = 137L,
|
||||
depositAddress = "0xdeposit",
|
||||
tokenContractAddress = "0xcontract",
|
||||
balance = BigDecimal.TEN,
|
||||
),
|
||||
tariffPlan: TangemPayCustomerTariffPlan? = null,
|
||||
) = CustomerInfo(
|
||||
customerId = "cust_1",
|
||||
kycStatus = KycStatus.APPROVED,
|
||||
state = CustomerInfo.State.ACTIVE,
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
availableForWithdrawal = BigDecimal.TEN,
|
||||
cards = listOf(cardInfo),
|
||||
productInstances = productInstances,
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
|
|
@ -151,10 +187,14 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardDetailsRepository,
|
||||
issueCardRepository,
|
||||
virtualAccountFeatureToggles,
|
||||
tangemPayFeatureToggles,
|
||||
getTangemPayTariffPlanStateUseCase,
|
||||
)
|
||||
// Relaxed mocks don't need clearing — deviceSecurity, eligibilityManager, paymentAccountStatusesStore
|
||||
// are relaxed and consistent with their relaxed defaults (false, empty, etc.)
|
||||
clearMocks(paymentAccountStatusesStore, answers = false)
|
||||
// Tiers off by default — legacy auto-order-creation behavior. Individual tests override.
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -228,6 +268,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
fiatRate = null,
|
||||
error = null,
|
||||
virtualAccount = virtualAccount,
|
||||
tariffPlan = null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -466,6 +507,8 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
cardDetailsRepository = cardDetailsRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
virtualAccountFeatureToggles = virtualAccountFeatureToggles,
|
||||
tangemPayFeatureToggles = tangemPayFeatureToggles,
|
||||
getTangemPayTariffPlanStateUseCase = getTangemPayTariffPlanStateUseCase,
|
||||
)
|
||||
|
||||
private val account = Account.Payment(userWalletId = userWalletId)
|
||||
|
|
@ -506,4 +549,142 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
assertThat(realStore.getSyncOrNull(userWalletId)?.value).isEqualTo(issuingCard)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class TiersPlanSelection {
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and KYC approved and no plan order WHEN invoke THEN stores AwaitingPlanSelection`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = customerTariffPlan,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns emptyList()
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.AwaitingPlanSelection::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and fallback plan is missing WHEN invoke THEN stores IssuingCard without order`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = null,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and plan selected but no balance yet WHEN invoke THEN stores Inactive`() = runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(
|
||||
productInstances = emptyList(),
|
||||
fiatBalance = null,
|
||||
cryptoBalance = null,
|
||||
tariffPlan = customerTariffPlan,
|
||||
)
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery {
|
||||
getTangemPayTariffPlanStateUseCase(userWalletId = userWalletId, tariff = customerTariffPlan)
|
||||
} returns TangemPayTariffPlanState(tariff = customerTariffPlan, order = null)
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.Inactive::class.java)
|
||||
coVerify(exactly = 0) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers off and KYC approved without card WHEN invoke THEN creates order and stays issuing`() = runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
coEvery { onboardingRepository.createOrder(userWalletId) } returns Either.Right("order_1")
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
coVerify(exactly = 1) { onboardingRepository.createOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers off and balance without instances but local issue order WHEN invoke THEN stores IssuingCard`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns false
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery { onboardingRepository.createOrder(userWalletId) } returns Either.Right("order_1")
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
assertThat(storedStatuses.last().value)
|
||||
.isInstanceOf(PaymentAccountStatusValue.IssuingCard::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers on and balance without instances but local issue order WHEN invoke THEN stores Loaded with placeholder`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val customerInfo = buildCustomerInfo(productInstances = emptyList())
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns false
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_1")
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, "order_1")
|
||||
} returns VisaApiError.UnknownWithoutCode.left()
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// Act
|
||||
fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
val loaded = storedStatuses.lastLoaded()
|
||||
assertThat(loaded.cards).hasSize(1)
|
||||
assertThat(loaded.cards.single().state).isEqualTo(TangemPayCardState.Issuing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ internal class StableOrderTest {
|
|||
limit = null,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
lastDigits = "0000",
|
||||
images = emptyList(),
|
||||
state = TangemPayCardState.Active,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.pay.util.CashbackAccrualDocsConverter
|
||||
import com.tangem.data.pay.util.CashbackPromotionsConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class DefaultCashbackRepositoryTest {
|
||||
|
||||
private val tangemPayApi: TangemPayApi = mockk()
|
||||
private val requestHelper: TangemPayRequestPerformer = mockk()
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(tangemPayApi, requestHelper)
|
||||
coEvery {
|
||||
requestHelper.performRequest<Any>(userWalletId = any(), requestBlock = any())
|
||||
} coAnswers {
|
||||
val block = secondArg<suspend (String) -> ApiResponse<Any>>()
|
||||
when (val response = block(AUTH_HEADER)) {
|
||||
is ApiResponse.Success -> response.data.right()
|
||||
is ApiResponse.Error -> VisaApiError.Unspecified.left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN promotions response WHEN getCashbackPromotions THEN converter result is returned`() = runTest {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(cashbackOnCards = null, additionalCashback = null)
|
||||
coEvery { tangemPayApi.getCashbackPromotions(any()) } returns ApiResponse.Success(response)
|
||||
|
||||
// Act
|
||||
val actual = createRepository().getCashbackPromotions(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(CashbackPromotionsConverter.convert(response).right())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN getCashbackPromotions THEN error is propagated`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemPayApi.getCashbackPromotions(any()) } returns
|
||||
ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<CashbackPromotionsResponse>
|
||||
|
||||
// Act
|
||||
val actual = createRepository().getCashbackPromotions(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(VisaApiError.Unspecified.left())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN docs response WHEN getCashbackAccrualDocs THEN converter result is returned`() = runTest {
|
||||
// Arrange
|
||||
val response = CashbackAccrualDocsResponse(
|
||||
docs = listOf(CashbackAccrualDocsResponse.Doc(id = "1", title = "Terms", url = "https://a")),
|
||||
)
|
||||
coEvery { tangemPayApi.getCashbackAccrualDocs(any()) } returns ApiResponse.Success(response)
|
||||
|
||||
// Act
|
||||
val actual = createRepository().getCashbackAccrualDocs(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(CashbackAccrualDocsConverter.convert(response).right())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN getCashbackAccrualDocs THEN error is propagated`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemPayApi.getCashbackAccrualDocs(any()) } returns
|
||||
ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<CashbackAccrualDocsResponse>
|
||||
|
||||
// Act
|
||||
val actual = createRepository().getCashbackAccrualDocs(userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(VisaApiError.Unspecified.left())
|
||||
}
|
||||
|
||||
private fun createRepository() = DefaultCashbackRepository(
|
||||
tangemPayApi = tangemPayApi,
|
||||
requestHelper = requestHelper,
|
||||
storage = mockk(),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val AUTH_HEADER = "auth-header"
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ internal class DefaultCustomerOffersRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN offers fetched once WHEN getOffers called twice THEN backend is hit only once`() = runTest {
|
||||
fun `GIVEN offers response WHEN getOffers called twice THEN backend is hit each time`() = runTest {
|
||||
// Arrange
|
||||
val repository = createRepository()
|
||||
|
||||
|
|
@ -68,11 +68,11 @@ internal class DefaultCustomerOffersRepositoryTest {
|
|||
// Assert
|
||||
assertThat(first.isRight()).isTrue()
|
||||
assertThat(second).isEqualTo(first)
|
||||
coVerify(exactly = 1) { tangemPayApi.getCustomerOffers(any()) }
|
||||
coVerify(exactly = 2) { tangemPayApi.getCustomerOffers(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN getOffers called again THEN error is not cached and backend is hit again`() = runTest {
|
||||
fun `GIVEN backend error WHEN getOffers called again THEN backend is hit again`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemPayApi.getCustomerOffers(any()) } returnsMany listOf(
|
||||
ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<CustomerOffersResponse>,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.datasource.api.pay.TangemPayApi
|
|||
import com.tangem.datasource.api.pay.models.response.WithdrawDataResponse
|
||||
import com.tangem.datasource.api.pay.models.response.WithdrawResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.data.pay.store.TangemPayStorage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionsResponse
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class DefaultTariffPlanTransitionsRepositoryTest {
|
||||
|
||||
private val tangemPayApi: TangemPayApi = mockk()
|
||||
private val requestHelper: TangemPayRequestPerformer = mockk()
|
||||
|
||||
private val repository = DefaultTariffPlanTransitionsRepository(
|
||||
tangemPayApi = tangemPayApi,
|
||||
requestHelper = requestHelper,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
coEvery {
|
||||
requestHelper.performRequest<Any>(userWalletId = any(), requestBlock = any())
|
||||
} coAnswers {
|
||||
val block = secondArg<suspend (String) -> ApiResponse<Any>>()
|
||||
when (val response = block(AUTH_HEADER)) {
|
||||
is ApiResponse.Success -> response.data.right()
|
||||
is ApiResponse.Error -> VisaApiError.Unspecified.left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN getTransitions THEN returns error`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns
|
||||
ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<TariffPlanTransitionsResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.getTransitions(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null result WHEN getTransitions THEN returns empty list`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns
|
||||
ApiResponse.Success(TariffPlanTransitionsResponse(result = null))
|
||||
|
||||
// WHEN
|
||||
val result = repository.getTransitions(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.getOrNull()).isEqualTo(emptyList<TangemPayTariffPlanTransition>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid transitions WHEN getTransitions THEN maps them to domain`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns ApiResponse.Success(
|
||||
TariffPlanTransitionsResponse(
|
||||
result = listOf(
|
||||
TariffPlanTransitionResponse(type = "UPGRADE", tariffPlan = tariffPlan()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getTransitions(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
val expected = listOf(
|
||||
TangemPayTariffPlanTransition(
|
||||
type = TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
plan = TangemPayTariffPlan(
|
||||
id = PLAN_ID,
|
||||
tierId = "PLUS",
|
||||
isBasicTier = false,
|
||||
name = PLAN_NAME,
|
||||
programName = PROGRAM_NAME,
|
||||
descriptionItems = emptyList(),
|
||||
images = emptyList(),
|
||||
fees = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
assertThat(result.getOrNull()).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a transition with unconvertible plan WHEN getTransitions THEN it is filtered out`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns ApiResponse.Success(
|
||||
TariffPlanTransitionsResponse(
|
||||
result = listOf(
|
||||
TariffPlanTransitionResponse(type = "UPGRADE", tariffPlan = null),
|
||||
TariffPlanTransitionResponse(type = "DOWNGRADE", tariffPlan = tariffPlan(id = null)),
|
||||
TariffPlanTransitionResponse(type = "ACTIVATION", tariffPlan = tariffPlan()),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.getTransitions(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
val transitions = result.getOrNull().orEmpty()
|
||||
assertThat(transitions.map { it.type })
|
||||
.containsExactly(TangemPayTariffPlanTransition.Type.ACTIVATION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN success WHEN setPendingTransition THEN sends pending plan id and returns Unit`() = runTest {
|
||||
// GIVEN
|
||||
val bodySlot = slot<SetPendingTariffPlanTransitionRequest>()
|
||||
coEvery {
|
||||
tangemPayApi.setPendingTariffPlanTransition(any(), capture(bodySlot))
|
||||
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
assertThat(bodySlot.captured.pendingTariffPlanId).isEqualTo(PENDING_PLAN_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN setPendingTransition THEN returns error`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.setPendingTariffPlanTransition(any(), any())
|
||||
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN success WHEN cancelPendingTransition THEN returns Unit`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(any())
|
||||
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.cancelPendingTransition(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { tangemPayApi.cancelPendingTariffPlanTransition(AUTH_HEADER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN cancelPendingTransition THEN returns error`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(any())
|
||||
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.cancelPendingTransition(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
private fun tariffPlan(id: String? = PLAN_ID) = CustomerMeResponse.TariffPlan(
|
||||
id = id,
|
||||
type = "PLUS",
|
||||
name = PLAN_NAME,
|
||||
programName = PROGRAM_NAME,
|
||||
descriptionItems = null,
|
||||
images = null,
|
||||
fees = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val AUTH_HEADER = "auth-header"
|
||||
const val PLAN_ID = "plan-plus"
|
||||
const val PLAN_NAME = "Plus"
|
||||
const val PROGRAM_NAME = "program-plus"
|
||||
const val PENDING_PLAN_ID = "plan-basic"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.data.pay.store
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.test.core.TestAppCoroutineScope
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class PaymentAccountStatusesStoreTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored status WHEN remove single THEN cleared from both runtime and persistence`() = runTest {
|
||||
// Arrange
|
||||
val persistenceDataStore = MockStateDataStore<WalletIdWithPaymentStatusDM>(default = emptyMap())
|
||||
val store = createStore(persistenceDataStore)
|
||||
advanceUntilIdle()
|
||||
store.storeNotCreated(WALLET_A)
|
||||
advanceUntilIdle()
|
||||
// sanity: present in both stores before removal
|
||||
assertThat(store.getSyncOrNull(WALLET_A)).isNotNull()
|
||||
assertThat(persistenceDataStore.data.first()).containsKey(WALLET_A.stringValue)
|
||||
|
||||
// Act
|
||||
store.remove(WALLET_A)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A)).isNull()
|
||||
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_A.stringValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN statuses for two wallets WHEN remove list THEN both cleared from both stores`() = runTest {
|
||||
// Arrange
|
||||
val persistenceDataStore = MockStateDataStore<WalletIdWithPaymentStatusDM>(default = emptyMap())
|
||||
val store = createStore(persistenceDataStore)
|
||||
advanceUntilIdle()
|
||||
store.storeNotCreated(WALLET_A)
|
||||
store.storeNotCreated(WALLET_B)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
store.remove(listOf(WALLET_A, WALLET_B))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B)).isNull()
|
||||
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_A.stringValue)
|
||||
assertThat(persistenceDataStore.data.first()).doesNotContainKey(WALLET_B.stringValue)
|
||||
}
|
||||
|
||||
private fun TestScope.createStore(
|
||||
persistenceDataStore: MockStateDataStore<WalletIdWithPaymentStatusDM>,
|
||||
) = PaymentAccountStatusesStore(
|
||||
runtimeStore = RuntimeSharedStore<WalletIdWithPaymentStatus>(),
|
||||
persistenceDataStore = persistenceDataStore,
|
||||
converter = PaymentAccountStatusValueDMConverter(mockk<TangemPayCurrencyFactory>(relaxed = true)),
|
||||
scope = TestAppCoroutineScope(this),
|
||||
)
|
||||
|
||||
private suspend fun PaymentAccountStatusesStore.storeNotCreated(userWalletId: UserWalletId) {
|
||||
store(
|
||||
userWalletId = userWalletId,
|
||||
status = AccountStatus.Payment(
|
||||
account = Account.Payment(userWalletId),
|
||||
value = PaymentAccountStatusValue.NotCreated,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val WALLET_A = UserWalletId("011")
|
||||
val WALLET_B = UserWalletId("022")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse.Doc
|
||||
import com.tangem.domain.pay.model.CashbackDocument
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CashbackAccrualDocsConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN valid docs WHEN convert THEN all docs mapped in order`() {
|
||||
// Arrange
|
||||
val response = response(
|
||||
doc(id = "1", title = "All categories", url = "https://a"),
|
||||
doc(id = "2", title = "Full terms", url = "https://b"),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackAccrualDocsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result).containsExactly(
|
||||
CashbackDocument(id = "1", title = "All categories", url = "https://a"),
|
||||
CashbackDocument(id = "2", title = "Full terms", url = "https://b"),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN doc missing a required field WHEN convert THEN it is dropped`(malformed: Doc) {
|
||||
// Arrange
|
||||
val response = response(doc(), malformed)
|
||||
|
||||
// Act
|
||||
val result = CashbackAccrualDocsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result).containsExactly(CashbackDocument(id = "1", title = "Title", url = "https://a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null docs WHEN convert THEN empty list`() {
|
||||
// Act
|
||||
val result = CashbackAccrualDocsConverter.convert(CashbackAccrualDocsResponse(docs = null))
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
doc(id = null),
|
||||
doc(title = null),
|
||||
doc(url = null),
|
||||
)
|
||||
|
||||
private fun response(vararg docs: Doc) = CashbackAccrualDocsResponse(docs = docs.toList())
|
||||
|
||||
private fun doc(id: String? = "1", title: String? = "Title", url: String? = "https://a") =
|
||||
Doc(id = id, title = title, url = url)
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackHistoryResponse
|
||||
import com.tangem.domain.pay.model.CashbackHistory
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CashbackHistoryConverterTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun convert(model: ConvertModel) {
|
||||
// Act
|
||||
val actual = CashbackHistoryConverter.convert(model.response)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ConvertModel(
|
||||
name = "full response -> mapped history ordered oldest to newest",
|
||||
response = createResponse(
|
||||
currency = "USD",
|
||||
items = listOf(
|
||||
createItem(year = 2026, month = 2, confirmedAmount = BigDecimal("5.00")),
|
||||
createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")),
|
||||
),
|
||||
),
|
||||
expected = CashbackHistory(
|
||||
currency = "USD",
|
||||
months = listOf(
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 2, confirmedAmount = BigDecimal("5.00")),
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")),
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "negative amount preserved for current-month refund",
|
||||
response = createResponse(
|
||||
items = listOf(createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("-2.15"))),
|
||||
),
|
||||
expected = CashbackHistory(
|
||||
currency = "USD",
|
||||
months = listOf(
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("-2.15")),
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "null confirmed_amount -> ZERO",
|
||||
response = createResponse(
|
||||
items = listOf(createItem(year = 2026, month = 6, confirmedAmount = null)),
|
||||
),
|
||||
expected = CashbackHistory(
|
||||
currency = "USD",
|
||||
months = listOf(
|
||||
CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal.ZERO),
|
||||
),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "null currency -> empty string",
|
||||
response = createResponse(currency = null, items = emptyList()),
|
||||
expected = CashbackHistory(currency = "", months = emptyList()),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "null items -> empty months",
|
||||
response = createResponse(items = null),
|
||||
expected = CashbackHistory(currency = "USD", months = emptyList()),
|
||||
),
|
||||
)
|
||||
|
||||
internal data class ConvertModel(
|
||||
val name: String,
|
||||
val response: CashbackHistoryResponse,
|
||||
val expected: CashbackHistory,
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
fun createResponse(
|
||||
currency: String? = "USD",
|
||||
items: List<CashbackHistoryResponse.Item>? = listOf(
|
||||
createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")),
|
||||
),
|
||||
) = CashbackHistoryResponse(
|
||||
currency = currency,
|
||||
items = items,
|
||||
)
|
||||
|
||||
fun createItem(year: Int, month: Int, confirmedAmount: BigDecimal?) = CashbackHistoryResponse.Item(
|
||||
year = year,
|
||||
month = month,
|
||||
confirmedAmount = confirmedAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse
|
||||
import com.tangem.domain.pay.model.CashbackPromotions
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class CashbackPromotionsConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN tiers WHEN convert THEN each tier mapped with its fields`() {
|
||||
// Arrange
|
||||
val response = response(tier(min = BigDecimal("30"), cap = BigDecimal("100")))
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
CashbackPromotions(
|
||||
cardTiers = listOf(
|
||||
CashbackPromotions.CardTier(
|
||||
tier = "basic",
|
||||
label = "Basic",
|
||||
scope = "All purchases",
|
||||
minTransactionAmount = BigDecimal("30"),
|
||||
monthlyCapAmount = BigDecimal("100"),
|
||||
),
|
||||
),
|
||||
additionalCashback = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback WHEN convert THEN each promo mapped with its fields`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(
|
||||
additional(id = "p1", name = "Groceries", description = "+1%", isPermanent = true, endDate = null),
|
||||
additional(
|
||||
id = "p2",
|
||||
name = "Cashback",
|
||||
description = "+2%",
|
||||
isPermanent = false,
|
||||
endDate = "2026-09-26",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback).containsExactly(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "p1",
|
||||
name = "Groceries",
|
||||
description = "+1%",
|
||||
isPermanent = true,
|
||||
endDate = null,
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "p2",
|
||||
name = "Cashback",
|
||||
description = "+2%",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback with null isPermanent WHEN convert THEN it is derived from end date`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(
|
||||
additional(isPermanent = null, endDate = null),
|
||||
additional(isPermanent = null, endDate = "2026-09-26"),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback.map { it.isPermanent }).containsExactly(true, false).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback with malformed end date WHEN convert THEN end date null and payload kept`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(additional(isPermanent = false, endDate = "not-a-date")),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback).containsExactly(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "id",
|
||||
name = "name",
|
||||
description = "description",
|
||||
isPermanent = false,
|
||||
endDate = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null cashbackOnCards WHEN convert THEN no card tiers`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(cashbackOnCards = null, additionalCashback = null)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.cardTiers).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tier with null strings WHEN convert THEN strings default to empty and amounts stay null`() {
|
||||
// Arrange
|
||||
val response = response(tier(tier = null, label = null, scope = null))
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.cardTiers).containsExactly(
|
||||
CashbackPromotions.CardTier(
|
||||
tier = "",
|
||||
label = "",
|
||||
scope = "",
|
||||
minTransactionAmount = null,
|
||||
monthlyCapAmount = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun response(vararg tiers: CashbackPromotionsResponse.CardTier) = CashbackPromotionsResponse(
|
||||
cashbackOnCards = CashbackPromotionsResponse.CashbackOnCards(
|
||||
tiers = tiers.toList(),
|
||||
monthlyCapAmount = null,
|
||||
monthlyCapCurrency = null,
|
||||
),
|
||||
additionalCashback = null,
|
||||
)
|
||||
|
||||
private fun tier(
|
||||
tier: String? = "basic",
|
||||
label: String? = "Basic",
|
||||
scope: String? = "All purchases",
|
||||
min: BigDecimal? = null,
|
||||
cap: BigDecimal? = null,
|
||||
) = CashbackPromotionsResponse.CardTier(
|
||||
tier = tier,
|
||||
label = label,
|
||||
scope = scope,
|
||||
minTransactionAmount = min,
|
||||
tierMonthlyCapAmount = cap,
|
||||
promotionId = null,
|
||||
)
|
||||
|
||||
private fun additional(
|
||||
id: String? = "id",
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
isPermanent: Boolean? = false,
|
||||
endDate: String? = null,
|
||||
) = CashbackPromotionsResponse.AdditionalCashback(
|
||||
id = id,
|
||||
name = name,
|
||||
description = description,
|
||||
isPermanent = isPermanent,
|
||||
endDate = endDate,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackSummaryResponse
|
||||
import com.tangem.domain.pay.model.CashbackDisplayMode
|
||||
import com.tangem.domain.pay.model.CashbackSummary
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class CashbackSummaryConverterTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
fun convert(model: ConvertModel) {
|
||||
// Act
|
||||
val actual = CashbackSummaryConverter.convert(model.response)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo(model.expected)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun provideTestModels() = listOf(
|
||||
ConvertModel(
|
||||
name = "enabled + full -> Enabled(FULL)",
|
||||
response = createResponse(status = "enabled", displayMode = "full"),
|
||||
expected = CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.FULL,
|
||||
cashback = expectedCashback(),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "enabled + alt_block -> Enabled(ALT_BLOCK)",
|
||||
response = createResponse(status = "enabled", displayMode = "alt_block"),
|
||||
expected = CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.ALT_BLOCK,
|
||||
cashback = expectedCashback(),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "enabled + missing display mode -> Enabled(FULL)",
|
||||
response = createResponse(status = "enabled", displayMode = null),
|
||||
expected = CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.FULL,
|
||||
cashback = expectedCashback(),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "enabled + zero amounts -> Enabled with zeros",
|
||||
response = createResponse(
|
||||
status = "enabled",
|
||||
displayMode = "full",
|
||||
confirmedAmount = null,
|
||||
pendingAmount = null,
|
||||
),
|
||||
expected = CashbackSummary.Enabled(
|
||||
displayMode = CashbackDisplayMode.FULL,
|
||||
cashback = expectedCashback(confirmedAmount = BigDecimal.ZERO, pendingAmount = BigDecimal.ZERO),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
name = "enabled but missing period -> Unknown",
|
||||
response = createResponse(status = "enabled", period = null),
|
||||
expected = CashbackSummary.Unknown,
|
||||
),
|
||||
ConvertModel(
|
||||
name = "deactivated -> Deactivated",
|
||||
response = createResponse(status = "deactivated"),
|
||||
expected = CashbackSummary.Deactivated,
|
||||
),
|
||||
ConvertModel(
|
||||
name = "disabled -> Disabled",
|
||||
response = createResponse(status = "disabled"),
|
||||
expected = CashbackSummary.Disabled,
|
||||
),
|
||||
ConvertModel(
|
||||
name = "unavailable -> Disabled",
|
||||
response = createResponse(status = "unavailable"),
|
||||
expected = CashbackSummary.Disabled,
|
||||
),
|
||||
ConvertModel(
|
||||
name = "unrecognized status -> Unknown",
|
||||
response = createResponse(status = "something_new"),
|
||||
expected = CashbackSummary.Unknown,
|
||||
),
|
||||
)
|
||||
|
||||
internal data class ConvertModel(
|
||||
val name: String,
|
||||
val response: CashbackSummaryResponse,
|
||||
val expected: CashbackSummary,
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
fun createResponse(
|
||||
status: String = "enabled",
|
||||
displayMode: String? = "full",
|
||||
period: CashbackSummaryResponse.Period? = CashbackSummaryResponse.Period(
|
||||
year = 2026,
|
||||
month = 6,
|
||||
payoutStartDate = "2026-07-02",
|
||||
payoutEndDate = "2026-07-05",
|
||||
),
|
||||
confirmedAmount: BigDecimal? = BigDecimal("22.54"),
|
||||
pendingAmount: BigDecimal? = BigDecimal("13.65"),
|
||||
currency: String? = "USD",
|
||||
payoutCurrency: String? = "USDC",
|
||||
payoutNetwork: String? = "Polygon",
|
||||
) = CashbackSummaryResponse(
|
||||
cashbackProgramStatus = status,
|
||||
cashbackDisplayMode = displayMode,
|
||||
period = period,
|
||||
confirmedAmount = confirmedAmount,
|
||||
pendingAmount = pendingAmount,
|
||||
currency = currency,
|
||||
payoutCurrency = payoutCurrency,
|
||||
payoutNetwork = payoutNetwork,
|
||||
)
|
||||
|
||||
fun expectedCashback(
|
||||
confirmedAmount: BigDecimal = BigDecimal("22.54"),
|
||||
pendingAmount: BigDecimal = BigDecimal("13.65"),
|
||||
) = TangemPayCashback(
|
||||
confirmedAmount = confirmedAmount,
|
||||
pendingAmount = pendingAmount,
|
||||
currency = "USD",
|
||||
payoutCurrency = "USDC",
|
||||
payoutNetwork = "Polygon",
|
||||
period = TangemPayCashback.Period(
|
||||
year = 2026,
|
||||
month = 6,
|
||||
payoutStart = DateTime.parse("2026-07-02"),
|
||||
payoutEnd = DateTime.parse("2026-07-05"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue