Updated on 2026-08-14
This commit is contained in:
parent
b4d11538ea
commit
454c08b894
21 changed files with 982 additions and 112 deletions
|
|
@ -3,6 +3,7 @@ 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
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/** Maps [CashbackPromotionsResponse] (BFF) to the domain [CashbackPromotions]. */
|
||||
internal object CashbackPromotionsConverter : Converter<CashbackPromotionsResponse, CashbackPromotions> {
|
||||
|
|
@ -10,6 +11,7 @@ internal object CashbackPromotionsConverter : Converter<CashbackPromotionsRespon
|
|||
override fun convert(value: CashbackPromotionsResponse): CashbackPromotions {
|
||||
return CashbackPromotions(
|
||||
cardTiers = value.cashbackOnCards?.tiers.orEmpty().map(::convertTier),
|
||||
additionalCashback = value.additionalCashback.orEmpty().map(::convertAdditional),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -22,4 +24,17 @@ internal object CashbackPromotionsConverter : Converter<CashbackPromotionsRespon
|
|||
monthlyCapAmount = tier.tierMonthlyCapAmount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertAdditional(
|
||||
promo: CashbackPromotionsResponse.AdditionalCashback,
|
||||
): CashbackPromotions.AdditionalCashback {
|
||||
val endDate = promo.endDate?.let { runCatching { DateTime.parse(it) }.getOrNull() }
|
||||
return CashbackPromotions.AdditionalCashback(
|
||||
id = promo.id.orEmpty(),
|
||||
name = promo.name.orEmpty(),
|
||||
description = promo.description.orEmpty(),
|
||||
isPermanent = promo.isPermanent ?: (endDate == null),
|
||||
endDate = endDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +99,29 @@ internal class MockAwareCashbackRepository @Inject constructor(
|
|||
monthlyCapAmount = BigDecimal("300"),
|
||||
),
|
||||
),
|
||||
additionalCashback = listOf(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-permanent",
|
||||
name = "Groceries increase",
|
||||
description = "+1% cashback for groceries stores",
|
||||
isPermanent = true,
|
||||
endDate = null,
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-groceries-2026",
|
||||
name = "Groceries increase",
|
||||
description = "+1% cashback for groceries stores. Max \$10/month",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "promo-cashback-2026",
|
||||
name = "Cashback increase",
|
||||
description = "+2% cashback for groceries stores. Max \$10/month",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val MOCK_DOCS = listOf(
|
||||
|
|
|
|||
|
|
@ -7,16 +7,25 @@ import com.tangem.datasource.api.common.config.ApiConfig
|
|||
import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.models.account.BankCredentials
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** In MOCK env skips local-storage / signing enrollment; server calls go to WireMock. */
|
||||
/**
|
||||
* In MOCK env returns canned onboarding data so the Payment Account shows up as fully loaded on the main
|
||||
* screen (the entry point to the TangemPay details & cashback screens), and skips local-storage / signing
|
||||
* enrollment; remaining server calls go to WireMock.
|
||||
*/
|
||||
@Singleton
|
||||
internal class MockAwareOnboardingRepository @Inject constructor(
|
||||
private val real: DefaultOnboardingRepository,
|
||||
|
|
@ -46,8 +55,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
real.produceInitialData(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
|
||||
real.getCustomerInfo(userWalletId)
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
|
||||
if (isMockMode) return MOCK_CUSTOMER_INFO.right()
|
||||
return real.getCustomerInfo(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getBankCredentials(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -100,8 +111,10 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
real.storeVirtualAccountOrderId(userWalletId, vaOrderId)
|
||||
}
|
||||
|
||||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
||||
real.hasTangemPayInWallet(userWalletId)
|
||||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
|
||||
if (isMockMode) return true.right()
|
||||
return real.hasTangemPayInWallet(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||
real.checkCustomerEligibility()
|
||||
|
|
@ -139,5 +152,52 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
private companion object {
|
||||
const val MOCK_ORDER_ID = "mock-order-id"
|
||||
const val MOCK_VA_ORDER_ID = "mock-va-order-id"
|
||||
|
||||
const val MOCK_CUSTOMER_ID = "mock-customer-id"
|
||||
const val MOCK_CARD_ID = "mock-card-id"
|
||||
const val MOCK_PRODUCT_INSTANCE_ID = "mock-product-instance-id"
|
||||
const val MOCK_CUSTOMER_WALLET_ADDRESS = "0x0000000000000000000000000000000000000002"
|
||||
const val MOCK_TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
const val MOCK_POLYGON_CHAIN_ID = 137L
|
||||
|
||||
val MOCK_CUSTOMER_INFO = CustomerInfo(
|
||||
customerId = MOCK_CUSTOMER_ID,
|
||||
productInstances = listOf(
|
||||
CustomerInfo.ProductInstance(
|
||||
id = MOCK_PRODUCT_INSTANCE_ID,
|
||||
cardId = MOCK_CARD_ID,
|
||||
frozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayName = null,
|
||||
actualCardLimit = null,
|
||||
adminCardLimit = null,
|
||||
status = CustomerInfo.ProductInstance.Status.ACTIVE,
|
||||
specificationDataType = CustomerInfo.ProductInstance.SpecificationDataType.CARD,
|
||||
),
|
||||
),
|
||||
cards = listOf(
|
||||
CustomerInfo.CardInfo(
|
||||
cardId = MOCK_CARD_ID,
|
||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
lastFourDigits = "4242",
|
||||
isPinSet = true,
|
||||
images = emptyList(),
|
||||
),
|
||||
),
|
||||
kycStatus = KycStatus.APPROVED,
|
||||
state = CustomerInfo.State.ACTIVE,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal("123.45"),
|
||||
currency = "USD",
|
||||
),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = "usd-coin",
|
||||
chainId = MOCK_POLYGON_CHAIN_ID,
|
||||
depositAddress = MOCK_CUSTOMER_WALLET_ADDRESS,
|
||||
tokenContractAddress = MOCK_TOKEN_CONTRACT_ADDRESS,
|
||||
balance = BigDecimal("123.45"),
|
||||
),
|
||||
availableForWithdrawal = BigDecimal("123.45"),
|
||||
tariffPlan = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.pay.util
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.pay.models.response.CashbackPromotionsResponse
|
||||
import com.tangem.domain.pay.model.CashbackPromotions
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -28,6 +29,87 @@ internal class CashbackPromotionsConverterTest {
|
|||
monthlyCapAmount = BigDecimal("100"),
|
||||
),
|
||||
),
|
||||
additionalCashback = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback WHEN convert THEN each promo mapped with its fields`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(
|
||||
additional(id = "p1", name = "Groceries", description = "+1%", isPermanent = true, endDate = null),
|
||||
additional(
|
||||
id = "p2",
|
||||
name = "Cashback",
|
||||
description = "+2%",
|
||||
isPermanent = false,
|
||||
endDate = "2026-09-26",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback).containsExactly(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "p1",
|
||||
name = "Groceries",
|
||||
description = "+1%",
|
||||
isPermanent = true,
|
||||
endDate = null,
|
||||
),
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "p2",
|
||||
name = "Cashback",
|
||||
description = "+2%",
|
||||
isPermanent = false,
|
||||
endDate = DateTime.parse("2026-09-26"),
|
||||
),
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback with null isPermanent WHEN convert THEN it is derived from end date`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(
|
||||
additional(isPermanent = null, endDate = null),
|
||||
additional(isPermanent = null, endDate = "2026-09-26"),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback.map { it.isPermanent }).containsExactly(true, false).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN additional cashback with malformed end date WHEN convert THEN end date null and payload kept`() {
|
||||
// Arrange
|
||||
val response = CashbackPromotionsResponse(
|
||||
cashbackOnCards = null,
|
||||
additionalCashback = listOf(additional(isPermanent = false, endDate = "not-a-date")),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = CashbackPromotionsConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result.additionalCashback).containsExactly(
|
||||
CashbackPromotions.AdditionalCashback(
|
||||
id = "id",
|
||||
name = "name",
|
||||
description = "description",
|
||||
isPermanent = false,
|
||||
endDate = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,4 +169,18 @@ internal class CashbackPromotionsConverterTest {
|
|||
tierMonthlyCapAmount = cap,
|
||||
promotionId = null,
|
||||
)
|
||||
|
||||
private fun additional(
|
||||
id: String? = "id",
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
isPermanent: Boolean? = false,
|
||||
endDate: String? = null,
|
||||
) = CashbackPromotionsResponse.AdditionalCashback(
|
||||
id = id,
|
||||
name = name,
|
||||
description = description,
|
||||
isPermanent = isPermanent,
|
||||
endDate = endDate,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue