Updated on 2026-08-14
This commit is contained in:
parent
aef7c97866
commit
eda701fed9
21 changed files with 1006 additions and 40 deletions
|
|
@ -27,6 +27,7 @@ data class CustomerMeResponse(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerTariffPlan(
|
||||
@Json(name = "status") val status: String?,
|
||||
@Json(name = "source") val source: String?,
|
||||
@Json(name = "next_billing_at") val nextBillingAt: String?,
|
||||
@Json(name = "pending_transition_at") val pendingTransitionAt: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: TariffPlan?,
|
||||
|
|
|
|||
|
|
@ -398,13 +398,52 @@ internal interface TangemPayDataModule {
|
|||
issueCardRepository: TangemPayIssueCardRepository,
|
||||
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CreateTariffPlanTransitionOrderUseCase {
|
||||
return CreateTariffPlanTransitionOrderUseCase(
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSubmitTariffTransitionUseCase(
|
||||
createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
|
||||
setPendingTransition: SetTariffPlanPendingTransitionUseCase,
|
||||
): SubmitTariffTransitionUseCase {
|
||||
return SubmitTariffTransitionUseCase(
|
||||
createTransitionOrder = createTransitionOrder,
|
||||
setPendingTransition = setPendingTransition,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCancelTariffTransitionUseCase(
|
||||
cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase,
|
||||
getTariffTransitionUseCase: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
submitTariffTransitionUseCase: SubmitTariffTransitionUseCase,
|
||||
getCurrentTariffUseCase: GetCurrentTariffUseCase,
|
||||
): CancelTariffTransitionUseCase {
|
||||
return CancelTariffTransitionUseCase(
|
||||
cancelTangemPayOrderUseCase = cancelTangemPayOrderUseCase,
|
||||
getTariffTransitionUseCase = getTariffTransitionUseCase,
|
||||
submitTariffTransitionUseCase = submitTariffTransitionUseCase,
|
||||
getCurrentTariffUseCase = getCurrentTariffUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetCurrentTariffUseCase(
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
): GetCurrentTariffUseCase {
|
||||
return GetCurrentTariffUseCase(
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
|
|||
val plan = tariffPlan?.toDomain() ?: return null
|
||||
return TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.fromString(status),
|
||||
source = TangemPayCustomerTariffPlan.Source.fromString(source),
|
||||
plan = plan,
|
||||
nextBillingAt = nextBillingAt.toDateTimeOrNull(),
|
||||
pendingPlan = pendingTariffPlan?.toDomain(),
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ internal class DefaultPaymentAccountStatusFetcherTest {
|
|||
|
||||
private val customerTariffPlan = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = TangemPayCustomerTariffPlan.Source.CUSTOMER,
|
||||
plan = basicPlan,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package com.tangem.domain.models.account
|
|||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue.Loaded
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue.Deactivated
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
|
|
@ -326,10 +324,30 @@ private fun buildCryptoCurrencyStatusValue(
|
|||
}
|
||||
}
|
||||
|
||||
fun PaymentAccountStatusValue.hasAccountData(): Boolean = this is Loaded || this is Deactivated
|
||||
val PaymentAccountStatusValue.tariffPlan: TangemPayCustomerTariffPlan?
|
||||
get() = when (this) {
|
||||
is PaymentAccountStatusValue.Error,
|
||||
is PaymentAccountStatusValue.IssuingCard,
|
||||
is PaymentAccountStatusValue.Empty,
|
||||
is PaymentAccountStatusValue.NotCreated,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> null
|
||||
is PaymentAccountStatusValue.Inactive -> tariffPlan.tariff
|
||||
is PaymentAccountStatusValue.AwaitingPlanSelection -> tariffPlan
|
||||
is PaymentAccountStatusValue.Loaded -> tariffPlan?.tariff
|
||||
}
|
||||
|
||||
fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
|
||||
fun PaymentAccountStatusValue.hasAccountData(): Boolean = this is PaymentAccountStatusValue.Loaded ||
|
||||
this is PaymentAccountStatusValue.Deactivated
|
||||
|
||||
fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId }
|
||||
fun PaymentAccountStatusValue.Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId }
|
||||
|
||||
fun Loaded.requireCardWithId(cardId: String): TangemPayCard = requireNotNull(findCardWithId(cardId))
|
||||
fun PaymentAccountStatusValue.Loaded.findCardWithId(cardId: String): TangemPayCard? {
|
||||
return cards.firstOrNull { it.id == cardId }
|
||||
}
|
||||
|
||||
fun PaymentAccountStatusValue.Loaded.requireCardWithId(cardId: String): TangemPayCard {
|
||||
return requireNotNull(findCardWithId(cardId))
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import java.util.Locale
|
|||
* Customer's current tariff plan.
|
||||
*
|
||||
* @property status Lifecycle status of the subscription.
|
||||
* @property source Source of tariff plan. Where [Source.DEFAULT] is basic value users with no tariff selection.
|
||||
* @property plan The currently active plan ([TangemPayTariffPlan]).
|
||||
* @property nextBillingAt When the next plan fee is charged; `null` for free plans.
|
||||
* @property pendingPlan Plan the customer will be moved to (scheduled downgrade), or `null`.
|
||||
|
|
@ -17,6 +18,7 @@ import java.util.Locale
|
|||
@Serializable
|
||||
data class TangemPayCustomerTariffPlan(
|
||||
@SerialName("status") val status: Status,
|
||||
@SerialName("source") val source: Source,
|
||||
@SerialName("plan") val plan: TangemPayTariffPlan,
|
||||
@SerialName("next_billing_at") val nextBillingAt: SerializedDateTime?,
|
||||
@SerialName("pending_plan") val pendingPlan: TangemPayTariffPlan?,
|
||||
|
|
@ -55,4 +57,28 @@ data class TangemPayCustomerTariffPlan(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class Source {
|
||||
@SerialName("DEFAULT")
|
||||
DEFAULT,
|
||||
|
||||
@SerialName("CUSTOMER")
|
||||
CUSTOMER,
|
||||
|
||||
@SerialName("UNKNOWN")
|
||||
UNKNOWN,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?) = when (value?.uppercase(Locale.US)) {
|
||||
"DEFAULT" -> DEFAULT
|
||||
"CUSTOMER" -> CUSTOMER
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val TangemPayCustomerTariffPlan.isDefaultTariff: Boolean
|
||||
get() = source == TangemPayCustomerTariffPlan.Source.DEFAULT && plan.isBasicTier
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.account.isDefaultTariff
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
||||
class CancelTariffTransitionUseCase(
|
||||
private val cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase,
|
||||
private val getTariffTransitionUseCase: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
private val submitTariffTransitionUseCase: SubmitTariffTransitionUseCase,
|
||||
private val getCurrentTariffUseCase: GetCurrentTariffUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, Unit> = either {
|
||||
cancelTangemPayOrderUseCase.invoke(userWalletId, orderId).bind()
|
||||
|
||||
val (source, tariff) = getCurrentTariffUseCase(userWalletId) ?: return@either
|
||||
|
||||
if (source.isActual() && tariff.isDefaultTariff) {
|
||||
val transitions = getTariffTransitionUseCase.invoke(userWalletId).bind()
|
||||
val basicPlanTransition = transitions.find { it.plan.isBasicTier } ?: return@either
|
||||
submitTariffTransitionUseCase.invoke(userWalletId, basicPlanTransition).bind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import arrow.core.Either
|
|||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.OrderType
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
|
|
@ -19,6 +20,7 @@ class CreateTariffPlanTransitionOrderUseCase(
|
|||
private val customerOrderRepository: CustomerOrderRepository,
|
||||
private val issueCardRepository: TangemPayIssueCardRepository,
|
||||
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -57,5 +59,7 @@ class CreateTariffPlanTransitionOrderUseCase(
|
|||
onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) },
|
||||
)
|
||||
}
|
||||
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.tariffPlan
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
class GetCurrentTariffUseCase(
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Pair<StatusSource, TangemPayCustomerTariffPlan>? {
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
val currentStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull()
|
||||
val statusSource = currentStatus?.value?.source
|
||||
val tariffPlan = currentStatus?.value?.tariffPlan
|
||||
return if (statusSource != null && tariffPlan != null) {
|
||||
statusSource to tariffPlan
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
||||
class SubmitTariffTransitionUseCase(
|
||||
private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
|
||||
private val setPendingTransition: SetTariffPlanPendingTransitionUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
transition: TangemPayTariffPlanTransition,
|
||||
): Either<VisaApiError, Unit> {
|
||||
return when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.ACTIVATION,
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
-> {
|
||||
createTransitionOrder(
|
||||
userWalletId = userWalletId,
|
||||
targetTariffPlanId = transition.plan.id,
|
||||
transitionType = transition.type,
|
||||
)
|
||||
}
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
|
||||
setPendingTransition(
|
||||
userWalletId = userWalletId,
|
||||
pendingTariffPlanId = transition.plan.id,
|
||||
)
|
||||
}
|
||||
TangemPayTariffPlanTransition.Type.SYSTEM_DOWNGRADE,
|
||||
TangemPayTariffPlanTransition.Type.UNKNOWN,
|
||||
-> Unit.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class CancelTariffTransitionUseCaseTest {
|
||||
|
||||
private val cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase = mockk()
|
||||
private val getTariffTransitionUseCase: GetTangemPayTariffPlanTransitionsUseCase = mockk()
|
||||
private val submitTariffTransitionUseCase: SubmitTariffTransitionUseCase = mockk()
|
||||
private val getCurrentTariffUseCase: GetCurrentTariffUseCase = mockk()
|
||||
|
||||
private val useCase = CancelTariffTransitionUseCase(
|
||||
cancelTangemPayOrderUseCase = cancelTangemPayOrderUseCase,
|
||||
getTariffTransitionUseCase = getTariffTransitionUseCase,
|
||||
submitTariffTransitionUseCase = submitTariffTransitionUseCase,
|
||||
getCurrentTariffUseCase = getCurrentTariffUseCase,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN cancel order fails WHEN invoke THEN returns Left and skips further steps`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
coVerify(exactly = 0) { getCurrentTariffUseCase(any()) }
|
||||
coVerify(exactly = 0) { getTariffTransitionUseCase(any()) }
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN current tariff is null WHEN invoke THEN returns Right and skips transition`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns null
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { getTariffTransitionUseCase(any()) }
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN source is not actual WHEN invoke THEN returns Right and skips transition`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.CACHE to DEFAULT_BASIC_TARIFF)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { getTariffTransitionUseCase(any()) }
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tariff is not default WHEN invoke THEN returns Right and skips transition`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.ACTUAL to CUSTOMER_PLUS_TARIFF)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { getTariffTransitionUseCase(any()) }
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual default tariff AND getTransitions fails WHEN invoke THEN returns Left`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.ACTUAL to DEFAULT_BASIC_TARIFF)
|
||||
coEvery { getTariffTransitionUseCase(USER_WALLET_ID) } returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual default tariff AND basic transition exists WHEN invoke THEN submits basic transition`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.ACTUAL to DEFAULT_BASIC_TARIFF)
|
||||
coEvery { getTariffTransitionUseCase(USER_WALLET_ID) } returns
|
||||
listOf(PLUS_TRANSITION, BASIC_TRANSITION).right()
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, BASIC_TRANSITION) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { submitTariffTransitionUseCase(USER_WALLET_ID, BASIC_TRANSITION) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual default tariff AND no basic transition WHEN invoke THEN returns Right without submit`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.ACTUAL to DEFAULT_BASIC_TARIFF)
|
||||
coEvery { getTariffTransitionUseCase(USER_WALLET_ID) } returns listOf(PLUS_TRANSITION).right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN actual default tariff AND submit fails WHEN invoke THEN returns Left`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { cancelTangemPayOrderUseCase(USER_WALLET_ID, ORDER_ID) } returns Unit.right()
|
||||
coEvery { getCurrentTariffUseCase(USER_WALLET_ID) } returns (StatusSource.ACTUAL to DEFAULT_BASIC_TARIFF)
|
||||
coEvery { getTariffTransitionUseCase(USER_WALLET_ID) } returns listOf(BASIC_TRANSITION).right()
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, BASIC_TRANSITION) } returns
|
||||
VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, ORDER_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val ORDER_ID = "order-test-1"
|
||||
|
||||
val BASIC_PLAN = TangemPayTariffPlan(
|
||||
id = "plan-basic",
|
||||
tierId = "BASIC",
|
||||
isBasicTier = true,
|
||||
name = "Basic",
|
||||
programName = "program-basic",
|
||||
descriptionItems = emptyList(),
|
||||
)
|
||||
val PLUS_PLAN = TangemPayTariffPlan(
|
||||
id = "plan-plus",
|
||||
tierId = "PLUS",
|
||||
isBasicTier = false,
|
||||
name = "Plus",
|
||||
programName = "program-plus",
|
||||
descriptionItems = emptyList(),
|
||||
)
|
||||
val DEFAULT_BASIC_TARIFF = customerTariff(TangemPayCustomerTariffPlan.Source.DEFAULT, BASIC_PLAN)
|
||||
val CUSTOMER_PLUS_TARIFF = customerTariff(TangemPayCustomerTariffPlan.Source.CUSTOMER, PLUS_PLAN)
|
||||
|
||||
val BASIC_TRANSITION = TangemPayTariffPlanTransition(
|
||||
type = TangemPayTariffPlanTransition.Type.SYSTEM_DOWNGRADE,
|
||||
plan = BASIC_PLAN,
|
||||
)
|
||||
val PLUS_TRANSITION = TangemPayTariffPlanTransition(
|
||||
type = TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
plan = PLUS_PLAN,
|
||||
)
|
||||
|
||||
private fun customerTariff(source: TangemPayCustomerTariffPlan.Source, plan: TangemPayTariffPlan) =
|
||||
TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = source,
|
||||
plan = plan,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
pendingTransitionAt = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.pay.model.Order
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.OrderStep
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.OrderType
|
||||
import com.tangem.domain.pay.repository.CustomerOrderRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayIssueCardRepository
|
||||
|
|
@ -24,11 +25,13 @@ internal class CreateTariffPlanTransitionOrderUseCaseTest {
|
|||
private val customerOrderRepository: CustomerOrderRepository = mockk()
|
||||
private val issueCardRepository: TangemPayIssueCardRepository = mockk(relaxed = true)
|
||||
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true)
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk(relaxed = true)
|
||||
|
||||
private val useCase = CreateTariffPlanTransitionOrderUseCase(
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
issueCardRepository = issueCardRepository,
|
||||
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
appCoroutineScope = TestAppCoroutineScope(),
|
||||
)
|
||||
|
||||
|
|
@ -48,6 +51,7 @@ internal class CreateTariffPlanTransitionOrderUseCaseTest {
|
|||
customerOrderRepository.createOrder(any(), any(), any(), any(), any(), any())
|
||||
}
|
||||
coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) }
|
||||
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -118,6 +122,7 @@ internal class CreateTariffPlanTransitionOrderUseCaseTest {
|
|||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(USER_WALLET_ID, newOrder.id) }
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
private fun order(id: String, status: OrderStatus): Order = Order(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
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.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class GetCurrentTariffUseCaseTest {
|
||||
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk(relaxed = true)
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
|
||||
|
||||
private val useCase = GetCurrentTariffUseCase(
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN status with tariff WHEN invoke THEN fetches and returns source with tariff`() = runTest {
|
||||
// GIVEN
|
||||
val status = paymentStatus(value = awaitingPlanSelection(StatusSource.ACTUAL, CUSTOMER_TARIFF))
|
||||
every { paymentAccountStatusSupplier.invoke(USER_WALLET_ID) } returns flowOf(status)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(StatusSource.ACTUAL to CUSTOMER_TARIFF)
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty supplier flow WHEN invoke THEN returns null`() = runTest {
|
||||
// GIVEN
|
||||
every { paymentAccountStatusSupplier.invoke(USER_WALLET_ID) } returns emptyFlow()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN status without tariff WHEN invoke THEN returns null`() = runTest {
|
||||
// GIVEN
|
||||
val status = paymentStatus(value = PaymentAccountStatusValue.NotCreated)
|
||||
every { paymentAccountStatusSupplier.invoke(USER_WALLET_ID) } returns flowOf(status)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
private fun paymentStatus(value: PaymentAccountStatusValue) = AccountStatus.Payment(
|
||||
account = mockk(),
|
||||
value = value,
|
||||
)
|
||||
|
||||
private fun awaitingPlanSelection(source: StatusSource, tariff: TangemPayCustomerTariffPlan) =
|
||||
PaymentAccountStatusValue.AwaitingPlanSelection(
|
||||
source = source,
|
||||
cryptoCurrency = mockk(),
|
||||
tariffPlan = tariff,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
|
||||
val PLAN = TangemPayTariffPlan(
|
||||
id = "plan-plus",
|
||||
tierId = "PLUS",
|
||||
isBasicTier = false,
|
||||
name = "Plus",
|
||||
programName = "program-plus",
|
||||
descriptionItems = emptyList(),
|
||||
)
|
||||
val CUSTOMER_TARIFF = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = TangemPayCustomerTariffPlan.Source.CUSTOMER,
|
||||
plan = PLAN,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
pendingTransitionAt = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +169,7 @@ internal class GetTangemPayTariffPlanStateUseCaseTest {
|
|||
)
|
||||
val CUSTOMER_TARIFF = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = TangemPayCustomerTariffPlan.Source.CUSTOMER,
|
||||
plan = CURRENT_PLAN,
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SubmitTariffTransitionUseCaseTest {
|
||||
|
||||
private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase = mockk()
|
||||
private val setPendingTransition: SetTariffPlanPendingTransitionUseCase = mockk()
|
||||
|
||||
private val useCase = SubmitTariffTransitionUseCase(
|
||||
createTransitionOrder = createTransitionOrder,
|
||||
setPendingTransition = setPendingTransition,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation transition WHEN invoke THEN creates transition order`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.ACTIVATION)
|
||||
coEvery {
|
||||
createTransitionOrder(USER_WALLET_ID, PLAN_ID, TangemPayTariffPlanTransition.Type.ACTIVATION)
|
||||
} returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) {
|
||||
createTransitionOrder(USER_WALLET_ID, PLAN_ID, TangemPayTariffPlanTransition.Type.ACTIVATION)
|
||||
}
|
||||
coVerify(exactly = 0) { setPendingTransition(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN upgrade transition WHEN invoke THEN creates transition order`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.UPGRADE)
|
||||
coEvery {
|
||||
createTransitionOrder(USER_WALLET_ID, PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE)
|
||||
} returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) {
|
||||
createTransitionOrder(USER_WALLET_ID, PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE)
|
||||
}
|
||||
coVerify(exactly = 0) { setPendingTransition(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN createTransitionOrder fails WHEN invoke THEN returns Left`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.UPGRADE)
|
||||
coEvery {
|
||||
createTransitionOrder(USER_WALLET_ID, PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE)
|
||||
} returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN downgrade transition WHEN invoke THEN sets pending transition`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.DOWNGRADE)
|
||||
coEvery { setPendingTransition(USER_WALLET_ID, PLAN_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { setPendingTransition(USER_WALLET_ID, PLAN_ID) }
|
||||
coVerify(exactly = 0) { createTransitionOrder(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN setPendingTransition fails WHEN invoke THEN returns Left`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.DOWNGRADE)
|
||||
coEvery { setPendingTransition(USER_WALLET_ID, PLAN_ID) } returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN system downgrade transition WHEN invoke THEN returns Right without side effects`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.SYSTEM_DOWNGRADE)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { createTransitionOrder(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setPendingTransition(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown transition WHEN invoke THEN returns Right without side effects`() = runTest {
|
||||
// GIVEN
|
||||
val transition = transition(TangemPayTariffPlanTransition.Type.UNKNOWN)
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, transition)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 0) { createTransitionOrder(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setPendingTransition(any(), any()) }
|
||||
}
|
||||
|
||||
private fun transition(type: TangemPayTariffPlanTransition.Type) = TangemPayTariffPlanTransition(
|
||||
type = type,
|
||||
plan = PLAN,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val PLAN_ID = "plan-plus"
|
||||
|
||||
val PLAN = TangemPayTariffPlan(
|
||||
id = PLAN_ID,
|
||||
tierId = "PLUS",
|
||||
isBasicTier = false,
|
||||
name = "Plus",
|
||||
programName = "program-plus",
|
||||
descriptionItems = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ internal class TangemPayDetailsNotificationFactory(
|
|||
R.string.tangempay_card_details_awaiting_deposit_cancel_button,
|
||||
wrappedList(orderStep.toPlan.name, orderStep.fromPlan.name),
|
||||
),
|
||||
onClick = { intents.onCancelPlusTransition(order.orderId) },
|
||||
onClick = { intents.onCancelTariffTransition(order.orderId) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val getCustomerOffers: GetCustomerOffersUseCase,
|
||||
private val cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase,
|
||||
private val cancelTariffTransitionUseCase: CancelTariffTransitionUseCase,
|
||||
private val getCashbackSummaryUseCase: GetCashbackSummaryUseCase,
|
||||
private val getCashbackDeactivationDismissedUseCase: GetCashbackDeactivationDismissedUseCase,
|
||||
private val setCashbackDeactivationDismissedUseCase: SetCashbackDeactivationDismissedUseCase,
|
||||
|
|
@ -156,11 +156,11 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onCancelPlusTransition(orderId: String) {
|
||||
override fun onCancelTariffTransition(orderId: String) {
|
||||
analytics.send(TangemPayAnalyticsEvents.Tiers.CancelPlusMoveToBasicClicked())
|
||||
uiState.update(TangemPayErrorNotificationTransformer(shouldShowProgress = true))
|
||||
modelScope.launch {
|
||||
cancelTangemPayOrderUseCase(userWalletId = userWalletId, orderId = orderId)
|
||||
cancelTariffTransitionUseCase(userWalletId = userWalletId, orderId = orderId)
|
||||
.onLeft {
|
||||
uiMessageSender.send(TangemPayMessagesFactory.createGenericError())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.tangempay.tiers.select
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -14,11 +13,9 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.pay.usecase.CreateTariffPlanTransitionOrderUseCase
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanTransitionsUseCase
|
||||
import com.tangem.domain.pay.usecase.SetTariffPlanPendingTransitionUseCase
|
||||
import com.tangem.domain.pay.usecase.SubmitTariffTransitionUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull
|
||||
|
|
@ -41,8 +38,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
|
||||
private val setPendingTransition: SetTariffPlanPendingTransitionUseCase,
|
||||
private val submitTariffTransitionUseCase: SubmitTariffTransitionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
|
@ -140,31 +136,11 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
if (transition.type == TangemPayTariffPlanTransition.Type.UPGRADE) {
|
||||
analytics.send(TangemPayAnalyticsEvents.Tiers.PlanChangeUpgradeClicked())
|
||||
}
|
||||
when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.ACTIVATION,
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
-> submitTransition {
|
||||
createTransitionOrder(
|
||||
userWalletId = params.userWalletId,
|
||||
targetTariffPlanId = transition.plan.id,
|
||||
transitionType = transition.type,
|
||||
)
|
||||
}
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> submitTransition {
|
||||
setPendingTransition(
|
||||
userWalletId = params.userWalletId,
|
||||
pendingTariffPlanId = transition.plan.id,
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitTransition(action: suspend () -> Either<VisaApiError, Unit>) {
|
||||
isProcessing = true
|
||||
state.update { buildState() }
|
||||
modelScope.launch {
|
||||
action().fold(
|
||||
submitTariffTransitionUseCase(params.userWalletId, transition).fold(
|
||||
ifRight = {
|
||||
when (params.source) {
|
||||
TangemPaySelectPlanSource.TIERS_ONBOARDING -> {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal interface TangemPayDetailIntents {
|
|||
fun onClickVisaBenefits()
|
||||
fun onClickCashback()
|
||||
fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan)
|
||||
fun onCancelPlusTransition(orderId: String)
|
||||
fun onCancelTariffTransition(orderId: String)
|
||||
fun onCardClick(cardId: String)
|
||||
fun onAddCardClick(tariffState: TangemPayTariffPlanState?)
|
||||
fun onRemoveAccount()
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ internal class TangemPayDetailsModelTest {
|
|||
produceTangemPayInitialDataUseCase = mockk(relaxed = true),
|
||||
onboardingRepository = mockk(relaxed = true),
|
||||
getCustomerOffers = mockk(relaxed = true),
|
||||
cancelTangemPayOrderUseCase = mockk(relaxed = true),
|
||||
cancelTariffTransitionUseCase = mockk(relaxed = true),
|
||||
getCashbackSummaryUseCase = mockk(relaxed = true),
|
||||
getCashbackDeactivationDismissedUseCase = mockk(relaxed = true),
|
||||
setCashbackDeactivationDismissedUseCase = mockk(relaxed = true),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
package com.tangem.features.tangempay.tiers.select
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanTransitionsUseCase
|
||||
import com.tangem.domain.pay.usecase.SubmitTariffTransitionUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class TangemPaySelectPlanModelTest {
|
||||
|
||||
private val router: Router = mockk(relaxed = true)
|
||||
private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase = mockk()
|
||||
private val submitTariffTransitionUseCase: SubmitTariffTransitionUseCase = mockk()
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
|
||||
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
|
||||
private fun createModel(
|
||||
source: TangemPaySelectPlanSource = TangemPaySelectPlanSource.CHANGE_PLAN,
|
||||
transitions: List<TangemPayTariffPlanTransition> = DEFAULT_TRANSITIONS,
|
||||
): TangemPaySelectPlanModel {
|
||||
coEvery { getTransitions(USER_WALLET_ID) } returns transitions.right()
|
||||
return TangemPaySelectPlanModel(
|
||||
paramsContainer = MutableParamsContainer(
|
||||
TangemPaySelectPlanComponent.Params(
|
||||
userWalletId = USER_WALLET_ID,
|
||||
tariffPlan = CUSTOMER_TARIFF,
|
||||
source = source,
|
||||
),
|
||||
),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
getTransitions = getTransitions,
|
||||
submitTariffTransitionUseCase = submitTariffTransitionUseCase,
|
||||
uiMessageSender = uiMessageSender,
|
||||
analytics = analytics,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mixed transitions WHEN model created THEN only allowed types are shown as plans`() {
|
||||
// GIVEN + WHEN
|
||||
val model = createModel()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.plans).hasSize(3)
|
||||
verify(exactly = 1) {
|
||||
analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.TierSelectionScreenShowed>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN getTransitions fails WHEN model created THEN plans are empty`() {
|
||||
// GIVEN
|
||||
coEvery { getTransitions(USER_WALLET_ID) } returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val model = TangemPaySelectPlanModel(
|
||||
paramsContainer = MutableParamsContainer(
|
||||
TangemPaySelectPlanComponent.Params(
|
||||
userWalletId = USER_WALLET_ID,
|
||||
tariffPlan = CUSTOMER_TARIFF,
|
||||
source = TangemPaySelectPlanSource.CHANGE_PLAN,
|
||||
),
|
||||
),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
router = router,
|
||||
getTransitions = getTransitions,
|
||||
submitTariffTransitionUseCase = submitTariffTransitionUseCase,
|
||||
uiMessageSender = uiMessageSender,
|
||||
analytics = analytics,
|
||||
)
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.plans).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new index WHEN onPlanSelected THEN selectedIndex updates and swipe analytics sent`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
|
||||
// WHEN
|
||||
model.state.value.onPlanSelected(1)
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.selectedIndex).isEqualTo(1)
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.TiersSwiped>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same index WHEN onPlanSelected THEN nothing changes and no swipe analytics`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
|
||||
// WHEN
|
||||
model.state.value.onPlanSelected(0)
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.selectedIndex).isEqualTo(0)
|
||||
verify(exactly = 0) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.TiersSwiped>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN change plan source WHEN onSelectClick THEN switches to confirm content`() {
|
||||
// GIVEN
|
||||
val model = createModel(source = TangemPaySelectPlanSource.CHANGE_PLAN)
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.content).isInstanceOf(TangemPaySelectPlanUM.Content.Confirm::class.java)
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlanSelectedClick>()) }
|
||||
verify(exactly = 1) {
|
||||
analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlanChangeConfirmationScreenShowed>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onboarding source AND submit succeeds WHEN onSelectClick THEN replaces to account details`() {
|
||||
// GIVEN
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, any()) } returns Unit.right()
|
||||
val model = createModel(source = TangemPaySelectPlanSource.TIERS_ONBOARDING)
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) { submitTariffTransitionUseCase(USER_WALLET_ID, UPGRADE_TRANSITION) }
|
||||
verify(exactly = 1) { router.replaceAll(TangemPayAccountDetailsInnerRoute.AccountDetails) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no allowed transitions WHEN onSelectClick THEN nothing happens`() {
|
||||
// GIVEN
|
||||
val model = createModel(transitions = listOf(SYSTEM_DOWNGRADE_TRANSITION))
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlanSelectedClick>()) }
|
||||
coVerify(exactly = 0) { submitTariffTransitionUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN allowed transitions WHEN onComparePlansClick THEN compare is shown with analytics`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onComparePlansClick()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.compare).isNotNull()
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.ComparePlansClicked>()) }
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlansComparisonPopupShowed>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no allowed transitions WHEN onComparePlansClick THEN compare stays hidden`() {
|
||||
// GIVEN
|
||||
val model = createModel(transitions = listOf(SYSTEM_DOWNGRADE_TRANSITION))
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onComparePlansClick()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.compare).isNull()
|
||||
verify(exactly = 0) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.ComparePlansClicked>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN compare shown WHEN onDismiss THEN compare hidden with analytics`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
selectContent(model).onComparePlansClick()
|
||||
|
||||
// WHEN
|
||||
model.state.value.compare!!.onDismiss()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.compare).isNull()
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlansComparisonPopupClosed>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm shown WHEN onBackClick THEN returns to select content without pop`() {
|
||||
// GIVEN
|
||||
val model = createModel(source = TangemPaySelectPlanSource.CHANGE_PLAN)
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// WHEN
|
||||
model.onBackClick()
|
||||
|
||||
// THEN
|
||||
assertThat(model.state.value.content).isInstanceOf(TangemPaySelectPlanUM.Content.Select::class.java)
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlanChangeCancelClicked>()) }
|
||||
verify(exactly = 0) { router.pop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN select shown WHEN onBackClick THEN router pops`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
|
||||
// WHEN
|
||||
model.onBackClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { router.pop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN not processing WHEN onCloseClick THEN router pops`() {
|
||||
// GIVEN
|
||||
val model = createModel()
|
||||
|
||||
// WHEN
|
||||
model.state.value.onCloseClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { router.pop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm AND submit fails WHEN onConfirmClick THEN processing reset and error shown`() {
|
||||
// GIVEN
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, any()) } returns VisaApiError.Unspecified.left()
|
||||
val model = createModel(source = TangemPaySelectPlanSource.CHANGE_PLAN)
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// WHEN
|
||||
confirmContent(model).onConfirmClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { uiMessageSender.send(any()) }
|
||||
assertThat(confirmContent(model).isProcessing).isFalse()
|
||||
verify(exactly = 0) { router.pop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN confirm AND submit succeeds WHEN onConfirmClick THEN router pops`() {
|
||||
// GIVEN
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, any()) } returns Unit.right()
|
||||
val model = createModel(source = TangemPaySelectPlanSource.CHANGE_PLAN)
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// WHEN
|
||||
confirmContent(model).onConfirmClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { router.pop() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN upgrade transition WHEN applied THEN upgrade analytics sent`() {
|
||||
// GIVEN
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, any()) } returns Unit.right()
|
||||
val model = createModel(source = TangemPaySelectPlanSource.CHANGE_PLAN)
|
||||
selectContent(model).onSelectClick()
|
||||
|
||||
// WHEN
|
||||
confirmContent(model).onConfirmClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.Tiers.PlanChangeUpgradeClicked>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN processing in progress WHEN onCloseClick THEN pop is ignored`() {
|
||||
// GIVEN
|
||||
val pending = CompletableDeferred<arrow.core.Either<VisaApiError, Unit>>()
|
||||
coEvery { submitTariffTransitionUseCase(USER_WALLET_ID, any()) } coAnswers { pending.await() }
|
||||
val model = createModel(source = TangemPaySelectPlanSource.TIERS_ONBOARDING)
|
||||
|
||||
// WHEN
|
||||
selectContent(model).onSelectClick()
|
||||
model.state.value.onCloseClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { router.pop() }
|
||||
}
|
||||
|
||||
private fun selectContent(model: TangemPaySelectPlanModel) =
|
||||
model.state.value.content as TangemPaySelectPlanUM.Content.Select
|
||||
|
||||
private fun confirmContent(model: TangemPaySelectPlanModel) =
|
||||
model.state.value.content as TangemPaySelectPlanUM.Content.Confirm
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
|
||||
val UPGRADE_TRANSITION = transition(TangemPayTariffPlanTransition.Type.UPGRADE, "PLUS", isBasic = false)
|
||||
val DOWNGRADE_TRANSITION = transition(TangemPayTariffPlanTransition.Type.DOWNGRADE, "BASIC", isBasic = true)
|
||||
val ACTIVATION_TRANSITION = transition(TangemPayTariffPlanTransition.Type.ACTIVATION, "PLUS", isBasic = false)
|
||||
val SYSTEM_DOWNGRADE_TRANSITION =
|
||||
transition(TangemPayTariffPlanTransition.Type.SYSTEM_DOWNGRADE, "BASIC", isBasic = true)
|
||||
val UNKNOWN_TRANSITION = transition(TangemPayTariffPlanTransition.Type.UNKNOWN, "BASIC", isBasic = true)
|
||||
|
||||
val DEFAULT_TRANSITIONS = listOf(
|
||||
UPGRADE_TRANSITION,
|
||||
DOWNGRADE_TRANSITION,
|
||||
ACTIVATION_TRANSITION,
|
||||
SYSTEM_DOWNGRADE_TRANSITION,
|
||||
UNKNOWN_TRANSITION,
|
||||
)
|
||||
|
||||
val CUSTOMER_TARIFF = TangemPayCustomerTariffPlan(
|
||||
status = TangemPayCustomerTariffPlan.Status.ACTIVE,
|
||||
source = TangemPayCustomerTariffPlan.Source.CUSTOMER,
|
||||
plan = plan("PLUS", isBasic = false),
|
||||
nextBillingAt = null,
|
||||
pendingPlan = null,
|
||||
pendingTransitionAt = null,
|
||||
)
|
||||
|
||||
private fun transition(
|
||||
type: TangemPayTariffPlanTransition.Type,
|
||||
tierId: String,
|
||||
isBasic: Boolean,
|
||||
) = TangemPayTariffPlanTransition(type = type, plan = plan(tierId, isBasic))
|
||||
|
||||
private fun plan(tierId: String, isBasic: Boolean) = TangemPayTariffPlan(
|
||||
id = "plan-${tierId.lowercase()}",
|
||||
tierId = tierId,
|
||||
isBasicTier = isBasic,
|
||||
name = tierId,
|
||||
programName = "program-$tierId",
|
||||
descriptionItems = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue