From 78cb0fc750da5bbac360262c35fe3e79f84aab7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jul 2026 17:43:51 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 6 + .../response/CashbackHistoryResponse.kt | 25 +++ .../core/ui/utils/DateTimeFormatters.kt | 7 + .../repository/DefaultCashbackRepository.kt | 11 + .../data/pay/util/CashbackHistoryConverter.kt | 23 ++ .../repository/MockAwareCashbackRepository.kt | 20 ++ .../pay/util/CashbackHistoryConverterTest.kt | 104 +++++++++ .../domain/pay/model/CashbackHistory.kt | 24 ++ .../pay/repository/CashbackRepository.kt | 8 + .../model/TangemPayCashbackDateFormatter.kt | 3 + .../TangemPayCashbackHistogramConverter.kt | 44 ++++ .../impl/model/TangemPayCashbackModel.kt | 11 + .../impl/ui/TangemPayCashbackHistogram.kt | 205 ++++++++++++++++++ .../impl/ui/TangemPayCashbackScreen.kt | 71 ++++-- .../ui/state/TangemPayCashbackHistogramUM.kt | 33 +++ .../ui/state/TangemPayCashbackScreenUM.kt | 1 + ...TangemPayCashbackHistogramConverterTest.kt | 106 +++++++++ .../impl/model/TangemPayCashbackModelTest.kt | 60 +++++ 18 files changed, 747 insertions(+), 15 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackHistoryResponse.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackHistoryConverter.kt create mode 100644 data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackHistoryConverterTest.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackHistory.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverter.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackHistogram.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackHistogramUM.kt create mode 100644 features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverterTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 073a298504..32badb05e6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -202,4 +202,10 @@ interface TangemPayApi { @Header("Authorization") authHeader: String, @Header("Accept-Language") language: String, ): ApiResponse + + @GET("v1/customer/cashback/history") + suspend fun getCashbackHistory( + @Header("Authorization") authHeader: String, + @Query("months") months: Int, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackHistoryResponse.kt new file mode 100644 index 0000000000..672d91fe6d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackHistoryResponse.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +/** + * Response from `GET /v1/customer/cashback/history`. + * + * Confirmed cashback grouped by calendar month, ordered oldest to newest. Drives the monthly + * earnings histogram on the Cashback screen. + */ +@JsonClass(generateAdapter = true) +data class CashbackHistoryResponse( + @Json(name = "currency") val currency: String?, + @Json(name = "items") val items: List?, +) { + + @JsonClass(generateAdapter = true) + data class Item( + @Json(name = "year") val year: Int, + @Json(name = "month") val month: Int, + @Json(name = "confirmed_amount") val confirmedAmount: BigDecimal?, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index e7630f1f28..3b70d4ffd6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -115,6 +115,13 @@ object DateTimeFormatters { getBestFormatterBySkeleton("MMMM") } + /** + * Example: "Jun" + */ + val dateMMM: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("MMM") + } + /** * Example: "June 1" */ diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepository.kt index 1b83e2d96a..27e121ef41 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCashbackRepository.kt @@ -3,11 +3,13 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.data.pay.store.TangemPayStorage import com.tangem.data.pay.util.CashbackAccrualDocsConverter +import com.tangem.data.pay.util.CashbackHistoryConverter import com.tangem.data.pay.util.CashbackPromotionsConverter import com.tangem.data.pay.util.CashbackSummaryConverter import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.domain.models.wallet.UserWalletId 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 @@ -47,6 +49,15 @@ internal class DefaultCashbackRepository @Inject constructor( }.map(CashbackAccrualDocsConverter::convert) } + override suspend fun getCashbackHistory( + userWalletId: UserWalletId, + months: Int, + ): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getCashbackHistory(authHeader = authHeader, months = months) + }.map(CashbackHistoryConverter::convert) + } + override suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean { val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId) return storage.getCashbackDeactivationDismissed(customerWalletAddress) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackHistoryConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackHistoryConverter.kt new file mode 100644 index 0000000000..396103475d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackHistoryConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.pay.util + +import com.tangem.datasource.api.pay.models.response.CashbackHistoryResponse +import com.tangem.domain.pay.model.CashbackHistory +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +/** Maps [CashbackHistoryResponse] (BFF) to the domain [CashbackHistory]. */ +internal object CashbackHistoryConverter : Converter { + + override fun convert(value: CashbackHistoryResponse): CashbackHistory { + return CashbackHistory( + currency = value.currency.orEmpty(), + months = value.items.orEmpty().map { item -> + CashbackHistory.MonthlyCashback( + year = item.year, + month = item.month, + confirmedAmount = item.confirmedAmount ?: BigDecimal.ZERO, + ) + }, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareCashbackRepository.kt b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareCashbackRepository.kt index 3862081f6a..c8289a67f9 100644 --- a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareCashbackRepository.kt +++ b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareCashbackRepository.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager 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.TangemPayCashback @@ -49,6 +50,14 @@ internal class MockAwareCashbackRepository @Inject constructor( return real.getCashbackAccrualDocs(userWalletId) } + override suspend fun getCashbackHistory( + userWalletId: UserWalletId, + months: Int, + ): Either { + if (isMockMode) return MOCK_HISTORY.copy(months = MOCK_HISTORY.months.takeLast(months)).right() + return real.getCashbackHistory(userWalletId, months) + } + override suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean = real.isDeactivationBannerDismissed(userWalletId) @@ -104,5 +113,16 @@ internal class MockAwareCashbackRepository @Inject constructor( url = "https://tangem.com/docs/en/tangem-pay-cashback-terms.pdf", ), ) + + val MOCK_HISTORY = CashbackHistory( + currency = "USD", + months = listOf( + CashbackHistory.MonthlyCashback(year = 2026, month = 2, confirmedAmount = BigDecimal("12.02")), + CashbackHistory.MonthlyCashback(year = 2026, month = 3, confirmedAmount = BigDecimal("44.22")), + CashbackHistory.MonthlyCashback(year = 2026, month = 4, confirmedAmount = BigDecimal("38.52")), + CashbackHistory.MonthlyCashback(year = 2026, month = 5, confirmedAmount = BigDecimal("26.10")), + CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")), + ), + ) } } \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackHistoryConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackHistoryConverterTest.kt new file mode 100644 index 0000000000..5b451ccd42 --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackHistoryConverterTest.kt @@ -0,0 +1,104 @@ +package com.tangem.data.pay.util + +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.pay.models.response.CashbackHistoryResponse +import com.tangem.domain.pay.model.CashbackHistory +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CashbackHistoryConverterTest { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convert(model: ConvertModel) { + // Act + val actual = CashbackHistoryConverter.convert(model.response) + + // Assert + assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + ConvertModel( + name = "full response -> mapped history ordered oldest to newest", + response = createResponse( + currency = "USD", + items = listOf( + createItem(year = 2026, month = 2, confirmedAmount = BigDecimal("5.00")), + createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")), + ), + ), + expected = CashbackHistory( + currency = "USD", + months = listOf( + CashbackHistory.MonthlyCashback(year = 2026, month = 2, confirmedAmount = BigDecimal("5.00")), + CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")), + ), + ), + ), + ConvertModel( + name = "negative amount preserved for current-month refund", + response = createResponse( + items = listOf(createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("-2.15"))), + ), + expected = CashbackHistory( + currency = "USD", + months = listOf( + CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal("-2.15")), + ), + ), + ), + ConvertModel( + name = "null confirmed_amount -> ZERO", + response = createResponse( + items = listOf(createItem(year = 2026, month = 6, confirmedAmount = null)), + ), + expected = CashbackHistory( + currency = "USD", + months = listOf( + CashbackHistory.MonthlyCashback(year = 2026, month = 6, confirmedAmount = BigDecimal.ZERO), + ), + ), + ), + ConvertModel( + name = "null currency -> empty string", + response = createResponse(currency = null, items = emptyList()), + expected = CashbackHistory(currency = "", months = emptyList()), + ), + ConvertModel( + name = "null items -> empty months", + response = createResponse(items = null), + expected = CashbackHistory(currency = "USD", months = emptyList()), + ), + ) + + internal data class ConvertModel( + val name: String, + val response: CashbackHistoryResponse, + val expected: CashbackHistory, + ) { + override fun toString(): String = name + } + + private companion object { + + fun createResponse( + currency: String? = "USD", + items: List? = listOf( + createItem(year = 2026, month = 6, confirmedAmount = BigDecimal("22.54")), + ), + ) = CashbackHistoryResponse( + currency = currency, + items = items, + ) + + fun createItem(year: Int, month: Int, confirmedAmount: BigDecimal?) = CashbackHistoryResponse.Item( + year = year, + month = month, + confirmedAmount = confirmedAmount, + ) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackHistory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackHistory.kt new file mode 100644 index 0000000000..3e546855b1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackHistory.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.pay.model + +import java.math.BigDecimal + +/** + * Customer cashback history from `GET /v1/customer/cashback/history`. + * + * Confirmed cashback grouped by calendar month. Drives the monthly earnings histogram on the + * Cashback screen. + */ +data class CashbackHistory( + val currency: String, + /** Confirmed cashback per calendar month, ordered oldest to newest. */ + val months: List, +) { + + data class MonthlyCashback( + val year: Int, + /** 1-based calendar month (6 = June). */ + val month: Int, + /** Total confirmed cashback for this month; negative for refunds. */ + val confirmedAmount: BigDecimal, + ) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CashbackRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CashbackRepository.kt index bd2e31b95b..ede7c681b9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CashbackRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CashbackRepository.kt @@ -3,6 +3,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.domain.models.wallet.UserWalletId 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.visa.error.VisaApiError @@ -19,6 +20,13 @@ interface CashbackRepository { suspend fun getCashbackAccrualDocs(userWalletId: UserWalletId): Either> + /** + * Loads the confirmed cashback history for the customer of [userWalletId], grouped by month. + * + * @param months number of calendar months to return, counting back from and including the current month. + */ + suspend fun getCashbackHistory(userWalletId: UserWalletId, months: Int): Either + /** Whether the "Cashback deactivated" banner was permanently dismissed for [userWalletId]. */ suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDateFormatter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDateFormatter.kt index 0fcd238c87..6b8eb416ae 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDateFormatter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackDateFormatter.kt @@ -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 { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverter.kt new file mode 100644 index 0000000000..d45579dbeb --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverter.kt @@ -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 { + + // 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(), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModel.kt index 96a2aa454a..c27354104e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModel.kt @@ -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 = 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() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackHistogram.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackHistogram.kt new file mode 100644 index 0000000000..9136aa0eee --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackHistogram.kt @@ -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, 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( + 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, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackScreen.kt index f58e8f6783..2331337c08 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/TangemPayCashbackScreen.kt @@ -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 = {}, ), -) \ No newline at end of file +) + +@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), + ), + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackHistogramUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackHistogramUM.kt new file mode 100644 index 0000000000..feb4c7c31f --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackHistogramUM.kt @@ -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, +) { + + /** + * @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 } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackScreenUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackScreenUM.kt index f04de5e5d6..466b74d6d9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackScreenUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/ui/state/TangemPayCashbackScreenUM.kt @@ -6,4 +6,5 @@ import androidx.compose.runtime.Immutable internal data class TangemPayCashbackScreenUM( val cashback: TangemPayCashbackUM, val infoTiles: TangemPayCashbackInfoTilesUM?, + val histogram: TangemPayCashbackHistogramUM?, ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverterTest.kt new file mode 100644 index 0000000000..86ef48b6a8 --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackHistogramConverterTest.kt @@ -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) +} \ 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 index 0539d5b783..208e902811 100644 --- 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 @@ -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",