Updated on 2026-08-14
This commit is contained in:
parent
4faf6e9cb0
commit
e06dc1297d
16 changed files with 287 additions and 8 deletions
|
|
@ -48,6 +48,7 @@ dependencies {
|
|||
implementation(projects.domain.quotes)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.virtualAccounts.details.api)
|
||||
|
||||
|
||||
/** Project - Utils */
|
||||
|
|
|
|||
|
|
@ -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.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.pay.*
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
|
|
@ -118,6 +119,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
)
|
||||
},
|
||||
error = null,
|
||||
virtualAccount = VirtualAccountOnramp.None,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
|
|
|
|||
|
|
@ -4,21 +4,16 @@ import arrow.core.Either
|
|||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.StatusSource
|
||||
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.account.hasAccountData
|
||||
import com.tangem.domain.models.account.*
|
||||
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.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.pay.*
|
||||
import com.tangem.domain.models.quote.QuoteStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.SpecificationDataType
|
||||
import com.tangem.domain.pay.model.OrderData
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
|
|
@ -26,6 +21,7 @@ import com.tangem.domain.pay.repository.*
|
|||
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.virtualaccount.VirtualAccountFeatureToggles
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
import com.tangem.security.isSecurityExposed
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -66,6 +62,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val closeCardRepository: TangemPayCloseCardRepository,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val issueCardRepository: TangemPayIssueCardRepository,
|
||||
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
|
@ -382,6 +379,8 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
// the previously shown order and append newly seen cards at the end.
|
||||
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
||||
|
||||
val virtualAccount = resolveVirtualAccountOnramp(userWalletId)
|
||||
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
|
|
@ -395,6 +394,50 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
availableForWithdrawal = availableForWithdrawal.orZero(),
|
||||
),
|
||||
error = null,
|
||||
virtualAccount = virtualAccount,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Virtual Account on-ramp dimension (VA MVP0, TWI-1638). Gated by the feature toggle.
|
||||
* If a product instance with [SpecificationDataType.ACCOUNT] exists, eagerly fetches its bank credentials
|
||||
* ([VirtualAccountOnramp.Available]); otherwise surfaces [VirtualAccountOnramp.Eligible] when the wallet has
|
||||
* the `VISA_VIRTUAL_ACCOUNT` eligibility channel (fetched fresh via the user token), else
|
||||
* [VirtualAccountOnramp.None].
|
||||
*/
|
||||
private suspend fun CustomerInfo.resolveVirtualAccountOnramp(userWalletId: UserWalletId): VirtualAccountOnramp {
|
||||
if (!virtualAccountFeatureToggles.isVaMvp0Enabled) return VirtualAccountOnramp.None
|
||||
|
||||
val accountInstance = productInstances.firstOrNull {
|
||||
it.specificationDataType == SpecificationDataType.ACCOUNT
|
||||
}
|
||||
if (accountInstance != null) {
|
||||
return onboardingRepository.getBankCredentials(userWalletId, accountInstance.id).fold(
|
||||
ifLeft = { error ->
|
||||
logger.e("getBankCredentials failed for ${accountInstance.id}: $error")
|
||||
VirtualAccountOnramp.None
|
||||
},
|
||||
ifRight = { credentials ->
|
||||
VirtualAccountOnramp.Available(
|
||||
productInstanceId = accountInstance.id,
|
||||
bankCredentials = credentials,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return onboardingRepository.fetchCustomerEligibility(userWalletId).fold(
|
||||
ifLeft = { error ->
|
||||
logger.e("fetchCustomerEligibility failed for $userWalletId: $error")
|
||||
VirtualAccountOnramp.None
|
||||
},
|
||||
ifRight = { channels ->
|
||||
if (channels.contains(TangemPayEligibilityType.VISA_VIRTUAL_ACCOUNT)) {
|
||||
VirtualAccountOnramp.Eligible
|
||||
} else {
|
||||
VirtualAccountOnramp.None
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import arrow.core.left
|
|||
import arrow.core.right
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.data.pay.util.BankCredentialsConverter
|
||||
import com.tangem.data.pay.util.CustomerInfoConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.datasource.local.visa.TangemPayStorage
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
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.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
|
|
@ -105,6 +107,15 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getBankCredentials(
|
||||
userWalletId: UserWalletId,
|
||||
productInstanceId: String,
|
||||
): Either<VisaApiError, BankCredentials> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getBankCredentials(authHeader = authHeader, productInstanceId = productInstanceId)
|
||||
}.map { response -> BankCredentialsConverter.convert(response) }
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||
return tangemPayStorage.isTangemPayDeactivated(userWalletId)
|
||||
}
|
||||
|
|
@ -226,6 +237,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
return tangemPayStorage.getTangemPayEligibility().map(TangemPayEligibilityType::fromString)
|
||||
}
|
||||
|
||||
override suspend fun fetchCustomerEligibility(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, List<TangemPayEligibilityType>> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getUserEligibilityChannels(authHeader)
|
||||
}.map { response ->
|
||||
response.result.channels.map(TangemPayEligibilityType::fromString)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.tangem.datasource.api.pay.models.response.BankCredentialsResponse
|
||||
import com.tangem.domain.models.account.BankCredentials
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object BankCredentialsConverter : Converter<BankCredentialsResponse, BankCredentials> {
|
||||
override fun convert(value: BankCredentialsResponse): BankCredentials {
|
||||
return BankCredentials(
|
||||
type = value.type.orEmpty(),
|
||||
beneficiaryName = value.beneficiaryName.orEmpty(),
|
||||
beneficiaryAddress = value.beneficiaryAddress.orEmpty(),
|
||||
beneficiaryBankName = value.beneficiaryBankName.orEmpty(),
|
||||
beneficiaryBankAddress = value.beneficiaryBankAddress.orEmpty(),
|
||||
accountNumber = value.accountNumber.orEmpty(),
|
||||
routingNumber = value.routingNumber.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
|||
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.SpecificationDataType
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -62,6 +63,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
displayName = if (name != null) CardDisplayName(name).getOrElse { null } else null,
|
||||
actualCardLimit = actualCardLimit?.parseCardLimit(),
|
||||
adminCardLimit = adminCardLimit?.parseCardLimit(),
|
||||
specificationDataType = specificationDataType.toDomain(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -108,4 +110,10 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
CustomerMeResponse.ProductInstance.Status.CANCELED -> Status.CANCELED
|
||||
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> Status.UNKNOWN
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.ProductInstance.SpecificationDataType.toDomain(): SpecificationDataType =
|
||||
when (this) {
|
||||
CustomerMeResponse.ProductInstance.SpecificationDataType.ACCOUNT -> SpecificationDataType.ACCOUNT
|
||||
CustomerMeResponse.ProductInstance.SpecificationDataType.CARD -> SpecificationDataType.CARD
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ 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.BankCredentials
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
|
|
@ -47,6 +48,11 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
|
||||
real.getCustomerInfo(userWalletId)
|
||||
|
||||
override suspend fun getBankCredentials(
|
||||
userWalletId: UserWalletId,
|
||||
productInstanceId: String,
|
||||
): Either<VisaApiError, BankCredentials> = real.getBankCredentials(userWalletId, productInstanceId)
|
||||
|
||||
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> {
|
||||
if (isMockMode) {
|
||||
mockOrderIds.add(userWalletId)
|
||||
|
|
@ -77,6 +83,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||
real.getCustomerEligibility()
|
||||
|
||||
override suspend fun fetchCustomerEligibility(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<VisaApiError, List<TangemPayEligibilityType>> = real.fetchCustomerEligibility(userWalletId)
|
||||
|
||||
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
|
||||
real.getSavedCustomerInfo(userWalletId)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.data.pay.util
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.BankCredentialsResponse
|
||||
import com.tangem.domain.models.account.BankCredentials
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class BankCredentialsConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN full response WHEN convert THEN all fields mapped`() {
|
||||
// Arrange
|
||||
val response = BankCredentialsResponse(
|
||||
type = "fiat",
|
||||
beneficiaryName = "Ivan Ivanov",
|
||||
beneficiaryAddress = "18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
|
||||
beneficiaryBankName = "SSB BANK",
|
||||
beneficiaryBankAddress = "8700 Perry Highway, Pittsburgh, PA 15237, US",
|
||||
accountNumber = "707613210122",
|
||||
routingNumber = "043087080",
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = BankCredentialsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
val expected = BankCredentials(
|
||||
type = "fiat",
|
||||
beneficiaryName = "Ivan Ivanov",
|
||||
beneficiaryAddress = "18, Rue Rubens 20, Paris, Ile-de-France 75013, US",
|
||||
beneficiaryBankName = "SSB BANK",
|
||||
beneficiaryBankAddress = "8700 Perry Highway, Pittsburgh, PA 15237, US",
|
||||
accountNumber = "707613210122",
|
||||
routingNumber = "043087080",
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fields WHEN convert THEN mapped to empty strings`() {
|
||||
// Arrange
|
||||
val response = BankCredentialsResponse(
|
||||
type = null,
|
||||
beneficiaryName = null,
|
||||
beneficiaryAddress = null,
|
||||
beneficiaryBankName = null,
|
||||
beneficiaryBankAddress = null,
|
||||
accountNumber = null,
|
||||
routingNumber = null,
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = BankCredentialsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
val expected = BankCredentials(
|
||||
type = "",
|
||||
beneficiaryName = "",
|
||||
beneficiaryAddress = "",
|
||||
beneficiaryBankName = "",
|
||||
beneficiaryBankAddress = "",
|
||||
accountNumber = "",
|
||||
routingNumber = "",
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue