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 de50b2d81a..073a298504 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 @@ -190,4 +190,16 @@ interface TangemPayApi { @GET("v1/customer/cashback/summary") suspend fun getCashbackSummary(@Header("Authorization") authHeader: String): ApiResponse + + @GET("v1/customer/cashback/promotions") + suspend fun getCashbackPromotions( + @Header("Authorization") authHeader: String, + @Header("Accept-Language") language: String, + ): ApiResponse + + @GET("v1/customer/cashback/accruals/docs") + suspend fun getCashbackAccrualDocs( + @Header("Authorization") authHeader: String, + @Header("Accept-Language") language: String, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackAccrualDocsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackAccrualDocsResponse.kt new file mode 100644 index 0000000000..0a5eaa5719 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackAccrualDocsResponse.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Response from `GET v1/customer/cashback/accruals/docs` — cashback program documents (always English). + */ +@JsonClass(generateAdapter = true) +data class CashbackAccrualDocsResponse( + @Json(name = "docs") val docs: List?, +) { + + @JsonClass(generateAdapter = true) + data class Doc( + @Json(name = "id") val id: String?, + @Json(name = "title") val title: String?, + @Json(name = "url") val url: String?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackPromotionsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackPromotionsResponse.kt new file mode 100644 index 0000000000..4594d721b7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackPromotionsResponse.kt @@ -0,0 +1,39 @@ +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/promotions` — cashback program configuration for the customer. + */ +@JsonClass(generateAdapter = true) +data class CashbackPromotionsResponse( + @Json(name = "cashback_on_cards") val cashbackOnCards: CashbackOnCards?, + @Json(name = "additional_cashback") val additionalCashback: List?, +) { + + @JsonClass(generateAdapter = true) + data class CashbackOnCards( + @Json(name = "tiers") val tiers: List?, + @Json(name = "monthly_cap_amount") val monthlyCapAmount: BigDecimal?, + @Json(name = "monthly_cap_currency") val monthlyCapCurrency: String?, + ) + + @JsonClass(generateAdapter = true) + data class CardTier( + @Json(name = "tier") val tier: String?, + @Json(name = "label") val label: String?, + @Json(name = "scope") val scope: String?, + @Json(name = "min_transaction_amount") val minTransactionAmount: BigDecimal?, + @Json(name = "tier_monthly_cap_amount") val tierMonthlyCapAmount: BigDecimal?, + @Json(name = "promotion_id") val promotionId: String?, + ) + + @JsonClass(generateAdapter = true) + data class AdditionalCashback( + @Json(name = "promotion_id") val promotionId: String?, + @Json(name = "label") val label: String?, + @Json(name = "scope") val scope: String?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackSummaryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackSummaryResponse.kt index 381711b83b..90321e58f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackSummaryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CashbackSummaryResponse.kt @@ -17,6 +17,8 @@ data class CashbackSummaryResponse( @Json(name = "confirmed_amount") val confirmedAmount: BigDecimal?, @Json(name = "pending_amount") val pendingAmount: BigDecimal?, @Json(name = "currency") val currency: String?, + @Json(name = "payout_currency") val payoutCurrency: String?, + @Json(name = "payout_network") val payoutNetwork: String?, ) { @JsonClass(generateAdapter = true) 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 6a18ef489c..1b83e2d96a 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 @@ -1,13 +1,18 @@ 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.CashbackPromotionsConverter import com.tangem.data.pay.util.CashbackSummaryConverter import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.data.pay.store.TangemPayStorage 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.repository.CashbackRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.utils.SupportedLanguages import javax.inject.Inject internal class DefaultCashbackRepository @Inject constructor( @@ -22,6 +27,26 @@ internal class DefaultCashbackRepository @Inject constructor( }.map(CashbackSummaryConverter::convert) } + override suspend fun getCashbackPromotions(userWalletId: UserWalletId): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getCashbackPromotions( + authHeader = authHeader, + language = SupportedLanguages.getCurrentSupportedLanguageCode(), + ) + }.map(CashbackPromotionsConverter::convert) + } + + override suspend fun getCashbackAccrualDocs( + userWalletId: UserWalletId, + ): Either> { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getCashbackAccrualDocs( + authHeader = authHeader, + language = SupportedLanguages.getCurrentSupportedLanguageCode(), + ) + }.map(CashbackAccrualDocsConverter::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/CashbackAccrualDocsConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackAccrualDocsConverter.kt new file mode 100644 index 0000000000..5e4c5e0faa --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackAccrualDocsConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.pay.util + +import com.tangem.datasource.api.pay.models.response.CashbackAccrualDocsResponse +import com.tangem.domain.pay.model.CashbackDocument +import com.tangem.utils.converter.Converter + +/** Maps [CashbackAccrualDocsResponse] (BFF) to domain [CashbackDocument]s, dropping malformed entries. */ +internal object CashbackAccrualDocsConverter : Converter> { + + override fun convert(value: CashbackAccrualDocsResponse): List { + return value.docs.orEmpty().mapNotNull(::convertDoc) + } + + private fun convertDoc(doc: CashbackAccrualDocsResponse.Doc): CashbackDocument? { + return CashbackDocument( + id = doc.id ?: return null, + title = doc.title ?: return null, + url = doc.url ?: return null, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverter.kt new file mode 100644 index 0000000000..e695ec1e64 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackPromotionsConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.data.pay.util + +import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse +import com.tangem.domain.pay.model.CashbackPromotions +import com.tangem.utils.converter.Converter + +/** Maps [CashbackPromotionsResponse] (BFF) to the domain [CashbackPromotions]. */ +internal object CashbackPromotionsConverter : Converter { + + override fun convert(value: CashbackPromotionsResponse): CashbackPromotions { + return CashbackPromotions( + cardTiers = value.cashbackOnCards?.tiers.orEmpty().map(::convertTier), + ) + } + + private fun convertTier(tier: CashbackPromotionsResponse.CardTier): CashbackPromotions.CardTier { + return CashbackPromotions.CardTier( + tier = tier.tier.orEmpty(), + label = tier.label.orEmpty(), + scope = tier.scope.orEmpty(), + minTransactionAmount = tier.minTransactionAmount, + monthlyCapAmount = tier.tierMonthlyCapAmount, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackSummaryConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackSummaryConverter.kt index f626b5cb70..b5a92abb05 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackSummaryConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CashbackSummaryConverter.kt @@ -30,7 +30,10 @@ internal object CashbackSummaryConverter : Converter { + if (isMockMode) return MOCK_PROMOTIONS.right() + return real.getCashbackPromotions(userWalletId) + } + + override suspend fun getCashbackAccrualDocs( + userWalletId: UserWalletId, + ): Either> { + if (isMockMode) return MOCK_DOCS.right() + return real.getCashbackAccrualDocs(userWalletId) + } + override suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean = real.isDeactivationBannerDismissed(userWalletId) @@ -44,7 +60,10 @@ internal class MockAwareCashbackRepository @Inject constructor( displayMode = CashbackDisplayMode.FULL, cashback = TangemPayCashback( confirmedAmount = BigDecimal("22.54"), + pendingAmount = BigDecimal("13.65"), currency = "USD", + payoutCurrency = "USDC", + payoutNetwork = "Polygon", period = TangemPayCashback.Period( year = 2026, month = 6, @@ -52,7 +71,38 @@ internal class MockAwareCashbackRepository @Inject constructor( payoutEnd = DateTime.parse("2026-07-05"), ), ), - pendingAmount = BigDecimal("13.65"), + ) + + val MOCK_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"), + ), + ), + ) + + val MOCK_DOCS = listOf( + CashbackDocument( + id = "excluded", + title = "All categories without cashback", + url = "https://tangem.com/docs/en/tangem-pay-cashback-excluded-mccs.pdf", + ), + CashbackDocument( + id = "terms", + title = "Full terms of cashback program", + url = "https://tangem.com/docs/en/tangem-pay-cashback-terms.pdf", + ), ) } } \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackSummaryConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackSummaryConverterTest.kt index 086e619ed3..700ef03e64 100644 --- a/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackSummaryConverterTest.kt +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/util/CashbackSummaryConverterTest.kt @@ -32,7 +32,6 @@ internal class CashbackSummaryConverterTest { expected = CashbackSummary.Enabled( displayMode = CashbackDisplayMode.FULL, cashback = expectedCashback(), - pendingAmount = BigDecimal("13.65"), ), ), ConvertModel( @@ -41,7 +40,6 @@ internal class CashbackSummaryConverterTest { expected = CashbackSummary.Enabled( displayMode = CashbackDisplayMode.ALT_BLOCK, cashback = expectedCashback(), - pendingAmount = BigDecimal("13.65"), ), ), ConvertModel( @@ -50,7 +48,6 @@ internal class CashbackSummaryConverterTest { expected = CashbackSummary.Enabled( displayMode = CashbackDisplayMode.FULL, cashback = expectedCashback(), - pendingAmount = BigDecimal("13.65"), ), ), ConvertModel( @@ -63,8 +60,7 @@ internal class CashbackSummaryConverterTest { ), expected = CashbackSummary.Enabled( displayMode = CashbackDisplayMode.FULL, - cashback = expectedCashback(confirmedAmount = BigDecimal.ZERO), - pendingAmount = BigDecimal.ZERO, + cashback = expectedCashback(confirmedAmount = BigDecimal.ZERO, pendingAmount = BigDecimal.ZERO), ), ), ConvertModel( @@ -116,6 +112,8 @@ internal class CashbackSummaryConverterTest { confirmedAmount: BigDecimal? = BigDecimal("22.54"), pendingAmount: BigDecimal? = BigDecimal("13.65"), currency: String? = "USD", + payoutCurrency: String? = "USDC", + payoutNetwork: String? = "Polygon", ) = CashbackSummaryResponse( cashbackProgramStatus = status, cashbackDisplayMode = displayMode, @@ -123,11 +121,19 @@ internal class CashbackSummaryConverterTest { confirmedAmount = confirmedAmount, pendingAmount = pendingAmount, currency = currency, + payoutCurrency = payoutCurrency, + payoutNetwork = payoutNetwork, ) - fun expectedCashback(confirmedAmount: BigDecimal = BigDecimal("22.54")) = TangemPayCashback( + fun expectedCashback( + confirmedAmount: BigDecimal = BigDecimal("22.54"), + pendingAmount: BigDecimal = BigDecimal("13.65"), + ) = TangemPayCashback( confirmedAmount = confirmedAmount, + pendingAmount = pendingAmount, currency = "USD", + payoutCurrency = "USDC", + payoutNetwork = "Polygon", period = TangemPayCashback.Period( year = 2026, month = 6, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackDocument.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackDocument.kt new file mode 100644 index 0000000000..88d26cf6de --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackDocument.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.model + +/** Cashback program document, from `GET v1/customer/cashback/accruals/docs`. */ +data class CashbackDocument( + val id: String, + val title: String, + val url: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackPromotions.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackPromotions.kt new file mode 100644 index 0000000000..bc829c5a5b --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackPromotions.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.pay.model + +import java.math.BigDecimal + +/** Cashback program configuration for the customer, from `GET v1/customer/cashback/promotions`. */ +data class CashbackPromotions( + val cardTiers: List, +) { + + data class CardTier( + val tier: String, + val label: String, + val scope: String, + val minTransactionAmount: BigDecimal?, + val monthlyCapAmount: BigDecimal?, + ) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackSummary.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackSummary.kt index 463d6985a5..2499308a60 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackSummary.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CashbackSummary.kt @@ -1,7 +1,5 @@ package com.tangem.domain.pay.model -import java.math.BigDecimal - /** * Customer cashback summary from `GET /v1/customer/cashback/summary`. * @@ -13,7 +11,6 @@ sealed interface CashbackSummary { data class Enabled( val displayMode: CashbackDisplayMode, val cashback: TangemPayCashback, - val pendingAmount: BigDecimal, ) : CashbackSummary /** Customer blocked due to fraud; client shows the "Cashback deactivated" banner. */ diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCashback.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCashback.kt index aa1545971b..984153a256 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCashback.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCashback.kt @@ -5,7 +5,10 @@ import java.math.BigDecimal data class TangemPayCashback( val confirmedAmount: BigDecimal, + val pendingAmount: BigDecimal, val currency: String, + val payoutCurrency: String, + val payoutNetwork: String, val period: Period, ) { data class Period( 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 ca0b4bb904..bd2e31b95b 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 @@ -2,6 +2,8 @@ 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.CashbackPromotions import com.tangem.domain.pay.model.CashbackSummary import com.tangem.domain.visa.error.VisaApiError @@ -13,6 +15,10 @@ interface CashbackRepository { /** Loads the cashback summary for the customer of [userWalletId]. */ suspend fun getCashbackSummary(userWalletId: UserWalletId): Either + suspend fun getCashbackPromotions(userWalletId: UserWalletId): Either + + suspend fun getCashbackAccrualDocs(userWalletId: UserWalletId): 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/TangemPayCashbackModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackModel.kt index 5804d12434..8a8bb76b1a 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 @@ -29,7 +29,10 @@ internal class TangemPayCashbackModel @Inject constructor( // TODO([REDACTED_TASK_KEY]): replace stub with repository load val STUB_CASHBACK = TangemPayCashback( confirmedAmount = BigDecimal("22.54"), + pendingAmount = BigDecimal("13.65"), currency = "USD", + payoutCurrency = "USDC", + payoutNetwork = "Polygon", period = TangemPayCashback.Period( year = 2026, month = 6, diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackUmConverterTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackUmConverterTest.kt index 309467f17c..d73f1af43e 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackUmConverterTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/cashback/impl/model/TangemPayCashbackUmConverterTest.kt @@ -141,7 +141,10 @@ internal class TangemPayCashbackUmConverterTest { payoutEnd: DateTime = DateTime.parse("2026-07-05"), ): TangemPayCashback = TangemPayCashback( confirmedAmount = confirmedAmount, + pendingAmount = BigDecimal.ZERO, currency = currency, + payoutCurrency = "USDC", + payoutNetwork = "Polygon", period = TangemPayCashback.Period( year = year, month = month, diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/CashbackBlockTransformerTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/CashbackBlockTransformerTest.kt index e4496e5896..ccfed3f4c4 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/CashbackBlockTransformerTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/transformers/CashbackBlockTransformerTest.kt @@ -128,7 +128,10 @@ internal class CashbackBlockTransformerTest { displayMode = CashbackDisplayMode.FULL, cashback = TangemPayCashback( confirmedAmount = confirmedAmount, + pendingAmount = BigDecimal("13.65"), currency = currency, + payoutCurrency = "USDC", + payoutNetwork = "Polygon", period = TangemPayCashback.Period( year = year, month = month, @@ -136,7 +139,6 @@ internal class CashbackBlockTransformerTest { payoutEnd = payoutEnd, ), ), - pendingAmount = BigDecimal("13.65"), ) private fun contentState(): TangemPayDetailsUM = TangemPayDetailsUM(