Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 18:45:24 +05:00
parent 9bc5ee1684
commit 4cd1bb77a1
21 changed files with 279 additions and 20 deletions

View file

@ -40,6 +40,10 @@ data class CustomerInfo(
/** Transitional single-card accessor — returns the first card, or null if none. */
val cardInfo: CardInfo? get() = cards.firstOrNull()
/** Card-level product instances only (excludes the VA ACCOUNT instance). */
val cardProductInstances: List<ProductInstance>
get() = productInstances.filter { it.specificationDataType == ProductInstance.SpecificationDataType.CARD }
enum class State {
NEW,
ACTIVE,

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.visa.error.VisaApiError
@Suppress("TooManyFunctions")
interface OnboardingRepository {
suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean>
@ -30,6 +31,16 @@ interface OnboardingRepository {
suspend fun getOrderId(userWalletId: UserWalletId): String?
/** Creates a Virtual Account on-ramp order (VA MVP0, TWI-1638); returns the created order id. */
suspend fun createVirtualAccountOrder(
userWalletId: UserWalletId,
paymentAccountAddress: String,
): Either<VisaApiError, String>
suspend fun getVirtualAccountOrderId(userWalletId: UserWalletId): String?
suspend fun storeVirtualAccountOrderId(userWalletId: UserWalletId, vaOrderId: String)
suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType>

View file

@ -0,0 +1,40 @@
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.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
/**
* Creates the Virtual Account on-ramp order (VA MVP0, TWI-1638) and persists the returned id as `vaOrderId`.
*
* Idempotent: if an order id was already stored for the wallet, it is returned without hitting the network.
* Otherwise creates the order (`ACCOUNT_ISSUE_VIRTUAL_RAIN`) and stores the returned id.
*
* @property onboardingRepository resolves the customer wallet address, creates the order, and persists the id.
*/
class CreateVirtualAccountOrderUseCase(
private val onboardingRepository: OnboardingRepository,
private val pollingUseCase: StartTangemPayOrderPollingUseCase,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
paymentAccountAddress: String,
): Either<VisaApiError, Unit> = either {
onboardingRepository.getVirtualAccountOrderId(userWalletId)
?: run {
val vaOrderId = onboardingRepository.createVirtualAccountOrder(
userWalletId = userWalletId,
paymentAccountAddress = paymentAccountAddress,
).bind()
onboardingRepository.storeVirtualAccountOrderId(userWalletId = userWalletId, vaOrderId = vaOrderId)
pollingUseCase.invoke(
order = TangemPayOrderInfo(orderId = vaOrderId, orderStatus = OrderStatus.NEW),
userWalletId = userWalletId,
)
}
}
}

View file

@ -0,0 +1,63 @@
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.repository.OnboardingRepository
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 CreateVirtualAccountOrderUseCaseTest {
private val onboardingRepository: OnboardingRepository = mockk(relaxUnitFun = true)
private val pollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true)
private val useCase = CreateVirtualAccountOrderUseCase(onboardingRepository, pollingUseCase)
private val userWalletId = UserWalletId("1234567890ABCDEF")
private val paymentAccountAddress = "0xcollateral"
@Test
fun `GIVEN stored va order id WHEN invoke THEN skips creation and polling`() = runTest {
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns "existing-id"
val result = useCase(userWalletId, paymentAccountAddress)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 0) { onboardingRepository.createVirtualAccountOrder(any(), any()) }
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
}
@Test
fun `GIVEN no stored id and create succeeds WHEN invoke THEN stores id and starts polling`() = runTest {
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns null
coEvery {
onboardingRepository.createVirtualAccountOrder(userWalletId, paymentAccountAddress)
} returns "new-id".right()
val result = useCase(userWalletId, paymentAccountAddress)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { onboardingRepository.storeVirtualAccountOrderId(userWalletId, "new-id") }
coVerify(exactly = 1) { pollingUseCase.invoke(any(), userWalletId) }
}
@Test
fun `GIVEN no stored id and create fails WHEN invoke THEN returns error and does not store or poll`() = runTest {
coEvery { onboardingRepository.getVirtualAccountOrderId(userWalletId) } returns null
coEvery {
onboardingRepository.createVirtualAccountOrder(userWalletId, paymentAccountAddress)
} returns VisaApiError.Unspecified.left()
val result = useCase(userWalletId, paymentAccountAddress)
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
coVerify(exactly = 0) { onboardingRepository.storeVirtualAccountOrderId(any(), any()) }
coVerify(exactly = 0) { pollingUseCase.invoke(any(), any()) }
}
}