Updated on 2026-08-14
This commit is contained in:
parent
25444c3a2d
commit
2ee2edeeef
18 changed files with 255 additions and 14 deletions
|
|
@ -190,4 +190,16 @@ interface TangemPayApi {
|
|||
|
||||
@GET("v1/customer/cashback/summary")
|
||||
suspend fun getCashbackSummary(@Header("Authorization") authHeader: String): ApiResponse<CashbackSummaryResponse>
|
||||
|
||||
@GET("v1/customer/cashback/promotions")
|
||||
suspend fun getCashbackPromotions(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Header("Accept-Language") language: String,
|
||||
): ApiResponse<CashbackPromotionsResponse>
|
||||
|
||||
@GET("v1/customer/cashback/accruals/docs")
|
||||
suspend fun getCashbackAccrualDocs(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Header("Accept-Language") language: String,
|
||||
): ApiResponse<CashbackAccrualDocsResponse>
|
||||
}
|
||||
|
|
@ -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<Doc>?,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Doc(
|
||||
@Json(name = "id") val id: String?,
|
||||
@Json(name = "title") val title: String?,
|
||||
@Json(name = "url") val url: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<AdditionalCashback>?,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CashbackOnCards(
|
||||
@Json(name = "tiers") val tiers: List<CardTier>?,
|
||||
@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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<CardTier>,
|
||||
) {
|
||||
|
||||
data class CardTier(
|
||||
val tier: String,
|
||||
val label: String,
|
||||
val scope: String,
|
||||
val minTransactionAmount: BigDecimal?,
|
||||
val monthlyCapAmount: BigDecimal?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<VisaApiError, CashbackSummary>
|
||||
|
||||
suspend fun getCashbackPromotions(userWalletId: UserWalletId): Either<VisaApiError, CashbackPromotions>
|
||||
|
||||
suspend fun getCashbackAccrualDocs(userWalletId: UserWalletId): Either<VisaApiError, List<CashbackDocument>>
|
||||
|
||||
/** Whether the "Cashback deactivated" banner was permanently dismissed for [userWalletId]. */
|
||||
suspend fun isDeactivationBannerDismissed(userWalletId: UserWalletId): Boolean
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue