Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-14 14:11:50 +03:00
commit 4401c6e2eb
1448 changed files with 61482 additions and 16922 deletions

View file

@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
@ -16,14 +16,8 @@ import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
/**
* Custom token parameters. Will be used only for F&F.
*/
private const val TOKEN_ID = "usd-coin"
private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
@Deprecated("Use TangemPayCurrencyFactory instead")
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val errorConverter: TangemPayErrorConverter,
@ -47,32 +41,11 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
)
cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}.mapLeft { exception ->
TangemLogger.withTag(TAG).e("Error", exception)
errorConverter.convert(exception)
}
}
override fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token> {
return catch {
val network = networkFactory.create(
blockchain = VisaUtilities.visaBlockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID),
name = TangemPayCurrencyFactory.TOKEN_NAME,
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
)
}.mapLeft { exception ->
TangemLogger.withTag(TAG).e("Error", exception)

View file

@ -2,7 +2,7 @@ package com.tangem.data.pay
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -1,11 +1,18 @@
package com.tangem.data.pay.converter
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack
import arrow.core.getOrElse
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
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.utils.converter.TwoWayConverter
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
import javax.inject.Singleton
/**
* Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM].
@ -15,10 +22,12 @@ import com.tangem.utils.converter.TwoWayConverter
*
* [convertBack] maps data model domain. All restored statuses have [StatusSource.CACHE] as source.
*/
internal object PaymentAccountStatusValueDMConverter :
TwoWayConverter<PaymentAccountStatusValue, PaymentAccountStatusValueDM?> {
@Singleton
internal class PaymentAccountStatusValueDMConverter @Inject constructor(
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
) {
override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
return when (value) {
is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated()
is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview(
@ -26,27 +35,24 @@ internal object PaymentAccountStatusValueDMConverter :
customerId = value.customerId,
)
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard(
isLocked = true,
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
)
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard(
isLocked = false,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
availableForWithdrawal = value.availableForWithdrawal,
cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard(
id = card.id,
hasPinCode = card.hasPinCode,
displayName = card.displayName?.value,
actualDailyLimit = card.limit?.actualCardLimit?.amount,
adminDailyLimit = card.limit?.adminCardLimit?.amount,
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
)
},
)
is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed(
customerId = value.customerId,
@ -64,7 +70,7 @@ internal object PaymentAccountStatusValueDMConverter :
}
}
override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
return when (value) {
is PaymentAccountStatusValueDM.Empty -> PaymentAccountStatusValue.Empty
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
@ -74,31 +80,33 @@ internal object PaymentAccountStatusValueDMConverter :
is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard(
source = StatusSource.CACHE,
)
is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) {
PaymentAccountStatusValue.Locked(
source = StatusSource.CACHE,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
)
} else {
PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE,
customerId = value.customerId,
cardId = value.cardId,
lastFourDigits = value.lastFourDigits,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
isPinSet = value.isPinSet,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
)
}
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE,
customerId = value.customerId,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
cards = value.cards.map { card ->
TangemPayCard(
id = card.id,
hasPinCode = card.hasPinCode,
displayName = card.displayName?.let { CardDisplayName(it).getOrElse { null } },
limit = TangemPayCardLimitData(
actualCardLimit = card.actualDailyLimit?.let { limit ->
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
},
adminCardLimit = card.adminDailyLimit?.let { limit ->
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
},
),
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
)
},
)
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
source = StatusSource.CACHE,
kycStatus = value.kycStatus,

View file

@ -2,10 +2,12 @@ package com.tangem.data.pay.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
import com.tangem.data.pay.repository.*
@ -24,13 +26,14 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds
@ -49,18 +52,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindKycRepository(repository: DefaultKycRepository): KycRepository
@Binds
@Singleton
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
@Binds
@Singleton
fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository
@ -69,6 +64,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository
@Binds
@Singleton
fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository
@Binds
@Singleton
fun bindTangemPayCryptoCurrencyFactory(
@ -112,6 +111,7 @@ internal interface TangemPayDataModule {
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
scope: AppCoroutineScope,
converter: PaymentAccountStatusValueDMConverter,
): PaymentAccountStatusesStore {
return PaymentAccountStatusesStore(
runtimeStore = RuntimeSharedStore(),
@ -121,9 +121,11 @@ internal interface TangemPayDataModule {
types = mapWithStringKeyTypes<PaymentAccountStatusValueDM>(),
defaultValue = emptyMap(),
),
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
scope = scope,
),
converter = converter,
scope = scope,
)
}
@ -139,21 +141,28 @@ internal interface TangemPayDataModule {
) {}
}
@Provides
fun provideSetTangemPayCardLimitUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): SetTangemPayCardLimitUseCase {
return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
@Provides
fun provideUpdateTangemPayCardNameUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): UpdateTangemPayCardNameUseCase {
return UpdateTangemPayCardNameUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
@Provides
@Singleton
fun provideTangemPayMainScreenCustomerInfoUseCase(
repository: OnboardingRepository,
customerOrderRepository: CustomerOrderRepository,
tangemPayOnboardingRepository: OnboardingRepository,
eligibilityManager: TangemPayEligibilityManager,
deviceSecurity: DeviceSecurityInfoProvider,
): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(
onboardingRepository = repository,
customerOrderRepository = customerOrderRepository,
eligibilityManager = eligibilityManager,
deviceSecurity = deviceSecurity,
)
fun provideGetTangemPayCryptoCurrencyStatusUseCase(
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
): GetPaymentAccountCryptoCurrencyStatusUseCase {
return GetPaymentAccountCryptoCurrencyStatusUseCase(paymentAccountStatusSupplier)
}
@Provides

View file

@ -0,0 +1,49 @@
package com.tangem.data.pay.entity
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.requireUserWalletsSync
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class TangemPayCurrencyFactory @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val userWalletsListRepository: UserWalletsListRepository,
private val networkFactory: NetworkFactory,
) {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
val userWallet = userWalletsListRepository.requireUserWalletsSync()
.firstOrNull { it.walletId == userWalletId }
?: error("User wallet with id $userWalletId not found")
val network = networkFactory.create(
blockchain = VisaUtilities.visaBlockchain,
userWallet = userWallet,
extraDerivationPath = null,
)
return cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}
companion object {
internal const val TOKEN_ID = "usd-coin"
internal const val TOKEN_NAME = "USDC"
internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
internal const val TOKEN_DECIMALS = 6
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay.flow
import arrow.core.Either
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.data.pay.store.PaymentAccountStatusesStore
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.models.StatusSource
@ -8,6 +9,8 @@ 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.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
@ -38,6 +41,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val customerOrderRepository: CustomerOrderRepository,
private val deviceSecurity: DeviceSecurityInfoProvider,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val eligibilityManager: TangemPayEligibilityManager,
) : PaymentAccountStatusFetcher {
@ -143,7 +147,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
},
ifRight = { customerInfo ->
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
val status = customerInfo.mapToPaymentAccountStatus()
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)
@ -170,7 +174,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
KycStatus.PENDING,
KycStatus.INIT,
KycStatus.REJECTED,
-> return customerInfo.mapToPaymentAccountStatus()
-> return customerInfo.mapToPaymentAccountStatus(account.userWalletId)
KycStatus.APPROVED -> Unit // proceed to order check
}
@ -185,7 +189,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
when (orderData.status) {
OrderStatus.CANCELED -> handleCanceledOrder(account, orderData)
OrderStatus.COMPLETED -> handleCompletedOrder(account)
OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> {
@ -225,7 +228,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
OrderStatus.COMPLETED -> return handleCompletedOrder(account)
OrderStatus.NEW,
OrderStatus.PROCESSING,
OrderStatus.UNKNOWN,
-> Unit // Continue polling
}
},
@ -248,11 +250,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
.fold(
ifLeft = { it.mapToPaymentAccountStatus(account.userWalletId) },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus(account.userWalletId) },
)
}
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val cardInfo = this.cardInfo
val productInstance = this.productInstance
@ -275,6 +277,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
)
}
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
userWalletId = userWalletId,
productInstance = productInstance,
cardInfo = cardInfo,
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
@ -284,34 +287,35 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
}
private fun convertToContentState(
userWalletId: UserWalletId,
productInstance: CustomerInfo.ProductInstance,
cardInfo: CustomerInfo.CardInfo,
customerId: String,
): PaymentAccountStatusValue {
return when (productInstance.frozenState) {
TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked(
source = StatusSource.ACTUAL,
customerId = customerId,
cardId = productInstance.cardId,
lastFourDigits = cardInfo.lastFourDigits,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
isPinSet = cardInfo.isPinSet,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
)
else -> PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
cardId = productInstance.cardId,
lastFourDigits = cardInfo.lastFourDigits,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
isPinSet = cardInfo.isPinSet,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
)
}
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal,
cryptoCurrency = cryptoCurrency,
cards = listOf(
TangemPayCard(
id = productInstance.cardId,
hasPinCode = cardInfo.isPinSet,
displayName = productInstance.displayName,
limit = TangemPayCardLimitData(
actualCardLimit = productInstance.actualCardLimit,
adminCardLimit = productInstance.adminCardLimit,
),
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
lastDigits = cardInfo.lastFourDigits,
),
),
)
}
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {

View file

@ -1,8 +1,8 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
@ -19,13 +19,7 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
}.map { response ->
val status = when (response.result?.status) {
null -> OrderStatus.PROCESSING
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
}
val status = response.result?.status?.let(OrderStatusConverter::convert) ?: OrderStatus.PROCESSING
OrderData(
customerId = response.result?.customerId.orEmpty(),
status = status,

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.pay.store.PaymentAccountStatusesStore
@ -15,11 +16,14 @@ 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.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TangemPayEligibilityType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
@ -31,6 +35,7 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ -170,6 +175,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
val card = response?.card
val fiatBalance = response?.balance?.fiat
val cryptoBalance = response?.balance?.crypto
val availableForWithdrawal = response?.balance?.availableForWithdrawal
val paymentAccount = response?.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) {
CardInfo(
@ -186,6 +192,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
tokenContractAddress = cryptoBalance.tokenContractAddress,
balance = cryptoBalance.balance,
),
availableForWithdrawal = availableForWithdrawal?.amount.orZero(),
)
} else {
null
@ -197,11 +204,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
val displayName = instance.displayName?.ifEmpty { null }
ProductInstance(
id = instance.id,
cardId = instance.cardId,
frozenState = cardFrozenState,
status = instance.status.toDomain(),
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
)
}
return CustomerInfo(
@ -216,6 +228,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
private fun CustomerMeResponse.CardLimit.parseCardLimit(): TangemPayCardLimit {
return TangemPayCardLimit(
amount = amount,
period = TangemPayCardLimitPeriod.fromString(periodType),
)
}
private fun sendKycAnalytics(kycStatus: KycStatus) {
val event = when (kycStatus) {
KycStatus.APPROVED -> TangemPayAnalyticsEvents.KycPassedAndOrderCreated()

View file

@ -0,0 +1,107 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.ReissueCardRequest
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching
import javax.inject.Inject
internal class DefaultReissueCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
) : TangemPayReissueCardRepository {
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
either {
runSuspendCatching {
tangemPayReissueCardStore.getReissueFee(userWalletId)?.let { return Either.Right(it) }
}
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getFee(
authHeader = authHeader,
type = CARD_REPLACEMENT_FEE_TYPE,
)
}.bind()
val result = response.result
val fee = TangemPayReissueCardFee(
amount = result.amount.toBigDecimal(),
currencyCode = result.currency,
)
runSuspendCatching {
tangemPayReissueCardStore.storeReissueFee(userWalletId, fee)
}
fee
}
override suspend fun reissueCard(
userWalletId: UserWalletId,
cardId: String,
): Either<VisaApiError, TangemPayReissueOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.reissueCard(
authHeader = authHeader,
body = ReissueCardRequest(cardId = cardId),
)
}.bind()
TangemPayReissueOrderInfo(
orderId = response.result.orderId,
orderStatus = OrderStatusConverter.convert(response.result.status),
)
}
override suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit> =
runSuspendCatching {
tangemPayReissueCardStore.storeReissueOrderId(cardId, orderId)
}.fold(
onSuccess = { Unit.right() },
onFailure = { Either.Left(VisaApiError.Unspecified) },
)
override suspend fun getReissueOrderInfo(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayReissueOrderInfo?> = either {
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
if (orderId == null) {
return null.right()
}
val order = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.bind()
val result = order.result ?: raise(VisaApiError.Unspecified)
TangemPayReissueOrderInfo(
orderId = result.id,
orderStatus = when (result.status) {
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
},
)
}
private companion object {
const val CARD_REPLACEMENT_FEE_TYPE = "CARD_REPLACEMENT"
}
}

View file

@ -14,11 +14,13 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest
import com.tangem.datasource.api.pay.models.request.UpdateCardRequest
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.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
@ -39,7 +41,7 @@ import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPay: CardDetailsRepository"
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
@ -318,6 +320,54 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
}
}
override suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit> {
return catch(
block = {
requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.updateCard(
authHeader = authHeader,
body = UpdateCardRequest(
displayName = displayName.value,
),
cardId = cardId,
)
}.fold(
ifLeft = { error -> error.left() },
ifRight = { Unit.right() },
)
},
catch = ::catchException,
)
}
override suspend fun updateCardLimit(
cardId: String,
userWalletId: UserWalletId,
limit: String,
): Either<UniversalError, Unit> {
return catch(
block = {
requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.updateCard(
authHeader = authHeader,
body = UpdateCardRequest(
cardLimit = UpdateCardRequest.CardLimit(limit),
),
cardId = cardId,
)
}.fold(
ifLeft = { error -> error.left() },
ifRight = { Unit.right() },
)
},
catch = ::catchException,
)
}
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> {
return cardFrozenStateStore.get(cardId)
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@ -33,6 +34,7 @@ private const val TAG = "PaymentAccountStatusesStore"
internal class PaymentAccountStatusesStore(
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
private val converter: PaymentAccountStatusValueDMConverter,
scope: AppCoroutineScope,
) {
@ -51,12 +53,13 @@ internal class PaymentAccountStatusesStore(
runtimeStore.store(
value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId))
val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM)
val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM)
AccountStatus.Payment(account = account, value = statusValue)
},
)
logger.i("init: runtimeStore populated with ${cachedStatuses.size} entries")
} catch (e: Exception) {
runSuspendCatching { persistenceDataStore.updateData { emptyMap() } }
logger.e("Error while loading cached payment account statuses", e)
}
}
@ -114,7 +117,7 @@ internal class PaymentAccountStatusesStore(
}
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) {
val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return
val statusDM = converter.convert(value = status) ?: return
persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply {
put(key = userWalletId.stringValue, value = statusDM)

View file

@ -0,0 +1,16 @@
package com.tangem.data.pay.util
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.utils.converter.Converter
internal object OrderStatusConverter : Converter<OrderResponse.Result.Status, OrderStatus> {
override fun convert(value: OrderResponse.Result.Status): OrderStatus {
return when (value) {
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.repository.MockAwareOnboardingRepository
import com.tangem.data.pay.repository.MockAwareTangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
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 TangemPayDataMockedModule {
@Binds
@Singleton
fun bindOnboardingRepository(repository: MockAwareOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: MockAwareTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
}

View file

@ -0,0 +1,106 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.right
import com.tangem.core.error.UniversalError
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.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
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. */
@Singleton
internal class MockAwareOnboardingRepository @Inject constructor(
private val real: DefaultOnboardingRepository,
private val apiConfigsManager: ApiConfigsManager,
) : OnboardingRepository {
private val mockOrderIds: MutableSet<UserWalletId> = ConcurrentHashMap.newKeySet()
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
if (isMockMode) return true.right()
return real.validateDeeplink(link)
}
override suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean {
if (isMockMode) return true
return real.isTangemPayInitialDataProduced(userWalletId)
}
override suspend fun produceInitialData(userWalletId: UserWalletId) {
if (isMockMode) return
real.produceInitialData(userWalletId)
}
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
real.getCustomerInfo(userWalletId)
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> {
if (isMockMode) {
mockOrderIds.add(userWalletId)
return MOCK_ORDER_ID.right()
}
return real.createOrder(userWalletId)
}
override suspend fun clearOrderId(userWalletId: UserWalletId) {
if (isMockMode) {
mockOrderIds.remove(userWalletId)
return
}
real.clearOrderId(userWalletId)
}
override suspend fun getOrderId(userWalletId: UserWalletId): String? {
if (isMockMode) return MOCK_ORDER_ID.takeIf { userWalletId in mockOrderIds }
return real.getOrderId(userWalletId)
}
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
real.hasTangemPayInWallet(userWalletId)
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> =
real.checkCustomerEligibility()
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
real.getCustomerEligibility()
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
real.getSavedCustomerInfo(userWalletId)
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
if (isMockMode) return false
return real.getHideMainOnboardingBanner(userWalletId)
}
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
if (isMockMode) return
real.setHideMainOnboardingBanner(userWalletId)
}
override suspend fun disableTangemPay(userWalletId: UserWalletId): Either<VisaApiError, Unit> {
if (isMockMode) return Unit.right()
return real.disableTangemPay(userWalletId)
}
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
if (isMockMode) return false
return real.isTangemPayDeactivated(userWalletId)
}
private companion object {
const val MOCK_ORDER_ID = "mock-order-id"
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.right
import com.tangem.core.error.UniversalError
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.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
/** In MOCK env short-circuits RSA-encrypted flows (reveal/getPin/setPin) with hardcoded values. */
@Singleton
internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
private val real: DefaultTangemPayCardDetailsRepository,
private val apiConfigsManager: ApiConfigsManager,
) : TangemPayCardDetailsRepository {
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> =
real.getCardBalance(userWalletId)
override suspend fun revealCardDetails(
userWalletId: UserWalletId,
): Either<UniversalError, TangemPayCardDetails> {
if (isMockMode) {
return TangemPayCardDetails(
pan = MOCK_PAN,
cvv = MOCK_CVV,
expirationYear = MOCK_EXPIRATION_YEAR,
expirationMonth = MOCK_EXPIRATION_MONTH,
).right()
}
return real.revealCardDetails(userWalletId)
}
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
if (isMockMode) return MOCK_PIN.right()
return real.getPin(userWalletId, cardId)
}
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
if (isMockMode) return SetPinResult.SUCCESS.right()
return real.setPin(userWalletId, pin)
}
override suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean> =
real.isAddToWalletDone(userWalletId)
override suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit> =
real.setAddToWalletAsDone(userWalletId)
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.freezeCard(userWalletId, cardId)
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.unfreezeCard(userWalletId, cardId)
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> =
real.cardFrozenState(cardId)
override suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? =
real.cardFrozenStateSync(cardId)
override suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit> = real.updateCardDisplayName(cardId, userWalletId, displayName)
override suspend fun updateCardLimit(
cardId: String,
userWalletId: UserWalletId,
limit: String,
): Either<UniversalError, Unit> = real.updateCardLimit(cardId, userWalletId, limit)
private companion object {
const val MOCK_PAN = "4242 4242 4242 4242"
const val MOCK_CVV = "123"
const val MOCK_EXPIRATION_YEAR = "2028"
const val MOCK_EXPIRATION_MONTH = "12"
const val MOCK_PIN = "1234"
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
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 TangemPayDataProductionModule {
@Binds
@Singleton
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
}

View file

@ -1,9 +1,12 @@
package com.tangem.data.pay.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.mockk
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@ -12,6 +15,11 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class PaymentAccountStatusValueDMConverterTest {
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
private val userWalletId = UserWalletId("1234567890ABCDEF")
private val converter = PaymentAccountStatusValueDMConverter(tangemPayCurrencyFactory)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Convert {
@ -22,7 +30,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val domain = PaymentAccountStatusValue.Empty
// WHEN
val result = PaymentAccountStatusValueDMConverter.convert(domain)
val result = converter.convert(domain)
// THEN
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.Empty::class.java)
@ -40,7 +48,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
)
// WHEN
val result = PaymentAccountStatusValueDMConverter.convert(domain)
val result = converter.convert(domain)
// THEN
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.DeactivatedAccount::class.java)
@ -55,7 +63,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val domain = PaymentAccountStatusValue.Loading
// WHEN
val result = PaymentAccountStatusValueDMConverter.convert(domain)
val result = converter.convert(domain)
// THEN
assertThat(result).isNull()
@ -67,7 +75,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val domain = PaymentAccountStatusValue.Error.Unavailable
// WHEN
val result = PaymentAccountStatusValueDMConverter.convert(domain)
val result = converter.convert(domain)
// THEN
assertThat(result).isNull()
@ -79,7 +87,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val domain = PaymentAccountStatusValue.NotCreated
// WHEN
val result = PaymentAccountStatusValueDMConverter.convert(domain)
val result = converter.convert(domain)
// THEN
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.NotCreated::class.java)
@ -96,7 +104,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val dm = PaymentAccountStatusValueDM.Empty()
// WHEN
val result = PaymentAccountStatusValueDMConverter.convertBack(dm)
val result = converter.convertBack(userWalletId, dm)
// THEN
assertThat(result).isEqualTo(PaymentAccountStatusValue.Empty)
@ -113,7 +121,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
)
// WHEN
val result = PaymentAccountStatusValueDMConverter.convertBack(dm)
val result = converter.convertBack(userWalletId, dm)
// THEN
assertThat(result).isInstanceOf(PaymentAccountStatusValue.Deactivated::class.java)
@ -126,7 +134,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
@Test
fun `GIVEN null DM WHEN convertBack THEN returns Error Unavailable`() {
// GIVEN / WHEN
val result = PaymentAccountStatusValueDMConverter.convertBack(null)
val result = converter.convertBack(userWalletId, null)
// THEN
assertThat(result).isEqualTo(PaymentAccountStatusValue.Error.Unavailable)
@ -138,7 +146,7 @@ internal class PaymentAccountStatusValueDMConverterTest {
val dm = PaymentAccountStatusValueDM.NotCreated()
// WHEN
val result = PaymentAccountStatusValueDMConverter.convertBack(dm)
val result = converter.convertBack(userWalletId, dm)
// THEN
assertThat(result).isEqualTo(PaymentAccountStatusValue.NotCreated)