Updated on 2026-08-14

This commit is contained in:
Tangem 2026-08-04 02:56:26 -07:00
parent 50a384f13d
commit 457e8870a4
7 changed files with 165 additions and 10 deletions

View file

@ -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
}
}

View file

@ -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"