Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-09 08:02:24 -07:00
parent e1c58bed0e
commit 8d37a2bc1f
20 changed files with 470 additions and 54 deletions

View file

@ -34,6 +34,15 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
): ApiResponse<TariffPlanTransitionsResponse>
@POST("v1/customer/tariff-plan/pending-transition")
suspend fun setPendingTariffPlanTransition(
@Header("Authorization") authHeader: String,
@Body body: SetPendingTariffPlanTransitionRequest,
): ApiResponse<Any>
@POST("v1/customer/tariff-plan/pending-transition/cancel")
suspend fun cancelPendingTariffPlanTransition(@Header("Authorization") authHeader: String): ApiResponse<Any>
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
@GET("v1/account/bank-credentials/{product_instance_id}")
suspend fun getBankCredentials(

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPendingTariffPlanTransitionRequest(
@Json(name = "pending_tariff_plan_id") val pendingTariffPlanId: String,
)

View file

@ -140,6 +140,28 @@ internal interface TangemPayDataModule {
return GetTangemPayTariffPlanTransitionsUseCase(repository)
}
@Provides
fun provideSetTariffPlanPendingTransitionUseCase(
repository: TangemPayTariffPlanTransitionsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): SetTariffPlanPendingTransitionUseCase {
return SetTariffPlanPendingTransitionUseCase(
repository = repository,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
}
@Provides
fun provideCancelTariffPlanPendingTransitionUseCase(
repository: TangemPayTariffPlanTransitionsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): CancelTariffPlanPendingTransitionUseCase {
return CancelTariffPlanPendingTransitionUseCase(
repository = repository,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
}
@Provides
fun provideGetTangemPayTariffPlanStateUseCase(
customerOrderRepository: CustomerOrderRepository,

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.raise.either
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
import com.tangem.domain.models.wallet.UserWalletId
@ -26,6 +27,21 @@ internal class DefaultTariffPlanTransitionsRepository @Inject constructor(
response.result.orEmpty().mapNotNull { it.toDomain() }
}
override suspend fun setPendingTransition(
userWalletId: UserWalletId,
pendingTariffPlanId: String,
): Either<VisaApiError, Unit> = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.setPendingTariffPlanTransition(
authHeader = authHeader,
body = SetPendingTariffPlanTransitionRequest(pendingTariffPlanId = pendingTariffPlanId),
)
}.map {}
override suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either<VisaApiError, Unit> =
requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.cancelPendingTariffPlanTransition(authHeader = authHeader)
}.map {}
private fun TariffPlanTransitionResponse.toDomain(): TangemPayTariffPlanTransition? {
val plan = TangemPayTariffPlanConverter.convert(tariffPlan) ?: return null
return TangemPayTariffPlanTransition(

View file

@ -6,6 +6,7 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionsResponse
@ -14,7 +15,9 @@ 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 io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@ -121,6 +124,65 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
.containsExactly(TangemPayTariffPlanTransition.Type.ACTIVATION)
}
@Test
fun `GIVEN success WHEN setPendingTransition THEN sends pending plan id and returns Unit`() = runTest {
// GIVEN
val bodySlot = slot<SetPendingTariffPlanTransitionRequest>()
coEvery {
tangemPayApi.setPendingTariffPlanTransition(any(), capture(bodySlot))
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
// WHEN
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
// THEN
assertThat(result.isRight()).isTrue()
assertThat(bodySlot.captured.pendingTariffPlanId).isEqualTo(PENDING_PLAN_ID)
}
@Test
fun `GIVEN backend error WHEN setPendingTransition THEN returns error`() = runTest {
// GIVEN
coEvery {
tangemPayApi.setPendingTariffPlanTransition(any(), any())
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
// WHEN
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
// THEN
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
}
@Test
fun `GIVEN success WHEN cancelPendingTransition THEN returns Unit`() = runTest {
// GIVEN
coEvery {
tangemPayApi.cancelPendingTariffPlanTransition(any())
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
// WHEN
val result = repository.cancelPendingTransition(USER_WALLET_ID)
// THEN
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { tangemPayApi.cancelPendingTariffPlanTransition(AUTH_HEADER) }
}
@Test
fun `GIVEN backend error WHEN cancelPendingTransition THEN returns error`() = runTest {
// GIVEN
coEvery {
tangemPayApi.cancelPendingTariffPlanTransition(any())
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
// WHEN
val result = repository.cancelPendingTransition(USER_WALLET_ID)
// THEN
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
}
private fun tariffPlan(id: String? = PLAN_ID) = CustomerMeResponse.TariffPlan(
id = id,
type = "PLUS",
@ -135,5 +197,6 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
const val AUTH_HEADER = "auth-header"
const val PLAN_ID = "plan-plus"
const val PLAN_NAME = "Plus"
const val PENDING_PLAN_ID = "plan-basic"
}
}

View file

@ -8,4 +8,11 @@ import com.tangem.domain.visa.error.VisaApiError
interface TangemPayTariffPlanTransitionsRepository {
suspend fun getTransitions(userWalletId: UserWalletId): Either<VisaApiError, List<TangemPayTariffPlanTransition>>
suspend fun setPendingTransition(
userWalletId: UserWalletId,
pendingTariffPlanId: String,
): Either<VisaApiError, Unit>
suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either<VisaApiError, Unit>
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
import com.tangem.domain.visa.error.VisaApiError
class CancelTariffPlanPendingTransitionUseCase(
private val repository: TangemPayTariffPlanTransitionsRepository,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<VisaApiError, Unit> = either {
repository.cancelPendingTransition(userWalletId).bind()
paymentAccountStatusFetcher.invoke(userWalletId)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
import com.tangem.domain.visa.error.VisaApiError
class SetTariffPlanPendingTransitionUseCase(
private val repository: TangemPayTariffPlanTransitionsRepository,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) {
suspend operator fun invoke(userWalletId: UserWalletId, pendingTariffPlanId: String): Either<VisaApiError, Unit> =
either {
repository.setPendingTransition(userWalletId, pendingTariffPlanId).bind()
paymentAccountStatusFetcher.invoke(userWalletId)
}
}

View file

@ -0,0 +1,56 @@
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.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
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 CancelTariffPlanPendingTransitionUseCaseTest {
private val repository: TangemPayTariffPlanTransitionsRepository = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
private val useCase = CancelTariffPlanPendingTransitionUseCase(
repository = repository,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
@Test
fun `GIVEN cancelPendingTransition fails WHEN invoke THEN returns Left and skips fetch`() = runTest {
// GIVEN
coEvery { repository.cancelPendingTransition(USER_WALLET_ID) } returns VisaApiError.Unspecified.left()
// WHEN
val result = useCase(USER_WALLET_ID)
// THEN
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
}
@Test
fun `GIVEN cancelPendingTransition succeeds WHEN invoke THEN refreshes account status`() = runTest {
// GIVEN
coEvery { repository.cancelPendingTransition(USER_WALLET_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
// WHEN
val result = useCase(USER_WALLET_ID)
// THEN
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
}
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
}
}

View file

@ -0,0 +1,59 @@
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.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
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 SetTariffPlanPendingTransitionUseCaseTest {
private val repository: TangemPayTariffPlanTransitionsRepository = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
private val useCase = SetTariffPlanPendingTransitionUseCase(
repository = repository,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
@Test
fun `GIVEN setPendingTransition fails WHEN invoke THEN returns Left and skips fetch`() = runTest {
// GIVEN
coEvery {
repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
} returns VisaApiError.Unspecified.left()
// WHEN
val result = useCase(USER_WALLET_ID, PENDING_PLAN_ID)
// THEN
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
}
@Test
fun `GIVEN setPendingTransition succeeds WHEN invoke THEN refreshes account status`() = runTest {
// GIVEN
coEvery { repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
// WHEN
val result = useCase(USER_WALLET_ID, PENDING_PLAN_ID)
// THEN
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
}
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
const val PENDING_PLAN_ID = "plan-basic"
}
}

View file

@ -92,13 +92,15 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
is TangemPayAccountDetailsInnerRoute.CurrentPlan -> TangemPayCurrentPlanComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = TangemPayCurrentPlanComponent.Params(
userWalletId = params.initialStatus.userWalletId,
tariffPlan = config.tariffPlan,
),
)
TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
is TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = TangemPaySelectPlanComponent.Params(
userWalletId = params.initialStatus.userWalletId,
tariffPlan = config.tariffPlan,
),
)
TangemPayAccountDetailsInnerRoute.VirtualAccountDepositSuccess ->

View file

@ -37,7 +37,7 @@ internal class TangemPayDetailsNotificationFactory(
},
)
// TODO v_rodionov: strings hardcoded for now - wait for localization
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? {
if (!isTiersPlusPlanEnabled) return null
if (tariffPlan == null) return null
@ -81,7 +81,7 @@ internal class TangemPayDetailsNotificationFactory(
iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20,
)
// TODO v_rodionov: strings hardcoded for now - wait for localization
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun createTariffSystemDownGradePendingConfig(tariffPlan: TangemPayTariffPlanState): NotificationConfig? {
val date = tariffPlan.tariff.formatNextBillingDateOrNull() ?: return null
val planName = tariffPlan.tariff.plan.name

View file

@ -22,7 +22,9 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
) : TangemPayAccountDetailsInnerRoute()
@Serializable
data object SelectPlan : TangemPayAccountDetailsInnerRoute()
data class SelectPlan(
val tariffPlan: TangemPayCustomerTariffPlan,
) : TangemPayAccountDetailsInnerRoute()
@Serializable
data object VirtualAccountDepositSuccess : TangemPayAccountDetailsInnerRoute()

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
import com.tangem.domain.models.wallet.UserWalletId
internal class TangemPayCurrentPlanComponent(
appComponentContext: AppComponentContext,
@ -22,5 +23,8 @@ internal class TangemPayCurrentPlanComponent(
TangemPayCurrentPlanScreen(state = state, modifier = modifier)
}
data class Params(val tariffPlan: TangemPayCustomerTariffPlan)
data class Params(
val userWalletId: UserWalletId,
val tariffPlan: TangemPayCustomerTariffPlan,
)
}

View file

@ -7,18 +7,22 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
import com.tangem.domain.models.account.TangemPayTariffPlan
import com.tangem.domain.pay.usecase.CancelTariffPlanPendingTransitionUseCase
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull
import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ -27,10 +31,14 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val cancelPendingTransition: CancelTariffPlanPendingTransitionUseCase,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<TangemPayCurrentPlanComponent.Params>()
private var isProcessing: Boolean = false
val state: StateFlow<TangemPayCurrentPlanUM>
field = MutableStateFlow(createState(params.tariffPlan))
@ -39,10 +47,10 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
notification = createNotification(customerPlan),
sections = buildSections(customerPlan.plan),
onBackClick = router::pop,
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan) },
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan(params.tariffPlan)) },
)
// TODO v_rodionov: strings hardcoded for now - wait for localization
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun createNotification(customerPlan: TangemPayCustomerTariffPlan): TangemPayCurrentPlanUM.Notification? {
val date = customerPlan.formatNextBillingDateOrNull(formatter = DateTimeFormatters.dateMMMd) ?: return null
val feeText = customerPlan.plan.formatRecurringFeeOrNull() ?: return null
@ -56,7 +64,8 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
),
button = TangemPayCurrentPlanUM.Notification.Button(
text = stringReference("Stay on ${customerPlan.plan.name}"),
onClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade
isProcessing = isProcessing,
onClick = ::onStayOnPlanClick,
),
)
}
@ -67,6 +76,35 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
}
}
private fun onStayOnPlanClick() {
if (isProcessing) return
val customerPlan = params.tariffPlan
val targetPlanName = customerPlan.pendingPlan?.name ?: return
uiMessageSender.send(
message = TangemPayMessagesFactory.createStayOnPlanMessage(
planName = customerPlan.plan.name,
targetPlanName = targetPlanName,
onStayClick = ::confirmStayOnPlan,
),
)
}
private fun confirmStayOnPlan() {
if (isProcessing) return
isProcessing = true
state.value = createState(params.tariffPlan)
modelScope.launch {
cancelPendingTransition(params.userWalletId).fold(
ifRight = { router.pop() },
ifLeft = {
isProcessing = false
state.value = createState(params.tariffPlan)
uiMessageSender.send(message = TangemPayMessagesFactory.createGenericError())
},
)
}
}
private fun buildSections(plan: TangemPayTariffPlan) = persistentListOf(
sectionOf(plan, TangemPayTariffPlan.Section.CARD_RELATED, R.string.tangempay_current_plan_section_card),
sectionOf(plan, TangemPayTariffPlan.Section.PLAN_RELATED, R.string.tangempay_current_plan_section_plan),

View file

@ -109,6 +109,7 @@ private fun PlanNotification(notification: TangemPayCurrentPlanUM.Notification,
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X11,
text = button.text,
isLoading = button.isProcessing,
onClick = button.onClick,
)
}
@ -205,6 +206,7 @@ private fun previewState() = TangemPayCurrentPlanUM(
),
button = TangemPayCurrentPlanUM.Notification.Button(
text = stringReference("Stay on Plus"),
isProcessing = false,
onClick = {},
),
),

View file

@ -20,6 +20,7 @@ internal data class TangemPayCurrentPlanUM(
@Immutable
data class Button(
val text: TextReference,
val isProcessing: Boolean,
val onClick: () -> Unit,
)
}

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
import com.tangem.domain.models.wallet.UserWalletId
internal class TangemPaySelectPlanComponent(
@ -22,5 +23,8 @@ internal class TangemPaySelectPlanComponent(
TangemPaySelectPlanScreen(state = state, modifier = modifier)
}
data class Params(val userWalletId: UserWalletId)
data class Params(
val userWalletId: UserWalletId,
val tariffPlan: TangemPayCustomerTariffPlan,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.tiers.select
import androidx.compose.runtime.Stable
import arrow.core.Either
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -8,12 +9,16 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
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.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
import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -25,6 +30,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class TangemPaySelectPlanModel @Inject constructor(
@ -33,6 +39,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
private val router: Router,
private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase,
private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
private val setPendingTransition: SetTariffPlanPendingTransitionUseCase,
private val uiMessageSender: UiMessageSender,
) : Model() {
@ -95,21 +102,34 @@ internal class TangemPaySelectPlanModel @Inject constructor(
}
private fun onConfirmClick() {
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return
// TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade
if (transition.type != TangemPayTariffPlanTransition.Type.UPGRADE) return
if (isProcessing) return
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return
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 {
createTransitionOrder(
userWalletId = params.userWalletId,
targetTariffPlanId = transition.plan.id,
transitionType = transition.type,
).fold(
action().fold(
ifRight = { router.popTo(TangemPayAccountDetailsInnerRoute.AccountDetails) },
ifLeft = {
isProcessing = false
@ -121,11 +141,13 @@ internal class TangemPaySelectPlanModel @Inject constructor(
}
private fun buildState(showPlanCompare: Boolean = false): TangemPaySelectPlanUM = TangemPaySelectPlanUM(
topBarTitle = if (isConfirm) {
resourceReference(R.string.tangempay_select_plan_confirm_title)
} else {
resourceReference(R.string.tangempay_select_plan_title)
},
topBarTitle = resourceReference(
if (isConfirm) {
R.string.tangempay_select_plan_confirm_title
} else {
R.string.tangempay_select_plan_title
},
),
plans = allowedTransitions.map { it.plan.toPlanUM() }.toImmutableList(),
selectedIndex = selectedIndex,
onPlanSelected = ::onPlanSelected,
@ -141,7 +163,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
)
private fun buildCompare(): TangemPaySelectPlanUM.ComparePlans {
val plans = transitions.map { it.plan }
val plans = listOf(params.tariffPlan.plan) + transitions.map { it.plan }
val orderedTitles = plans
.flatMap { plan -> plan.descriptionItems.filter { it.section in COMPARE_SECTIONS } }
.sortedWith(compareBy({ it.section.ordinal }, { it.order }))
@ -164,19 +186,35 @@ internal class TangemPaySelectPlanModel @Inject constructor(
)
}
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun buildConfirmContent(): TangemPaySelectPlanUM.Content {
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return buildSelectContent()
val planName = transition.plan.name
val isUpgrade = transition.type == TangemPayTariffPlanTransition.Type.UPGRADE
val targetProgramme = transition.plan.name
return TangemPaySelectPlanUM.Content.Confirm(
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
title = stringReference("We will issue Visa $planName for you"),
points = buildConfirmPoints(transition, planName, isUpgrade),
title = when (transition.type) {
TangemPayTariffPlanTransition.Type.UPGRADE -> stringReference(
"We will issue Visa $targetProgramme for you",
)
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
val nextBillingDate = nextBillingDate()
if (nextBillingDate != null) {
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
val planName = params.tariffPlan.plan.name
stringReference(
"Your $planName plan and $programName cards will be active till $nextBillingDate",
)
} else {
stringReference("You are switching to $targetProgramme")
}
}
else -> stringReference("You are switching to $targetProgramme")
},
points = buildConfirmPoints(transition),
confirmButtonText = resourceReference(
if (isUpgrade) {
R.string.tangempay_select_plan_btn_upgrade
} else {
R.string.tangempay_select_plan_btn_downgrade
when (transition.type) {
TangemPayTariffPlanTransition.Type.UPGRADE -> R.string.tangempay_select_plan_btn_upgrade
TangemPayTariffPlanTransition.Type.DOWNGRADE -> R.string.tangempay_select_plan_btn_downgrade
else -> R.string.common_continue
},
),
isProcessing = isProcessing,
@ -185,29 +223,39 @@ internal class TangemPaySelectPlanModel @Inject constructor(
)
}
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
private fun buildConfirmPoints(
transition: TangemPayTariffPlanTransition,
planName: String,
isUpgrade: Boolean,
): ImmutableList<TangemPaySelectPlanUM.PointUM> = if (isUpgrade) {
val feeText = transition.plan.formatRecurringFeeOrNull()
listOf(
stringReference("You will get your virtual Visa $planName in minutes"),
if (feeText != null) {
stringReference("$feeText monthly fee will be taken from your account")
} else {
stringReference("No fee applied")
},
)
} else {
listOf(
stringReference("Your current Visa cards will be closed"),
stringReference("No fee applied"),
)
): ImmutableList<TangemPaySelectPlanUM.PointUM> {
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
return when (transition.type) {
TangemPayTariffPlanTransition.Type.UPGRADE -> {
val feeText = transition.plan.formatRecurringFeeOrNull()
buildList {
add("You will get your virtual Visa $programName in minutes")
if (feeText != null) {
add("$feeText monthly fee will be taken from your account")
}
}
}
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
val date = nextBillingDate()
buildList {
if (date != null) {
add("On $date we will move you to ${transition.plan.name} plan")
}
add("Your Visa $programName cards will be closed")
if (date != null) {
add("You can cancel this transition till $date")
}
add("No fee applied")
}
}
else -> listOf("No fee applied")
}
.map { TangemPaySelectPlanUM.PointUM(title = stringReference(it), body = null) }
.toImmutableList()
}
.map { TangemPaySelectPlanUM.PointUM(title = it, body = null) }
.toImmutableList()
private fun TangemPayTariffPlan.toPlanUM() = TangemPaySelectPlanUM.PlanUM(
name = stringReference(name),
@ -224,10 +272,15 @@ internal class TangemPaySelectPlanModel @Inject constructor(
.toImmutableList(),
)
private fun nextBillingDate(): String? {
return params.tariffPlan.formatNextBillingDateOrNull(DateTimeFormatters.dateMMMd)
}
companion object {
private val ALLOWED_TYPES = setOf(
TangemPayTariffPlanTransition.Type.UPGRADE,
TangemPayTariffPlanTransition.Type.DOWNGRADE,
TangemPayTariffPlanTransition.Type.ACTIVATION,
)
private val COMPARE_SECTIONS = setOf(
TangemPayTariffPlan.Section.CARD_RELATED,

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.DialogMessage
@ -170,6 +171,35 @@ internal object TangemPayMessagesFactory {
}
}
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
fun createStayOnPlanMessage(
planName: String,
targetPlanName: String,
onStayClick: () -> Unit,
): BottomSheetMessage {
return bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_heart_20) {
type = MessageBottomSheetUM.Icon.Type.Informative
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Informative
}
title = stringReference("Do you want to stay on $planName?")
body = stringReference("Your transition on $targetPlanName will be canceled")
}
secondaryButton {
text = resourceReference(R.string.common_cancel)
onClick { closeBs() }
}
primaryButton {
text = stringReference("Stay on $planName")
onClick {
onStayClick()
closeBs()
}
}
}
}
fun createGenericError(): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),