Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-25 22:45:58 +04:00
parent 84af4950a1
commit ff2559df3e
27 changed files with 904 additions and 20 deletions

View file

@ -0,0 +1,19 @@
package com.tangem.features.tangempay.components
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
interface TangemPayTransactionBottomSheetComponent : ComposableBottomSheetComponent {
data class Params(
val isBalanceHidden: Boolean,
val transaction: TangemPayTxHistoryItem,
val userWalletId: UserWalletId,
val customerId: String,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, TangemPayTransactionBottomSheetComponent>
}

View file

@ -94,7 +94,7 @@ internal class TangemPayDetailsComponent(
)
is TangemPayDetailsNavigation.TransactionDetails -> TangemPayTxHistoryDetailsComponent(
appComponentContext = context,
params = TangemPayTxHistoryDetailsComponent.Params(
params = TangemPayTransactionBottomSheetComponent.Params(
transaction = navigation.transaction,
isBalanceHidden = navigation.isBalanceHidden,
userWalletId = params.initialStatus.userWalletId,

View file

@ -5,16 +5,17 @@ import androidx.compose.runtime.getValue
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.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class TangemPayTxHistoryDetailsComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
internal class TangemPayTxHistoryDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: TangemPayTransactionBottomSheetComponent.Params,
) : TangemPayTransactionBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayTxHistoryDetailsModel = getOrCreateModel(params = params)
@ -28,11 +29,11 @@ internal class TangemPayTxHistoryDetailsComponent(
TangemPayTxHistoryDetailsContent(state = state)
}
data class Params(
val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
val userWalletId: UserWalletId,
val customerId: String,
val onDismiss: () -> Unit,
)
@AssistedFactory
interface Factory : TangemPayTransactionBottomSheetComponent.Factory {
override fun create(
context: AppComponentContext,
params: TangemPayTransactionBottomSheetComponent.Params,
): TangemPayTxHistoryDetailsComponent
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.tangempay.di
import com.tangem.features.tangempay.components.DefaultTangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.listener.DefaultCardDetailsEventListener
import dagger.Binds
@ -23,4 +25,10 @@ internal interface TangemPayDetailsFeatureModule {
@Binds
@Singleton
fun bindCardDetailsEventListener(impl: DefaultCardDetailsEventListener): CardDetailsEventListener
@Binds
@Singleton
fun bindTangemPayTransactionBottomSheetComponentFactory(
factory: TangemPayTxHistoryDetailsComponent.Factory,
): TangemPayTransactionBottomSheetComponent.Factory
}

View file

@ -13,7 +13,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -34,7 +34,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>()
private val params = paramsContainer.require<TangemPayTransactionBottomSheetComponent.Params>()
val uiState: StateFlow<TangemPayTxHistoryDetailsUM>
field = MutableStateFlow(
value = TangemPayTxHistoryDetailsConverter.convert(

View file

@ -0,0 +1,10 @@
package com.tangem.features.tangempay.deeplink
import kotlinx.coroutines.CoroutineScope
interface TangemPayMainDeepLinkHandler {
interface Factory {
fun create(scope: CoroutineScope, payload: Map<String, String>): TangemPayMainDeepLinkHandler
}
}

View file

@ -0,0 +1,118 @@
package com.tangem.features.tangempay.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_WALLET_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.visa.model.TangemPayPushNotificationType
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
internal class DefaultTangemPayMainDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@Assisted private val payload: Map<String, String>,
private val appRouter: AppRouter,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val paymentAccountSupplier: PaymentAccountStatusSupplier,
) : TangemPayMainDeepLinkHandler {
init {
handleDeepLink()
}
private fun handleDeepLink() {
val walletId = payload[CUSTOMER_WALLET_ID_KEY]
scope.launch {
val userWalletId = walletId?.let(::UserWalletId) ?: run {
appRouter.popTo(AppRoute.Wallet)
return@launch
}
val userWallet = getUserWalletUseCase(userWalletId).getOrNull()
if (userWallet == null || userWallet.isLocked) {
appRouter.popTo(AppRoute.Wallet)
return@launch
}
if (selectWalletUseCase(userWalletId).getOrNull() == null) {
appRouter.popTo(AppRoute.Wallet)
return@launch
}
val pushAction = buildPushAction()
appRouter.popTo(
route = AppRoute.Wallet,
onComplete = {
walletDeepLinkActionTrigger.selectWallet(userWalletId)
when (pushAction) {
is TangemPayPushAction.CardReady,
is TangemPayPushAction.TopUp,
-> navigateToTangemPayDetails(userWalletId)
is TangemPayPushAction.TransactionSpend -> {
walletDeepLinkActionTrigger.showTangemPayTransaction(
transaction = pushAction.transaction,
customerId = pushAction.customerId,
)
}
is TangemPayPushAction.CollateralTransaction -> {
walletDeepLinkActionTrigger.showTangemPayTransaction(
transaction = pushAction.transaction,
customerId = pushAction.customerId,
)
}
null -> Unit
}
},
)
}
}
private fun buildPushAction(): TangemPayPushAction? {
val type = payload[TYPE_KEY]?.let(TangemPayPushNotificationType::fromValue) ?: return null
val customerId = payload[CUSTOMER_ID_KEY].orEmpty()
return when (type) {
TangemPayPushNotificationType.CARD_READY -> TangemPayPushAction.CardReady
TangemPayPushNotificationType.TRANSACTION_SPEND -> {
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
if (transaction != null) TangemPayPushAction.TransactionSpend(transaction, customerId) else null
}
TangemPayPushNotificationType.TOP_UP -> TangemPayPushAction.TopUp
TangemPayPushNotificationType.COLLATERAL -> {
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
if (transaction != null) TangemPayPushAction.CollateralTransaction(transaction, customerId) else null
}
}
}
private fun navigateToTangemPayDetails(walletId: UserWalletId) {
scope.launch {
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(walletId))
val paymentAccountStatus = paymentAccountSupplier.invoke(userWalletId = walletId)
.firstOrNull()
?: return@launch
appRouter.push(route = AppRoute.TangemPayDetails(status = paymentAccountStatus))
}
}
@AssistedFactory
interface Factory : TangemPayMainDeepLinkHandler.Factory {
override fun create(scope: CoroutineScope, payload: Map<String, String>): DefaultTangemPayMainDeepLinkHandler
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.tangempay.deeplink
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
internal sealed class TangemPayPushAction {
data object CardReady : TangemPayPushAction()
data class TransactionSpend(
val transaction: TangemPayTxHistoryItem,
val customerId: String,
) : TangemPayPushAction()
data object TopUp : TangemPayPushAction()
data class CollateralTransaction(
val transaction: TangemPayTxHistoryItem,
val customerId: String,
) : TangemPayPushAction()
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.tangempay.deeplink
import com.tangem.domain.pay.utils.TangemPayTxHistoryItemStatusConverter
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.extensions.orZero
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import java.math.BigDecimal
import java.util.Currency
object TangemPayPushPayloadToTxHistoryItemConverter {
private const val KEY_ID = "transaction_id"
private const val KEY_AMOUNT = "amount"
private const val KEY_CURRENCY = "currency"
private const val KEY_LOCAL_AMOUNT = "local_amount"
private const val KEY_LOCAL_CURRENCY = "local_currency"
private const val KEY_AUTHORIZED_AMOUNT = "authorized_amount"
private const val KEY_MERCHANT_NAME = "merchant_name"
private const val KEY_ENRICHED_MERCHANT_NAME = "enriched_merchant_name"
private const val KEY_ENRICHED_MERCHANT_ICON = "enriched_merchant_icon"
private const val KEY_ENRICHED_MERCHANT_CATEGORY = "enriched_merchant_category"
private const val KEY_MERCHANT_CATEGORY = "merchant_category"
private const val KEY_MERCHANT_CATEGORY_CODE = "merchant_category_code"
private const val KEY_STATUS = "status"
private const val KEY_DECLINED_REASON = "declined_reason"
private const val KEY_AUTHORIZED_AT = "authorized_at"
private const val KEY_POSTED_AT = "posted_at"
private const val KEY_TRANSACTION_HASH = "transaction_hash"
@Suppress("ComplexCondition")
fun convertSpend(payload: Map<String, String>): TangemPayTxHistoryItem.Spend? {
val id = payload[KEY_ID]?.ifEmpty { null } ?: return null
val amount = payload[KEY_AMOUNT]?.toBigDecimalOrNull() ?: return null
val currency = payload[KEY_CURRENCY]?.let(::parseCurrency) ?: return null
val merchantName = payload[KEY_MERCHANT_NAME] ?: payload[KEY_ENRICHED_MERCHANT_NAME] ?: return null
val status = payload[KEY_STATUS]?.ifEmpty { null } ?: return null
val authorizedAt = payload[KEY_AUTHORIZED_AT]?.let(::parseDateTime) ?: return null
return TangemPayTxHistoryItem.Spend(
id = id,
jsonRepresentation = payload.toString(),
date = authorizedAt.withZone(DateTimeZone.getDefault()),
amount = amount,
currency = currency,
authorizedAmount = payload[KEY_AUTHORIZED_AMOUNT]?.toBigDecimalOrNull().orZero(),
localAmount = payload[KEY_LOCAL_AMOUNT]?.toBigDecimalOrNull(),
localCurrency = payload[KEY_LOCAL_CURRENCY]?.let(::parseCurrency),
enrichedMerchantName = payload[KEY_ENRICHED_MERCHANT_NAME],
merchantName = merchantName,
enrichedMerchantCategory = payload[KEY_ENRICHED_MERCHANT_CATEGORY],
merchantCategoryCode = payload[KEY_MERCHANT_CATEGORY_CODE],
merchantCategory = payload[KEY_MERCHANT_CATEGORY],
status = TangemPayTxHistoryItemStatusConverter.convert(status),
enrichedMerchantIconUrl = payload[KEY_ENRICHED_MERCHANT_ICON],
declinedReason = payload[KEY_DECLINED_REASON],
)
}
@Suppress("ComplexCondition")
fun convertCollateral(payload: Map<String, String>): TangemPayTxHistoryItem.Collateral? {
val id = payload[KEY_ID]?.ifEmpty { null } ?: return null
val amount = payload[KEY_AMOUNT]?.toBigDecimalOrNull() ?: return null
val transactionHash = payload[KEY_TRANSACTION_HASH]?.ifEmpty { null } ?: return null
val postedAt = payload[KEY_POSTED_AT]?.let(::parseDateTime) ?: return null
val currency = payload[KEY_CURRENCY]?.let(::parseCurrency) ?: return null
return TangemPayTxHistoryItem.Collateral(
id = id,
jsonRepresentation = payload.toString(),
date = postedAt.withZone(DateTimeZone.getDefault()),
currency = currency,
amount = amount,
transactionHash = transactionHash,
type = if (amount >= BigDecimal.ZERO) {
TangemPayTxHistoryItem.Type.Deposit
} else {
TangemPayTxHistoryItem.Type.Withdrawal
},
)
}
private fun parseCurrency(code: String): Currency? = runCatching {
return Currency.getInstance(code.uppercase())
}.getOrNull()
private fun parseDateTime(value: String): DateTime? = runCatching {
return DateTime.parse(value).withZone(DateTimeZone.getDefault())
}.getOrNull()
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.tangempay.di
import com.tangem.features.tangempay.deeplink.DefaultOnboardVisaDeepLinkHandler
import com.tangem.features.tangempay.deeplink.DefaultTangemPayMainDeepLinkHandler
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -15,4 +17,10 @@ internal interface TangemPayDeeplinkModule {
@Binds
@Singleton
fun bindDeepLinkHandlerFactory(impl: DefaultOnboardVisaDeepLinkHandler.Factory): OnboardVisaDeepLinkHandler.Factory
@Binds
@Singleton
fun bindTangemPayMainDeepLinkHandlerFactory(
impl: DefaultTangemPayMainDeepLinkHandler.Factory,
): TangemPayMainDeepLinkHandler.Factory
}

View file

@ -0,0 +1,400 @@
package com.tangem.features.tangempay.deeplink
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import java.math.BigDecimal
class TangemPayPushPayloadToTxHistoryItemConverterTest {
@Test
fun `convertSpend returns Spend tx with all fields when payload is complete`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"local_amount" to "5.24",
"local_currency" to "usd",
"authorized_amount" to "5.24",
"merchant_name" to "PLAYSTATION NETWORK",
"enriched_merchant_name" to "Playstation",
"enriched_merchant_icon" to "https://example.com/icon.png",
"enriched_merchant_category" to "Gaming",
"merchant_category" to "Digital Goods",
"merchant_category_code" to "5818",
"status" to "completed",
"declined_reason" to "",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNotNull()
assertThat(result!!.id).isEqualTo("txn-123")
assertThat(result.amount).isEqualTo(BigDecimal("5.24"))
assertThat(result.currency.currencyCode).isEqualTo("USD")
assertThat(result.localAmount).isEqualTo(BigDecimal("5.24"))
assertThat(result.localCurrency?.currencyCode).isEqualTo("USD")
assertThat(result.authorizedAmount).isEqualTo(BigDecimal("5.24"))
assertThat(result.merchantName).isEqualTo("PLAYSTATION NETWORK")
assertThat(result.enrichedMerchantName).isEqualTo("Playstation")
assertThat(result.enrichedMerchantIconUrl).isEqualTo("https://example.com/icon.png")
assertThat(result.enrichedMerchantCategory).isEqualTo("Gaming")
assertThat(result.merchantCategory).isEqualTo("Digital Goods")
assertThat(result.merchantCategoryCode).isEqualTo("5818")
assertThat(result.status).isEqualTo(TangemPayTxHistoryItem.Status.COMPLETED)
}
@Test
fun `convertSpend returns null when transaction_id is missing`() {
val payload = mapOf(
"amount" to "5.24",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when amount is missing`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when currency is missing`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when merchant_name is missing`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend falls back to enriched_merchant_name when merchant_name is absent`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"enriched_merchant_name" to "Playstation",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNotNull()
assertThat(result!!.merchantName).isEqualTo("Playstation")
}
@Test
fun `convertSpend returns null when status is missing`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"merchant_name" to "Test",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when authorized_at is missing`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when amount is not a number`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "abc",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend returns null when currency is invalid`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "INVALID",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend maps all statuses correctly`() {
fun spendPayloadWithStatus(status: String) = mapOf(
"transaction_id" to "txn-123",
"amount" to "1.00",
"currency" to "usd",
"merchant_name" to "Test",
"status" to status,
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("pending"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.PENDING)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("reserved"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.RESERVED)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("completed"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.COMPLETED)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("declined"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.DECLINED)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("reversed"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.REVERSED)
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("unknown_value"))!!.status)
.isEqualTo(TangemPayTxHistoryItem.Status.UNKNOWN)
}
@Test
fun `convertSpend returns null when transaction_id is empty`() {
val payload = mapOf(
"transaction_id" to "",
"amount" to "5.24",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns Collateral with all fields when payload is complete`() {
val payload = mapOf(
"transaction_id" to "col-123",
"amount" to "50.00",
"currency" to "usd",
"transaction_hash" to "0xabc123",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNotNull()
assertThat(result!!.id).isEqualTo("col-123")
assertThat(result.amount).isEqualTo(BigDecimal("50.00"))
assertThat(result.currency.currencyCode).isEqualTo("USD")
assertThat(result.transactionHash).isEqualTo("0xabc123")
assertThat(result.type).isEqualTo(TangemPayTxHistoryItem.Type.Deposit)
}
@Test
fun `convertCollateral returns Withdrawal type for negative amount`() {
val payload = mapOf(
"transaction_id" to "col-456",
"amount" to "-10.00",
"currency" to "usd",
"transaction_hash" to "0xdef789",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNotNull()
assertThat(result!!.type).isEqualTo(TangemPayTxHistoryItem.Type.Withdrawal)
}
@Test
fun `convertCollateral returns null when transaction_id is missing`() {
val payload = mapOf(
"amount" to "50.00",
"currency" to "usd",
"transaction_hash" to "0xabc123",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null when amount is missing`() {
val payload = mapOf(
"transaction_id" to "col-123",
"currency" to "usd",
"transaction_hash" to "0xabc123",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null when transaction_hash is missing`() {
val payload = mapOf(
"transaction_id" to "col-123",
"amount" to "50.00",
"currency" to "usd",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null when posted_at is missing`() {
val payload = mapOf(
"transaction_id" to "col-123",
"amount" to "50.00",
"currency" to "usd",
"transaction_hash" to "0xabc123",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null when currency is missing`() {
val payload = mapOf(
"transaction_id" to "col-123",
"amount" to "50.00",
"transaction_hash" to "0xabc123",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null when transaction_hash is empty`() {
val payload = mapOf(
"transaction_id" to "col-123",
"amount" to "50.00",
"currency" to "usd",
"transaction_hash" to "",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNull()
}
@Test
fun `convertSpend handles optional fields as null`() {
val payload = mapOf(
"transaction_id" to "txn-123",
"amount" to "5.24",
"currency" to "usd",
"merchant_name" to "Test",
"status" to "completed",
"authorized_at" to "2025-10-24T10:32:24.496Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
assertThat(result).isNotNull()
assertThat(result!!.localAmount).isNull()
assertThat(result.localCurrency).isNull()
assertThat(result.enrichedMerchantName).isNull()
assertThat(result.enrichedMerchantIconUrl).isNull()
assertThat(result.enrichedMerchantCategory).isNull()
assertThat(result.merchantCategory).isNull()
assertThat(result.merchantCategoryCode).isNull()
assertThat(result.declinedReason).isNull()
}
@Test
fun `convertCollateral returns Deposit type for zero amount`() {
val payload = mapOf(
"transaction_id" to "col-789",
"amount" to "0",
"currency" to "usd",
"transaction_hash" to "0xdef",
"posted_at" to "2025-10-25T19:22:22.597Z",
)
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
assertThat(result).isNotNull()
assertThat(result!!.type).isEqualTo(TangemPayTxHistoryItem.Type.Deposit)
}
@Test
fun `convertSpend returns null for empty payload`() {
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(emptyMap())
assertThat(result).isNull()
}
@Test
fun `convertCollateral returns null for empty payload`() {
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(emptyMap())
assertThat(result).isNull()
}
}