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 721b1c855f..dcac795aca 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 @@ -34,6 +34,15 @@ interface TangemPayApi { @Header("Authorization") authHeader: String, ): ApiResponse + @POST("v1/customer/tariff-plan/pending-transition") + suspend fun setPendingTariffPlanTransition( + @Header("Authorization") authHeader: String, + @Body body: SetPendingTariffPlanTransitionRequest, + ): ApiResponse + + @POST("v1/customer/tariff-plan/pending-transition/cancel") + suspend fun cancelPendingTariffPlanTransition(@Header("Authorization") authHeader: String): ApiResponse + /** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */ @GET("v1/account/bank-credentials/{product_instance_id}") suspend fun getBankCredentials( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPendingTariffPlanTransitionRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPendingTariffPlanTransitionRequest.kt new file mode 100644 index 0000000000..f03d209dbf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/SetPendingTariffPlanTransitionRequest.kt @@ -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, +) \ No newline at end of file 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 04fe90743d..acecee8de6 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 @@ -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, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepository.kt index 5ae0560c06..1c922db762 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTariffPlanTransitionsRepository.kt @@ -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 = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.setPendingTariffPlanTransition( + authHeader = authHeader, + body = SetPendingTariffPlanTransitionRequest(pendingTariffPlanId = pendingTariffPlanId), + ) + }.map {} + + override suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either = + requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.cancelPendingTariffPlanTransition(authHeader = authHeader) + }.map {} + private fun TariffPlanTransitionResponse.toDomain(): TangemPayTariffPlanTransition? { val plan = TangemPayTariffPlanConverter.convert(tariffPlan) ?: return null return TangemPayTariffPlanTransition( 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 index c8badc3fea..ee46d3ab16 100644 --- 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 @@ -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() + coEvery { + tangemPayApi.setPendingTariffPlanTransition(any(), capture(bodySlot)) + } returns ApiResponse.Success(Unit) as ApiResponse + + // 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 + + // 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 + + // 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 + + // 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" } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayTariffPlanTransitionsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayTariffPlanTransitionsRepository.kt index 34b1e82e7f..4c1aa75a9a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayTariffPlanTransitionsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayTariffPlanTransitionsRepository.kt @@ -8,4 +8,11 @@ import com.tangem.domain.visa.error.VisaApiError interface TangemPayTariffPlanTransitionsRepository { suspend fun getTransitions(userWalletId: UserWalletId): Either> + + suspend fun setPendingTransition( + userWalletId: UserWalletId, + pendingTariffPlanId: String, + ): Either + + suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCase.kt new file mode 100644 index 0000000000..5a2f106c56 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCase.kt @@ -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 = either { + repository.cancelPendingTransition(userWalletId).bind() + + paymentAccountStatusFetcher.invoke(userWalletId) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCase.kt new file mode 100644 index 0000000000..400462dc83 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCase.kt @@ -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 = + either { + repository.setPendingTransition(userWalletId, pendingTariffPlanId).bind() + + paymentAccountStatusFetcher.invoke(userWalletId) + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCaseTest.kt new file mode 100644 index 0000000000..c8f03f5939 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/CancelTariffPlanPendingTransitionUseCaseTest.kt @@ -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()) } + } + + @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") + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCaseTest.kt new file mode 100644 index 0000000000..d980cbc210 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/SetTariffPlanPendingTransitionUseCaseTest.kt @@ -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()) } + } + + @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" + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index 68c7090dcf..3bb93950a6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -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 -> diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt index 3d522360b7..41a53fa256 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNotificationFactory.kt @@ -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 diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt index 5978bb651f..540efd54e8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt @@ -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() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt index 24cbb4cbae..afc74f1b6c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanComponent.kt @@ -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, + ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt index a7ad0f5ca4..d8fe2b240a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanModel.kt @@ -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() + private var isProcessing: Boolean = false + val state: StateFlow 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), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt index d9c058cd85..2e05a6dfa1 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanScreen.kt @@ -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 = {}, ), ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt index 43b54b452f..82a0fc9b55 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/current/TangemPayCurrentPlanUM.kt @@ -20,6 +20,7 @@ internal data class TangemPayCurrentPlanUM( @Immutable data class Button( val text: TextReference, + val isProcessing: Boolean, val onClick: () -> Unit, ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanComponent.kt index 68b4da1190..261d3cbdfd 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/tiers/select/TangemPaySelectPlanComponent.kt @@ -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, + ) } \ No newline at end of file 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 f62f83147c..08daf711bf 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 @@ -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) { 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 = 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 { + 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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt index 55c4d6944f..4751a87b79 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -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),