Updated on 2026-08-14
This commit is contained in:
parent
50a384f13d
commit
457e8870a4
7 changed files with 165 additions and 10 deletions
|
|
@ -604,13 +604,25 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private suspend fun buildIssuingCards(userWalletId: UserWalletId): List<TangemPayCard> {
|
||||
val orderIds = issueCardRepository.getIssueOrderIds(userWalletId)
|
||||
return orderIds.mapNotNull { orderId ->
|
||||
val order = cardDetailsRepository.getOrderInfo(userWalletId, orderId).getOrNull()
|
||||
if (order != null && order.orderStatus.isTerminal) {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, orderId).fold(
|
||||
ifLeft = { error ->
|
||||
if (error == VisaApiError.OrderNotFound) {
|
||||
logger.i("buildIssuingCards $userWalletId: dropping missing order $orderId")
|
||||
issueCardRepository.removeIssueOrderId(userWalletId, orderId)
|
||||
null
|
||||
} else {
|
||||
issuingPlaceholderCard(orderId)
|
||||
}
|
||||
},
|
||||
ifRight = { order ->
|
||||
if (order.orderStatus.isTerminal) {
|
||||
issueCardRepository.removeIssueOrderId(userWalletId, orderId)
|
||||
null
|
||||
} else {
|
||||
issuingPlaceholderCard(orderId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -308,6 +308,8 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
): Either<UniversalError, TangemPayOrderInfo> = either {
|
||||
val order = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getOrder(authHeader, orderId)
|
||||
}.mapLeft { error ->
|
||||
if (error is VisaApiError.NotFound) VisaApiError.OrderNotFound else error
|
||||
}.bind()
|
||||
|
||||
val result = order.result ?: raise(VisaApiError.Unspecified)
|
||||
|
|
|
|||
|
|
@ -769,5 +769,29 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
assertThat(loaded.cards).hasSize(1)
|
||||
assertThat(loaded.cards.single().state).isEqualTo(TangemPayCardState.Issuing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN local issue order missing on backend WHEN invoke THEN placeholder dropped and order forgotten`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val customerInfo = buildCustomerInfo()
|
||||
stubHappyPath(customerInfo)
|
||||
every { tangemPayFeatureToggles.isTiersPlusPlanEnabled } returns true
|
||||
every { virtualAccountFeatureToggles.isVaMvp0Enabled } returns false
|
||||
coEvery { issueCardRepository.getIssueOrderIds(userWalletId) } returns listOf("order_gone")
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, "order_gone")
|
||||
} returns VisaApiError.OrderNotFound.left()
|
||||
coEvery { issueCardRepository.removeIssueOrderId(userWalletId, "order_gone") } just Runs
|
||||
val storedStatuses = captureStoredStatuses()
|
||||
|
||||
// WHEN
|
||||
fetcher.invoke(params)
|
||||
|
||||
// THEN
|
||||
val loaded = storedStatuses.lastLoaded()
|
||||
assertThat(loaded.cards.map { it.state }).doesNotContain(TangemPayCardState.Issuing)
|
||||
coVerify(exactly = 1) { issueCardRepository.removeIssueOrderId(userWalletId, "order_gone") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
|||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
|
@ -13,6 +15,8 @@ class StartTangemPayOrderPollingUseCase(
|
|||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
) {
|
||||
|
||||
private val logger = TangemLogger.withTag("StartTangemPayOrderPollingUseCase")
|
||||
|
||||
/**
|
||||
* Order keys (`walletId:orderId`) currently being polled. Keeps polling idempotent so callers that
|
||||
* may fire repeatedly for the same order (e.g. order restore on every wallet (re)load) never spawn a
|
||||
|
|
@ -36,7 +40,7 @@ class StartTangemPayOrderPollingUseCase(
|
|||
val newOrder = if (order.orderStatus.isTerminal) {
|
||||
order
|
||||
} else {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull()
|
||||
getOrderInfo(userWalletId = userWalletId, orderId = order.orderId)
|
||||
}
|
||||
|
||||
if (newOrder != null && newOrder != currentOrder) {
|
||||
|
|
@ -56,7 +60,22 @@ class StartTangemPayOrderPollingUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrderInfo(userWalletId: UserWalletId, orderId: String): TangemPayOrderInfo? {
|
||||
return cardDetailsRepository.getOrderInfo(userWalletId, orderId).fold(
|
||||
ifLeft = { error ->
|
||||
if (error == VisaApiError.OrderNotFound) {
|
||||
logger.i("$orderId: does not exist on the backend, resolving as CANCELED")
|
||||
TangemPayOrderInfo(orderId = orderId, orderStatus = OrderStatus.CANCELED)
|
||||
} else {
|
||||
logger.e("$orderId: poll failed, will retry — $error")
|
||||
null
|
||||
}
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val POLLING_DELAY = 3000L
|
||||
private const val POLLING_DELAY = 5_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -230,6 +230,65 @@ internal class StartTangemPayOrderPollingUseCaseTest {
|
|||
firstPoller.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN processing order WHEN backend reports order not found THEN resolves as CANCELED and stops`() = runTest {
|
||||
// GIVEN
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
val changes = mutableListOf<TangemPayOrderInfo>()
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returns VisaApiError.OrderNotFound.left()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(order, USER_WALLET_ID, onOrderStateChange = { changes.add(it) })
|
||||
|
||||
// THEN
|
||||
assertThat(result).isFalse()
|
||||
assertThat(changes).containsExactly(TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED))
|
||||
coVerify(exactly = 1) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN long outage WHEN backend recovers THEN order is still polled to terminal`() = runTest {
|
||||
// GIVEN
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
val outage = List(size = 100) { VisaApiError.ServerUnavailable.left() }
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returnsMany outage + TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
coVerify(exactly = 101) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-terminal order WHEN polling THEN polls at a flat 5s cadence`() = runTest {
|
||||
// GIVEN
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returnsMany listOf(
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW).right(),
|
||||
VisaApiError.ServerUnavailable.left(),
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(),
|
||||
)
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isTrue()
|
||||
assertThat(testScheduler.currentTime).isEqualTo(10_000L)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val ORDER_ID = "order-test-1"
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import com.tangem.features.tangempay.details.impl.R
|
|||
import com.tangem.features.tangempay.entity.*
|
||||
import com.tangem.features.tangempay.model.transformers.*
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanSource
|
||||
import com.tangem.features.tangempay.utils.*
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsEvent
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
|
||||
|
|
@ -151,6 +152,12 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
is PaymentAccountStatusValue.Inactive -> uiState.update {
|
||||
stateFactory.getInactiveState(state)
|
||||
}
|
||||
is PaymentAccountStatusValue.AwaitingPlanSelection -> router.replaceAll(
|
||||
TangemPayAccountDetailsInnerRoute.SelectPlan(
|
||||
tariffPlan = state.tariffPlan,
|
||||
source = TangemPaySelectPlanSource.TIERS_ONBOARDING,
|
||||
),
|
||||
)
|
||||
else -> uiState.update { stateFactory.getLoadingState() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import arrow.core.right
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
|
||||
|
|
@ -18,6 +20,8 @@ import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
|||
import com.tangem.features.tangempay.addFundsButton
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanSource
|
||||
import com.tangem.features.tangempay.tangemPayCard
|
||||
import com.tangem.features.tangempay.withdrawButton
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
|
|
@ -44,6 +48,7 @@ internal class TangemPayDetailsModelTest {
|
|||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxed = true)
|
||||
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideMutedCases")
|
||||
|
|
@ -114,6 +119,33 @@ internal class TangemPayDetailsModelTest {
|
|||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN awaiting plan selection WHEN model created THEN inner stack is replaced with plan selection`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
val tariffPlan: TangemPayCustomerTariffPlan = mockk(relaxed = true)
|
||||
val awaitingPlanSelection = PaymentAccountStatusValue.AwaitingPlanSelection(
|
||||
source = StatusSource.ACTUAL,
|
||||
tariffPlan = tariffPlan,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val model = createModel(testScope = this, statusValue = awaitingPlanSelection)
|
||||
advanceUntilIdle()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) {
|
||||
router.replaceAll(
|
||||
TangemPayAccountDetailsInnerRoute.SelectPlan(
|
||||
tariffPlan = tariffPlan,
|
||||
source = TangemPaySelectPlanSource.TIERS_ONBOARDING,
|
||||
),
|
||||
)
|
||||
}
|
||||
verify(exactly = 0) { router.push(any(), any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun createModel(
|
||||
testScope: TestScope,
|
||||
statusSource: StatusSource = StatusSource.ACTUAL,
|
||||
|
|
@ -164,7 +196,7 @@ internal class TangemPayDetailsModelTest {
|
|||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
analytics = analytics,
|
||||
router = mockk(relaxed = true),
|
||||
router = router,
|
||||
urlOpener = mockk(relaxed = true),
|
||||
cardDetailsRepository = cardDetailsRepository,
|
||||
getBalanceHidingSettingsUseCase = mockk(relaxed = true),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue