Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-23 06:30:53 -07:00
parent aef7c97866
commit eda701fed9
21 changed files with 1006 additions and 40 deletions

View file

@ -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))
}

View file

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

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

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

View file

@ -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()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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(),
)
}
}