From e06dc1297dcc98f3b5ce0bc65ec6246736e5083a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jun 2026 18:24:12 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 13 ++++ .../response/BankCredentialsResponse.kt | 19 ++++++ data/visa/build.gradle.kts | 1 + .../PaymentAccountStatusValueDMConverter.kt | 2 + .../DefaultPaymentAccountStatusFetcher.kt | 59 +++++++++++++--- .../repository/DefaultOnboardingRepository.kt | 21 ++++++ .../data/pay/util/BankCredentialsConverter.kt | 19 ++++++ .../data/pay/util/CustomerInfoConverter.kt | 8 +++ .../MockAwareOnboardingRepository.kt | 10 +++ .../pay/util/BankCredentialsConverterTest.kt | 67 +++++++++++++++++++ .../domain/models/account/BankCredentials.kt | 20 ++++++ .../account/PaymentAccountStatusValue.kt | 3 + .../models/account/VirtualAccountOnramp.kt | 28 ++++++++ .../models/pay/TangemPayEligibilityType.kt | 3 + .../tangem/domain/pay/model/CustomerInfo.kt | 7 ++ .../pay/repository/OnboardingRepository.kt | 15 +++++ 16 files changed, 287 insertions(+), 8 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BankCredentialsResponse.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/util/BankCredentialsConverter.kt create mode 100644 data/visa/src/test/kotlin/com/tangem/data/pay/util/BankCredentialsConverterTest.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/BankCredentials.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/VirtualAccountOnramp.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 80fcf19f81..37f8928d79 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -23,6 +23,13 @@ interface TangemPayApi { @GET("v1/customer/me") suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse + /** 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 + @GET("v1/customer/wallets/{customer_wallet_id}") suspend fun checkCustomerWalletId( @Path("customer_wallet_id") customerWalletId: String, @@ -40,6 +47,12 @@ interface TangemPayApi { @GET("v1/eligibility/channels") suspend fun getEligibilityChannels(): ApiResponse + /** 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 + @GET("v1/order/{order_id}") suspend fun getOrder( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BankCredentialsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BankCredentialsResponse.kt new file mode 100644 index 0000000000..409984fabc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/BankCredentialsResponse.kt @@ -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?, +) \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 2cc54a86b8..420ab0b3d2 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -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 */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 3b7fe7face..55b9d892dd 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -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, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 0c90bc8ef9..e9df89acad 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -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 + } + }, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 932e7041bc..786fc9397c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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 { + 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> { + 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) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/BankCredentialsConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/BankCredentialsConverter.kt new file mode 100644 index 0000000000..4f027487a6 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/BankCredentialsConverter.kt @@ -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 { + 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(), + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt index cc7ac9f9a8..2fd76d8480 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt @@ -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 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 + } } \ No newline at end of file diff --git a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt index 38ccbe554a..14dcad9d79 100644 --- a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt +++ b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt @@ -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 = real.getCustomerInfo(userWalletId) + override suspend fun getBankCredentials( + userWalletId: UserWalletId, + productInstanceId: String, + ): Either = real.getBankCredentials(userWalletId, productInstanceId) + override suspend fun createOrder(userWalletId: UserWalletId): Either { if (isMockMode) { mockOrderIds.add(userWalletId) @@ -77,6 +83,10 @@ internal class MockAwareOnboardingRepository @Inject constructor( override suspend fun getCustomerEligibility(): List = real.getCustomerEligibility() + override suspend fun fetchCustomerEligibility( + userWalletId: UserWalletId, + ): Either> = real.fetchCustomerEligibility(userWalletId) + override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? = real.getSavedCustomerInfo(userWalletId) diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/util/BankCredentialsConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/util/BankCredentialsConverterTest.kt new file mode 100644 index 0000000000..6ec1cceb6c --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/util/BankCredentialsConverterTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/BankCredentials.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/BankCredentials.kt new file mode 100644 index 0000000000..adbeacdb40 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/BankCredentials.kt @@ -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, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index d32bc79f81..e9699e8118 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -149,6 +149,8 @@ sealed class PaymentAccountStatusValue { * [totalFiatBalance] resolves to [TotalFiatBalance.Failed]. * @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. + * @property virtualAccount Virtual Account (Visa on-ramp) availability — VA MVP0 (TWI-1638). + * Transient: not persisted in the local cache. */ @Serializable data class Loaded( @@ -160,6 +162,7 @@ sealed class PaymentAccountStatusValue { val cards: List, val fiatRate: SerializedBigDecimal?, val error: Error?, + val virtualAccount: VirtualAccountOnramp, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/VirtualAccountOnramp.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/VirtualAccountOnramp.kt new file mode 100644 index 0000000000..2b504b4c0f --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/VirtualAccountOnramp.kt @@ -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 +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayEligibilityType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayEligibilityType.kt index 8c2bf08afa..2bac433902 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayEligibilityType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayEligibilityType.kt @@ -10,6 +10,8 @@ enum class TangemPayEligibilityType { DETAILS_VIRTUAL_ACCOUNT, DEEPLINK_VIRTUAL_ACCOUNT, + VISA_VIRTUAL_ACCOUNT, + UNKNOWN, ; @@ -21,6 +23,7 @@ enum class TangemPayEligibilityType { "BANNER_VIRTUAL_ACCOUNT" -> BANNER_VIRTUAL_ACCOUNT "DETAILS_VIRTUAL_ACCOUNT" -> DETAILS_VIRTUAL_ACCOUNT "DEEPLINK_VIRTUAL_ACCOUNT" -> DEEPLINK_VIRTUAL_ACCOUNT + "VISA_VIRTUAL_ACCOUNT" -> VISA_VIRTUAL_ACCOUNT else -> UNKNOWN } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 2b96bd312f..752773bb5f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -67,6 +67,7 @@ data class CustomerInfo( val actualCardLimit: TangemPayCardLimit?, val adminCardLimit: TangemPayCardLimit?, val status: Status, + val specificationDataType: SpecificationDataType, ) { enum class Status { NEW, @@ -82,6 +83,12 @@ data class CustomerInfo( CANCELED, UNKNOWN, } + + /** `ACCOUNT` marks a Virtual Account instance (vs. a `CARD`); used by VA MVP0 (TWI-1638). */ + enum class SpecificationDataType { + ACCOUNT, + CARD, + } } data class CardInfo( diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index bce59b45c7..169c6ed172 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either 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.wallet.UserWalletId import com.tangem.domain.pay.model.CustomerInfo @@ -17,6 +18,12 @@ interface OnboardingRepository { suspend fun getCustomerInfo(userWalletId: UserWalletId): Either + /** Fiat bank requisites for the wallet's Virtual Account on-ramp instance (VA MVP0, TWI-1638). */ + suspend fun getBankCredentials( + userWalletId: UserWalletId, + productInstanceId: String, + ): Either + suspend fun createOrder(userWalletId: UserWalletId): Either suspend fun clearOrderId(userWalletId: UserWalletId) @@ -28,6 +35,14 @@ interface OnboardingRepository { suspend fun checkCustomerEligibility(): List suspend fun getCustomerEligibility(): List + /** + * 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> + fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean