From 8a83664e9a1077ac10c44fc83bc5821fbeb06ac3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 11:04:50 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../visa/DefaultTangemPayCloseCardStore.kt | 18 ++-- .../visa/DefaultTangemPayReissueCardStore.kt | 5 ++ .../local/visa/TangemPayCloseCardStore.kt | 4 +- .../local/visa/TangemPayReissueCardStore.kt | 2 + .../DefaultTangemPayReissueCardStoreTest.kt | 88 +++++++++++++++++++ .../DefaultPaymentAccountStatusFetcher.kt | 3 +- .../repository/DefaultCloseCardRepository.kt | 11 ++- .../DefaultReissueCardRepository.kt | 7 ++ .../TangemPayCloseCardRepository.kt | 4 +- .../TangemPayReissueCardRepository.kt | 2 + .../pay/usecase/CloseTangemPayCardUseCase.kt | 8 +- .../pay/usecase/IssueAdditionalCardUseCase.kt | 1 + .../usecase/ReissueTangemPayCardUseCase.kt | 6 +- .../RestoreActiveIssueOrdersUseCase.kt | 1 + .../StartTangemPayOrderPollingUseCase.kt | 12 ++- .../ChangeCardFrozenStateUseCaseTest.kt | 8 +- .../usecase/CloseTangemPayCardUseCaseTest.kt | 24 ++--- .../RestoreActiveIssueOrdersUseCaseTest.kt | 10 +-- .../StartTangemPayOrderPollingUseCaseTest.kt | 43 +++++++++ 19 files changed, 218 insertions(+), 39 deletions(-) create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStoreTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayCloseCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayCloseCardStore.kt index 1bc95d5f94..c7cadf74d6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayCloseCardStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayCloseCardStore.kt @@ -10,15 +10,15 @@ internal class DefaultTangemPayCloseCardStore( private val prefs: AppPreferencesStore, ) : TangemPayCloseCardStore { - override suspend fun setCloseOrderId(cardId: String, orderId: String?) { - if (orderId == null) { - prefs.edit { it.remove(getCloseKey(cardId)) } - } else { - prefs.store( - key = getCloseKey(cardId), - value = orderId, - ) - } + override suspend fun storeCloseOrderId(cardId: String, orderId: String) { + prefs.store( + key = getCloseKey(cardId), + value = orderId, + ) + } + + override suspend fun removeCloseOrderId(cardId: String) { + prefs.edit { it.remove(getCloseKey(cardId)) } } override suspend fun getOrderId(cardId: String): String? { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt index bb25810fba..c9bb40fcd5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.local.visa +import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -31,6 +32,10 @@ internal class DefaultTangemPayReissueCardStore( ) } + override suspend fun removeReissueOrderId(cardId: String) { + prefs.edit { it.remove(getReissueKey(cardId)) } + } + override suspend fun getOrderId(cardId: String): String? { return prefs.getSyncOrNull(key = getReissueKey(cardId)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayCloseCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayCloseCardStore.kt index 1aca215a85..0200a40ab1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayCloseCardStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayCloseCardStore.kt @@ -2,7 +2,9 @@ package com.tangem.datasource.local.visa interface TangemPayCloseCardStore { - suspend fun setCloseOrderId(cardId: String, orderId: String?) + suspend fun storeCloseOrderId(cardId: String, orderId: String) + + suspend fun removeCloseOrderId(cardId: String) suspend fun getOrderId(cardId: String): String? } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt index 0925c1ea31..4439684898 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt @@ -11,5 +11,7 @@ interface TangemPayReissueCardStore { suspend fun storeReissueOrderId(cardId: String, orderId: String) + suspend fun removeReissueOrderId(cardId: String) + suspend fun getOrderId(cardId: String): String? } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStoreTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStoreTest.kt new file mode 100644 index 0000000000..65f6816a69 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStoreTest.kt @@ -0,0 +1,88 @@ +package com.tangem.datasource.local.visa + +import androidx.datastore.preferences.core.emptyPreferences +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.models.pay.TangemPayReissueCardFee +import com.tangem.test.core.datastore.MockStateDataStore +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Tests for [DefaultTangemPayReissueCardStore], focused on the reissue order-id lifecycle. + * + * Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] so the preferences + * round-trip (store / read / remove) is exercised end-to-end. The remove path backs the [REDACTED_TASK_KEY] fix: + * a terminal reissue order must be forgotten so the payment-account refresh stops re-polling + * `GET /order/{id}` for it. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultTangemPayReissueCardStoreTest { + + private val dataStore = MockStateDataStore(default = emptyPreferences()) + private val prefs = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = dataStore, + ) + private val feeStore: RuntimeDataStore = mockk(relaxed = true) + + private val store = DefaultTangemPayReissueCardStore(feeStore = feeStore, prefs = prefs) + + @BeforeEach + fun resetStore() { + runBlocking { dataStore.updateData { emptyPreferences() } } + } + + @Test + fun `GIVEN order id stored WHEN getOrderId THEN returns stored id`() = runTest { + // Arrange + store.storeReissueOrderId(CARD_ID, ORDER_ID) + + // Act + val result = store.getOrderId(CARD_ID) + + // Assert + assertThat(result).isEqualTo(ORDER_ID) + } + + @Test + fun `GIVEN order id stored WHEN removeReissueOrderId THEN order id is cleared`() = runTest { + // Arrange + store.storeReissueOrderId(CARD_ID, ORDER_ID) + + // Act + store.removeReissueOrderId(CARD_ID) + + // Assert + assertThat(store.getOrderId(CARD_ID)).isNull() + } + + @Test + fun `GIVEN clearing one card WHEN another card has an order THEN the other is untouched`() = runTest { + // Arrange + store.storeReissueOrderId(CARD_ID, ORDER_ID) + store.storeReissueOrderId(OTHER_CARD_ID, OTHER_ORDER_ID) + + // Act + store.removeReissueOrderId(CARD_ID) + + // Assert + assertThat(store.getOrderId(CARD_ID)).isNull() + assertThat(store.getOrderId(OTHER_CARD_ID)).isEqualTo(OTHER_ORDER_ID) + } + + private companion object { + const val CARD_ID = "card-1" + const val OTHER_CARD_ID = "card-2" + const val ORDER_ID = "reissue-order-1" + const val OTHER_ORDER_ID = "reissue-order-2" + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index f2741234f6..d013b6d98c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -462,7 +462,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( return if (closingOrderId != null) { val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull() if (order != null && order.orderStatus.isTerminal) { - closeCardRepository.setCloseOrderId(cardId, null) + closeCardRepository.removeCloseOrderId(cardId) TangemPayCardState.Active } else { TangemPayCardState.Closing @@ -470,6 +470,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } else if (reissueOrderId != null) { val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull() if (order != null && order.orderStatus.isTerminal) { + reissueCardRepository.removeReissueOrderId(cardId) TangemPayCardState.Active } else { TangemPayCardState.Reissuing diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCloseCardRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCloseCardRepository.kt index 3dfe1e7804..e1b7a8e12a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCloseCardRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCloseCardRepository.kt @@ -37,14 +37,21 @@ internal class DefaultCloseCardRepository @Inject constructor( ) } - override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either = + override suspend fun storeCloseOrderId(cardId: String, orderId: String): Either = runSuspendCatching { - tangemPayCloseCardStore.setCloseOrderId(cardId, orderId) + tangemPayCloseCardStore.storeCloseOrderId(cardId, orderId) }.fold( onSuccess = { Unit.right() }, onFailure = { Either.Left(VisaApiError.Unspecified) }, ) + override suspend fun removeCloseOrderId(cardId: String): Either = runSuspendCatching { + tangemPayCloseCardStore.removeCloseOrderId(cardId) + }.fold( + onSuccess = { Unit.right() }, + onFailure = { Either.Left(VisaApiError.Unspecified) }, + ) + override suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either = either { runSuspendCatching { tangemPayCloseCardStore.getOrderId(cardId) }.getOrNull() diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt index 6f4f0fed54..b28aae7f98 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt @@ -72,6 +72,13 @@ internal class DefaultReissueCardRepository @Inject constructor( onFailure = { Either.Left(VisaApiError.Unspecified) }, ) + override suspend fun removeReissueOrderId(cardId: String): Either = runSuspendCatching { + tangemPayReissueCardStore.removeReissueOrderId(cardId) + }.fold( + onSuccess = { Unit.right() }, + onFailure = { Either.Left(VisaApiError.Unspecified) }, + ) + override suspend fun getReissueOrderId( userWalletId: UserWalletId, cardId: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCloseCardRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCloseCardRepository.kt index 14cc9376e6..416ab8792c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCloseCardRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCloseCardRepository.kt @@ -10,7 +10,9 @@ interface TangemPayCloseCardRepository { suspend fun closeCard(userWalletId: UserWalletId, cardId: String): Either - suspend fun setCloseOrderId(cardId: String, orderId: String?): Either + suspend fun storeCloseOrderId(cardId: String, orderId: String): Either + + suspend fun removeCloseOrderId(cardId: String): Either suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt index f83ce75da9..f987f50d00 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt @@ -15,5 +15,7 @@ interface TangemPayReissueCardRepository { suspend fun storeReissueOrderId(cardId: String, orderId: String): Either + suspend fun removeReissueOrderId(cardId: String): Either + suspend fun getReissueOrderId(userWalletId: UserWalletId, cardId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCase.kt index cf761d96a9..e193105082 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCase.kt @@ -23,11 +23,15 @@ class CloseTangemPayCardUseCase( raise(VisaApiError.Unspecified) } - closeCardRepository.setCloseOrderId(cardId, order.orderId) + closeCardRepository.storeCloseOrderId(cardId, order.orderId) paymentAccountStatusFetcher.invoke(userWalletId) appCoroutineScope.launch { - startTangemPayOrderPollingUseCase(order, userWalletId) + startTangemPayOrderPollingUseCase( + order = order, + userWalletId = userWalletId, + onTerminalReached = { closeCardRepository.removeCloseOrderId(cardId) }, + ) } } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt index 23b67e2b72..1c115ed53c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt @@ -79,6 +79,7 @@ class IssueAdditionalCardUseCase( startTangemPayOrderPollingUseCase( order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status), userWalletId = userWalletId, + onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) }, ) } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt index 7420980320..88dda725e3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt @@ -27,7 +27,11 @@ class ReissueTangemPayCardUseCase( paymentAccountStatusFetcher.invoke(userWalletId) appCoroutineScope.launch { - startTangemPayOrderPollingUseCase(order, userWalletId) + startTangemPayOrderPollingUseCase( + order = order, + userWalletId = userWalletId, + onTerminalReached = { reissueCardRepository.removeReissueOrderId(cardId) }, + ) } } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCase.kt index f4b83bd2a5..32439c2536 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCase.kt @@ -56,6 +56,7 @@ class RestoreActiveIssueOrdersUseCase( startTangemPayOrderPollingUseCase( order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status), userWalletId = userWalletId, + onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) }, ) } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt index dc2b5ea60b..e1f61bf615 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt @@ -20,7 +20,16 @@ class StartTangemPayOrderPollingUseCase( */ private val activeOrders = ConcurrentHashMap.newKeySet() - suspend operator fun invoke(order: TangemPayOrderInfo, userWalletId: UserWalletId): Boolean { + /** + * @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( + order: TangemPayOrderInfo, + userWalletId: UserWalletId, + onTerminalReached: (suspend () -> Unit)? = null, + ): Boolean { // 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. val key = "${userWalletId.stringValue}:${order.orderId}" @@ -35,6 +44,7 @@ class StartTangemPayOrderPollingUseCase( } if (newOrder != null && newOrder.orderStatus.isTerminal) { + onTerminalReached?.invoke() paymentAccountStatusFetcher.invoke(userWalletId) return newOrder.orderStatus == OrderStatus.COMPLETED } diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt index 87d91a7123..25a32d65ed 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt @@ -38,7 +38,7 @@ internal class ChangeCardFrozenStateUseCaseTest { cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen) } - coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startPollingUseCase(any(), any(), any()) } } @Test @@ -56,7 +56,7 @@ internal class ChangeCardFrozenStateUseCaseTest { cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen) } - coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startPollingUseCase(any(), any(), any()) } } @Test @@ -65,7 +65,7 @@ internal class ChangeCardFrozenStateUseCaseTest { val useCase = createUseCase() val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) coEvery { cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID) } returns order.right() - coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + coEvery { startPollingUseCase(order, USER_WALLET_ID, any()) } returns true val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true) @@ -82,7 +82,7 @@ internal class ChangeCardFrozenStateUseCaseTest { val useCase = createUseCase() val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) coEvery { cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID) } returns order.right() - coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + coEvery { startPollingUseCase(order, USER_WALLET_ID, any()) } returns true val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false) diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCaseTest.kt index 22d2c58fe4..ab4c1ac531 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CloseTangemPayCardUseCaseTest.kt @@ -31,9 +31,9 @@ internal class CloseTangemPayCardUseCaseTest { val result = useCase(USER_WALLET_ID, CARD_ID) assertThat(result.isLeft()).isTrue() - coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) } + coVerify(exactly = 0) { closeCardRepository.storeCloseOrderId(any(), any()) } coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any()) } - coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startPollingUseCase(any(), any(), any()) } } @Test @@ -46,9 +46,9 @@ internal class CloseTangemPayCardUseCaseTest { val result = useCase(USER_WALLET_ID, CARD_ID) assertThat(result.isLeft()).isTrue() - coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) } + coVerify(exactly = 0) { closeCardRepository.storeCloseOrderId(any(), any()) } coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any()) } - coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startPollingUseCase(any(), any(), any()) } } @Test @@ -57,17 +57,17 @@ internal class CloseTangemPayCardUseCaseTest { val useCase = createUseCase() val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right() - coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right() + coEvery { closeCardRepository.storeCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right() coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() - coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + coEvery { startPollingUseCase(order, USER_WALLET_ID, any()) } returns true val result = useCase(USER_WALLET_ID, CARD_ID) assertThat(result.isRight()).isTrue() coVerifyOrder { - closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) + closeCardRepository.storeCloseOrderId(CARD_ID, ORDER_ID) paymentAccountStatusFetcher.invoke(USER_WALLET_ID) - startPollingUseCase(order, USER_WALLET_ID) + startPollingUseCase(order, USER_WALLET_ID, any()) } } @@ -77,16 +77,16 @@ internal class CloseTangemPayCardUseCaseTest { val useCase = createUseCase() val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right() - coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right() + coEvery { closeCardRepository.storeCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right() coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() - coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + coEvery { startPollingUseCase(order, USER_WALLET_ID, any()) } returns true val result = useCase(USER_WALLET_ID, CARD_ID) assertThat(result.isRight()).isTrue() - coVerify(exactly = 1) { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } + coVerify(exactly = 1) { closeCardRepository.storeCloseOrderId(CARD_ID, ORDER_ID) } coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } - coVerify(exactly = 1) { startPollingUseCase(order, USER_WALLET_ID) } + coVerify(exactly = 1) { startPollingUseCase(order, USER_WALLET_ID, any()) } } private fun createUseCase() = CloseTangemPayCardUseCase( diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCaseTest.kt index a6d1b46934..b18d650fb7 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveIssueOrdersUseCaseTest.kt @@ -52,10 +52,10 @@ internal class RestoreActiveIssueOrdersUseCaseTest { coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, first.id) } coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, second.id) } coVerify(exactly = 1) { - startTangemPayOrderPollingUseCase(TangemPayOrderInfo(first.id, first.status), userWalletId) + startTangemPayOrderPollingUseCase(TangemPayOrderInfo(first.id, first.status), userWalletId, any()) } coVerify(exactly = 1) { - startTangemPayOrderPollingUseCase(TangemPayOrderInfo(second.id, second.status), userWalletId) + startTangemPayOrderPollingUseCase(TangemPayOrderInfo(second.id, second.status), userWalletId, any()) } } @@ -72,7 +72,7 @@ internal class RestoreActiveIssueOrdersUseCaseTest { // Assert assertThat(result.isRight()).isTrue() coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) } - coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any(), any()) } } @Test @@ -89,7 +89,7 @@ internal class RestoreActiveIssueOrdersUseCaseTest { // Assert assertThat(result.isRight()).isTrue() coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) } - coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any(), any()) } } @Test @@ -105,7 +105,7 @@ internal class RestoreActiveIssueOrdersUseCaseTest { // Assert assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified) coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) } - coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any()) } + coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any(), any()) } } private fun order(id: String, type: OrderType, status: OrderStatus): Order = Order( diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt index c14025f994..e8f3d9157b 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt @@ -138,6 +138,49 @@ internal class StartTangemPayOrderPollingUseCaseTest { firstPoller.cancel() } + @Test + fun `GIVEN processing order WHEN poll returns terminal THEN onTerminalReached runs before status fetch`() = runTest { + // Arrange — recording both callbacks proves the order hint is cleared before the refresh that + // would otherwise re-poll GET /order/{id} for the just-resolved order. + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) + val events = mutableListOf() + coEvery { + cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) + } returns TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right() + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } answers { + events.add("fetch") + Unit.right() + } + + // Act + val result = useCase(order, USER_WALLET_ID, onTerminalReached = { events.add("clear") }) + + // Assert + assertThat(result).isTrue() + assertThat(events).containsExactly("clear", "fetch").inOrder() + } + + @Test + fun `GIVEN order already being polled WHEN invoke again THEN onTerminalReached is not invoked for duplicate`() = + runTest { + // Arrange — first poller never reaches terminal, so it keeps polling. + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) + coEvery { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } returns + TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right() + var duplicateCleared = false + + // Act — start the first poller, then invoke again for the same order. + val firstPoller = launch { useCase(order, USER_WALLET_ID) } + runCurrent() + val secondResult = useCase(order, USER_WALLET_ID, onTerminalReached = { duplicateCleared = true }) + + // Assert — the duplicate is a no-op: it must not clear the hint of the live poller. + assertThat(secondResult).isFalse() + assertThat(duplicateCleared).isFalse() + + firstPoller.cancel() + } + private companion object { val USER_WALLET_ID = UserWalletId("aabbcc112233") const val ORDER_ID = "order-test-1"