Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 04:32:24 -07:00
parent b27ff31652
commit ad0b09deae
24 changed files with 397 additions and 40 deletions

View file

@ -97,6 +97,12 @@ interface TangemPayApi {
@Body body: ReissueCardRequest, @Body body: ReissueCardRequest,
): ApiResponse<ReissueCardResponse> ): ApiResponse<ReissueCardResponse>
@POST("v1/customer/card/close")
suspend fun closeCard(
@Header("Authorization") authHeader: String,
@Body body: CloseCardRequest,
): ApiResponse<CloseCardResponse>
@POST("v1/customer/card/withdraw/data") @POST("v1/customer/card/withdraw/data")
suspend fun getWithdrawData( suspend fun getWithdrawData(
@Header("Authorization") authHeader: String, @Header("Authorization") authHeader: String,

View file

@ -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 CloseCardRequest(
@Json(name = "card_id") val cardId: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CloseCardResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: OrderResponse.Result.Status,
)
}

View file

@ -3,8 +3,10 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.DefaultTangemPayCloseCardStore
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
import com.tangem.datasource.local.visa.TangemPayReissueCardStore import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -32,4 +34,12 @@ internal object TangemPayStoresModule {
prefs = prefs, prefs = prefs,
) )
} }
@Provides
@Singleton
fun provideTangemPayCloseCardStore(prefs: AppPreferencesStore): TangemPayCloseCardStore {
return DefaultTangemPayCloseCardStore(
prefs = prefs,
)
}
} }

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.visa
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
internal class DefaultTangemPayCloseCardStore(
private val prefs: AppPreferencesStore,
) : TangemPayCloseCardStore {
override suspend fun setCloseOrderId(cardId: String, orderId: String?) {
if (orderId == null) {
prefs.edit { it.remove(getCloseKey(cardId)) }
} else {
prefs.store(
key = getCloseKey(cardId),
value = orderId,
)
}
}
override suspend fun getOrderId(cardId: String): String? {
return prefs.getSyncOrNull(key = getCloseKey(cardId))
}
private fun getCloseKey(cardId: String) = stringPreferencesKey("tangem_pay_close_card_$cardId")
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.visa
interface TangemPayCloseCardStore {
suspend fun setCloseOrderId(cardId: String, orderId: String?)
suspend fun getOrderId(cardId: String): String?
}

View file

@ -86,6 +86,6 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?, @Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "frozen_state") val frozenState: String, @Json(name = "frozen_state") val frozenState: String,
@Json(name = "last_digits") val lastDigits: String, @Json(name = "last_digits") val lastDigits: String,
@Json(name = "is_reissuing") val isReissuing: Boolean, @Json(name = "state") val state: String,
) )
} }

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardLimit import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -52,7 +53,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
adminDailyLimit = card.limit?.adminCardLimit?.amount, adminDailyLimit = card.limit?.adminCardLimit?.amount,
frozenState = card.frozenState.toString(), frozenState = card.frozenState.toString(),
lastDigits = card.lastDigits, lastDigits = card.lastDigits,
isReissuing = card.isReissuing, state = card.state.toString(),
) )
}, },
) )
@ -108,7 +109,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
), ),
frozenState = TangemPayCardFrozenState.fromString(card.frozenState), frozenState = TangemPayCardFrozenState.fromString(card.frozenState),
lastDigits = card.lastDigits, lastDigits = card.lastDigits,
isReissuing = card.isReissuing, state = TangemPayCardState.fromString(card.state),
) )
}, },
) )

View file

@ -28,6 +28,7 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase
import com.tangem.domain.pay.usecase.CloseTangemPayCardUseCase
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
@ -73,6 +74,10 @@ internal interface TangemPayDataModule {
@Singleton @Singleton
fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository
@Binds
@Singleton
fun bindCloseCardRepository(repository: DefaultCloseCardRepository): TangemPayCloseCardRepository
@Binds @Binds
@Singleton @Singleton
fun bindTangemPayCryptoCurrencyFactory( fun bindTangemPayCryptoCurrencyFactory(
@ -220,5 +225,20 @@ internal interface TangemPayDataModule {
paymentAccountStatusFetcher = paymentAccountStatusFetcher, paymentAccountStatusFetcher = paymentAccountStatusFetcher,
) )
} }
@Provides
fun provideCloseTangemPayCardUseCase(
closeCardRepository: TangemPayCloseCardRepository,
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
appCoroutineScope: AppCoroutineScope,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): CloseTangemPayCardUseCase {
return CloseTangemPayCardUseCase(
closeCardRepository = closeCardRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = appCoroutineScope,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
}
} }
} }

View file

@ -23,7 +23,10 @@ import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.pay.model.isFinalStatus
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -36,7 +39,7 @@ import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher" private const val TAG = "PaymentAccountStatusFetcher"
@Suppress("LongParameterList") @Suppress("LongParameterList", "LargeClass")
internal class DefaultPaymentAccountStatusFetcher @Inject constructor( internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
@ -46,6 +49,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val eligibilityManager: TangemPayEligibilityManager, private val eligibilityManager: TangemPayEligibilityManager,
private val reissueCardRepository: TangemPayReissueCardRepository, private val reissueCardRepository: TangemPayReissueCardRepository,
private val closeCardRepository: TangemPayCloseCardRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : PaymentAccountStatusFetcher { ) : PaymentAccountStatusFetcher {
@ -299,16 +303,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
cardInfo: CustomerInfo.CardInfo, cardInfo: CustomerInfo.CardInfo,
customerId: String, customerId: String,
): PaymentAccountStatusValue { ): PaymentAccountStatusValue {
val reissueOrder = reissueCardRepository.getReissueOrderInfo( val cardId = productInstance.cardId
userWalletId = userWalletId, val cardState = getCardState(cardId, userWalletId)
cardId = productInstance.cardId, val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(cardId)
).getOrNull()
val isReissuing = reissueOrder != null &&
reissueOrder.orderStatus != OrderStatus.CANCELED &&
reissueOrder.orderStatus != OrderStatus.COMPLETED
val cardFrozenState = cardDetailsRepository.cardFrozenStateSync(productInstance.cardId)
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId) val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return PaymentAccountStatusValue.Loaded( return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
@ -321,7 +318,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
cards = listOf( cards = listOf(
TangemPayCard( TangemPayCard(
id = productInstance.cardId, id = cardId,
hasPinCode = cardInfo.isPinSet, hasPinCode = cardInfo.isPinSet,
displayName = productInstance.displayName, displayName = productInstance.displayName,
limit = TangemPayCardLimitData( limit = TangemPayCardLimitData(
@ -334,12 +331,35 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
productInstance.frozenState productInstance.frozenState
}, },
lastDigits = cardInfo.lastFourDigits, lastDigits = cardInfo.lastFourDigits,
isReissuing = isReissuing, state = cardState,
), ),
), ),
) )
} }
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()
return if (closingOrderId != null) {
val order = cardDetailsRepository.getOrderInfo(userWalletId, closingOrderId).getOrNull()
if (order != null && order.orderStatus.isFinalStatus) {
closeCardRepository.setCloseOrderId(cardId, null)
TangemPayCardState.Active
} else {
TangemPayCardState.Closing
}
} else if (reissueOrderId != null) {
val order = cardDetailsRepository.getOrderInfo(userWalletId, reissueOrderId).getOrNull()
if (order != null && order.orderStatus.isFinalStatus) {
TangemPayCardState.Active
} else {
TangemPayCardState.Reissuing
}
} else {
TangemPayCardState.Active
}
}
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
return when (this) { return when (this) {
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced

View file

@ -0,0 +1,52 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CloseCardRequest
import com.tangem.datasource.local.visa.TangemPayCloseCardStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching
import javax.inject.Inject
internal class DefaultCloseCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayCloseCardStore: TangemPayCloseCardStore,
) : TangemPayCloseCardRepository {
override suspend fun closeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<VisaApiError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.closeCard(
authHeader = authHeader,
body = CloseCardRequest(cardId = cardId),
)
}.bind()
TangemPayOrderInfo(
orderId = response.result.orderId,
orderStatus = OrderStatusConverter.convert(response.result.status),
)
}
override suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit> =
runSuspendCatching {
tangemPayCloseCardStore.setCloseOrderId(cardId, orderId)
}.fold(
onSuccess = { Unit.right() },
onFailure = { Either.Left(VisaApiError.Unspecified) },
)
override suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> =
either {
runSuspendCatching { tangemPayCloseCardStore.getOrderId(cardId) }.getOrNull()
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.runSuspendCatching
@ -21,7 +20,6 @@ internal class DefaultReissueCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi, private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer, private val requestHelper: TangemPayRequestPerformer,
private val tangemPayReissueCardStore: TangemPayReissueCardStore, private val tangemPayReissueCardStore: TangemPayReissueCardStore,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : TangemPayReissueCardRepository { ) : TangemPayReissueCardRepository {
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> = override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
@ -74,17 +72,11 @@ internal class DefaultReissueCardRepository @Inject constructor(
onFailure = { Either.Left(VisaApiError.Unspecified) }, onFailure = { Either.Left(VisaApiError.Unspecified) },
) )
override suspend fun getReissueOrderInfo( override suspend fun getReissueOrderId(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cardId: String, cardId: String,
): Either<UniversalError, TangemPayOrderInfo?> = either { ): Either<UniversalError, String?> = either {
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull() runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
if (orderId == null) {
return null.right()
}
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
} }
private companion object { private companion object {

View file

@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
* @property limit spending limit configuration for the card; `null` if not configured or not yet loaded. * @property limit spending limit configuration for the card; `null` if not configured or not yet loaded.
* @property frozenState whether the card is currently frozen (blocked for payments). * @property frozenState whether the card is currently frozen (blocked for payments).
* @property lastDigits The last four digits of the card number. * @property lastDigits The last four digits of the card number.
* @property state current lifecycle state of the card.
*/ */
@Serializable @Serializable
data class TangemPayCard( data class TangemPayCard(
@ -22,7 +23,7 @@ data class TangemPayCard(
@SerialName("limit") val limit: TangemPayCardLimitData?, @SerialName("limit") val limit: TangemPayCardLimitData?,
@SerialName("frozen_state") val frozenState: TangemPayCardFrozenState, @SerialName("frozen_state") val frozenState: TangemPayCardFrozenState,
@SerialName("last_digits") val lastDigits: String, @SerialName("last_digits") val lastDigits: String,
@SerialName("is_reissuing") val isReissuing: Boolean, @SerialName("state") val state: TangemPayCardState,
) )
val TangemPayCard.isFrozen val TangemPayCard.isFrozen

View file

@ -0,0 +1,38 @@
package com.tangem.domain.models.pay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
/**
* Lifecycle state of a Tangem Pay card.
*/
@Serializable
enum class TangemPayCardState {
/** Card is operational and ready to use. */
@SerialName("Active")
Active,
/** A reissue order is in progress; the card is being replaced. */
@SerialName("Reissuing")
Reissuing,
/** A close order is in progress; the card is being closed. */
@SerialName("Closing")
Closing,
;
override fun toString() = when (this) {
Active -> "Active"
Reissuing -> "Reissuing"
Closing -> "Closing"
}
companion object {
fun fromString(value: String) = when (value.lowercase(Locale.US)) {
"reissuing" -> Reissuing
"closing" -> Closing
else -> Active
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.visa.error.VisaApiError
interface TangemPayCloseCardRepository {
suspend fun closeCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayOrderInfo>
suspend fun setCloseOrderId(cardId: String, orderId: String?): Either<UniversalError, Unit>
suspend fun getCloseOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
}

View file

@ -15,8 +15,5 @@ interface TangemPayReissueCardRepository {
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit> suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
suspend fun getReissueOrderInfo( suspend fun getReissueOrderId(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayOrderInfo?>
} }

View file

@ -0,0 +1,33 @@
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.repository.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.launch
class CloseTangemPayCardUseCase(
private val closeCardRepository: TangemPayCloseCardRepository,
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val appCoroutineScope: AppCoroutineScope,
) {
suspend operator fun invoke(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, Unit> = either {
val order = closeCardRepository.closeCard(userWalletId, cardId).bind()
if (order.orderStatus == OrderStatus.CANCELED) {
raise(VisaApiError.Unspecified)
}
closeCardRepository.setCloseOrderId(cardId, order.orderId)
paymentAccountStatusFetcher.invoke(userWalletId)
appCoroutineScope.launch {
startTangemPayOrderPollingUseCase(order, userWalletId)
}
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.domain.pay.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.TestAppCoroutineScope
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.TangemPayCloseCardRepository
import com.tangem.domain.visa.error.VisaApiError
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class CloseTangemPayCardUseCaseTest {
private val closeCardRepository: TangemPayCloseCardRepository = mockk(relaxUnitFun = true)
private val startPollingUseCase: StartTangemPayOrderPollingUseCase = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
@Test
fun `GIVEN closeCard fails WHEN invoke THEN returns Left and skips store, fetch and polling`() = runTest {
val useCase = createUseCase()
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns VisaApiError.Unspecified.left()
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) }
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN closeCard returns CANCELED order WHEN invoke THEN returns Left and skips store, fetch and polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { closeCardRepository.setCloseOrderId(any(), any()) }
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN closeCard returns PROCESSING order WHEN invoke THEN stores order id, fetches status and starts polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isRight()).isTrue()
coVerifyOrder {
closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID)
paymentAccountStatusFetcher.invoke(USER_WALLET_ID)
startPollingUseCase(order, USER_WALLET_ID)
}
}
@Test
fun `GIVEN closeCard returns COMPLETED order WHEN invoke THEN stores order id, fetches status and starts polling`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
coEvery { closeCardRepository.closeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) } returns Unit.right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID)
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { closeCardRepository.setCloseOrderId(CARD_ID, ORDER_ID) }
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
coVerify(exactly = 1) { startPollingUseCase(order, USER_WALLET_ID) }
}
private fun createUseCase() = CloseTangemPayCardUseCase(
closeCardRepository = closeCardRepository,
startTangemPayOrderPollingUseCase = startPollingUseCase,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
appCoroutineScope = TestAppCoroutineScope(),
)
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
const val CARD_ID = "card-test-id"
const val ORDER_ID = "order-test-1"
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -95,7 +96,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
val status = state.value val status = state.value
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) { if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
val card = state.findCard(initialCard.id, params.initialStatus) ?: return@onEach val card = state.findCard(initialCard.id, params.initialStatus) ?: return@onEach
if (card.isReissuing) { if (card.state != TangemPayCardState.Active) {
requestHide() requestHide()
} }
card.displayName?.let { uiState.update(TangemPayCardDetailsUpdateNameTransformer(it)) } card.displayName?.let { uiState.update(TangemPayCardDetailsUpdateNameTransformer(it)) }
@ -103,7 +104,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
uiState.copy( uiState.copy(
numberShort = "${StringsSigns.ASTERISK}${card.lastDigits}", numberShort = "${StringsSigns.ASTERISK}${card.lastDigits}",
cardFrozenState = card.frozenState, cardFrozenState = card.frozenState,
isActionsAvailable = !card.isReissuing, isActionsAvailable = card.state == TangemPayCardState.Active,
) )
} }
subscribeToCardFrozenState(card.id) subscribeToCardFrozenState(card.id)

View file

@ -28,6 +28,7 @@ import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.pay.isFrozen import com.tangem.domain.models.pay.isFrozen
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.model.TangemPayTopUpData
@ -125,7 +126,7 @@ internal class TangemPayCardPageModel @Inject constructor(
dailyLimitState = dailyLimitState, dailyLimitState = dailyLimitState,
settings = buildSettings(card), settings = buildSettings(card),
settingsV2 = buildSettingsV2(card), settingsV2 = buildSettingsV2(card),
isReissueInProgress = card.isReissuing, isReissueInProgress = card.state == TangemPayCardState.Reissuing,
) )
} }
} else { } else {

View file

@ -30,6 +30,7 @@ import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.AddFundsListener
@ -101,7 +102,7 @@ internal class TangemPayDetailsModel @Inject constructor(
stateFactory.getInitialState( stateFactory.getInitialState(
isTangemPayDeactivated = isTangemPayDeactivated, isTangemPayDeactivated = isTangemPayDeactivated,
cardNumberEnd = firstCard?.lastDigits.orEmpty(), cardNumberEnd = firstCard?.lastDigits.orEmpty(),
isReissuing = firstCard?.isReissuing ?: false, isReissuing = firstCard == null || firstCard.state != TangemPayCardState.Active,
), ),
) )

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.model.transformers package com.tangem.features.tangempay.model.transformers
import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
@ -15,7 +16,7 @@ internal class TangemPayCardDataTransformer(
val updatedCard = TangemPayDetailsBalanceBlockState.Card( val updatedCard = TangemPayDetailsBalanceBlockState.Card(
lastDigits = card.lastDigits, lastDigits = card.lastDigits,
onClick = onCardClick, onClick = onCardClick,
isReissuing = card.isReissuing, isReissuing = card.state != TangemPayCardState.Active,
) )
val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy( val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy(
cards = persistentListOf(updatedCard), cards = persistentListOf(updatedCard),

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardLimit import com.tangem.domain.models.pay.TangemPayCardLimit
import com.tangem.domain.models.pay.TangemPayCardLimitData import com.tangem.domain.models.pay.TangemPayCardLimitData
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
@ -49,7 +50,7 @@ internal class TangemPayCardLimitSetupModelTest {
frozenState = TangemPayCardFrozenState.Unfrozen, frozenState = TangemPayCardFrozenState.Unfrozen,
lastDigits = "1234", lastDigits = "1234",
limit = null, limit = null,
isReissuing = false, state = TangemPayCardState.Active,
) )
private val initialStatus: AccountStatus.Payment = AccountStatus.Payment( private val initialStatus: AccountStatus.Payment = AccountStatus.Payment(
@ -79,7 +80,7 @@ internal class TangemPayCardLimitSetupModelTest {
) )
} }
), ),
isReissuing = false, state = TangemPayCardState.Active,
) )
val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) { val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
every { source } returns StatusSource.ACTUAL every { source } returns StatusSource.ACTUAL

View file

@ -13,6 +13,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
@ -67,7 +68,7 @@ internal class TangemPayMainBlockConverter(
is PaymentAccountStatusValue.Loaded -> { is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
TangemPayMainUM.Content( TangemPayMainUM.Content(
subtitle = if (card.isReissuing) { subtitle = if (card.state == TangemPayCardState.Reissuing) {
resourceReference(R.string.tangempay_status_replacing) resourceReference(R.string.tangempay_status_replacing)
} else { } else {
stringReference("*${card.lastDigits}") stringReference("*${card.lastDigits}")