diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepositoryTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepositoryTest.kt new file mode 100644 index 0000000000..9d50b6eec1 --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepositoryTest.kt @@ -0,0 +1,106 @@ +package com.tangem.data.pay.repository + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.data.pay.util.CashbackAccrualDocsConverter +import com.tangem.data.pay.util.CashbackPromotionsConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse +import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.error.VisaApiError +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class DefaultCashbackRepositoryTest { + + private val tangemPayApi: TangemPayApi = mockk() + private val requestHelper: TangemPayRequestPerformer = mockk() + + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun setUp() { + clearMocks(tangemPayApi, requestHelper) + coEvery { + requestHelper.performRequest(userWalletId = any(), requestBlock = any()) + } coAnswers { + val block = secondArg ApiResponse>() + when (val response = block(AUTH_HEADER)) { + is ApiResponse.Success -> response.data.right() + is ApiResponse.Error -> VisaApiError.Unspecified.left() + } + } + } + + @Test + fun `GIVEN promotions response WHEN getCashbackPromotions THEN converter result is returned`() = runTest { + // Arrange + val response = CashbackPromotionsResponse(cashbackOnCards = null, additionalCashback = null) + coEvery { tangemPayApi.getCashbackPromotions(any(), any()) } returns ApiResponse.Success(response) + + // Act + val actual = createRepository().getCashbackPromotions(userWalletId) + + // Assert + assertThat(actual).isEqualTo(CashbackPromotionsConverter.convert(response).right()) + } + + @Test + fun `GIVEN backend error WHEN getCashbackPromotions THEN error is propagated`() = runTest { + // Arrange + coEvery { tangemPayApi.getCashbackPromotions(any(), any()) } returns + ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse + + // Act + val actual = createRepository().getCashbackPromotions(userWalletId) + + // Assert + assertThat(actual).isEqualTo(VisaApiError.Unspecified.left()) + } + + @Test + fun `GIVEN docs response WHEN getCashbackAccrualDocs THEN converter result is returned`() = runTest { + // Arrange + val response = CashbackAccrualDocsResponse( + docs = listOf(CashbackAccrualDocsResponse.Doc(id = "1", title = "Terms", url = "https://a")), + ) + coEvery { tangemPayApi.getCashbackAccrualDocs(any(), any()) } returns ApiResponse.Success(response) + + // Act + val actual = createRepository().getCashbackAccrualDocs(userWalletId) + + // Assert + assertThat(actual).isEqualTo(CashbackAccrualDocsConverter.convert(response).right()) + } + + @Test + fun `GIVEN backend error WHEN getCashbackAccrualDocs THEN error is propagated`() = runTest { + // Arrange + coEvery { tangemPayApi.getCashbackAccrualDocs(any(), any()) } returns + ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse + + // Act + val actual = createRepository().getCashbackAccrualDocs(userWalletId) + + // Assert + assertThat(actual).isEqualTo(VisaApiError.Unspecified.left()) + } + + private fun createRepository() = DefaultCashbackRepository( + tangemPayApi = tangemPayApi, + requestHelper = requestHelper, + storage = mockk(), + ) + + private companion object { + const val AUTH_HEADER = "auth-header" + } +} \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackAccrualDocsConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackAccrualDocsConverterTest.kt new file mode 100644 index 0000000000..fd60be24a7 --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackAccrualDocsConverterTest.kt @@ -0,0 +1,65 @@ +package com.tangem.data.pay.util + +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse +import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse.Doc +import com.tangem.domain.pay.model.CashbackDocument +import com.tangem.test.core.ProvideTestModels +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CashbackAccrualDocsConverterTest { + + @Test + fun `GIVEN valid docs WHEN convert THEN all docs mapped in order`() { + // Arrange + val response = response( + doc(id = "1", title = "All categories", url = "https://a"), + doc(id = "2", title = "Full terms", url = "https://b"), + ) + + // Act + val result = CashbackAccrualDocsConverter.convert(response) + + // Assert + assertThat(result).containsExactly( + CashbackDocument(id = "1", title = "All categories", url = "https://a"), + CashbackDocument(id = "2", title = "Full terms", url = "https://b"), + ).inOrder() + } + + @ParameterizedTest + @ProvideTestModels + fun `GIVEN doc missing a required field WHEN convert THEN it is dropped`(malformed: Doc) { + // Arrange + val response = response(doc(), malformed) + + // Act + val result = CashbackAccrualDocsConverter.convert(response) + + // Assert + assertThat(result).containsExactly(CashbackDocument(id = "1", title = "Title", url = "https://a")) + } + + @Test + fun `GIVEN null docs WHEN convert THEN empty list`() { + // Act + val result = CashbackAccrualDocsConverter.convert(CashbackAccrualDocsResponse(docs = null)) + + // Assert + assertThat(result).isEmpty() + } + + private fun provideTestModels() = listOf( + doc(id = null), + doc(title = null), + doc(url = null), + ) + + private fun response(vararg docs: Doc) = CashbackAccrualDocsResponse(docs = docs.toList()) + + private fun doc(id: String? = "1", title: String? = "Title", url: String? = "https://a") = + Doc(id = id, title = title, url = url) +} \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverterTest.kt new file mode 100644 index 0000000000..5a2a342f14 --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverterTest.kt @@ -0,0 +1,90 @@ +package com.tangem.data.pay.util + +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse +import com.tangem.domain.pay.model.CashbackPromotions +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class CashbackPromotionsConverterTest { + + @Test + fun `GIVEN tiers WHEN convert THEN each tier mapped with its fields`() { + // Arrange + val response = response(tier(min = BigDecimal("30"), cap = BigDecimal("100"))) + + // Act + val result = CashbackPromotionsConverter.convert(response) + + // Assert + assertThat(result).isEqualTo( + CashbackPromotions( + cardTiers = listOf( + CashbackPromotions.CardTier( + tier = "basic", + label = "Basic", + scope = "All purchases", + minTransactionAmount = BigDecimal("30"), + monthlyCapAmount = BigDecimal("100"), + ), + ), + ), + ) + } + + @Test + fun `GIVEN null cashbackOnCards WHEN convert THEN no card tiers`() { + // Arrange + val response = CashbackPromotionsResponse(cashbackOnCards = null, additionalCashback = null) + + // Act + val result = CashbackPromotionsConverter.convert(response) + + // Assert + assertThat(result.cardTiers).isEmpty() + } + + @Test + fun `GIVEN tier with null strings WHEN convert THEN strings default to empty and amounts stay null`() { + // Arrange + val response = response(tier(tier = null, label = null, scope = null)) + + // Act + val result = CashbackPromotionsConverter.convert(response) + + // Assert + assertThat(result.cardTiers).containsExactly( + CashbackPromotions.CardTier( + tier = "", + label = "", + scope = "", + minTransactionAmount = null, + monthlyCapAmount = null, + ), + ) + } + + private fun response(vararg tiers: CashbackPromotionsResponse.CardTier) = CashbackPromotionsResponse( + cashbackOnCards = CashbackPromotionsResponse.CashbackOnCards( + tiers = tiers.toList(), + monthlyCapAmount = null, + monthlyCapCurrency = null, + ), + additionalCashback = null, + ) + + private fun tier( + tier: String? = "basic", + label: String? = "Basic", + scope: String? = "All purchases", + min: BigDecimal? = null, + cap: BigDecimal? = null, + ) = CashbackPromotionsResponse.CardTier( + tier = tier, + label = label, + scope = scope, + minTransactionAmount = min, + tierMonthlyCapAmount = cap, + promotionId = null, + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackAccrualsConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackAccrualsConverterTest.kt new file mode 100644 index 0000000000..d875bb4fd9 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackAccrualsConverterTest.kt @@ -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") + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDetailsConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDetailsConverterTest.kt new file mode 100644 index 0000000000..5ae2df9fd0 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDetailsConverterTest.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackInfoTilesConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackInfoTilesConverterTest.kt new file mode 100644 index 0000000000..0a232a6454 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackInfoTilesConverterTest.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModelTest.kt new file mode 100644 index 0000000000..febeef9bc1 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModelTest.kt @@ -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 { + every { type } returns planType + every { name } returns planType.name + } + val customerTariffPlanMock = mockk { every { plan } returns tariffPlanMock } + return mockk { every { tariffPlan } returns customerTariffPlanMock } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackTiersConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackTiersConverterTest.kt new file mode 100644 index 0000000000..76e614d636 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackTiersConverterTest.kt @@ -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), + ) +} \ No newline at end of file