Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 15:35:19 +04:00
commit a3216e8e23
57 changed files with 532 additions and 468 deletions

View file

@ -4,6 +4,7 @@ import arrow.core.Option
import arrow.core.some
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
import com.tangem.domain.core.flow.FlowProducerTools
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.*
* @property params params
* @property flowProducerTools tools for producing flows
* @property userWalletsListRepository repository for getting user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property singleAccountListSupplier supplier for getting [AccountList] per wallet
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
@ -31,7 +32,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
@Assisted val params: Unit,
override val flowProducerTools: FlowProducerTools,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiAccountListProducer {
@ -44,7 +45,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
.distinctUntilChanged()
.flatMapLatest { ids ->
combine(
flows = ids.map(walletAccountListFlowFactory::create),
flows = ids.map(singleAccountListSupplier::invoke),
transform = ::listOf,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.TokensSortType
@ -28,14 +29,14 @@ import org.junit.jupiter.api.TestInstance
class DefaultMultiAccountListProducerTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
private val flowProducerTools: FlowProducerTools = mockk()
private val producer = DefaultMultiAccountListProducer(
params = Unit,
flowProducerTools = flowProducerTools,
userWalletsListRepository = userWalletsListRepository,
walletAccountListFlowFactory = walletAccountListFlowFactory,
singleAccountListSupplier = singleAccountListSupplier,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -46,7 +47,7 @@ class DefaultMultiAccountListProducerTest {
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsListRepository, walletAccountListFlowFactory)
clearMocks(userWalletsListRepository, singleAccountListSupplier)
}
@Test
@ -56,7 +57,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -68,7 +69,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -82,7 +83,7 @@ class DefaultMultiAccountListProducerTest {
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -101,10 +102,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -117,7 +118,7 @@ class DefaultMultiAccountListProducerTest {
val accountList = AccountList.empty(userWalletId)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -136,10 +137,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -151,7 +152,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWalletId) } throws exception
every { singleAccountListSupplier.invoke(userWalletId) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
@ -163,7 +164,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -183,7 +184,7 @@ class DefaultMultiAccountListProducerTest {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
coVerify(inverse = true) { singleAccountListSupplier.invoke(any<UserWalletId>()) }
}
@Test
@ -192,7 +193,7 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -203,7 +204,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -219,8 +220,8 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId2) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -231,8 +232,8 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId2)
singleAccountListSupplier.invoke(userWalletId)
singleAccountListSupplier.invoke(userWalletId2)
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.wallet.UserWalletId
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
@ -21,7 +22,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"
@ -147,53 +152,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(account.userWalletId)
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(account.userWalletId)
} 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 = { it.mapToPaymentAccountStatus(account.userWalletId) },
)
}
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(account.userWalletId) },
)
}
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val cardInfo = this.cardInfo
val productInstance = this.productInstance