Updated on 2026-08-14
This commit is contained in:
commit
d0b35bc331
680 changed files with 26556 additions and 2709 deletions
|
|
@ -6,9 +6,11 @@ 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.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
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.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import javax.inject.Inject
|
||||
|
|
@ -50,9 +52,9 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
displayName = card.displayName?.value,
|
||||
actualDailyLimit = card.limit?.actualCardLimit?.amount,
|
||||
adminDailyLimit = card.limit?.adminCardLimit?.amount,
|
||||
isFrozen = card.isFrozen,
|
||||
frozenState = card.frozenState.toString(),
|
||||
lastDigits = card.lastDigits,
|
||||
isReissuing = card.isReissuing,
|
||||
state = card.state.toString(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -108,9 +110,9 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
|
||||
},
|
||||
),
|
||||
isFrozen = card.isFrozen,
|
||||
frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
|
||||
lastDigits = card.lastDigits,
|
||||
isReissuing = card.isReissuing,
|
||||
state = TangemPayCardState.fromString(card.state),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
|||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
|
|
@ -30,6 +31,7 @@ import com.tangem.domain.pay.usecase.*
|
|||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -65,6 +67,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCryptoCurrencyFactory(factory: DefaultTangemPayCurrencyFactory): TangemPayCurrencyFactory
|
||||
|
|
@ -75,6 +81,12 @@ internal interface TangemPayDataModule {
|
|||
impl: DefaultGetTangemPayCurrencyStatusUseCase,
|
||||
): GetTangemPayCurrencyStatusUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayWithdrawWithSwapUseCase(
|
||||
impl: DefaultTangemPayWithdrawWithSwapUseCase,
|
||||
): TangemPayWithdrawWithSwapUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
|
@ -204,5 +216,20 @@ internal interface TangemPayDataModule {
|
|||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCloseTangemPayCardUseCase(
|
||||
closeCardRepository: TangemPayCloseCardRepository,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CloseTangemPayCardUseCase {
|
||||
return CloseTangemPayCardUseCase(
|
||||
closeCardRepository = closeCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,11 @@ import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.pay.model.isFinalStatus
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -39,7 +43,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||
|
||||
private const val TAG = "PaymentAccountStatusFetcher"
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -50,6 +54,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
private val reissueCardRepository: TangemPayReissueCardRepository,
|
||||
private val singleQuoteSupplier: SingleQuoteStatusSupplier,
|
||||
private val closeCardRepository: TangemPayCloseCardRepository,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
|
@ -308,15 +314,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
customerId: String,
|
||||
fiatRate: BigDecimal?,
|
||||
): PaymentAccountStatusValue {
|
||||
val reissueOrder = reissueCardRepository.getReissueOrderInfo(
|
||||
userWalletId = userWalletId,
|
||||
cardId = productInstance.cardId,
|
||||
).getOrNull()
|
||||
|
||||
val isReissuing = reissueOrder != null &&
|
||||
reissueOrder.orderStatus != OrderStatus.CANCELED &&
|
||||
reissueOrder.orderStatus != OrderStatus.COMPLETED
|
||||
|
||||
val cardId = productInstance.cardId
|
||||
val cardState = getCardState(cardId, userWalletId)
|
||||
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(cardId)
|
||||
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
|
|
@ -330,21 +330,48 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
fiatRate = fiatRate,
|
||||
cards = listOf(
|
||||
TangemPayCard(
|
||||
id = productInstance.cardId,
|
||||
id = cardId,
|
||||
hasPinCode = cardInfo.isPinSet,
|
||||
displayName = productInstance.displayName,
|
||||
limit = TangemPayCardLimitData(
|
||||
actualCardLimit = productInstance.actualCardLimit,
|
||||
adminCardLimit = productInstance.adminCardLimit,
|
||||
),
|
||||
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
|
||||
frozenState = if (cardFrozenState == TangemPayCardFrozenState.Pending) {
|
||||
TangemPayCardFrozenState.Pending
|
||||
} else {
|
||||
productInstance.frozenState
|
||||
},
|
||||
lastDigits = cardInfo.lastFourDigits,
|
||||
isReissuing = isReissuing,
|
||||
state = cardState,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
|
||||
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
|
||||
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()
|
||||
return if (closingOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isFinalStatus) {
|
||||
closeCardRepository.setCloseOrderId(cardId, null)
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Closing
|
||||
}
|
||||
} else if (reissueOrderId != null) {
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isFinalStatus) {
|
||||
TangemPayCardState.Active
|
||||
} else {
|
||||
TangemPayCardState.Reissuing
|
||||
}
|
||||
} else {
|
||||
TangemPayCardState.Active
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
return when (this) {
|
||||
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
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.CloseCardRequest
|
||||
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultCloseCardRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayCloseCardStore: TangemPayCloseCardStore,
|
||||
) : TangemPayCloseCardRepository {
|
||||
|
||||
override suspend fun closeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<VisaApiError, TangemPayOrderInfo> = either {
|
||||
val response = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.closeCard(
|
||||
authHeader = authHeader,
|
||||
body = CloseCardRequest(cardId = cardId),
|
||||
)
|
||||
}.bind()
|
||||
TangemPayOrderInfo(
|
||||
orderId = response.result.orderId,
|
||||
orderStatus = OrderStatusConverter.convert(response.result.status),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit> =
|
||||
runSuspendCatching {
|
||||
tangemPayCloseCardStore.setCloseOrderId(cardId, orderId)
|
||||
}.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()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ 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.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
|
|
@ -21,7 +20,6 @@ internal class DefaultReissueCardRepository @Inject constructor(
|
|||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : TangemPayReissueCardRepository {
|
||||
|
||||
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
|
||||
|
|
@ -74,17 +72,11 @@ internal class DefaultReissueCardRepository @Inject constructor(
|
|||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getReissueOrderInfo(
|
||||
override suspend fun getReissueOrderId(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayOrderInfo?> = either {
|
||||
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
|
||||
|
||||
if (orderId == null) {
|
||||
return null.right()
|
||||
}
|
||||
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
|
||||
): Either<UniversalError, String?> = either {
|
||||
runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import com.tangem.domain.pay.model.TangemPayCardDetails
|
|||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
|
|
@ -12,10 +13,7 @@ import com.tangem.datasource.local.visa.TangemPayStorage
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.pay.*
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
|
|
@ -60,7 +58,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
|
|||
private val pollingJobs = mutableMapOf<PollingKey, Job>()
|
||||
private val pollingMutex = Mutex()
|
||||
|
||||
override suspend fun withdraw(
|
||||
override suspend fun withdrawWithSwap(
|
||||
userWallet: UserWallet,
|
||||
receiverAddress: String,
|
||||
cryptoAmount: BigDecimal,
|
||||
|
|
@ -100,7 +98,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
|
|||
WithdrawalResult.Success
|
||||
}
|
||||
}
|
||||
null -> return Either.Left(VisaApiError.SignWithdrawError)
|
||||
null -> Either.Left(VisaApiError.SignWithdrawError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -223,6 +221,46 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun withdraw(
|
||||
userWallet: UserWallet,
|
||||
receiverAddress: String,
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
|
||||
if (amountInCents.isNullOrEmpty()) return VisaApiError.WithdrawalDataError.left()
|
||||
|
||||
return requestHelper.performRequest(userWallet.walletId) { authHeader ->
|
||||
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
|
||||
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
|
||||
}.map { data ->
|
||||
val result = data.result ?: return VisaApiError.WithdrawalDataError.left()
|
||||
val signatureResult = authDataSource.getWithdrawalSignature(
|
||||
userWallet = userWallet,
|
||||
hash = result.hash,
|
||||
).getOrNull()
|
||||
return when (signatureResult) {
|
||||
is WithdrawalSignatureResult.Cancelled -> WithdrawalResult.Cancelled.right()
|
||||
is WithdrawalSignatureResult.Success -> requestHelper.performRequest(
|
||||
userWalletId = userWallet.walletId,
|
||||
) { authHeader ->
|
||||
val request = WithdrawRequest(
|
||||
amountInCents = amountInCents,
|
||||
recipientAddress = receiverAddress,
|
||||
adminSalt = result.salt,
|
||||
senderAddress = result.senderAddress,
|
||||
adminSignature = signatureResult.signature.addHexPrefix(),
|
||||
)
|
||||
tangemPayApi.withdraw(authHeader = authHeader, body = request)
|
||||
}.fold(
|
||||
ifLeft = { VisaApiError.WithdrawError.left() },
|
||||
ifRight = { WithdrawalResult.Success.right() },
|
||||
)
|
||||
null -> VisaApiError.SignWithdrawError.left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean {
|
||||
val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWalletId)
|
||||
if (orderId.isNullOrEmpty()) return false
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import arrow.core.Either
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
|
|
@ -20,14 +19,12 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
|
|||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
receiverCexAddress: String,
|
||||
exchangeData: TangemPayWithdrawExchangeState,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
return repository.withdraw(
|
||||
userWallet = userWallet,
|
||||
cryptoAmount = cryptoAmount,
|
||||
receiverAddress = receiverCexAddress,
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
exchangeData = exchangeData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultTangemPayWithdrawWithSwapUseCase @Inject constructor(
|
||||
private val repository: TangemPayWithdrawRepository,
|
||||
) : TangemPayWithdrawWithSwapUseCase {
|
||||
|
||||
override suspend fun invoke(
|
||||
userWallet: UserWallet,
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrencyId: CryptoCurrency.RawID,
|
||||
receiverCexAddress: String,
|
||||
exchangeData: TangemPayWithdrawExchangeState,
|
||||
): Either<UniversalError, WithdrawalResult> {
|
||||
return repository.withdrawWithSwap(
|
||||
userWallet = userWallet,
|
||||
cryptoAmount = cryptoAmount,
|
||||
receiverAddress = receiverCexAddress,
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
exchangeData = exchangeData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.pay.model.CustomerInfo
|
|||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ 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.pay.TangemPayCardFrozenState
|
||||
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.model.TangemPayOrderInfo
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.data.common.quote.QuotesFetcher
|
||||
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.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.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
|
||||
import com.tangem.domain.pay.TangemPayWithdrawState
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.OrderData
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultTangemPayWithdrawRepositoryTest {
|
||||
|
||||
private val tangemPayApi: TangemPayApi = mockk()
|
||||
private val requestHelper: TangemPayRequestPerformer = mockk()
|
||||
private val authDataSource: TangemPayAuthDataSource = mockk()
|
||||
private val quotesFetcher: QuotesFetcher = mockk()
|
||||
private val tangemPayStorage: TangemPayStorage = mockk(relaxUnitFun = true)
|
||||
private val swapRepository: SwapRepository = mockk()
|
||||
private val orderRepository: CustomerOrderRepository = mockk()
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
private val userWallet: UserWallet = mockk {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
private val cryptoCurrencyId = CryptoCurrency.RawID(CURRENCY_ID)
|
||||
private val exchangeData = TangemPayWithdrawExchangeState(
|
||||
txId = "txId",
|
||||
fromNetwork = "ETH",
|
||||
fromAddress = "0xFrom",
|
||||
payInAddress = "0xPayIn",
|
||||
payInExtraId = null,
|
||||
)
|
||||
|
||||
private val orderWithoutHash = OrderData(
|
||||
customerId = "customer",
|
||||
status = OrderStatus.PROCESSING,
|
||||
withdrawTxHash = null,
|
||||
)
|
||||
private val orderWithHash = orderWithoutHash.copy(withdrawTxHash = TX_HASH)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
// Valid fiat rate so amountInCents resolves to a non-empty value.
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currencyId = any(), field = any())
|
||||
} returns QuotesResponse(
|
||||
quotes = mapOf(CURRENCY_ID to QuotesResponse.Quote.EMPTY.copy(price = BigDecimal.ONE)),
|
||||
).right()
|
||||
|
||||
// performRequest is treated as a transparent pass-through: it invokes the request block and
|
||||
// maps the ApiResponse to Either, so each test can drive behaviour via the TangemPayApi mock.
|
||||
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.WithdrawError.left()
|
||||
}
|
||||
}
|
||||
|
||||
coEvery { tangemPayApi.getWithdrawData(any(), any()) } returns ApiResponse.Success(
|
||||
WithdrawDataResponse(
|
||||
result = WithdrawDataResponse.Result(hash = "hash", salt = "salt", senderAddress = "sender"),
|
||||
),
|
||||
)
|
||||
coEvery { tangemPayApi.withdraw(any(), any()) } returns ApiResponse.Success(
|
||||
WithdrawResponse(
|
||||
result = WithdrawResponse.Result(orderId = ORDER_ID, status = "NEW", type = "withdraw"),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authDataSource.getWithdrawalSignature(any(), any())
|
||||
} returns WithdrawalSignatureResult.Success(SIGNATURE).right()
|
||||
coEvery { swapRepository.exchangeSent(any(), any(), any(), any(), any(), any(), any()) } returns Unit.right()
|
||||
}
|
||||
|
||||
// region withdrawWithSwap
|
||||
|
||||
@Test
|
||||
fun `GIVEN amountInCents is null WHEN withdrawWithSwap THEN return WithdrawalDataError`() = runTest {
|
||||
coEvery {
|
||||
quotesFetcher.fetch(fiatCurrencyId = any(), currencyId = any(), field = any())
|
||||
} returns QuotesFetcher.Error.CacheOperationError.left()
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
|
||||
Assertions.assertEquals(VisaApiError.WithdrawalDataError.left(), result)
|
||||
coVerify(exactly = 0) { tangemPayApi.getWithdrawData(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getWithdrawData result is null WHEN withdrawWithSwap THEN return WithdrawalDataError`() = runTest {
|
||||
coEvery { tangemPayApi.getWithdrawData(any(), any()) } returns ApiResponse.Success(
|
||||
WithdrawDataResponse(result = null),
|
||||
)
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
|
||||
Assertions.assertEquals(VisaApiError.WithdrawalDataError.left(), result)
|
||||
coVerify(exactly = 0) { authDataSource.getWithdrawalSignature(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdrawal signature is null WHEN withdrawWithSwap THEN return SignWithdrawError`() = runTest {
|
||||
coEvery { authDataSource.getWithdrawalSignature(any(), any()) } returns RuntimeException("error").left()
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
|
||||
Assertions.assertEquals(VisaApiError.SignWithdrawError.left(), result)
|
||||
coVerify(exactly = 0) { tangemPayApi.withdraw(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdrawal signature is Cancelled WHEN withdrawWithSwap THEN return Cancelled`() = runTest {
|
||||
coEvery { authDataSource.getWithdrawalSignature(any(), any()) } returns WithdrawalSignatureResult.Cancelled.right()
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Cancelled.right(), result)
|
||||
coVerify(exactly = 0) { tangemPayApi.withdraw(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdraw returns error WHEN withdrawWithSwap THEN return WithdrawError`() = runTest {
|
||||
coEvery { tangemPayApi.withdraw(any(), any()) } returns
|
||||
ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<WithdrawResponse>
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
|
||||
Assertions.assertEquals(VisaApiError.WithdrawError.left(), result)
|
||||
coVerify(exactly = 0) { orderRepository.getOrderData(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no txHash on every attempt WHEN withdrawWithSwap THEN polling deletes order after max attempts`() =
|
||||
runTest {
|
||||
coEvery { orderRepository.getOrderData(any(), any()) } returns orderWithoutHash.right()
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Success.right(), result)
|
||||
// 1 initial check + MAX_POLLING_ATTEMPTS (6) polling attempts.
|
||||
coVerify(exactly = 7) { orderRepository.getOrderData(userWalletId, ORDER_ID) }
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
coVerify(exactly = 0) { swapRepository.exchangeSent(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getOrderData throws while polling WHEN withdrawWithSwap THEN polling deletes order`() = runTest {
|
||||
coEvery {
|
||||
orderRepository.getOrderData(any(), any())
|
||||
} returns orderWithoutHash.right() andThenThrows RuntimeException("boom")
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Success.right(), result)
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
coVerify(exactly = 0) { swapRepository.exchangeSent(any(), any(), any(), any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN txHash appears on the last attempt WHEN withdrawWithSwap THEN polling finalizes the withdrawal`() =
|
||||
runTest {
|
||||
// index 0 = initial check, 1..5 = polling attempts 1-5, 6 = polling attempt 6 (last) returns the hash.
|
||||
coEvery { orderRepository.getOrderData(any(), any()) } returnsMany
|
||||
List(size = 6) { orderWithoutHash.right() } + listOf(orderWithHash.right())
|
||||
|
||||
val result = createRepository().withdrawWithSwap()
|
||||
advanceUntilIdle()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Success.right(), result)
|
||||
assertExchangeSent()
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region withdraw
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdrawal signature is Cancelled WHEN withdraw THEN return Cancelled`() = runTest {
|
||||
coEvery { authDataSource.getWithdrawalSignature(any(), any()) } returns WithdrawalSignatureResult.Cancelled.right()
|
||||
|
||||
val result = createRepository().withdraw()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Cancelled.right(), result)
|
||||
coVerify(exactly = 0) { tangemPayApi.withdraw(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN withdraw succeeds WHEN withdraw THEN return Success`() = runTest {
|
||||
val result = createRepository().withdraw()
|
||||
|
||||
Assertions.assertEquals(WithdrawalResult.Success.right(), result)
|
||||
coVerify { tangemPayApi.withdraw(any(), any()) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region hasWithdrawOrder
|
||||
|
||||
@Test
|
||||
fun `GIVEN no active order id WHEN hasWithdrawOrder THEN return false`() = runTest {
|
||||
coEvery { tangemPayStorage.getActiveWithdrawOrderId(userWalletId) } returns null
|
||||
|
||||
val result = createRepository().hasWithdrawOrder(userWalletId)
|
||||
|
||||
Assertions.assertFalse(result)
|
||||
coVerify(exactly = 0) { orderRepository.getOrderData(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order is not active WHEN hasWithdrawOrder THEN delete active order and return false`() = runTest {
|
||||
coEvery { tangemPayStorage.getActiveWithdrawOrderId(userWalletId) } returns ORDER_ID
|
||||
coEvery {
|
||||
orderRepository.getOrderData(userWalletId, ORDER_ID)
|
||||
} returns orderWithoutHash.copy(status = OrderStatus.COMPLETED).right()
|
||||
|
||||
val result = createRepository().hasWithdrawOrder(userWalletId)
|
||||
|
||||
Assertions.assertFalse(result)
|
||||
coVerify { tangemPayStorage.deleteActiveWithdrawOrder(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order is active WHEN hasWithdrawOrder THEN return true and keep active order`() = runTest {
|
||||
coEvery { tangemPayStorage.getActiveWithdrawOrderId(userWalletId) } returns ORDER_ID
|
||||
coEvery {
|
||||
orderRepository.getOrderData(userWalletId, ORDER_ID)
|
||||
} returns orderWithoutHash.copy(status = OrderStatus.NEW).right()
|
||||
|
||||
val result = createRepository().hasWithdrawOrder(userWalletId)
|
||||
|
||||
Assertions.assertTrue(result)
|
||||
coVerify(exactly = 0) { tangemPayStorage.deleteActiveWithdrawOrder(userWalletId) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region pollWithdrawOrdersIfNeeds
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored hash is null and order hash appears on third attempt WHEN poll THEN finalize the withdrawal`() =
|
||||
runTest {
|
||||
coEvery { tangemPayStorage.getWithdrawOrders(userWalletId) } returns listOf(storedOrder(txHash = null))
|
||||
// index 0 = initial fetch, 1..2 = polling attempts 1-2, 3 = polling attempt 3 returns the hash.
|
||||
coEvery { orderRepository.getOrderData(any(), any()) } returnsMany
|
||||
List(size = 3) { orderWithoutHash.right() } + listOf(orderWithHash.right())
|
||||
|
||||
createRepository().pollWithdrawOrdersIfNeeds(userWallet)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 4) { orderRepository.getOrderData(userWalletId, ORDER_ID) }
|
||||
assertExchangeSent()
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored hash is null and order already has hash WHEN poll THEN finalize without polling`() = runTest {
|
||||
coEvery { tangemPayStorage.getWithdrawOrders(userWalletId) } returns listOf(storedOrder(txHash = null))
|
||||
coEvery { orderRepository.getOrderData(userWalletId, ORDER_ID) } returns orderWithHash.right()
|
||||
|
||||
createRepository().pollWithdrawOrdersIfNeeds(userWallet)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { orderRepository.getOrderData(userWalletId, ORDER_ID) }
|
||||
assertExchangeSent()
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored hash has value WHEN poll THEN finalize without fetching the order`() = runTest {
|
||||
coEvery { tangemPayStorage.getWithdrawOrders(userWalletId) } returns listOf(storedOrder(txHash = TX_HASH))
|
||||
|
||||
createRepository().pollWithdrawOrdersIfNeeds(userWallet)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { orderRepository.getOrderData(any(), any()) }
|
||||
assertExchangeSent()
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two identical orders WHEN poll THEN only a single polling job runs for the same order`() = runTest {
|
||||
val duplicatedOrder = storedOrder(txHash = null)
|
||||
coEvery {
|
||||
tangemPayStorage.getWithdrawOrders(userWalletId)
|
||||
} returns listOf(duplicatedOrder, duplicatedOrder)
|
||||
coEvery { orderRepository.getOrderData(any(), any()) } returns orderWithoutHash.right()
|
||||
|
||||
createRepository().pollWithdrawOrdersIfNeeds(userWallet)
|
||||
advanceUntilIdle()
|
||||
|
||||
// 2 initial fetches (one per order) + a single deduplicated polling job of 6 attempts = 8.
|
||||
coVerify(exactly = 8) { orderRepository.getOrderData(userWalletId, ORDER_ID) }
|
||||
coVerify { tangemPayStorage.deleteWithdrawOrder(userWalletId, ORDER_ID) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun assertExchangeSent() {
|
||||
coVerify {
|
||||
swapRepository.exchangeSent(
|
||||
userWallet = userWallet,
|
||||
txId = exchangeData.txId,
|
||||
fromNetwork = exchangeData.fromNetwork,
|
||||
fromAddress = exchangeData.fromAddress,
|
||||
payInAddress = exchangeData.payInAddress,
|
||||
txHash = TX_HASH,
|
||||
payInExtraId = exchangeData.payInExtraId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storedOrder(txHash: String?) = TangemPayWithdrawState(
|
||||
orderId = ORDER_ID,
|
||||
exchangeData = exchangeData,
|
||||
txHash = txHash,
|
||||
)
|
||||
|
||||
private suspend fun DefaultTangemPayWithdrawRepository.withdrawWithSwap() = withdrawWithSwap(
|
||||
userWallet = userWallet,
|
||||
receiverAddress = RECEIVER_ADDRESS,
|
||||
cryptoAmount = BigDecimal("1.5"),
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
exchangeData = exchangeData,
|
||||
)
|
||||
|
||||
private suspend fun DefaultTangemPayWithdrawRepository.withdraw() = withdraw(
|
||||
userWallet = userWallet,
|
||||
receiverAddress = RECEIVER_ADDRESS,
|
||||
cryptoAmount = BigDecimal("1.5"),
|
||||
cryptoCurrencyId = cryptoCurrencyId,
|
||||
)
|
||||
|
||||
private fun TestScope.createRepository() = DefaultTangemPayWithdrawRepository(
|
||||
tangemPayApi = tangemPayApi,
|
||||
requestHelper = requestHelper,
|
||||
authDataSource = authDataSource,
|
||||
quotesFetcher = quotesFetcher,
|
||||
tangemPayStorage = tangemPayStorage,
|
||||
swapRepository = swapRepository,
|
||||
orderRepository = orderRepository,
|
||||
withdrawPollingScope = TestAppCoroutineScope(this),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val CURRENCY_ID = "ethereum"
|
||||
const val ORDER_ID = "order-1"
|
||||
const val TX_HASH = "0xTxHash"
|
||||
const val SIGNATURE = "0xSignature"
|
||||
const val AUTH_HEADER = "auth-header"
|
||||
const val RECEIVER_ADDRESS = "0xReceiver"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue