Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-17 14:44:06 +05:00
parent 5618f44c8b
commit 084bad17ab
18 changed files with 452 additions and 192 deletions

View file

@ -20,18 +20,24 @@ enum class TangemPayCardState {
/** A close order is in progress; the card is being closed. */
@SerialName("Closing")
Closing,
/** An issue order is in progress; the (additional) card is being issued and not yet provisioned. */
@SerialName("Issuing")
Issuing,
;
override fun toString() = when (this) {
Active -> "Active"
Reissuing -> "Reissuing"
Closing -> "Closing"
Issuing -> "Issuing"
}
companion object {
fun fromString(value: String) = when (value.lowercase(Locale.US)) {
"reissuing" -> Reissuing
"closing" -> Closing
"issuing" -> Issuing
else -> Active
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.pay.repository
import com.tangem.domain.models.wallet.UserWalletId
/**
* Tracks in-flight additional-card issuance orders locally.
*
* The backend does not return a card while it is still being issued, so the issuance order id is
* stored locally to surface an "issuing" placeholder card until the order reaches a terminal state.
*/
interface TangemPayIssueCardRepository {
suspend fun storeIssueOrderId(userWalletId: UserWalletId, orderId: String)
suspend fun getIssueOrderIds(userWalletId: UserWalletId): List<String>
suspend fun removeIssueOrderId(userWalletId: UserWalletId, orderId: String)
}

View file

@ -6,12 +6,15 @@ import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.Offer
import com.tangem.domain.pay.model.Order
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.CustomerOffersRepository
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 com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.launch
import java.util.UUID
/**
@ -33,8 +36,11 @@ import java.util.UUID
class IssueAdditionalCardUseCase(
private val customerOffersRepository: CustomerOffersRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val issueCardRepository: TangemPayIssueCardRepository,
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
private val appCoroutineScope: AppCoroutineScope,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<VisaApiError, Result> = either {
suspend operator fun invoke(userWalletId: UserWalletId): Either<VisaApiError, Unit> = either {
val offer = catch(
block = {
customerOffersRepository.getOffers(userWalletId)
@ -61,20 +67,18 @@ class IssueAdditionalCardUseCase(
idempotencyKey = UUID.randomUUID().toString(),
).bind()
Result(order = order, offer = offer)
issueCardRepository.storeIssueOrderId(userWalletId = userWalletId, orderId = order.id)
appCoroutineScope.launch {
startTangemPayOrderPollingUseCase(
order = TangemPayOrderInfo(orderId = order.id, orderStatus = order.status),
userWalletId = userWalletId,
)
}
}
private fun Raise<VisaApiError>.handleError(throwable: Throwable): Nothing {
TangemLogger.e("Error in IssueAdditionalCardUseCase", throwable)
raise(VisaApiError.Unspecified)
}
/**
* Outcome of a successful run.
*
* @property offer the offer that authorised issuance, carried back so the caller can show pricing
* without an extra round trip.
*/
data class Result(val order: Order, val offer: Offer)
}

View file

@ -10,7 +10,9 @@ import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.OrderType
import com.tangem.domain.pay.repository.CustomerOffersRepository
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
@ -23,7 +25,15 @@ internal class IssueAdditionalCardUseCaseTest {
private val offersRepository: CustomerOffersRepository = mockk()
private val orderRepository: CustomerOrderRepository = mockk()
private val useCase = IssueAdditionalCardUseCase(offersRepository, orderRepository)
private val issueCardRepository: TangemPayIssueCardRepository = mockk(relaxed = true)
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase = mockk(relaxed = true)
private val useCase = IssueAdditionalCardUseCase(
customerOffersRepository = offersRepository,
customerOrderRepository = orderRepository,
issueCardRepository = issueCardRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = TestAppCoroutineScope(),
)
private val userWalletId = UserWalletId("1234567890ABCDEF")
private val spec = "SP_000004"
@ -62,10 +72,9 @@ internal class IssueAdditionalCardUseCaseTest {
val result = useCase(userWalletId)
val resultValue = result.getOrNull()
assertThat(resultValue?.order).isEqualTo(existing)
assertThat(resultValue?.offer).isEqualTo(offer)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 0) { orderRepository.createOrder(any(), any(), any(), any()) }
coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, existing.id) }
}
@Test
@ -118,7 +127,8 @@ internal class IssueAdditionalCardUseCaseTest {
val result = useCase(userWalletId)
assertThat(result.getOrNull()?.order).isEqualTo(newOrder)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { issueCardRepository.storeIssueOrderId(userWalletId, newOrder.id) }
}
private fun order(id: String, type: OrderType, status: OrderStatus): Order = Order(