Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 17:43:51 +03:00
parent 15a518c576
commit 78cb0fc750
18 changed files with 747 additions and 15 deletions

View file

@ -8,6 +8,9 @@ internal class TangemPayCashbackDateFormatter {
fun formatMonth(year: Int, month: Int): String =
DateTimeFormatters.formatDate(DateTime(year, month, 1, 0, 0), DateTimeFormatters.dateMMMM)
fun formatShortMonth(year: Int, month: Int): String =
DateTimeFormatters.formatDate(DateTime(year, month, 1, 0, 0), DateTimeFormatters.dateMMM)
fun formatMonthDay(date: DateTime): String = DateTimeFormatters.formatDate(date, DateTimeFormatters.dateMMMMd)
fun formatWindow(start: DateTime, end: DateTime): String {

View file

@ -0,0 +1,44 @@
package com.tangem.features.tangempay.cashback.impl.model
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.defaultAmount
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.format.bigdecimal.optionalDecimals
import com.tangem.domain.pay.model.CashbackHistory
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM.Style
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
internal class TangemPayCashbackHistogramConverter(
private val dateFormatter: TangemPayCashbackDateFormatter = TangemPayCashbackDateFormatter(),
) : Converter<CashbackHistory, TangemPayCashbackHistogramUM> {
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
override fun convert(value: CashbackHistory): TangemPayCashbackHistogramUM {
val currency = getJavaCurrencyByCode(value.currency)
val total = value.months.fold(BigDecimal.ZERO) { acc, month -> acc + month.confirmedAmount }
val formattedTotal = total.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }
val lastIndex = value.months.lastIndex
return TangemPayCashbackHistogramUM(
title = stringReference("$formattedTotal earned in total"),
bars = value.months.mapIndexed { index, month ->
TangemPayCashbackHistogramUM.Bar(
month = stringReference(dateFormatter.formatShortMonth(month.year, month.month)),
amount = stringReference(
month.confirmedAmount.format { fiat(currency.currencyCode, currency.symbol).defaultAmount() },
),
amountValue = month.confirmedAmount.toFloat(),
style = when {
index != lastIndex -> Style.Regular
month.confirmedAmount.signum() < 0 -> Style.HighlightedNegative
else -> Style.Highlighted
},
)
}.toImmutableList(),
)
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.model.CashbackDocument
import com.tangem.domain.pay.model.CashbackHistory
import com.tangem.domain.pay.model.CashbackPromotions
import com.tangem.domain.pay.model.CashbackSummary
import com.tangem.domain.pay.repository.CashbackRepository
@ -26,6 +27,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
private const val CASHBACK_HISTORY_MONTHS = 5
@Stable
@ModelScoped
internal class TangemPayCashbackModel @Inject constructor(
@ -43,6 +46,7 @@ internal class TangemPayCashbackModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<TangemPayCashbackNavigation> = SlotNavigation()
private val cashbackConverter = TangemPayCashbackUmConverter(onCloseClick = router::pop)
private val histogramConverter = TangemPayCashbackHistogramConverter()
private val tiersConverter = TangemPayCashbackTiersConverter()
private val infoTilesConverter = TangemPayCashbackInfoTilesConverter(
onRateClick = { bottomSheetNavigation.activate(TangemPayCashbackNavigation.Details) },
@ -62,6 +66,7 @@ internal class TangemPayCashbackModel @Inject constructor(
TangemPayCashbackScreenUM(
cashback = cashbackConverter.convert(value = null),
infoTiles = null,
histogram = null,
),
)
@ -77,6 +82,7 @@ internal class TangemPayCashbackModel @Inject constructor(
val planDeferred = async { loadPlan() }
val summary = summaryDeferred.await()
val history = if (summary is CashbackSummary.Enabled) loadHistory() else null
val promotions = promotionsDeferred.await()
val plan = planDeferred.await()
val tiers = promotions?.let(tiersConverter::convert).orEmpty()
@ -89,6 +95,7 @@ internal class TangemPayCashbackModel @Inject constructor(
currentPlan = plan,
)
},
histogram = history?.takeIf { it.months.isNotEmpty() }?.let(histogramConverter::convert),
)
detailsSheet.value = detailsConverter.convert(tiers)
accrualsSheet.value = accrualsConverter.convert(docsDeferred.await())
@ -98,6 +105,10 @@ internal class TangemPayCashbackModel @Inject constructor(
private suspend fun loadSummary(): CashbackSummary? =
runSuspendCatching { cashbackRepository.getCashbackSummary(userWalletId).getOrNull() }.getOrNull()
private suspend fun loadHistory(): CashbackHistory? = runSuspendCatching {
cashbackRepository.getCashbackHistory(userWalletId, CASHBACK_HISTORY_MONTHS).getOrNull()
}.getOrNull()
private suspend fun loadPromotions(): CashbackPromotions? =
runSuspendCatching { cashbackRepository.getCashbackPromotions(userWalletId).getOrNull() }.getOrNull()

View file

@ -0,0 +1,205 @@
package com.tangem.features.tangempay.cashback.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM.Style
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun TangemPayCashbackHistogram(state: TangemPayCashbackHistogramUM, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
.padding(16.dp),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
}
Chart(bars = state.bars)
}
}
@Composable
private fun Chart(bars: ImmutableList<TangemPayCashbackHistogramUM.Bar>, modifier: Modifier = Modifier) {
val maxValue = bars.maxOfOrNull { it.amountValue }?.coerceAtLeast(0f) ?: 0f
Column(modifier = modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.Bottom,
) {
bars.forEach { bar ->
BarColumn(bar = bar, maxValue = maxValue, modifier = Modifier.weight(1f))
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(TangemTheme.colors3.border.primary),
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
bars.forEach { bar ->
Text(
text = bar.month.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = monthColor(bar.style),
textAlign = TextAlign.Center,
modifier = Modifier.weight(1f),
)
}
}
}
}
@Composable
private fun BarColumn(bar: TangemPayCashbackHistogramUM.Bar, maxValue: Float, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
if (bar.style != Style.Regular || bar.amountValue != 0f) {
Text(
text = bar.amount.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = amountColor(bar.style),
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(4.dp))
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(barHeight(value = bar.amountValue, maxValue = maxValue))
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.background(barColor(bar.style)),
)
}
}
private fun barHeight(value: Float, maxValue: Float): Dp {
if (maxValue <= 0f) return 3.dp
val fraction = (value / maxValue).coerceIn(0f, 1f)
return maxOf(3.dp, 115.dp * fraction)
}
@Composable
private fun barColor(style: Style): Color = when (style) {
Style.Regular -> TangemTheme.colors3.bg.opaque.secondary
Style.Highlighted -> TangemTheme.colors3.bg.brand
Style.HighlightedNegative -> TangemTheme.colors3.bg.status.error
}
@Composable
private fun amountColor(style: Style): Color = when (style) {
Style.Regular -> TangemTheme.colors3.text.tertiary
else -> TangemTheme.colors3.text.primary
}
@Composable
private fun monthColor(style: Style): Color = when (style) {
Style.Regular -> TangemTheme.colors3.text.tertiary
else -> TangemTheme.colors3.text.secondary
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCashbackHistogramPreview(
@PreviewParameter(TangemPayCashbackHistogramUMProvider::class) state: TangemPayCashbackHistogramUM,
) {
TangemThemePreviewRedesign {
TangemPayCashbackHistogram(
state = state,
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.primary),
)
}
}
@Suppress("MagicNumber")
private class TangemPayCashbackHistogramUMProvider : CollectionPreviewParameterProvider<TangemPayCashbackHistogramUM>(
collection = listOf(
// Populated
TangemPayCashbackHistogramUM(
title = stringReference("$132.15 earned in total"),
bars = persistentListOf(
bar(month = "Feb", amount = "$12.02", value = 12.02f, style = Style.Regular),
bar(month = "Mar", amount = "$44.22", value = 44.22f, style = Style.Regular),
bar(month = "Apr", amount = "$38.52", value = 38.52f, style = Style.Regular),
bar(month = "May", amount = "$26.10", value = 26.10f, style = Style.Regular),
bar(month = "Jun", amount = "$32.15", value = 32.15f, style = Style.Highlighted),
),
),
// Negative current month
TangemPayCashbackHistogramUM(
title = stringReference("$132.15 earned in total"),
bars = persistentListOf(
bar(month = "Feb", amount = "$12.02", value = 12.02f, style = Style.Regular),
bar(month = "Mar", amount = "$44.22", value = 44.22f, style = Style.Regular),
bar(month = "Apr", amount = "$38.52", value = 38.52f, style = Style.Regular),
bar(month = "May", amount = "$26.10", value = 26.10f, style = Style.Regular),
bar(month = "Jun", amount = "-$2.15", value = -2.15f, style = Style.HighlightedNegative),
),
),
// Empty
TangemPayCashbackHistogramUM(
title = stringReference("$0 earned in total"),
bars = persistentListOf(
bar(month = "Feb", amount = "$0.00", value = 0f, style = Style.Regular),
bar(month = "Mar", amount = "$0.00", value = 0f, style = Style.Regular),
bar(month = "Apr", amount = "$0.00", value = 0f, style = Style.Regular),
bar(month = "May", amount = "$0.00", value = 0f, style = Style.Regular),
bar(month = "Jun", amount = "$0.00", value = 0f, style = Style.Highlighted),
),
),
),
)
private fun bar(month: String, amount: String, value: Float, style: Style) = TangemPayCashbackHistogramUM.Bar(
month = stringReference(month),
amount = stringReference(amount),
amountValue = value,
style = style,
)

View file

@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -35,10 +37,13 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM.Style
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackInfoTilesUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackScreenUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
import dev.chrisbanes.haze.HazeStyle
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.R as CoreUiR
private const val GLOW_RADIUS_FACTOR = 0.585f
@ -62,20 +67,33 @@ internal fun TangemPayCashbackScreen(state: TangemPayCashbackScreenUM, modifier:
contentAlign = TangemTopNavigation.ContentAlign.Center,
onClose = state.cashback.onCloseClick,
)
HeroBlock(state = state.cashback)
state.cashback.banner?.let { banner ->
CashbackBanner(
banner = banner,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
state.infoTiles?.let { infoTiles ->
TangemPayCashbackInfoTiles(
state = infoTiles,
modifier = Modifier.padding(top = 24.dp),
)
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
HeroBlock(state = state.cashback)
state.cashback.banner?.let { banner ->
CashbackBanner(
banner = banner,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
state.infoTiles?.let { infoTiles ->
TangemPayCashbackInfoTiles(
state = infoTiles,
modifier = Modifier.padding(top = 24.dp),
)
}
state.histogram?.let { histogram ->
TangemPayCashbackHistogram(
state = histogram,
modifier = Modifier.padding(top = 24.dp),
)
}
}
}
}
@ -193,6 +211,7 @@ private class TangemPayCashbackScreenUMProvider : CollectionPreviewParameterProv
onCloseClick = {},
),
infoTiles = previewInfoTiles(),
histogram = previewHistogram(),
),
TangemPayCashbackScreenUM(
cashback = TangemPayCashbackUM(
@ -208,6 +227,7 @@ private class TangemPayCashbackScreenUMProvider : CollectionPreviewParameterProv
onCloseClick = {},
),
infoTiles = null,
histogram = null,
),
TangemPayCashbackScreenUM(
cashback = TangemPayCashbackUM(
@ -218,6 +238,7 @@ private class TangemPayCashbackScreenUMProvider : CollectionPreviewParameterProv
onCloseClick = {},
),
infoTiles = null,
histogram = null,
),
),
)
@ -235,4 +256,24 @@ private fun previewInfoTiles() = TangemPayCashbackInfoTilesUM(
subtitle = stringReference("Limits and exceptions"),
onClick = {},
),
)
)
@Suppress("MagicNumber")
private fun previewHistogram(): TangemPayCashbackHistogramUM {
fun bar(month: String, amount: String, value: Float, style: Style) = TangemPayCashbackHistogramUM.Bar(
month = stringReference(month),
amount = stringReference(amount),
amountValue = value,
style = style,
)
return TangemPayCashbackHistogramUM(
title = stringReference("$132.15 earned in total"),
bars = persistentListOf(
bar(month = "Feb", amount = "$12.02", value = 12.02f, style = Style.Regular),
bar(month = "Mar", amount = "$44.22", value = 44.22f, style = Style.Regular),
bar(month = "Apr", amount = "$38.52", value = 38.52f, style = Style.Regular),
bar(month = "May", amount = "$26.10", value = 26.10f, style = Style.Regular),
bar(month = "Jun", amount = "$32.15", value = 32.15f, style = Style.Highlighted),
),
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.tangempay.cashback.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* State for the monthly earnings histogram on the Cashback screen.
*
* @property title formatted total, e.g. "$132.15 earned in total"
* @property bars one bar per calendar month, ordered oldest to newest
*/
@Immutable
internal data class TangemPayCashbackHistogramUM(
val title: TextReference,
val bars: ImmutableList<Bar>,
) {
/**
* @property amount formatted earnings for this month, e.g. "$12.02" (or "-$2.15" for a refund)
* @property amountValue numeric earnings, used only to size the bar relative to the others
* @property style visual treatment; only the current (last) month is highlighted
*/
@Immutable
data class Bar(
val month: TextReference,
val amount: TextReference,
val amountValue: Float,
val style: Style,
)
enum class Style { Regular, Highlighted, HighlightedNegative }
}

View file

@ -6,4 +6,5 @@ import androidx.compose.runtime.Immutable
internal data class TangemPayCashbackScreenUM(
val cashback: TangemPayCashbackUM,
val infoTiles: TangemPayCashbackInfoTilesUM?,
val histogram: TangemPayCashbackHistogramUM?,
)

View file

@ -0,0 +1,106 @@
package com.tangem.features.tangempay.cashback.impl.model
import android.text.format.DateFormat
import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.pay.model.CashbackHistory
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackHistogramUM.Style
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
import java.util.Locale
@Suppress("MagicNumber")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayCashbackHistogramConverterTest {
private val defaultLocale = Locale.getDefault()
private val converter = TangemPayCashbackHistogramConverter()
@BeforeEach
fun setup() {
Locale.setDefault(Locale.US)
mockkStatic(DateFormat::class)
every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
}
@AfterEach
fun tearDown() {
unmockkStatic(DateFormat::class)
Locale.setDefault(defaultLocale)
}
@Test
fun `GIVEN history WHEN convert THEN total sums months and last month highlighted`() {
// Arrange
val history = CashbackHistory(
currency = "USD",
months = listOf(
month(2, BigDecimal("12.02")),
month(3, BigDecimal("44.22")),
month(6, BigDecimal("32.15")),
),
)
// Act
val actual = converter.convert(history)
// Assert
val expected = TangemPayCashbackHistogramUM(
title = stringReference("$88.39 earned in total"),
bars = persistentListOf(
bar("Feb", "$12.02", 12.02f, Style.Regular),
bar("Mar", "$44.22", 44.22f, Style.Regular),
bar("Jun", "$32.15", 32.15f, Style.Highlighted),
),
)
assertThat(actual).isEqualTo(expected)
}
@Test
fun `GIVEN zero amounts WHEN convert THEN empty total and last month still highlighted`() {
// Arrange
val history = CashbackHistory(
currency = "USD",
months = listOf(month(5, BigDecimal.ZERO), month(6, BigDecimal.ZERO)),
)
// Act
val actual = converter.convert(history)
// Assert
assertThat(actual.title).isEqualTo(stringReference("$0 earned in total"))
assertThat(actual.bars.map { it.style }).containsExactly(Style.Regular, Style.Highlighted).inOrder()
assertThat(actual.bars.last().amount).isEqualTo(stringReference("$0.00"))
}
@Test
fun `GIVEN negative last month WHEN convert THEN last bar highlighted negative`() {
// Arrange
val history = CashbackHistory(
currency = "USD",
months = listOf(month(5, BigDecimal("26.10")), month(6, BigDecimal("-2.15"))),
)
// Act
val actual = converter.convert(history)
// Assert
assertThat(actual.title).isEqualTo(stringReference("$23.95 earned in total"))
assertThat(actual.bars.last().style).isEqualTo(Style.HighlightedNegative)
assertThat(actual.bars.last().amount).isEqualTo(stringReference("-$2.15"))
}
private fun bar(month: String, amount: String, value: Float, style: Style) =
TangemPayCashbackHistogramUM.Bar(stringReference(month), stringReference(amount), value, style)
private fun month(month: Int, amount: BigDecimal) =
CashbackHistory.MonthlyCashback(year = 2026, month = month, confirmedAmount = amount)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.cashback.impl.model
import android.text.format.DateFormat
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.core.decompose.model.MutableParamsContainer
@ -9,18 +10,25 @@ 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.CashbackDisplayMode
import com.tangem.domain.pay.model.CashbackDocument
import com.tangem.domain.pay.model.CashbackHistory
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.model.TangemPayCashback
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.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -40,6 +48,8 @@ internal class TangemPayCashbackModelTest {
@BeforeEach
fun setup() {
Locale.setDefault(Locale.US)
mockkStatic(DateFormat::class)
every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
clearMocks(cashbackRepository, onboardingRepository)
coEvery { cashbackRepository.getCashbackSummary(any()) } returns CashbackSummary.Disabled.right()
coEvery { cashbackRepository.getCashbackPromotions(any()) } returns promotions().right()
@ -50,6 +60,7 @@ internal class TangemPayCashbackModelTest {
@AfterEach
fun tearDown() {
unmockkStatic(DateFormat::class)
Locale.setDefault(defaultLocale)
}
@ -129,6 +140,30 @@ internal class TangemPayCashbackModelTest {
assertThat(model.accrualsSheet.value.infoRows).isNotEmpty()
}
@Test
fun `GIVEN enabled summary and history WHEN model created THEN histogram populated`() {
// Arrange
coEvery { cashbackRepository.getCashbackSummary(any()) } returns enabledSummary().right()
coEvery { cashbackRepository.getCashbackHistory(any(), any()) } returns history().right()
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.histogram).isNotNull()
assertThat(model.uiState.value.histogram?.bars).hasSize(2)
}
@Test
fun `GIVEN disabled summary WHEN model created THEN history not requested and histogram null`() {
// Act
val model = createModel()
// Assert
assertThat(model.uiState.value.histogram).isNull()
coVerify(exactly = 0) { cashbackRepository.getCashbackHistory(any(), any()) }
}
private fun createModel() = TangemPayCashbackModel(
dispatchers = TestingCoroutineDispatcherProvider(),
paramsContainer = MutableParamsContainer(TangemPayCashbackComponent.Params(userWalletId = userWalletId)),
@ -162,6 +197,31 @@ internal class TangemPayCashbackModelTest {
CashbackDocument(id = "terms", title = "Full terms of cashback program", url = "https://x/terms.pdf"),
)
private fun enabledSummary() = CashbackSummary.Enabled(
displayMode = CashbackDisplayMode.FULL,
cashback = TangemPayCashback(
confirmedAmount = BigDecimal("32.15"),
pendingAmount = BigDecimal.ZERO,
currency = "USD",
payoutCurrency = "USDC",
payoutNetwork = "Polygon",
period = TangemPayCashback.Period(
year = 2026,
month = 6,
payoutStart = DateTime.parse("2026-07-02"),
payoutEnd = DateTime.parse("2026-07-05"),
),
),
)
private fun history() = CashbackHistory(
currency = "USD",
months = listOf(
CashbackHistory.MonthlyCashback(year = 2026, month = 5, confirmedAmount = BigDecimal("26.10")),
CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("32.15")),
),
)
private fun customerInfo(tierId: String, planName: String): CustomerInfo {
val currentPlan = TangemPayTariffPlan(
id = "plan-$tierId",