Updated on 2026-08-14
This commit is contained in:
commit
a3216e8e23
57 changed files with 532 additions and 468 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue