Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 08:50:52 -07:00
parent 4fc52e2052
commit 017eece8b7
18 changed files with 938 additions and 14 deletions

View file

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

View file

@ -79,4 +79,10 @@ internal class DefaultCustomerOrderRepository @Inject constructor(
OrderConverter.convert(result)
}
}
override suspend fun cancelOrder(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, Unit> {
return requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.cancelOrder(authHeader = authHeader, orderId = orderId)
}.map {}
}
}

View file

@ -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<CustomerMeResponse.DescriptionItem>? = null,
images: List<CustomerMeResponse.Image>? = null,
fees: List<CustomerMeResponse.Fee>? = 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"
}
}

View file

@ -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<Any>(userWalletId = any(), requestBlock = any())
} coAnswers {
val block = secondArg<suspend (String) -> ApiResponse<Any>>()
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<TariffPlanTransitionsResponse>
// 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<TangemPayTariffPlanTransition>())
}
@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"
}
}