Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 15:25:29 +03:00
parent 5ce84d1500
commit f2891683e1
8 changed files with 780 additions and 0 deletions

View file

@ -0,0 +1,58 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.pay.model.CashbackDocument
import org.junit.jupiter.api.Test
internal class TangemPayCashbackAccrualsConverterTest {
@Test
fun `GIVEN docs present WHEN convert THEN doc rows carry titles and info rows are static`() {
// Arrange
val converter = TangemPayCashbackAccrualsConverter(onDocClick = {})
val docs = listOf(
CashbackDocument(id = "1", title = "All categories without cashback", url = "https://a"),
CashbackDocument(id = "2", title = "Full terms of cashback program", url = "https://b"),
)
// Act
val result = converter.convert(docs)
// Assert
assertThat(result.title).isEqualTo(stringReference("Accruals"))
assertThat(result.infoRows).hasSize(3)
assertThat(result.docRows.map { it.title }).containsExactly(
stringReference("All categories without cashback"),
stringReference("Full terms of cashback program"),
).inOrder()
}
@Test
fun `GIVEN empty docs WHEN convert THEN no doc rows but info rows remain`() {
// Arrange
val converter = TangemPayCashbackAccrualsConverter(onDocClick = {})
// Act
val result = converter.convert(emptyList())
// Assert
assertThat(result.docRows).isEmpty()
assertThat(result.infoRows).hasSize(3)
}
@Test
fun `GIVEN a doc row WHEN its onClick invoked THEN the doc url is opened`() {
// Arrange
var openedUrl: String? = null
val converter = TangemPayCashbackAccrualsConverter(onDocClick = { openedUrl = it })
val docs = listOf(CashbackDocument(id = "1", title = "Terms", url = "https://terms"))
// Act
val result = converter.convert(docs)
result.docRows.single().onClick()
// Assert
assertThat(openedUrl).isEqualTo("https://terms")
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayTariffPlan
import org.junit.jupiter.api.Test
internal class TangemPayCashbackDetailsConverterTest {
private val converter = TangemPayCashbackDetailsConverter()
@Test
fun `GIVEN single tier WHEN convert THEN title and one row with rate scope label min and cap`() {
// Act
val result = converter.convert(
listOf(tier(rate = 1, label = "Basic cards", scope = "All purchases", min = "$30", cap = "$100")),
)
// Assert
assertThat(result.title).isEqualTo(stringReference("Cashback 1%"))
assertThat(result.rows).containsExactly(
stringReference("1% for All purchases with your Basic cards, min purchase $30, up to $100 per month"),
)
}
@Test
fun `GIVEN two tiers WHEN convert THEN up-to title and one row per tier in order`() {
// Act
val result = converter.convert(
listOf(
tier(rate = 1, label = "Basic cards", scope = "All purchases", min = "$30", cap = "$100"),
tier(rate = 2, label = "Plus cards", scope = "All purchases", min = "$30", cap = "$300"),
),
)
// Assert
assertThat(result.title).isEqualTo(stringReference("Cashback up to 2%"))
assertThat(result.rows).containsExactly(
stringReference("1% for All purchases with your Basic cards, min purchase $30, up to $100 per month"),
stringReference("2% for All purchases with your Plus cards, min purchase $30, up to $300 per month"),
).inOrder()
}
@Test
fun `GIVEN tier without min or cap WHEN convert THEN row is rate scope and label only`() {
// Act
val result = converter.convert(
listOf(tier(rate = 1, label = "Basic cards", scope = "All purchases", min = null, cap = null)),
)
// Assert
assertThat(result.rows).containsExactly(stringReference("1% for All purchases with your Basic cards"))
}
@Test
fun `GIVEN tier with unknown rate WHEN convert THEN plain Cashback title and row without percent prefix`() {
// Act
val result = converter.convert(
listOf(tier(rate = null, label = "Gold cards", scope = "All purchases", min = null, cap = null)),
)
// Assert
assertThat(result.title).isEqualTo(stringReference("Cashback"))
assertThat(result.rows).containsExactly(stringReference("All purchases with your Gold cards"))
}
@Test
fun `GIVEN no tiers WHEN convert THEN plain Cashback title and empty rows`() {
// Act
val result = converter.convert(emptyList())
// Assert
assertThat(result.title).isEqualTo(stringReference("Cashback"))
assertThat(result.rows).isEmpty()
}
private fun tier(
rate: Int? = 1,
label: String = "Basic cards",
scope: String = "All purchases",
min: String? = null,
cap: String? = null,
) = CashbackTier(
planType = TangemPayTariffPlan.Type.UNKNOWN,
rate = rate,
label = label,
scope = scope,
minPurchase = min,
monthlyCap = cap,
)
}

View file

@ -0,0 +1,93 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayTariffPlan
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayCashbackInfoTilesConverterTest {
private val converter = TangemPayCashbackInfoTilesConverter(
onRateClick = {},
onAccrualsClick = {},
)
@ParameterizedTest
@MethodSource("rateSelectionModels")
fun `GIVEN plan WHEN convert THEN rate tile shows the tier rate and the plan subtitle`(model: RateSelectionModel) {
// Act
val result = converter.convert(twoTiers(), model.plan, model.planName)
// Assert
assertThat(result.rate.title).isEqualTo(stringReference(model.expectedTitle))
assertThat(result.rate.subtitle).isEqualTo(model.expectedSubtitle)
}
@Test
fun `GIVEN empty tiers and unknown plan WHEN convert THEN rate tile has no percent and empty subtitle`() {
// Act
val result = converter.convert(emptyList(), TangemPayTariffPlan.Type.UNKNOWN, currentPlanName = null)
// Assert
assertThat(result.rate.title).isEqualTo(stringReference("Cashback"))
assertThat(result.rate.subtitle).isEqualTo(TextReference.EMPTY)
}
@Test
fun `GIVEN any tiers WHEN convert THEN accruals tile is static`() {
// Act
val result = converter.convert(twoTiers(), TangemPayTariffPlan.Type.BASIC, currentPlanName = "Basic")
// Assert
assertThat(result.accruals.title).isEqualTo(stringReference("Accruals"))
assertThat(result.accruals.subtitle).isEqualTo(stringReference("Limits and exceptions"))
}
private fun rateSelectionModels() = listOf(
RateSelectionModel(
plan = TangemPayTariffPlan.Type.PLUS,
planName = "Plus",
expectedTitle = "Cashback 2%",
expectedSubtitle = stringReference("With your Plus plan"),
),
RateSelectionModel(
plan = TangemPayTariffPlan.Type.BASIC,
planName = "Basic",
expectedTitle = "Cashback 1%",
expectedSubtitle = stringReference("With your Basic plan"),
),
RateSelectionModel(
plan = TangemPayTariffPlan.Type.UNKNOWN,
planName = null,
expectedTitle = "Cashback 1%",
expectedSubtitle = TextReference.EMPTY,
),
)
private fun twoTiers() = listOf(
tier(TangemPayTariffPlan.Type.BASIC, rate = 1),
tier(TangemPayTariffPlan.Type.PLUS, rate = 2),
)
private fun tier(planType: TangemPayTariffPlan.Type = TangemPayTariffPlan.Type.UNKNOWN, rate: Int? = null) =
CashbackTier(
planType = planType,
rate = rate,
label = "label",
scope = "scope",
minPurchase = null,
monthlyCap = null,
)
data class RateSelectionModel(
val plan: TangemPayTariffPlan.Type,
val planName: String?,
val expectedTitle: String,
val expectedSubtitle: TextReference,
)
}

View file

@ -0,0 +1,173 @@
package com.tangem.features.tangempay.cashback.impl.model
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CashbackDocument
import com.tangem.domain.pay.model.CashbackPromotions
import com.tangem.domain.pay.model.CashbackSummary
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.repository.CashbackRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.Locale
internal class TangemPayCashbackModelTest {
private val defaultLocale = Locale.getDefault()
private val userWalletId = UserWalletId("123")
private val router: Router = mockk(relaxed = true)
private val urlOpener: UrlOpener = mockk(relaxed = true)
private val cashbackRepository: CashbackRepository = mockk()
private val onboardingRepository: OnboardingRepository = mockk()
@BeforeEach
fun setup() {
Locale.setDefault(Locale.US)
clearMocks(cashbackRepository, onboardingRepository)
coEvery { cashbackRepository.getCashbackSummary(any()) } returns CashbackSummary.Disabled.right()
coEvery { cashbackRepository.getCashbackPromotions(any()) } returns promotions().right()
coEvery { cashbackRepository.getCashbackAccrualDocs(any()) } returns docs().right()
coEvery { onboardingRepository.getCustomerInfo(any()) } returns
customerInfo(TangemPayTariffPlan.Type.BASIC).right()
}
@AfterEach
fun tearDown() {
Locale.setDefault(defaultLocale)
}
@Test
fun `GIVEN all sources load WHEN model created THEN tiles and sheets populated`() {
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.infoTiles).isNotNull()
assertThat(model.uiState.value.infoTiles?.rate?.title).isEqualTo(stringReference("Cashback 1%"))
assertThat(model.detailsSheet.value.rows).hasSize(2)
assertThat(model.accrualsSheet.value.docRows).hasSize(2)
}
@Test
fun `GIVEN PLUS plan WHEN model created THEN rate tile shows the Plus tier rate`() {
// Arrange
coEvery { onboardingRepository.getCustomerInfo(any()) } returns
customerInfo(TangemPayTariffPlan.Type.PLUS).right()
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.infoTiles?.rate?.title).isEqualTo(stringReference("Cashback 2%"))
}
@Test
fun `GIVEN promotions load fails WHEN model created THEN tiles hidden and details rows empty`() {
// Arrange
coEvery { cashbackRepository.getCashbackPromotions(any()) } throws RuntimeException("boom")
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.infoTiles).isNull()
assertThat(model.detailsSheet.value.rows).isEmpty()
}
@Test
fun `GIVEN summary load fails WHEN model created THEN tiles still shown`() {
// Arrange
coEvery { cashbackRepository.getCashbackSummary(any()) } throws RuntimeException("boom")
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.infoTiles).isNotNull()
assertThat(model.detailsSheet.value.rows).hasSize(2)
}
@Test
fun `GIVEN customer info fails WHEN model created THEN rate tile falls back to the first tier`() {
// Arrange
coEvery { onboardingRepository.getCustomerInfo(any()) } throws RuntimeException("boom")
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.infoTiles?.rate?.title).isEqualTo(stringReference("Cashback 1%"))
}
@Test
fun `GIVEN docs load fails WHEN model created THEN accrual doc rows empty but info rows kept`() {
// Arrange
coEvery { cashbackRepository.getCashbackAccrualDocs(any()) } throws RuntimeException("boom")
// Act
val model = createModel()
// Assert
assertThat(model.accrualsSheet.value.docRows).isEmpty()
assertThat(model.accrualsSheet.value.infoRows).isNotEmpty()
}
private fun createModel() = TangemPayCashbackModel(
dispatchers = TestingCoroutineDispatcherProvider(),
paramsContainer = MutableParamsContainer(TangemPayCashbackComponent.Params(userWalletId = userWalletId)),
router = router,
urlOpener = urlOpener,
cashbackRepository = cashbackRepository,
onboardingRepository = onboardingRepository,
)
private fun promotions() = CashbackPromotions(
cardTiers = listOf(
CashbackPromotions.CardTier(
tier = "basic",
label = "Basic cards",
scope = "All purchases",
minTransactionAmount = BigDecimal("30"),
monthlyCapAmount = BigDecimal("100"),
),
CashbackPromotions.CardTier(
tier = "plus",
label = "Plus cards",
scope = "All purchases",
minTransactionAmount = BigDecimal("30"),
monthlyCapAmount = BigDecimal("300"),
),
),
)
private fun docs() = listOf(
CashbackDocument(id = "excluded", title = "All categories without cashback", url = "https://x/excluded.pdf"),
CashbackDocument(id = "terms", title = "Full terms of cashback program", url = "https://x/terms.pdf"),
)
private fun customerInfo(planType: TangemPayTariffPlan.Type): CustomerInfo {
val tariffPlanMock = mockk<TangemPayTariffPlan> {
every { type } returns planType
every { name } returns planType.name
}
val customerTariffPlanMock = mockk<TangemPayCustomerTariffPlan> { every { plan } returns tariffPlanMock }
return mockk { every { tariffPlan } returns customerTariffPlanMock }
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackPromotions
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
import java.util.Locale
internal class TangemPayCashbackTiersConverterTest {
private val defaultLocale = Locale.getDefault()
private val converter = TangemPayCashbackTiersConverter()
@BeforeEach
fun setup() {
Locale.setDefault(Locale.US)
}
@AfterEach
fun tearDown() {
Locale.setDefault(defaultLocale)
}
@Test
fun `GIVEN known tiers WHEN convert THEN rate plan and formatted amounts resolved once`() {
// Arrange
val promotions = promotions(
tier(id = "basic", label = "Basic cards", scope = "All purchases", min = "30", cap = "100"),
tier(id = "plus", label = "Plus cards", scope = "Everywhere", min = null, cap = "300"),
)
// Act
val result = converter.convert(promotions)
// Assert
assertThat(result).containsExactly(
CashbackTier(
planType = TangemPayTariffPlan.Type.BASIC,
rate = 1,
label = "Basic cards",
scope = "All purchases",
minPurchase = "$30",
monthlyCap = "$100",
),
CashbackTier(
planType = TangemPayTariffPlan.Type.PLUS,
rate = 2,
label = "Plus cards",
scope = "Everywhere",
minPurchase = null,
monthlyCap = "$300",
),
).inOrder()
}
@Test
fun `GIVEN unknown tier WHEN convert THEN rate null and plan type UNKNOWN`() {
// Arrange
val promotions = promotions(tier(id = "gold", label = "Gold", scope = "All", min = null, cap = null))
// Act
val result = converter.convert(promotions)
// Assert
assertThat(result).containsExactly(
CashbackTier(
planType = TangemPayTariffPlan.Type.UNKNOWN,
rate = null,
label = "Gold",
scope = "All",
minPurchase = null,
monthlyCap = null,
),
)
}
@Test
fun `GIVEN no tiers WHEN convert THEN empty list`() {
// Act
val result = converter.convert(CashbackPromotions(cardTiers = emptyList()))
// Assert
assertThat(result).isEmpty()
}
private fun promotions(vararg tiers: CashbackPromotions.CardTier) = CashbackPromotions(cardTiers = tiers.toList())
private fun tier(
id: String = "basic",
label: String = "Basic cards",
scope: String = "All purchases",
min: String? = null,
cap: String? = null,
) = CashbackPromotions.CardTier(
tier = id,
label = label,
scope = scope,
minTransactionAmount = min?.let(::BigDecimal),
monthlyCapAmount = cap?.let(::BigDecimal),
)
}