Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 15:25:28 +03:00
parent 25444c3a2d
commit 2ee2edeeef
18 changed files with 255 additions and 14 deletions

View file

@ -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<VisaApiError, CashbackPromotions> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getCashbackPromotions(
authHeader = authHeader,
language = SupportedLanguages.getCurrentSupportedLanguageCode(),
)
}.map(CashbackPromotionsConverter::convert)
}
override suspend fun getCashbackAccrualDocs(
userWalletId: UserWalletId,
): Either<VisaApiError, List<CashbackDocument>> {
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)

View file

@ -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<CashbackAccrualDocsResponse, List<CashbackDocument>> {
override fun convert(value: CashbackAccrualDocsResponse): List<CashbackDocument> {
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,
)
}
}

View file

@ -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<CashbackPromotionsResponse, CashbackPromotions> {
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,
)
}
}

View file

@ -30,7 +30,10 @@ internal object CashbackSummaryConverter : Converter<CashbackSummaryResponse, Ca
displayMode = CashbackDisplayMode.fromString(value.cashbackDisplayMode),
cashback = TangemPayCashback(
confirmedAmount = value.confirmedAmount ?: BigDecimal.ZERO,
pendingAmount = value.pendingAmount ?: BigDecimal.ZERO,
currency = value.currency.orEmpty(),
payoutCurrency = value.payoutCurrency.orEmpty(),
payoutNetwork = value.payoutNetwork.orEmpty(),
period = TangemPayCashback.Period(
year = period.year,
month = period.month,
@ -38,7 +41,6 @@ internal object CashbackSummaryConverter : Converter<CashbackSummaryResponse, Ca
payoutEnd = payoutEnd,
),
),
pendingAmount = value.pendingAmount ?: BigDecimal.ZERO,
)
}
}

View file

@ -7,6 +7,8 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
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.CashbackPromotions
import com.tangem.domain.pay.model.CashbackSummary
import com.tangem.domain.pay.model.TangemPayCashback
import com.tangem.domain.pay.repository.CashbackRepository
@ -16,7 +18,7 @@ import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
/** In MOCK env returns a canned cashback summary; otherwise delegates to [DefaultCashbackRepository]. */
/** In MOCK env returns canned cashback data; otherwise delegates to [DefaultCashbackRepository]. */
@Singleton
internal class MockAwareCashbackRepository @Inject constructor(
private val real: DefaultCashbackRepository,
@ -33,6 +35,20 @@ internal class MockAwareCashbackRepository @Inject constructor(
return real.getCashbackSummary(userWalletId)
}
override suspend fun getCashbackPromotions(
userWalletId: UserWalletId,
): Either<VisaApiError, CashbackPromotions> {
if (isMockMode) return MOCK_PROMOTIONS.right()
return real.getCashbackPromotions(userWalletId)
}
override suspend fun getCashbackAccrualDocs(
userWalletId: UserWalletId,
): Either<VisaApiError, List<CashbackDocument>> {
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",
),
)
}
}

View file

@ -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,