Updated on 2026-08-14
This commit is contained in:
parent
4faf6e9cb0
commit
e06dc1297d
16 changed files with 287 additions and 8 deletions
|
|
@ -23,6 +23,13 @@ interface TangemPayApi {
|
||||||
@GET("v1/customer/me")
|
@GET("v1/customer/me")
|
||||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||||
|
|
||||||
|
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
|
||||||
|
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||||
|
suspend fun getBankCredentials(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Path("product_instance_id") productInstanceId: String,
|
||||||
|
): ApiResponse<BankCredentialsResponse>
|
||||||
|
|
||||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||||
suspend fun checkCustomerWalletId(
|
suspend fun checkCustomerWalletId(
|
||||||
@Path("customer_wallet_id") customerWalletId: String,
|
@Path("customer_wallet_id") customerWalletId: String,
|
||||||
|
|
@ -40,6 +47,12 @@ interface TangemPayApi {
|
||||||
@GET("v1/eligibility/channels")
|
@GET("v1/eligibility/channels")
|
||||||
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
||||||
|
|
||||||
|
/** Eligibility channels fetched with the user (customer-wallet) token (VA MVP0, TWI-1638). */
|
||||||
|
@GET("v1/eligibility/channels")
|
||||||
|
suspend fun getUserEligibilityChannels(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
): ApiResponse<TangemPayEligibilityChannels>
|
||||||
|
|
||||||
@GET("v1/order/{order_id}")
|
@GET("v1/order/{order_id}")
|
||||||
suspend fun getOrder(
|
suspend fun getOrder(
|
||||||
@Header("Authorization") authHeader: String,
|
@Header("Authorization") authHeader: String,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.datasource.api.pay.models.response
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response of `bff-v2/v1/account/bank-credentials/{product_instance_id}` — fiat bank requisites for the
|
||||||
|
* Virtual Account on-ramp (VA MVP0, TWI-1638).
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class BankCredentialsResponse(
|
||||||
|
@Json(name = "type") val type: String?,
|
||||||
|
@Json(name = "beneficiary_name") val beneficiaryName: String?,
|
||||||
|
@Json(name = "beneficiary_address") val beneficiaryAddress: String?,
|
||||||
|
@Json(name = "beneficiary_bank_name") val beneficiaryBankName: String?,
|
||||||
|
@Json(name = "beneficiary_bank_address") val beneficiaryBankAddress: String?,
|
||||||
|
@Json(name = "account_number") val accountNumber: String?,
|
||||||
|
@Json(name = "routing_number") val routingNumber: String?,
|
||||||
|
)
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.domain.quotes)
|
implementation(projects.domain.quotes)
|
||||||
implementation(projects.domain.common)
|
implementation(projects.domain.common)
|
||||||
implementation(projects.features.swap.domain)
|
implementation(projects.features.swap.domain)
|
||||||
|
implementation(projects.features.virtualAccounts.details.api)
|
||||||
|
|
||||||
|
|
||||||
/** Project - Utils */
|
/** 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.StatusSource
|
||||||
import com.tangem.domain.models.account.CardDisplayName
|
import com.tangem.domain.models.account.CardDisplayName
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
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.pay.*
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
|
|
@ -118,6 +119,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
error = null,
|
error = null,
|
||||||
|
virtualAccount = VirtualAccountOnramp.None,
|
||||||
)
|
)
|
||||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||||
source = StatusSource.CACHE,
|
source = StatusSource.CACHE,
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,16 @@ import arrow.core.Either
|
||||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||||
import com.tangem.domain.core.utils.catchOn
|
import com.tangem.domain.core.utils.catchOn
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.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.kyc.KycStatus
|
import com.tangem.domain.models.kyc.KycStatus
|
||||||
import com.tangem.domain.models.pay.TangemPayCard
|
import com.tangem.domain.models.pay.*
|
||||||
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.quote.QuoteStatus
|
import com.tangem.domain.models.quote.QuoteStatus
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.model.CustomerInfo
|
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.OrderData
|
||||||
import com.tangem.domain.pay.model.OrderStatus
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
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.SingleQuoteStatusProducer
|
||||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||||
import com.tangem.domain.visa.error.VisaApiError
|
import com.tangem.domain.visa.error.VisaApiError
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.security.DeviceSecurityInfoProvider
|
import com.tangem.security.DeviceSecurityInfoProvider
|
||||||
import com.tangem.security.isSecurityExposed
|
import com.tangem.security.isSecurityExposed
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
|
@ -66,6 +62,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
private val closeCardRepository: TangemPayCloseCardRepository,
|
private val closeCardRepository: TangemPayCloseCardRepository,
|
||||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||||
private val issueCardRepository: TangemPayIssueCardRepository,
|
private val issueCardRepository: TangemPayIssueCardRepository,
|
||||||
|
private val virtualAccountFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
) : PaymentAccountStatusFetcher {
|
) : PaymentAccountStatusFetcher {
|
||||||
|
|
||||||
private val logger = TangemLogger.withTag(TAG)
|
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.
|
// the previously shown order and append newly seen cards at the end.
|
||||||
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
||||||
|
|
||||||
|
val virtualAccount = resolveVirtualAccountOnramp(userWalletId)
|
||||||
|
|
||||||
return PaymentAccountStatusValue.Loaded(
|
return PaymentAccountStatusValue.Loaded(
|
||||||
source = StatusSource.ACTUAL,
|
source = StatusSource.ACTUAL,
|
||||||
customerId = customerId,
|
customerId = customerId,
|
||||||
|
|
@ -395,6 +394,50 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||||
availableForWithdrawal = availableForWithdrawal.orZero(),
|
availableForWithdrawal = availableForWithdrawal.orZero(),
|
||||||
),
|
),
|
||||||
error = null,
|
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 arrow.core.right
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||||
|
import com.tangem.data.pay.util.BankCredentialsConverter
|
||||||
import com.tangem.data.pay.util.CustomerInfoConverter
|
import com.tangem.data.pay.util.CustomerInfoConverter
|
||||||
import com.tangem.datasource.api.pay.TangemPayApi
|
import com.tangem.datasource.api.pay.TangemPayApi
|
||||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
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.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
|
import com.tangem.domain.models.account.BankCredentials
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||||
import com.tangem.domain.models.kyc.KycStatus
|
import com.tangem.domain.models.kyc.KycStatus
|
||||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
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 {
|
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
|
||||||
return tangemPayStorage.isTangemPayDeactivated(userWalletId)
|
return tangemPayStorage.isTangemPayDeactivated(userWalletId)
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +237,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
||||||
return tangemPayStorage.getTangemPayEligibility().map(TangemPayEligibilityType::fromString)
|
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 {
|
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
|
||||||
return tangemPayStorage.getHideMainOnboardingBanner(userWalletId)
|
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
|
||||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
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.domain.pay.model.CustomerInfo.ProductInstance.Status
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.extensions.orZero
|
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,
|
displayName = if (name != null) CardDisplayName(name).getOrElse { null } else null,
|
||||||
actualCardLimit = actualCardLimit?.parseCardLimit(),
|
actualCardLimit = actualCardLimit?.parseCardLimit(),
|
||||||
adminCardLimit = adminCardLimit?.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.CANCELED -> Status.CANCELED
|
||||||
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> Status.UNKNOWN
|
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.ApiConfig
|
||||||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
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.pay.TangemPayEligibilityType
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.model.CustomerInfo
|
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> =
|
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
|
||||||
real.getCustomerInfo(userWalletId)
|
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> {
|
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> {
|
||||||
if (isMockMode) {
|
if (isMockMode) {
|
||||||
mockOrderIds.add(userWalletId)
|
mockOrderIds.add(userWalletId)
|
||||||
|
|
@ -77,6 +83,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
||||||
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
|
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||||
real.getCustomerEligibility()
|
real.getCustomerEligibility()
|
||||||
|
|
||||||
|
override suspend fun fetchCustomerEligibility(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
): Either<VisaApiError, List<TangemPayEligibilityType>> = real.fetchCustomerEligibility(userWalletId)
|
||||||
|
|
||||||
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
|
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
|
||||||
real.getSavedCustomerInfo(userWalletId)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.domain.models.account
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank (fiat) credentials for a Virtual Account on-ramp — the wire/ACH requisites a user transfers funds to.
|
||||||
|
*
|
||||||
|
* Returned by `bff-v2/v1/account/bank-credentials/{product_instance_id}`. Sensitive data — kept transient
|
||||||
|
* (never persisted in the local payment-account cache).
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class BankCredentials(
|
||||||
|
val type: String,
|
||||||
|
val beneficiaryName: String,
|
||||||
|
val beneficiaryAddress: String,
|
||||||
|
val beneficiaryBankName: String,
|
||||||
|
val beneficiaryBankAddress: String,
|
||||||
|
val accountNumber: String,
|
||||||
|
val routingNumber: String,
|
||||||
|
)
|
||||||
|
|
@ -149,6 +149,8 @@ sealed class PaymentAccountStatusValue {
|
||||||
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
||||||
* @property error Transient error overlaid on top of cached data when a refresh fails
|
* @property error Transient error overlaid on top of cached data when a refresh fails
|
||||||
* (see [copySealed]), or `null` when the status is up to date. Not persisted.
|
* (see [copySealed]), or `null` when the status is up to date. Not persisted.
|
||||||
|
* @property virtualAccount Virtual Account (Visa on-ramp) availability — VA MVP0 (TWI-1638).
|
||||||
|
* Transient: not persisted in the local cache.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Loaded(
|
data class Loaded(
|
||||||
|
|
@ -160,6 +162,7 @@ sealed class PaymentAccountStatusValue {
|
||||||
val cards: List<TangemPayCard>,
|
val cards: List<TangemPayCard>,
|
||||||
val fiatRate: SerializedBigDecimal?,
|
val fiatRate: SerializedBigDecimal?,
|
||||||
val error: Error?,
|
val error: Error?,
|
||||||
|
val virtualAccount: VirtualAccountOnramp,
|
||||||
) : PaymentAccountStatusValue() {
|
) : PaymentAccountStatusValue() {
|
||||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||||
currency = cryptoCurrency,
|
currency = cryptoCurrency,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.tangem.domain.models.account
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual Account (Visa on-ramp) availability for a payment account — VA MVP0 (TWI-1638).
|
||||||
|
*
|
||||||
|
* Computed in the payment-account fetcher and surfaced on [PaymentAccountStatusValue.Loaded].
|
||||||
|
* Transient: [Available.bankCredentials] is never persisted in the local cache.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
sealed interface VirtualAccountOnramp {
|
||||||
|
|
||||||
|
/** On-ramp not applicable: feature toggle off, or wallet not eligible. */
|
||||||
|
@Serializable
|
||||||
|
data object None : VirtualAccountOnramp
|
||||||
|
|
||||||
|
/** No VA product instance yet, but the wallet is eligible to add funds (channel `VISA_VIRTUAL_ACCOUNT`). */
|
||||||
|
@Serializable
|
||||||
|
data object Eligible : VirtualAccountOnramp
|
||||||
|
|
||||||
|
/** VA product instance exists; [bankCredentials] are the fiat requisites for the bank-transfer top-up. */
|
||||||
|
@Serializable
|
||||||
|
data class Available(
|
||||||
|
val productInstanceId: String,
|
||||||
|
val bankCredentials: BankCredentials,
|
||||||
|
) : VirtualAccountOnramp
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ enum class TangemPayEligibilityType {
|
||||||
DETAILS_VIRTUAL_ACCOUNT,
|
DETAILS_VIRTUAL_ACCOUNT,
|
||||||
DEEPLINK_VIRTUAL_ACCOUNT,
|
DEEPLINK_VIRTUAL_ACCOUNT,
|
||||||
|
|
||||||
|
VISA_VIRTUAL_ACCOUNT,
|
||||||
|
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -21,6 +23,7 @@ enum class TangemPayEligibilityType {
|
||||||
"BANNER_VIRTUAL_ACCOUNT" -> BANNER_VIRTUAL_ACCOUNT
|
"BANNER_VIRTUAL_ACCOUNT" -> BANNER_VIRTUAL_ACCOUNT
|
||||||
"DETAILS_VIRTUAL_ACCOUNT" -> DETAILS_VIRTUAL_ACCOUNT
|
"DETAILS_VIRTUAL_ACCOUNT" -> DETAILS_VIRTUAL_ACCOUNT
|
||||||
"DEEPLINK_VIRTUAL_ACCOUNT" -> DEEPLINK_VIRTUAL_ACCOUNT
|
"DEEPLINK_VIRTUAL_ACCOUNT" -> DEEPLINK_VIRTUAL_ACCOUNT
|
||||||
|
"VISA_VIRTUAL_ACCOUNT" -> VISA_VIRTUAL_ACCOUNT
|
||||||
else -> UNKNOWN
|
else -> UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ data class CustomerInfo(
|
||||||
val actualCardLimit: TangemPayCardLimit?,
|
val actualCardLimit: TangemPayCardLimit?,
|
||||||
val adminCardLimit: TangemPayCardLimit?,
|
val adminCardLimit: TangemPayCardLimit?,
|
||||||
val status: Status,
|
val status: Status,
|
||||||
|
val specificationDataType: SpecificationDataType,
|
||||||
) {
|
) {
|
||||||
enum class Status {
|
enum class Status {
|
||||||
NEW,
|
NEW,
|
||||||
|
|
@ -82,6 +83,12 @@ data class CustomerInfo(
|
||||||
CANCELED,
|
CANCELED,
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `ACCOUNT` marks a Virtual Account instance (vs. a `CARD`); used by VA MVP0 (TWI-1638). */
|
||||||
|
enum class SpecificationDataType {
|
||||||
|
ACCOUNT,
|
||||||
|
CARD,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CardInfo(
|
data class CardInfo(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.core.error.UniversalError
|
import com.tangem.core.error.UniversalError
|
||||||
|
import com.tangem.domain.models.account.BankCredentials
|
||||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.model.CustomerInfo
|
import com.tangem.domain.pay.model.CustomerInfo
|
||||||
|
|
@ -17,6 +18,12 @@ interface OnboardingRepository {
|
||||||
|
|
||||||
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
|
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
|
||||||
|
|
||||||
|
/** Fiat bank requisites for the wallet's Virtual Account on-ramp instance (VA MVP0, TWI-1638). */
|
||||||
|
suspend fun getBankCredentials(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
productInstanceId: String,
|
||||||
|
): Either<VisaApiError, BankCredentials>
|
||||||
|
|
||||||
suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String>
|
suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String>
|
||||||
|
|
||||||
suspend fun clearOrderId(userWalletId: UserWalletId)
|
suspend fun clearOrderId(userWalletId: UserWalletId)
|
||||||
|
|
@ -28,6 +35,14 @@ interface OnboardingRepository {
|
||||||
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
|
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>
|
||||||
suspend fun getCustomerEligibility(): List<TangemPayEligibilityType>
|
suspend fun getCustomerEligibility(): List<TangemPayEligibilityType>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches eligibility channels fresh via the user token (always hits the network, no cache read/write).
|
||||||
|
* Differs from [checkCustomerEligibility] (static token, caches) and [getCustomerEligibility] (cache only).
|
||||||
|
*/
|
||||||
|
suspend fun fetchCustomerEligibility(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
): Either<VisaApiError, List<TangemPayEligibilityType>>
|
||||||
|
|
||||||
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
|
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
|
||||||
|
|
||||||
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
|
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue