From 017eece8b7f5b316948607c1376cb5e9a27dae07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 08:50:52 -0700 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 6 + .../tangem/data/pay/di/TangemPayDataModule.kt | 30 +++ .../DefaultCustomerOrderRepository.kt | 6 + .../TangemPayTariffPlanConverterTest.kt | 187 ++++++++++++++++++ ...aultTariffPlanTransitionsRepositoryTest.kt | 139 +++++++++++++ .../pay/repository/CustomerOrderRepository.kt | 2 + .../usecase/CancelTangemPayOrderUseCase.kt | 27 +++ .../CreateTariffPlanTransitionOrderUseCase.kt | 61 ++++++ .../CancelTangemPayOrderUseCaseTest.kt | 71 +++++++ ...ateTariffPlanTransitionOrderUseCaseTest.kt | 144 ++++++++++++++ .../GetTangemPayTariffPlanStateUseCaseTest.kt | 174 ++++++++++++++++ .../entity/TangemPayDetailsStateFactory.kt | 53 +++-- .../tangempay/model/TangemPayDetailsModel.kt | 11 ++ .../tiers/select/TangemPaySelectPlanModel.kt | 35 +++- .../tiers/select/TangemPaySelectPlanScreen.kt | 3 + .../tiers/select/TangemPaySelectPlanUM.kt | 1 + .../tangempay/utils/TangemPayDetailIntents.kt | 1 + .../model/TangemPayDetailsModelTest.kt | 1 + 18 files changed, 938 insertions(+), 14 deletions(-) create mode 100644 data/visa/src/test/kotlin/com/tangem/data/pay/converter/TangemPayTariffPlanConverterTest.kt create mode 100644 data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepositoryTest.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCase.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCase.kt create mode 100644 domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCaseTest.kt create mode 100644 domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCaseTest.kt create mode 100644 domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/GetTangemPayTariffPlanStateUseCaseTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index d59a199964..721b1c855f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -95,6 +95,12 @@ interface TangemPayApi { @Body body: VirtualAccountOrderRequest, ): ApiResponse + @POST("v1/order/{order_id}/cancel") + suspend fun cancelOrder( + @Header("Authorization") authHeader: String, + @Path("order_id") orderId: String, + ): ApiResponse + /** Customer offers — used to gate the issue-additional-card flow. */ @GET("v1/customer/offers") suspend fun getCustomerOffers(@Header("Authorization") authHeader: String): ApiResponse diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index f3b88d0807..04fe90743d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -130,6 +130,7 @@ internal interface TangemPayDataModule { repository: DefaultTariffPlanTransitionsRepository, ): TangemPayTariffPlanTransitionsRepository + @Suppress("TooManyFunctions") companion object { @Provides @@ -335,5 +336,34 @@ internal interface TangemPayDataModule { pollingUseCase = pollingUseCase, ) } + + @Provides + fun provideCancelTangemPayOrderUseCase( + customerOrderRepository: CustomerOrderRepository, + issueCardRepository: TangemPayIssueCardRepository, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + ): CancelTangemPayOrderUseCase { + return CancelTangemPayOrderUseCase( + customerOrderRepository = customerOrderRepository, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + ) + } + + @Provides + fun provideCreateTariffPlanTransitionOrderUseCase( + customerOrderRepository: CustomerOrderRepository, + issueCardRepository: TangemPayIssueCardRepository, + startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + appCoroutineScope: AppCoroutineScope, + ): CreateTariffPlanTransitionOrderUseCase { + return CreateTariffPlanTransitionOrderUseCase( + customerOrderRepository = customerOrderRepository, + issueCardRepository = issueCardRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = appCoroutineScope, + ) + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index 2ae89ee37b..6a945ab445 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -79,4 +79,10 @@ internal class DefaultCustomerOrderRepository @Inject constructor( OrderConverter.convert(result) } } + + override suspend fun cancelOrder(userWalletId: UserWalletId, orderId: String): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.cancelOrder(authHeader = authHeader, orderId = orderId) + }.map {} + } } \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/converter/TangemPayTariffPlanConverterTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/TangemPayTariffPlanConverterTest.kt new file mode 100644 index 0000000000..3413c7369f --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/converter/TangemPayTariffPlanConverterTest.kt @@ -0,0 +1,187 @@ +package com.tangem.data.pay.converter + +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.pay.models.response.CustomerMeResponse +import com.tangem.domain.models.account.TangemPayTariffPlan +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class TangemPayTariffPlanConverterTest { + + @Test + fun `GIVEN null value WHEN convert THEN returns null`() { + assertThat(TangemPayTariffPlanConverter.convert(null)).isNull() + } + + @Test + fun `GIVEN missing id WHEN convert THEN returns null`() { + val value = tariffPlan(id = null) + + assertThat(TangemPayTariffPlanConverter.convert(value)).isNull() + } + + @Test + fun `GIVEN missing name WHEN convert THEN returns null`() { + val value = tariffPlan(name = null) + + assertThat(TangemPayTariffPlanConverter.convert(value)).isNull() + } + + @Test + fun `GIVEN full valid plan WHEN convert THEN maps all fields`() { + // GIVEN + val value = tariffPlan( + type = "PLUS", + descriptionItems = listOf( + CustomerMeResponse.DescriptionItem( + type = "PLAN_RELATED", + order = 2, + title = "Title", + body = "Body", + ), + ), + images = listOf(CustomerMeResponse.Image(type = "MAIN", url = "https://img")), + fees = listOf( + CustomerMeResponse.Fee( + type = "RECURRING", + amount = BigDecimal("9.99"), + currency = "USD", + description = "Monthly", + period = "MONTH", + ), + ), + ) + + // WHEN + val result = TangemPayTariffPlanConverter.convert(value) + + // THEN + val expected = TangemPayTariffPlan( + id = PLAN_ID, + type = TangemPayTariffPlan.Type.PLUS, + name = PLAN_NAME, + descriptionItems = listOf( + TangemPayTariffPlan.DescriptionItem( + section = TangemPayTariffPlan.Section.PLAN_RELATED, + order = 2, + title = "Title", + body = "Body", + ), + ), + images = listOf( + TangemPayTariffPlan.Image(type = TangemPayTariffPlan.Image.Type.MAIN, url = "https://img"), + ), + fees = listOf( + TangemPayTariffPlan.Fee( + type = TangemPayTariffPlan.Fee.Type.RECURRING, + amount = BigDecimal("9.99"), + currency = "USD", + description = "Monthly", + period = TangemPayTariffPlan.Fee.Period.MONTH, + ), + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `GIVEN unknown enum strings and null optional fields WHEN convert THEN falls back to defaults`() { + // GIVEN + val value = tariffPlan( + type = "SOMETHING_NEW", + descriptionItems = listOf( + CustomerMeResponse.DescriptionItem(type = "wat", order = null, title = "Title", body = null), + ), + images = listOf(CustomerMeResponse.Image(type = null, url = "https://img")), + fees = listOf( + CustomerMeResponse.Fee( + type = null, + amount = BigDecimal.ONE, + currency = null, + description = null, + period = null, + ), + ), + ) + + // WHEN + val result = TangemPayTariffPlanConverter.convert(value) + + // THEN + val expected = TangemPayTariffPlan( + id = PLAN_ID, + type = TangemPayTariffPlan.Type.UNKNOWN, + name = PLAN_NAME, + descriptionItems = listOf( + TangemPayTariffPlan.DescriptionItem( + section = TangemPayTariffPlan.Section.UNKNOWN, + order = 0, + title = "Title", + body = "", + ), + ), + images = listOf( + TangemPayTariffPlan.Image(type = TangemPayTariffPlan.Image.Type.UNKNOWN, url = "https://img"), + ), + fees = listOf( + TangemPayTariffPlan.Fee( + type = TangemPayTariffPlan.Fee.Type.UNKNOWN, + amount = BigDecimal.ONE, + currency = "", + description = "", + period = null, + ), + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `GIVEN nested items with missing required fields WHEN convert THEN filters them out`() { + // GIVEN + val value = tariffPlan( + descriptionItems = listOf( + CustomerMeResponse.DescriptionItem(type = "PLAN_RELATED", order = 1, title = null, body = "Body"), + ), + images = listOf(CustomerMeResponse.Image(type = "MAIN", url = null)), + fees = listOf( + CustomerMeResponse.Fee( + type = "FREE", + amount = null, + currency = "USD", + description = "d", + period = null, + ), + ), + ) + + // WHEN + val result = TangemPayTariffPlanConverter.convert(value) + + // THEN + assertThat(result?.descriptionItems).isEmpty() + assertThat(result?.images).isEmpty() + assertThat(result?.fees).isEmpty() + } + + private fun tariffPlan( + id: String? = PLAN_ID, + type: String? = "BASIC", + name: String? = PLAN_NAME, + descriptionItems: List? = null, + images: List? = null, + fees: List? = null, + ) = CustomerMeResponse.TariffPlan( + id = id, + type = type, + name = name, + descriptionItems = descriptionItems, + images = images, + fees = fees, + ) + + private companion object { + const val PLAN_ID = "plan-1" + const val PLAN_NAME = "Plus" + } +} \ No newline at end of file diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepositoryTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepositoryTest.kt new file mode 100644 index 0000000000..c8badc3fea --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepositoryTest.kt @@ -0,0 +1,139 @@ +package com.tangem.data.pay.repository + +import arrow.core.left +import arrow.core.right +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.response.CustomerMeResponse +import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse +import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionsResponse +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.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class DefaultTariffPlanTransitionsRepositoryTest { + + private val tangemPayApi: TangemPayApi = mockk() + private val requestHelper: TangemPayRequestPerformer = mockk() + + private val repository = DefaultTariffPlanTransitionsRepository( + tangemPayApi = tangemPayApi, + requestHelper = requestHelper, + ) + + @BeforeEach + fun setUp() { + coEvery { + requestHelper.performRequest(userWalletId = any(), requestBlock = any()) + } coAnswers { + val block = secondArg ApiResponse>() + when (val response = block(AUTH_HEADER)) { + is ApiResponse.Success -> response.data.right() + is ApiResponse.Error -> VisaApiError.Unspecified.left() + } + } + } + + @Test + fun `GIVEN backend error WHEN getTransitions THEN returns error`() = runTest { + // GIVEN + coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns + ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse + + // WHEN + val result = repository.getTransitions(USER_WALLET_ID) + + // THEN + assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified) + } + + @Test + fun `GIVEN null result WHEN getTransitions THEN returns empty list`() = runTest { + // GIVEN + coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns + ApiResponse.Success(TariffPlanTransitionsResponse(result = null)) + + // WHEN + val result = repository.getTransitions(USER_WALLET_ID) + + // THEN + assertThat(result.getOrNull()).isEqualTo(emptyList()) + } + + @Test + fun `GIVEN valid transitions WHEN getTransitions THEN maps them to domain`() = runTest { + // GIVEN + coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns ApiResponse.Success( + TariffPlanTransitionsResponse( + result = listOf( + TariffPlanTransitionResponse(type = "UPGRADE", tariffPlan = tariffPlan()), + ), + ), + ) + + // WHEN + val result = repository.getTransitions(USER_WALLET_ID) + + // THEN + val expected = listOf( + TangemPayTariffPlanTransition( + type = TangemPayTariffPlanTransition.Type.UPGRADE, + plan = TangemPayTariffPlan( + id = PLAN_ID, + type = TangemPayTariffPlan.Type.PLUS, + name = PLAN_NAME, + descriptionItems = emptyList(), + images = emptyList(), + fees = emptyList(), + ), + ), + ) + assertThat(result.getOrNull()).isEqualTo(expected) + } + + @Test + fun `GIVEN a transition with unconvertible plan WHEN getTransitions THEN it is filtered out`() = runTest { + // GIVEN + coEvery { tangemPayApi.getTariffPlanTransitions(any()) } returns ApiResponse.Success( + TariffPlanTransitionsResponse( + result = listOf( + TariffPlanTransitionResponse(type = "UPGRADE", tariffPlan = null), + TariffPlanTransitionResponse(type = "DOWNGRADE", tariffPlan = tariffPlan(id = null)), + TariffPlanTransitionResponse(type = "ACTIVATION", tariffPlan = tariffPlan()), + ), + ), + ) + + // WHEN + val result = repository.getTransitions(USER_WALLET_ID) + + // THEN + val transitions = result.getOrNull().orEmpty() + assertThat(transitions.map { it.type }) + .containsExactly(TangemPayTariffPlanTransition.Type.ACTIVATION) + } + + private fun tariffPlan(id: String? = PLAN_ID) = CustomerMeResponse.TariffPlan( + id = id, + type = "PLUS", + name = PLAN_NAME, + descriptionItems = null, + images = null, + fees = null, + ) + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val AUTH_HEADER = "auth-header" + const val PLAN_ID = "plan-plus" + const val PLAN_NAME = "Plus" + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt index 4f172d5b4d..f5a6af6c74 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt @@ -40,4 +40,6 @@ interface CustomerOrderRepository { targetTariffPlanId: String? = null, transitionType: TangemPayTariffPlanTransition.Type? = null, ): Either + + suspend fun cancelOrder(userWalletId: UserWalletId, orderId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCase.kt new file mode 100644 index 0000000000..9685d410aa --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCase.kt @@ -0,0 +1,27 @@ +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.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.visa.error.VisaApiError + +class CancelTangemPayOrderUseCase( + private val customerOrderRepository: CustomerOrderRepository, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, +) { + suspend operator fun invoke(userWalletId: UserWalletId, orderId: String): Either = either { + customerOrderRepository.cancelOrder(userWalletId, orderId).bind() + + paymentAccountStatusFetcher.invoke(userWalletId) + + startTangemPayOrderPollingUseCase( + order = TangemPayOrderInfo(orderId = orderId, orderStatus = OrderStatus.PROCESSING), + userWalletId = userWalletId, + ) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCase.kt new file mode 100644 index 0000000000..6b79a04731 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.pay.usecase + +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.model.OrderStatus +import com.tangem.domain.pay.model.OrderType +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayIssueCardRepository +import com.tangem.domain.pay.util.OrderResolver +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.launch +import java.util.UUID + +class CreateTariffPlanTransitionOrderUseCase( + private val customerOrderRepository: CustomerOrderRepository, + private val issueCardRepository: TangemPayIssueCardRepository, + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + private val appCoroutineScope: AppCoroutineScope, +) { + suspend operator fun invoke( + userWalletId: UserWalletId, + targetTariffPlanId: String, + transitionType: TangemPayTariffPlanTransition.Type, + ): Either = either { + val orderType = OrderType.TARIFF_PLAN_TRANSITION + + val activeOrders = customerOrderRepository.findOrders( + userWalletId = userWalletId, + types = setOf(orderType), + statuses = OrderStatus.activeStatuses, + ).bind() + + val planTransitionOrder = OrderResolver.selectActive( + orders = activeOrders, + type = orderType, + ) + + val order = planTransitionOrder ?: customerOrderRepository.createOrder( + userWalletId = userWalletId, + type = orderType, + specificationName = null, + idempotencyKey = UUID.randomUUID().toString(), + targetTariffPlanId = targetTariffPlanId, + transitionType = transitionType, + ).bind() + + issueCardRepository.storeIssueOrderId(userWalletId = userWalletId, orderId = order.id) + + appCoroutineScope.launch { + startTangemPayOrderPollingUseCase( + order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status), + userWalletId = userWalletId, + onTerminalReached = { issueCardRepository.removeIssueOrderId(userWalletId, order.id) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCaseTest.kt new file mode 100644 index 0000000000..14d2547628 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTangemPayOrderUseCaseTest.kt @@ -0,0 +1,71 @@ +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.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.CustomerOrderRepository +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 CancelTangemPayOrderUseCaseTest { + + private val customerOrderRepository: CustomerOrderRepository = mockk() + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk() + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase = mockk() + + private val useCase = CancelTangemPayOrderUseCase( + customerOrderRepository = customerOrderRepository, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + ) + + @Test + fun `GIVEN cancelOrder fails WHEN invoke THEN returns Left and skips fetch and polling`() = runTest { + // GIVEN + coEvery { customerOrderRepository.cancelOrder(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) { paymentAccountStatusFetcher.invoke(any()) } + coVerify(exactly = 0) { startTangemPayOrderPollingUseCase(any(), any(), any()) } + } + + @Test + fun `GIVEN cancelOrder succeeds WHEN invoke THEN fetches status and starts polling with PROCESSING order`() = + runTest { + // GIVEN + coEvery { customerOrderRepository.cancelOrder(USER_WALLET_ID, ORDER_ID) } returns Unit.right() + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + coEvery { startTangemPayOrderPollingUseCase(any(), any(), any()) } returns true + + // WHEN + val result = useCase(USER_WALLET_ID, ORDER_ID) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + coVerify(exactly = 1) { + startTangemPayOrderPollingUseCase( + TangemPayOrderInfo(orderId = ORDER_ID, orderStatus = OrderStatus.PROCESSING), + USER_WALLET_ID, + any(), + ) + } + } + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val ORDER_ID = "order-test-1" + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCaseTest.kt new file mode 100644 index 0000000000..6e3fb6824e --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CreateTariffPlanTransitionOrderUseCaseTest.kt @@ -0,0 +1,144 @@ +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.TangemPayTariffPlanTransition +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.model.OrderType +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayIssueCardRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.test.core.TestAppCoroutineScope +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 CreateTariffPlanTransitionOrderUseCaseTest { + + private val customerOrderRepository: CustomerOrderRepository = mockk() + private val issueCardRepository: TangemPayIssueCardRepository = mockk(relaxed = true) + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true) + + private val useCase = CreateTariffPlanTransitionOrderUseCase( + customerOrderRepository = customerOrderRepository, + issueCardRepository = issueCardRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = TestAppCoroutineScope(), + ) + + @Test + fun `GIVEN findOrders fails WHEN invoke THEN returns Left and skips create and store`() = runTest { + // GIVEN + coEvery { + customerOrderRepository.findOrders(USER_WALLET_ID, ACTIVE_TRANSITION_TYPES, OrderStatus.activeStatuses) + } returns VisaApiError.Unspecified.left() + + // WHEN + val result = useCase(USER_WALLET_ID, TARGET_PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE) + + // THEN + assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified) + coVerify(exactly = 0) { + customerOrderRepository.createOrder(any(), any(), any(), any(), any(), any()) + } + coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) } + } + + @Test + fun `GIVEN active transition order exists WHEN invoke THEN reuses it without calling createOrder`() = runTest { + // GIVEN + val existing = order(id = "existing", status = OrderStatus.PROCESSING) + coEvery { + customerOrderRepository.findOrders(USER_WALLET_ID, ACTIVE_TRANSITION_TYPES, OrderStatus.activeStatuses) + } returns listOf(existing).right() + + // WHEN + val result = useCase(USER_WALLET_ID, TARGET_PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify(exactly = 0) { + customerOrderRepository.createOrder(any(), any(), any(), any(), any(), any()) + } + coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(USER_WALLET_ID, existing.id) } + } + + @Test + fun `GIVEN no active order AND createOrder fails WHEN invoke THEN returns Left and skips store`() = runTest { + // GIVEN + coEvery { + customerOrderRepository.findOrders(USER_WALLET_ID, ACTIVE_TRANSITION_TYPES, OrderStatus.activeStatuses) + } returns emptyList().right() + coEvery { + customerOrderRepository.createOrder( + userWalletId = USER_WALLET_ID, + type = OrderType.TARIFF_PLAN_TRANSITION, + specificationName = null, + targetTariffPlanId = TARGET_PLAN_ID, + transitionType = TangemPayTariffPlanTransition.Type.UPGRADE, + idempotencyKey = any(), + ) + } returns VisaApiError.Unspecified.left() + + // WHEN + val result = useCase(USER_WALLET_ID, TARGET_PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE) + + // THEN + assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified) + coVerify(exactly = 0) { issueCardRepository.storeIssueOrderId(any(), any()) } + } + + @Test + fun `GIVEN no active order AND createOrder succeeds WHEN invoke THEN stores the new order id`() = runTest { + // GIVEN + val newOrder = order(id = "new", status = OrderStatus.NEW) + coEvery { + customerOrderRepository.findOrders(USER_WALLET_ID, ACTIVE_TRANSITION_TYPES, OrderStatus.activeStatuses) + } returns emptyList().right() + coEvery { + customerOrderRepository.createOrder( + userWalletId = USER_WALLET_ID, + type = OrderType.TARIFF_PLAN_TRANSITION, + specificationName = null, + targetTariffPlanId = TARGET_PLAN_ID, + transitionType = TangemPayTariffPlanTransition.Type.UPGRADE, + idempotencyKey = any(), + ) + } returns newOrder.right() + + // WHEN + val result = useCase(USER_WALLET_ID, TARGET_PLAN_ID, TangemPayTariffPlanTransition.Type.UPGRADE) + + // THEN + assertThat(result.isRight()).isTrue() + coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(USER_WALLET_ID, newOrder.id) } + } + + private fun order(id: String, status: OrderStatus): Order = Order( + id = id, + customerId = "customer", + type = OrderType.TARIFF_PLAN_TRANSITION, + status = status, + step = OrderStep.UNKNOWN, + stepChangeCode = null, + productInstanceId = null, + paymentAccountId = null, + cardId = null, + toTariffPlanId = TARGET_PLAN_ID, + withdrawTxHash = null, + createdAt = null, + updatedAt = null, + ) + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val TARGET_PLAN_ID = "plan-plus" + val ACTIVE_TRANSITION_TYPES = setOf(OrderType.TARIFF_PLAN_TRANSITION) + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/GetTangemPayTariffPlanStateUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/GetTangemPayTariffPlanStateUseCaseTest.kt new file mode 100644 index 0000000000..e7b4bff205 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/GetTangemPayTariffPlanStateUseCaseTest.kt @@ -0,0 +1,174 @@ +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.TangemPayCustomerTariffPlan +import com.tangem.domain.models.account.TangemPayTariffPlan +import com.tangem.domain.models.account.TangemPayTariffPlanState +import com.tangem.domain.models.account.TangemPayTariffPlanTransition +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.model.OrderType +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.visa.error.VisaApiError +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +internal class GetTangemPayTariffPlanStateUseCaseTest { + + private val customerOrderRepository: CustomerOrderRepository = mockk() + private val getTariffPlanTransitions: GetTangemPayTariffPlanTransitionsUseCase = mockk() + + private val useCase = GetTangemPayTariffPlanStateUseCase( + customerOrderRepository = customerOrderRepository, + getTariffPlanTransitions = getTariffPlanTransitions, + ) + + @Test + fun `GIVEN no active transition order WHEN invoke THEN state has no order`() = runTest { + // GIVEN + coEvery { customerOrderRepository.findOrders(USER_WALLET_ID, TRANSITION_TYPES, ACTIVE) } returns + emptyList().right() + + // WHEN + val result = useCase(USER_WALLET_ID, CUSTOMER_TARIFF) + + // THEN + assertThat(result).isEqualTo(TangemPayTariffPlanState(tariff = CUSTOMER_TARIFF, order = null)) + } + + @Test + fun `GIVEN findOrders fails WHEN invoke THEN state has no order`() = runTest { + // GIVEN + coEvery { customerOrderRepository.findOrders(USER_WALLET_ID, TRANSITION_TYPES, ACTIVE) } returns + VisaApiError.Unspecified.left() + + // WHEN + val result = useCase(USER_WALLET_ID, CUSTOMER_TARIFF) + + // THEN + assertThat(result).isEqualTo(TangemPayTariffPlanState(tariff = CUSTOMER_TARIFF, order = null)) + } + + @Test + fun `GIVEN active order with non-awaiting step WHEN invoke THEN order step is Unknown`() = runTest { + // GIVEN + val order = order(step = OrderStep.UNKNOWN) + coEvery { customerOrderRepository.findOrders(USER_WALLET_ID, TRANSITION_TYPES, ACTIVE) } returns + listOf(order).right() + + // WHEN + val result = useCase(USER_WALLET_ID, CUSTOMER_TARIFF) + + // THEN + val expected = TangemPayTariffPlanState( + tariff = CUSTOMER_TARIFF, + order = TangemPayTariffPlanState.Order( + orderId = ORDER_ID, + step = TangemPayTariffPlanState.OrderStep.Unknown, + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `GIVEN awaiting-deposit order but no matching transition WHEN invoke THEN order step is Unknown`() = runTest { + // GIVEN + val order = order(step = OrderStep.AWAITING_DEPOSIT) + coEvery { customerOrderRepository.findOrders(USER_WALLET_ID, TRANSITION_TYPES, ACTIVE) } returns + listOf(order).right() + coEvery { getTariffPlanTransitions(USER_WALLET_ID) } returns emptyList().right() + + // WHEN + val result = useCase(USER_WALLET_ID, CUSTOMER_TARIFF) + + // THEN + val expected = TangemPayTariffPlanState( + tariff = CUSTOMER_TARIFF, + order = TangemPayTariffPlanState.Order( + orderId = ORDER_ID, + step = TangemPayTariffPlanState.OrderStep.Unknown, + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `GIVEN awaiting-deposit order with matching transition WHEN invoke THEN order step is AwaitingDeposit`() = + runTest { + // GIVEN + val order = order(step = OrderStep.AWAITING_DEPOSIT) + val transition = TangemPayTariffPlanTransition( + type = TangemPayTariffPlanTransition.Type.UPGRADE, + plan = TARGET_PLAN, + ) + coEvery { customerOrderRepository.findOrders(USER_WALLET_ID, TRANSITION_TYPES, ACTIVE) } returns + listOf(order).right() + coEvery { getTariffPlanTransitions(USER_WALLET_ID) } returns listOf(transition).right() + + // WHEN + val result = useCase(USER_WALLET_ID, CUSTOMER_TARIFF) + + // THEN + val expected = TangemPayTariffPlanState( + tariff = CUSTOMER_TARIFF, + order = TangemPayTariffPlanState.Order( + orderId = ORDER_ID, + step = TangemPayTariffPlanState.OrderStep.AwaitingDeposit( + fromPlan = CURRENT_PLAN, + toPlan = TARGET_PLAN, + ), + ), + ) + assertThat(result).isEqualTo(expected) + } + + private fun order(step: OrderStep): Order = Order( + id = ORDER_ID, + customerId = "customer", + type = OrderType.TARIFF_PLAN_TRANSITION, + status = OrderStatus.PROCESSING, + step = step, + stepChangeCode = null, + productInstanceId = null, + paymentAccountId = null, + cardId = null, + toTariffPlanId = TARGET_PLAN_ID, + withdrawTxHash = null, + createdAt = null, + updatedAt = null, + ) + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val ORDER_ID = "order-test-1" + const val TARGET_PLAN_ID = "plan-plus" + val TRANSITION_TYPES = setOf(OrderType.TARIFF_PLAN_TRANSITION) + val ACTIVE = OrderStatus.activeStatuses + + val CURRENT_PLAN = TangemPayTariffPlan( + id = "plan-basic", + type = TangemPayTariffPlan.Type.BASIC, + name = "Basic", + descriptionItems = emptyList(), + ) + val TARGET_PLAN = TangemPayTariffPlan( + id = TARGET_PLAN_ID, + type = TangemPayTariffPlan.Type.PLUS, + name = "Plus", + descriptionItems = emptyList(), + ) + val CUSTOMER_TARIFF = TangemPayCustomerTariffPlan( + status = TangemPayCustomerTariffPlan.Status.ACTIVE, + plan = CURRENT_PLAN, + nextBillingAt = null, + pendingPlan = null, + pendingTransitionAt = null, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 90a6987d78..b3c3ba3882 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -8,11 +8,16 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode +import com.tangem.core.ui.format.bigdecimal.optionalDecimals import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.ic_document_20 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.account.TangemPayTariffPlanState import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardState @@ -75,7 +80,7 @@ internal class TangemPayDetailsStateFactory( onBackClick = onBack, onOpenMenu = onOpenMenu, items = getTopBarMenuItems(), - itemsV2 = getTopBarMenuItemsV2(tariffPlan = status.tariffPlan?.tariff), + itemsV2 = getTopBarMenuItemsV2(tariffPlan = status.tariffPlan), ), pullToRefreshConfig = PullToRefreshConfig( isRefreshing = false, @@ -106,15 +111,17 @@ internal class TangemPayDetailsStateFactory( ), isBalanceHidden = false, addToWalletBlockState = null, - errorNotificationConfig = when (status.error) { - null -> null - PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig(isRedesignEnabled) - else -> createAccountUnavailableConfig(isRedesignEnabled) - }, + errorNotificationConfig = createErrorConfig(status.error) ?: createAwaitingDepositConfig(status.tariffPlan), accountDeactivatedNotificationConfig = null, ) } + private fun createErrorConfig(error: PaymentAccountStatusValue.Error?): NotificationConfig? = when (error) { + null -> null + PaymentAccountStatusValue.Error.NotSynced -> createRenewSessionNotificationConfig(isRedesignEnabled) + else -> createAccountUnavailableConfig(isRedesignEnabled) + } + private fun List.resolveProgressBanner(): CardsProgressBannerUM? = when { any { it.state == TangemPayCardState.Reissuing } -> CardsProgressBannerUM.Reissuing any { it.state == TangemPayCardState.Issuing } -> CardsProgressBannerUM.Issuing @@ -167,6 +174,30 @@ internal class TangemPayDetailsStateFactory( }, ) + // TODO v_rodionov: strings hardcoded for now - wait for localization + private fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? { + val order = tariffPlan?.order ?: return null + + val orderStep = order.step + if (orderStep !is TangemPayTariffPlanState.OrderStep.AwaitingDeposit) return null + + val recurringFee = orderStep.toPlan.fees.find { it.type == TangemPayTariffPlan.Fee.Type.RECURRING } + val feeText = recurringFee?.let { fee -> + val currency = getJavaCurrencyByCode(fee.currency) + fee.amount.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() } + } + val title = if (feeText != null) "Top-up your account on $feeText" else "Top-up your account" + return NotificationConfig( + title = stringReference(title), + subtitle = stringReference("To pay monthly fee for plan and start use card"), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Cancel ${orderStep.toPlan.name}, move to ${orderStep.fromPlan.name}"), + onClick = { intents.onCancelPlusTransition(order.orderId) }, + ), + ) + } + private fun createRenewSessionNotificationConfig(isRedesignEnabled: Boolean) = NotificationConfig( title = resourceReference(R.string.tangempay_sync_needed_title), subtitle = resourceReference(R.string.tangempay_sync_needed_body), @@ -242,20 +273,18 @@ internal class TangemPayDetailsStateFactory( }.toImmutableList() } - private fun getTopBarMenuItemsV2( - tariffPlan: TangemPayCustomerTariffPlan?, - ): ImmutableList { + private fun getTopBarMenuItemsV2(tariffPlan: TangemPayTariffPlanState?): ImmutableList { return buildList { if (isTiersPlusPlanEnabled && tariffPlan != null) { add( TangemPayDropDownItemUM( title = resourceReference(R.string.tangempay_current_plan_title), - onClick = { intents.onClickCurrentPlan(tariffPlan) }, + onClick = { intents.onClickCurrentPlan(tariffPlan.tariff) }, icon = TangemIconUM.Icon( iconRes = CoreUiR.drawable.ic_information_24, tintReference = { TangemTheme.colors3.icon.primary }, ), - subtitle = stringReference(tariffPlan.plan.name), + subtitle = stringReference(tariffPlan.tariff.plan.name), ), ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index df67a00b8a..0ccaaa5681 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -36,6 +36,7 @@ import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository +import com.tangem.domain.pay.usecase.CancelTangemPayOrderUseCase import com.tangem.domain.pay.usecase.GetCustomerOffersUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents @@ -87,6 +88,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase, private val onboardingRepository: OnboardingRepository, private val getCustomerOffers: GetCustomerOffersUseCase, + private val cancelTangemPayOrderUseCase: CancelTangemPayOrderUseCase, ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, @@ -170,6 +172,15 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } + override fun onCancelPlusTransition(orderId: String) { + modelScope.launch { + cancelTangemPayOrderUseCase(userWalletId = userWalletId, orderId = orderId) + .onLeft { + uiMessageSender.send(TangemPayMessagesFactory.createGenericError()) + } + } + } + fun onStart() { onRefreshSwipe(refreshState = ShowRefreshState(false)) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt index f5951f688e..ed80558a98 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanModel.kt @@ -5,12 +5,16 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer 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.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.features.tangempay.details.impl.R +import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute +import com.tangem.features.tangempay.utils.TangemPayMessagesFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -27,6 +31,8 @@ internal class TangemPaySelectPlanModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase, + private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -34,6 +40,7 @@ internal class TangemPaySelectPlanModel @Inject constructor( private var transitions: List = emptyList() private var selectedIndex: Int = 0 private var isConfirm: Boolean = false + private var isProcessing: Boolean = false val state: StateFlow field = MutableStateFlow(buildState()) @@ -73,6 +80,7 @@ internal class TangemPaySelectPlanModel @Inject constructor( } private fun onBackClick() { + if (isProcessing) return if (isConfirm) { isConfirm = false state.update { buildState() } @@ -81,6 +89,29 @@ internal class TangemPaySelectPlanModel @Inject constructor( } } + private fun onConfirmClick() { + val transition = transitions.getOrNull(selectedIndex) ?: return + if (transition.type != TangemPayTariffPlanTransition.Type.UPGRADE) return + if (isProcessing) return + + isProcessing = true + state.update { buildState() } + modelScope.launch { + createTransitionOrder( + userWalletId = params.userWalletId, + targetTariffPlanId = transition.plan.id, + transitionType = transition.type, + ).fold( + ifRight = { router.popTo(TangemPayAccountDetailsInnerRoute.AccountDetails) }, + ifLeft = { + isProcessing = false + state.update { buildState() } + uiMessageSender.send(message = TangemPayMessagesFactory.createGenericError()) + }, + ) + } + } + private fun buildState(showPlanCompare: Boolean = false): TangemPaySelectPlanUM = TangemPaySelectPlanUM( topBarTitle = if (isConfirm) { resourceReference(R.string.tangempay_select_plan_confirm_title) @@ -138,8 +169,9 @@ internal class TangemPaySelectPlanModel @Inject constructor( R.string.tangempay_select_plan_btn_downgrade }, ), + isProcessing = isProcessing, onCancelClick = ::onBackClick, - onConfirmClick = {}, + onConfirmClick = ::onConfirmClick, ) } @@ -187,7 +219,6 @@ internal class TangemPaySelectPlanModel @Inject constructor( private val ALLOWED_TYPES = setOf( TangemPayTariffPlanTransition.Type.UPGRADE, TangemPayTariffPlanTransition.Type.DOWNGRADE, - TangemPayTariffPlanTransition.Type.ACTIVATION, // TODO v_rodionov: Only for test, must be removed in future ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanScreen.kt index 3b6894551a..adad1adb87 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanScreen.kt @@ -250,6 +250,7 @@ private fun ConfirmFooter(content: TangemPaySelectPlanUM.Content.Confirm, modifi modifier = Modifier.fillMaxWidth(), variant = TangemButton.Variant.Secondary, size = TangemButton.Size.X12, + isEnabled = !content.isProcessing, text = resourceReference(R.string.tangempay_select_plan_btn_cancel), onClick = content.onCancelClick, ) @@ -257,6 +258,7 @@ private fun ConfirmFooter(content: TangemPaySelectPlanUM.Content.Confirm, modifi modifier = Modifier.fillMaxWidth(), variant = TangemButton.Variant.Primary, size = TangemButton.Size.X12, + isLoading = content.isProcessing, text = content.confirmButtonText, onClick = content.onConfirmClick, ) @@ -344,6 +346,7 @@ private fun previewState(isConfirm: Boolean) = TangemPaySelectPlanUM( ), ), confirmButtonText = stringReference("Upgrade plan"), + isProcessing = false, onCancelClick = {}, onConfirmClick = {}, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanUM.kt index d4ce6fd20b..a4b43ff274 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanUM.kt @@ -54,6 +54,7 @@ internal data class TangemPaySelectPlanUM( val title: TextReference, val points: ImmutableList, val confirmButtonText: TextReference, + val isProcessing: Boolean, val onCancelClick: () -> Unit, val onConfirmClick: () -> Unit, ) : Content diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index 50bba1b5b5..f3b98c9141 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -11,6 +11,7 @@ internal interface TangemPayDetailIntents { fun onClickWithdraw() fun onClickTermsAndLimits() fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan) + fun onCancelPlusTransition(orderId: String) fun onCardClick(cardId: String) fun onAddCardClick() fun onRemoveAccount() diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt index eb9f5dd782..4b96fb8183 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt @@ -135,6 +135,7 @@ internal class TangemPayDetailsModelTest { produceTangemPayInitialDataUseCase = mockk(relaxed = true), onboardingRepository = mockk(relaxed = true), getCustomerOffers = mockk(relaxed = true), + cancelTangemPayOrderUseCase = mockk(relaxed = true), ) }