Updated on 2026-08-14
This commit is contained in:
parent
a0e5656330
commit
d82dbdd1e6
12 changed files with 256 additions and 129 deletions
|
|
@ -40,6 +40,18 @@ import kotlin.time.Duration.Companion.minutes
|
|||
|
||||
private const val TAG = "PaymentAccountStatusFetcher"
|
||||
|
||||
/**
|
||||
* Reorders cards to match [previousOrder] (by [TangemPayCard.id]), appending any card absent from it at the
|
||||
* end while preserving the relative order among the new ones. Keeps the card layout stable when the backend
|
||||
* reorders `productInstances` (e.g. after a rename bumps `updated_at`). Returns the receiver unchanged when
|
||||
* [previousOrder] is empty (first load → backend order).
|
||||
*/
|
||||
internal fun List<TangemPayCard>.stableOrder(previousOrder: List<String>): List<TangemPayCard> {
|
||||
if (previousOrder.isEmpty()) return this
|
||||
val indexById = previousOrder.withIndex().associate { (index, id) -> id to index }
|
||||
return sortedBy { indexById[it.id] ?: Int.MAX_VALUE }
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
||||
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
|
||||
|
|
@ -365,13 +377,18 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
// placeholder for every locally tracked in-flight issuance order alongside the real cards.
|
||||
val issuingCards = buildIssuingCards(userWalletId)
|
||||
|
||||
// Keep the card order stable across refetches: the backend orders `productInstances` by a mutable
|
||||
// field (a rename bumps `updated_at`), which would otherwise make the renamed card jump. Anchor on
|
||||
// the previously shown order and append newly seen cards at the end.
|
||||
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
|
||||
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
depositAddress = cryptoBalance.depositAddress,
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
fiatRate = fiatRate,
|
||||
cards = tangemPayCards + issuingCards,
|
||||
cards = orderedCards + issuingCards,
|
||||
balance = PaymentAccountStatusValue.Balance(
|
||||
fiatBalance = fiatBalance,
|
||||
cryptoBalance = cryptoBalance,
|
||||
|
|
@ -381,6 +398,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Order of real (product-instance-backed) cards from the previously stored status, used as the stable
|
||||
* anchor for [stableOrder]. Issuing placeholders are excluded — they carry synthetic order ids and are
|
||||
* always appended last. Empty on the first load (no prior [PaymentAccountStatusValue.Loaded]), which makes
|
||||
* [stableOrder] fall back to the backend order.
|
||||
*/
|
||||
private suspend fun previousRealCardOrder(userWalletId: UserWalletId): List<String> {
|
||||
val previousValue = paymentAccountStatusesStore.getSyncOrNull(userWalletId)?.value
|
||||
return (previousValue as? PaymentAccountStatusValue.Loaded)
|
||||
?.cards
|
||||
?.filterNot { it.state == TangemPayCardState.Issuing }
|
||||
?.map { it.id }
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
|
||||
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
|
||||
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.data.pay.flow
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class StableOrderTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun stableOrder(model: Model) {
|
||||
// Act
|
||||
val result = model.cards.stableOrder(model.previousOrder).map { it.id }
|
||||
|
||||
// Assert
|
||||
assertThat(result).containsExactlyElementsIn(model.expectedIds).inOrder()
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
Model(
|
||||
name = "GIVEN no previous order WHEN stableOrder THEN backend order is kept",
|
||||
cards = listOf(card("a"), card("b"), card("c")),
|
||||
previousOrder = emptyList(),
|
||||
expectedIds = listOf("a", "b", "c"),
|
||||
),
|
||||
Model(
|
||||
// The reported bug: a rename bumps updated_at, so the backend moves the renamed card.
|
||||
name = "GIVEN backend reordered existing cards WHEN stableOrder THEN previous order is preserved",
|
||||
cards = listOf(card("b"), card("c"), card("a")),
|
||||
previousOrder = listOf("a", "b", "c"),
|
||||
expectedIds = listOf("a", "b", "c"),
|
||||
),
|
||||
Model(
|
||||
name = "GIVEN a newly seen card WHEN stableOrder THEN it is appended at the end",
|
||||
cards = listOf(card("c"), card("a"), card("b")),
|
||||
previousOrder = listOf("a", "b"),
|
||||
expectedIds = listOf("a", "b", "c"),
|
||||
),
|
||||
Model(
|
||||
name = "GIVEN several new cards WHEN stableOrder THEN they keep their backend order at the end",
|
||||
cards = listOf(card("d"), card("b"), card("a"), card("c")),
|
||||
previousOrder = listOf("a", "b"),
|
||||
expectedIds = listOf("a", "b", "d", "c"),
|
||||
),
|
||||
Model(
|
||||
name = "GIVEN previous order references a gone card WHEN stableOrder THEN the stale id is ignored",
|
||||
cards = listOf(card("b"), card("a")),
|
||||
previousOrder = listOf("a", "x", "b"),
|
||||
expectedIds = listOf("a", "b"),
|
||||
),
|
||||
)
|
||||
|
||||
internal data class Model(
|
||||
val name: String,
|
||||
val cards: List<TangemPayCard>,
|
||||
val previousOrder: List<String>,
|
||||
val expectedIds: List<String>,
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun card(id: String): TangemPayCard = TangemPayCard(
|
||||
id = id,
|
||||
productInstanceId = id,
|
||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
hasPinCode = false,
|
||||
displayName = null,
|
||||
limit = null,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
lastDigits = "0000",
|
||||
state = TangemPayCardState.Active,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue