Updated on 2026-08-14
This commit is contained in:
parent
71507af6c3
commit
589805424e
9 changed files with 129 additions and 66 deletions
|
|
@ -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,52 +149,102 @@ 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)
|
||||
-> {
|
||||
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.CANCELED -> {
|
||||
onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
|
||||
.fold(
|
||||
ifLeft = {
|
||||
PaymentAccountStatusValue.Error.CardIssueFailed(
|
||||
customerId = orderData.customerId,
|
||||
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 = { customerInfo ->
|
||||
if (customerInfo.kycStatus == KycStatus.REJECTED) {
|
||||
customerInfo.mapToPaymentAccountStatus()
|
||||
} else {
|
||||
PaymentAccountStatusValue.Error.CardIssueFailed(
|
||||
customerId = orderData.customerId,
|
||||
)
|
||||
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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
OrderStatus.COMPLETED -> {
|
||||
// Order was completed -> clear order id and get customer info
|
||||
|
||||
return PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
|
||||
private suspend fun handleCanceledOrder(
|
||||
account: Account.Payment,
|
||||
orderData: OrderData,
|
||||
): PaymentAccountStatusValue {
|
||||
onboardingRepository.clearOrderId(account.userWalletId)
|
||||
onboardingRepository.getCustomerInfo(userWalletId = 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() },
|
||||
)
|
||||
}
|
||||
OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
|
||||
val cardInfo = this.cardInfo
|
||||
|
|
|
|||
|
|
@ -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<Account.CryptoPortfolio>().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<Error, AccountList> = either {
|
||||
ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList }
|
||||
|
||||
ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
|
||||
val paymentAccounts = accounts.filterIsInstance<Account.Payment>()
|
||||
ensure(paymentAccounts.size <= MAX_PAYMENT_ACCOUNTS_COUNT) { Error.ExceedsMaxPaymentAccountsCount }
|
||||
|
||||
val cryptoAccounts = accounts.filterIsInstance<Account.CryptoPortfolio>()
|
||||
ensure(cryptoAccounts.size <= MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
|
||||
|
||||
val mainAccountsCount = accounts.mainAccountsCount()
|
||||
ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -160,33 +160,30 @@ class WalletBalanceFetcher internal constructor(
|
|||
paymentAccountRefactorEnabled: Boolean,
|
||||
) {
|
||||
coroutineScope {
|
||||
val errorDeferreds = fetchingSources.map { source ->
|
||||
// Fetch balance sources in parallel
|
||||
val balanceErrors = fetchingSources.filterIsInstance<WalletFetchingSource.Balance>()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.fold(emptyMap<String, Throwable>()) { acc, map -> acc + map }
|
||||
|
||||
val errors = errorDeferreds.awaitAll().fold(emptyMap<String, Throwable>()) { 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue