From 589805424e910206a290e5ad8bb3c8d0fcf3d730 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 16:07:12 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultPaymentAccountStatusFetcher.kt | 119 +++++++++++++----- .../domain/account/models/AccountList.kt | 18 ++- .../domain/account/models/AccountListTest.kt | 4 +- .../usecase/RecoverCryptoPortfolioUseCase.kt | 2 +- .../tokens/wallet/WalletBalanceFetcher.kt | 39 +++--- .../tokens/wallet/WalletFetchingSource.kt | 2 +- .../viewmodel/TesterAccountsViewModel.kt | 4 +- .../utils/AccountItemsDelegate.kt | 4 +- ...TangemPayHideOnboardingStateTransformer.kt | 3 +- 9 files changed, 129 insertions(+), 66 deletions(-) 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 cdd78706de..2802bbed8c 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 @@ -10,6 +10,7 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo +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.pay.repository.OnboardingRepository @@ -19,7 +20,11 @@ import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes private const val TAG = "PaymentAccountStatusFetcher" @@ -144,53 +149,103 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue { + // Step 1: Check KYC status first + val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold( + ifLeft = { error -> + logger.e("proceedWithOrderId KYC check ${account.userWalletId} error: $error") + return error.mapToPaymentAccountStatus() + }, + ifRight = { it }, + ) + + logger.i("proceedWithOrderId ${account.userWalletId} kycStatus: ${customerInfo.kycStatus}") + + when (customerInfo.kycStatus) { + KycStatus.PENDING, + KycStatus.INIT, + KycStatus.REJECTED, + -> return customerInfo.mapToPaymentAccountStatus() + KycStatus.APPROVED -> Unit // proceed to order check + } + + // Step 2: Check order status return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold( ifLeft = { error -> logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { orderData -> - logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}") + logger.i("proceedWithOrderId ${account.userWalletId}: $orderId status: ${orderData.status}") when (orderData.status) { - // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.CANCELED -> handleCanceledOrder(account, orderData) + OrderStatus.COMPLETED -> handleCompletedOrder(account) + OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable OrderStatus.NEW, OrderStatus.PROCESSING, - -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) - - OrderStatus.CANCELED -> { - onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) - .fold( - ifLeft = { - PaymentAccountStatusValue.Error.CardIssueFailed( - customerId = orderData.customerId, - ) - }, - ifRight = { customerInfo -> - if (customerInfo.kycStatus == KycStatus.REJECTED) { - customerInfo.mapToPaymentAccountStatus() - } else { - PaymentAccountStatusValue.Error.CardIssueFailed( - customerId = orderData.customerId, - ) - } - }, - ) + -> { + paymentAccountStatusesStore.store( + userWalletId = account.userWalletId, + status = AccountStatus.Payment( + account = account, + value = PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL), + ), + ) + // Start polling for terminal state + pollOrderStatus(account = account, orderId = orderId) } - OrderStatus.COMPLETED -> { - // Order was completed -> clear order id and get customer info - onboardingRepository.clearOrderId(account.userWalletId) - onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) - .fold( - ifLeft = { it.mapToPaymentAccountStatus() }, - ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, - ) - } - OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable } }, ) } + private suspend fun pollOrderStatus(account: Account.Payment, orderId: String): PaymentAccountStatusValue { + while (currentCoroutineContext().isActive) { + delay(1.minutes) + + val result = customerOrderRepository.getOrderData( + userWalletId = account.userWalletId, + orderId = orderId, + ) + + result.fold( + ifLeft = { error -> + logger.e("pollOrderStatus ${account.userWalletId} orderId: $orderId error: $error") + // Continue polling on transient errors + }, + ifRight = { orderData -> + logger.i("pollOrderStatus ${account.userWalletId}: $orderId status: ${orderData.status}") + when (orderData.status) { + OrderStatus.CANCELED -> return handleCanceledOrder(account, orderData) + OrderStatus.COMPLETED -> return handleCompletedOrder(account) + OrderStatus.NEW, + OrderStatus.PROCESSING, + OrderStatus.UNKNOWN, + -> Unit // Continue polling + } + }, + ) + } + + return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) + } + + private suspend fun handleCanceledOrder( + account: Account.Payment, + orderData: OrderData, + ): PaymentAccountStatusValue { + onboardingRepository.clearOrderId(account.userWalletId) + return PaymentAccountStatusValue.Error.CardIssueFailed(orderData.customerId) + } + + private suspend fun handleCompletedOrder(account: Account.Payment): PaymentAccountStatusValue { + onboardingRepository.clearOrderId(account.userWalletId) + return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) + .fold( + ifLeft = { it.mapToPaymentAccountStatus() }, + ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ) + } + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index f4ecc26395..9775180441 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -41,8 +41,8 @@ data class AccountList private constructor( get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio /** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */ - val canAddMoreAccounts: Boolean - get() = accounts.size < MAX_ACCOUNTS_COUNT + val canAddMoreCryptoAccounts: Boolean + get() = accounts.filterIsInstance().size < MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT /** Returns the number of active accounts in the list */ val activeAccounts: Int @@ -151,6 +151,11 @@ data class AccountList private constructor( override fun toString(): String = "$tag: The number of accounts must not exceed 20" } + data object ExceedsMaxPaymentAccountsCount : Error { + override fun toString(): String = + "$tag: The number of payment accounts must not exceed $MAX_PAYMENT_ACCOUNTS_COUNT" + } + @Serializable data object DuplicateAccountIds : Error { override fun toString(): String = "$tag: Account list contains duplicate account IDs" @@ -169,7 +174,8 @@ data class AccountList private constructor( companion object { - const val MAX_ACCOUNTS_COUNT = 20 + const val MAX_PAYMENT_ACCOUNTS_COUNT = 1 + const val MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT = 20 const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000 private const val MAX_MAIN_ACCOUNTS_COUNT = 1 @@ -191,7 +197,11 @@ data class AccountList private constructor( ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } - ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount } + val paymentAccounts = accounts.filterIsInstance() + ensure(paymentAccounts.size <= MAX_PAYMENT_ACCOUNTS_COUNT) { Error.ExceedsMaxPaymentAccountsCount } + + val cryptoAccounts = accounts.filterIsInstance() + ensure(cryptoAccounts.size <= MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount } val mainAccountsCount = accounts.mainAccountsCount() ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) { diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index a242546b21..2457408d52 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -45,8 +45,8 @@ internal class AccountListTest { val fullAccountList = MockAccounts.fullAccountList // Act & Assert - Truth.assertThat(accountList.canAddMoreAccounts).isTrue() - Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse() + Truth.assertThat(accountList.canAddMoreCryptoAccounts).isTrue() + Truth.assertThat(fullAccountList.canAddMoreCryptoAccounts).isFalse() } @Test diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index 36a47dc929..aa119597d6 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -46,7 +46,7 @@ class RecoverCryptoPortfolioUseCase( val accountList = getAccountList(userWalletId = accountId.userWalletId) - ensure(accountList.canAddMoreAccounts) { + ensure(accountList.canAddMoreCryptoAccounts) { raise(Error.AccountListRequirementsNotMet(cause = AccountList.Error.ExceedsMaxAccountsCount)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 27a9af10ba..7d6d8f4051 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -160,33 +160,30 @@ class WalletBalanceFetcher internal constructor( paymentAccountRefactorEnabled: Boolean, ) { coroutineScope { - val errorDeferreds = fetchingSources.map { source -> - async { - when (source) { - is WalletFetchingSource.Balance -> { - balanceFetchingOperations.fetchAll( - userWalletId = userWalletId, - currencies = currencies, - sources = source.sources, - ).mapKeys { (fetchingSource, _) -> fetchingSource.name } - } - is WalletFetchingSource.TangemPay -> { - fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled) - .leftOrNull() - ?.let { error -> mapOf(FetchErrorFormatter.TANGEM_PAY_SOURCE_NAME to error) } - .orEmpty() - } + // Fetch balance sources in parallel + val balanceErrors = fetchingSources.filterIsInstance() + .map { source -> + async { + balanceFetchingOperations.fetchAll( + userWalletId = userWalletId, + currencies = currencies, + sources = source.sources, + ).mapKeys { (fetchingSource, _) -> fetchingSource.name } } } - } + .awaitAll() + .fold(emptyMap()) { acc, map -> acc + map } - val errors = errorDeferreds.awaitAll().fold(emptyMap()) { acc, map -> acc + map } - - check(errors.isEmpty()) { - val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors) + check(balanceErrors.isEmpty()) { + val message = FetchErrorFormatter.formatWalletErrors(userWalletId, balanceErrors) TangemLogger.e(message) message } + + // Fetch TangemPay separately — may run long-polling, so it must not block balance error checking + if (fetchingSources.any { it is WalletFetchingSource.TangemPay }) { + fetchPaymentAccount(userWalletId, paymentAccountRefactorEnabled) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt index 69a2e52b58..56d00faf1e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletFetchingSource.kt @@ -14,7 +14,7 @@ sealed class WalletFetchingSource { /** * TangemPay account fetching source. - * Handled separately from standard balance sources via [PaymentAccountStatusFetcher]. + * Handled separately from standard balance sources via [com.tangem.domain.pay.flow.PaymentAccountStatusFetcher]. */ data object TangemPay : WalletFetchingSource() diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 2bc39f10d0..df07d56fe8 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -204,7 +204,7 @@ internal class TesterAccountsViewModel @Inject constructor( var nextIndex = accountList.totalAccounts @Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu - while (accountList.canAddMoreAccounts) { + while (accountList.canAddMoreCryptoAccounts) { val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break val newAccount = Account.CryptoPortfolio.invoke( @@ -246,7 +246,7 @@ internal class TesterAccountsViewModel @Inject constructor( withContext(dispatchers.default) { val updatedAccountList = AccountList.invoke( userWalletId = accountList.userWalletId, - accounts = if (possibleToArchive > AccountList.MAX_ACCOUNTS_COUNT - 1) { + accounts = if (possibleToArchive > AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT - 1) { listOf(accountList.mainAccount) } else { accountList.accounts.subList(fromIndex = 0, toIndex = accountList.accounts.size - possibleToArchive) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index 56be825aca..c53c4fab07 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -97,7 +97,7 @@ internal class AccountItemsDelegate @Inject constructor( add(header) addAll(accounts.map(::mapAccount).applySortingOrder(order = accountsOrder)) - val isAddAccountEnabled = accounts.size < AccountList.MAX_ACCOUNTS_COUNT + val isAddAccountEnabled = accounts.size < AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT val shouldShowDescription = accounts.size > 1 val isArchivedAccountsEnabled = accountStatusList.accountStatuses.size != accountStatusList.totalAccounts @@ -165,7 +165,7 @@ internal class AccountItemsDelegate @Inject constructor( title = resourceReference(R.string.account_add_limit_dialog_title), message = resourceReference( id = R.string.account_add_limit_dialog_description, - formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()), + formatArgs = wrappedList(AccountList.MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT.toString()), ), firstActionBuilder = { firstAction }, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt index 8c1f641c38..9997dc0bcb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import com.tangem.features.tangempay.entity.TangemPayMainUM internal class TangemPayHideOnboardingStateTransformer( userWalletId: UserWalletId, @@ -11,7 +12,7 @@ internal class TangemPayHideOnboardingStateTransformer( override fun transform(prevState: WalletState): WalletState { return if (prevState is WalletState.MultiCurrency.Content) { - prevState.copy(tangemPayState = TangemPayState.Empty) + prevState.copy(tangemPayState = TangemPayState.Empty, tangemPayMainUM = TangemPayMainUM.Empty) } else { prevState }