Updated on 2026-08-14
This commit is contained in:
parent
82bbe7ef8e
commit
8b06c1c5c7
10 changed files with 114 additions and 27 deletions
|
|
@ -24,6 +24,7 @@ import com.tangem.data.pay.store.TangemPayStorage
|
||||||
import com.tangem.domain.models.account.CardDisplayName
|
import com.tangem.domain.models.account.CardDisplayName
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.model.OrderStatus
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
|
import com.tangem.domain.pay.model.OrderStep
|
||||||
import com.tangem.domain.pay.model.SetPinResult
|
import com.tangem.domain.pay.model.SetPinResult
|
||||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||||
|
|
@ -319,6 +320,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
||||||
Status.COMPLETED -> OrderStatus.COMPLETED
|
Status.COMPLETED -> OrderStatus.COMPLETED
|
||||||
Status.CANCELED -> OrderStatus.CANCELED
|
Status.CANCELED -> OrderStatus.CANCELED
|
||||||
},
|
},
|
||||||
|
orderStep = OrderStep.fromString(result.step),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,13 @@ package com.tangem.domain.pay.model
|
||||||
data class TangemPayOrderInfo(
|
data class TangemPayOrderInfo(
|
||||||
val orderId: String,
|
val orderId: String,
|
||||||
val orderStatus: OrderStatus,
|
val orderStatus: OrderStatus,
|
||||||
)
|
val orderStep: OrderStep = OrderStep.UNKNOWN,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromOrder(order: Order) = TangemPayOrderInfo(
|
||||||
|
orderId = order.id,
|
||||||
|
orderStatus = order.status,
|
||||||
|
orderStep = order.step,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,10 @@ class CancelTangemPayOrderUseCase(
|
||||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||||
|
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = TangemPayOrderInfo(orderId = orderId, orderStatus = OrderStatus.PROCESSING),
|
order = TangemPayOrderInfo(
|
||||||
|
orderId = orderId,
|
||||||
|
orderStatus = OrderStatus.PROCESSING,
|
||||||
|
),
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ class CloseTangemPayCardUseCase(
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = order,
|
order = order,
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
onTerminalReached = { closeCardRepository.removeCloseOrderId(cardId) },
|
onOrderStateChange = { newOrder ->
|
||||||
|
if (newOrder.orderStatus.isTerminal) {
|
||||||
|
closeCardRepository.removeCloseOrderId(cardId)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.model.OrderStatus
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
|
import com.tangem.domain.pay.model.OrderStep
|
||||||
import com.tangem.domain.pay.model.OrderType
|
import com.tangem.domain.pay.model.OrderType
|
||||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||||
|
|
@ -54,9 +55,16 @@ class CreateTariffPlanTransitionOrderUseCase(
|
||||||
|
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status),
|
order = TangemPayOrderInfo.fromOrder(order),
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) },
|
onOrderStateChange = { newOrder ->
|
||||||
|
if (newOrder.orderStatus.isTerminal) {
|
||||||
|
issueCardRepository.removeIssueOrderId(userWalletId, order.id)
|
||||||
|
}
|
||||||
|
if (newOrder.orderStep == OrderStep.AWAITING_DEPOSIT) {
|
||||||
|
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,13 @@ class IssueAdditionalCardUseCase(
|
||||||
|
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status),
|
order = TangemPayOrderInfo.fromOrder(order),
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) },
|
onOrderStateChange = { newOrder ->
|
||||||
|
if (newOrder.orderStatus.isTerminal) {
|
||||||
|
issueCardRepository.removeIssueOrderId(userWalletId, order.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ class ReissueTangemPayCardUseCase(
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = order,
|
order = order,
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
onTerminalReached = { reissueCardRepository.removeReissueOrderId(cardId) },
|
onOrderStateChange = { newOrder ->
|
||||||
|
if (newOrder.orderStatus.isTerminal) {
|
||||||
|
reissueCardRepository.removeReissueOrderId(cardId)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,13 @@ class RestoreActiveIssueOrdersUseCase(
|
||||||
|
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
startTangemPayOrderPollingUseCase(
|
startTangemPayOrderPollingUseCase(
|
||||||
order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status),
|
order = TangemPayOrderInfo.fromOrder(order),
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) },
|
onOrderStateChange = { newOrder ->
|
||||||
|
if (newOrder.orderStatus.isTerminal) {
|
||||||
|
issueCardRepository.removeIssueOrderId(userWalletId, order.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,10 @@ class StartTangemPayOrderPollingUseCase(
|
||||||
*/
|
*/
|
||||||
private val activeOrders = ConcurrentHashMap.newKeySet<String>()
|
private val activeOrders = ConcurrentHashMap.newKeySet<String>()
|
||||||
|
|
||||||
/**
|
|
||||||
* @param onTerminalReached invoked once the order is terminal, **before** the status refresh. Callers
|
|
||||||
* use it to forget the locally stored order-id hint (issue / reissue / close) so the refresh does not
|
|
||||||
* re-issue a `GET /order/{id}` for the order that was just resolved.
|
|
||||||
*/
|
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
order: TangemPayOrderInfo,
|
order: TangemPayOrderInfo,
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
onTerminalReached: (suspend () -> Unit)? = null,
|
onOrderStateChange: (suspend (TangemPayOrderInfo) -> Unit)? = null,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
// A poller for this exact order is already running — `false` only reaches fire-and-forget issue
|
// A poller for this exact order is already running — `false` only reaches fire-and-forget issue
|
||||||
// callers (restore / issue-additional); the awaiting freeze caller always polls a fresh order id.
|
// callers (restore / issue-additional); the awaiting freeze caller always polls a fresh order id.
|
||||||
|
|
@ -36,6 +31,7 @@ class StartTangemPayOrderPollingUseCase(
|
||||||
if (!activeOrders.add(key)) return false
|
if (!activeOrders.add(key)) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var currentOrder: TangemPayOrderInfo? = null
|
||||||
while (true) {
|
while (true) {
|
||||||
val newOrder = if (order.orderStatus.isTerminal) {
|
val newOrder = if (order.orderStatus.isTerminal) {
|
||||||
order
|
order
|
||||||
|
|
@ -43,8 +39,12 @@ class StartTangemPayOrderPollingUseCase(
|
||||||
cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull()
|
cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newOrder != null && newOrder != currentOrder) {
|
||||||
|
currentOrder = newOrder
|
||||||
|
onOrderStateChange?.invoke(newOrder)
|
||||||
|
}
|
||||||
|
|
||||||
if (newOrder != null && newOrder.orderStatus.isTerminal) {
|
if (newOrder != null && newOrder.orderStatus.isTerminal) {
|
||||||
onTerminalReached?.invoke()
|
|
||||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||||
return newOrder.orderStatus == OrderStatus.COMPLETED
|
return newOrder.orderStatus == OrderStatus.COMPLETED
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.pay.model.OrderStatus
|
import com.tangem.domain.pay.model.OrderStatus
|
||||||
|
import com.tangem.domain.pay.model.OrderStep
|
||||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||||
import com.tangem.domain.visa.error.VisaApiError
|
import com.tangem.domain.visa.error.VisaApiError
|
||||||
|
|
@ -139,9 +140,52 @@ internal class StartTangemPayOrderPollingUseCaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN processing order WHEN poll returns terminal THEN onTerminalReached runs before status fetch`() = runTest {
|
fun `GIVEN processing order WHEN step changes while non-terminal THEN onOrderStateChange gets every new order`() =
|
||||||
// Arrange — recording both callbacks proves the order hint is cleared before the refresh that
|
runTest {
|
||||||
// would otherwise re-poll GET /order/{id} for the just-resolved order.
|
// Arrange — a step-only transition (PROCESSING/AWAITING_DEPOSIT) must be reported, which is only
|
||||||
|
// observable because the step is part of the order identity.
|
||||||
|
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||||
|
val awaitingDeposit = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING, OrderStep.AWAITING_DEPOSIT)
|
||||||
|
val completed = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
|
||||||
|
val changes = mutableListOf<TangemPayOrderInfo>()
|
||||||
|
coEvery {
|
||||||
|
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||||
|
} returnsMany listOf(awaitingDeposit.right(), completed.right())
|
||||||
|
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(order, USER_WALLET_ID, onOrderStateChange = { changes.add(it) })
|
||||||
|
|
||||||
|
// Assert — the terminal state is reported as well, so callers see the whole transition chain.
|
||||||
|
assertThat(result).isTrue()
|
||||||
|
assertThat(changes).containsExactly(awaitingDeposit, completed).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN order already in target step WHEN poll returns the same step THEN onOrderStateChange still reports it`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange — a restored order can already sit in AWAITING_DEPOSIT; the first poll then returns a
|
||||||
|
// value equal to the incoming one and must still be reported.
|
||||||
|
val awaitingDeposit = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING, OrderStep.AWAITING_DEPOSIT)
|
||||||
|
val completed = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
|
||||||
|
val changes = mutableListOf<TangemPayOrderInfo>()
|
||||||
|
coEvery {
|
||||||
|
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||||
|
} returnsMany listOf(awaitingDeposit.right(), completed.right())
|
||||||
|
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(awaitingDeposit, USER_WALLET_ID, onOrderStateChange = { changes.add(it) })
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isTrue()
|
||||||
|
assertThat(changes).containsExactly(awaitingDeposit, completed).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN processing order WHEN poll returns terminal THEN terminal is reported before status fetch`() = runTest {
|
||||||
|
// Arrange — recording both the callback and the fetch proves callers can clear the order hint before
|
||||||
|
// the refresh that would otherwise re-issue GET /order/{id} for the just-resolved order.
|
||||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||||
val events = mutableListOf<String>()
|
val events = mutableListOf<String>()
|
||||||
coEvery {
|
coEvery {
|
||||||
|
|
@ -153,30 +197,35 @@ internal class StartTangemPayOrderPollingUseCaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
val result = useCase(order, USER_WALLET_ID, onTerminalReached = { events.add("clear") })
|
val result = useCase(
|
||||||
|
order = order,
|
||||||
|
userWalletId = USER_WALLET_ID,
|
||||||
|
onOrderStateChange = { events.add(if (it.orderStatus.isTerminal) "terminal" else "changed") },
|
||||||
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).isTrue()
|
assertThat(result).isTrue()
|
||||||
assertThat(events).containsExactly("clear", "fetch").inOrder()
|
assertThat(events).containsExactly("terminal", "fetch").inOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN order already being polled WHEN invoke again THEN onTerminalReached is not invoked for duplicate`() =
|
fun `GIVEN order already being polled WHEN invoke again THEN onOrderStateChange is not invoked for duplicate`() =
|
||||||
runTest {
|
runTest {
|
||||||
// Arrange — first poller never reaches terminal, so it keeps polling.
|
// Arrange — first poller never reaches terminal, so it keeps polling.
|
||||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||||
coEvery { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } returns
|
coEvery { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } returns
|
||||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right()
|
TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right()
|
||||||
var duplicateCleared = false
|
var duplicateNotified = false
|
||||||
|
|
||||||
// Act — start the first poller, then invoke again for the same order.
|
// Act — start the first poller, then invoke again for the same order.
|
||||||
val firstPoller = launch { useCase(order, USER_WALLET_ID) }
|
val firstPoller = launch { useCase(order, USER_WALLET_ID) }
|
||||||
runCurrent()
|
runCurrent()
|
||||||
val secondResult = useCase(order, USER_WALLET_ID, onTerminalReached = { duplicateCleared = true })
|
val secondResult = useCase(order, USER_WALLET_ID, onOrderStateChange = { duplicateNotified = true })
|
||||||
|
|
||||||
// Assert — the duplicate is a no-op: it must not clear the hint of the live poller.
|
// Assert — the duplicate is a no-op: it must not report state for the live poller, otherwise the
|
||||||
|
// caller would clear the hint of that poller and hide the in-flight order from the status.
|
||||||
assertThat(secondResult).isFalse()
|
assertThat(secondResult).isFalse()
|
||||||
assertThat(duplicateCleared).isFalse()
|
assertThat(duplicateNotified).isFalse()
|
||||||
|
|
||||||
firstPoller.cancel()
|
firstPoller.cancel()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue