Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 11:50:47 +03:00
parent 1edf995ab0
commit 2dadb60975
26 changed files with 1110 additions and 48 deletions

View file

@ -1,14 +1,27 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import javax.inject.Singleton
@Module
@ -25,9 +38,34 @@ internal object TxHistoryItemsStoreModule {
@Provides
@Singleton
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
fun provideTangemPayTxHistoryItemsStore(
@ApplicationContext context: Context,
appScope: AppCoroutineScope,
toDMConverter: TangemPayTxHistoryItemToDMConverter,
toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
): TangemPayTxHistoryItemsStore {
return DefaultTangemPayTxHistoryItemsStore(
dataStore = RuntimeDataStore(),
dataStore = DataStoreFactory.create(
serializer = KotlinxDataStoreSerializer(
defaultValue = emptyMap(),
serializer = MapSerializer(
keySerializer = String.serializer(),
valueSerializer = MapSerializer(
keySerializer = String.serializer(),
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
),
),
// TangemPayTxHistoryItemDM.Collateral declares a `type` field that would clash with the
// default kotlinx polymorphic class discriminator ("type"), so persist sealed subtypes
// under a non-conflicting discriminator key.
json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" },
),
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
produceFile = { context.dataStoreFile(fileName = "tangem_pay_tx_history") },
scope = appScope,
),
toDMConverter = toDMConverter,
toDomainConverter = toDomainConverter,
)
}
}

View file

@ -1,23 +1,37 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import androidx.datastore.core.DataStore
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import kotlinx.coroutines.flow.first
internal typealias StoredTangemPayTxHistory = Map<String, Map<String, List<TangemPayTxHistoryItemDM>>>
internal class DefaultTangemPayTxHistoryItemsStore(
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
) : TangemPayTxHistoryItemsStore,
StringKeyDataStoreDecorator<String, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
override fun provideStringKey(key: String): String = key
private val dataStore: DataStore<StoredTangemPayTxHistory>,
private val toDMConverter: TangemPayTxHistoryItemToDMConverter,
private val toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
) : TangemPayTxHistoryItemsStore {
override suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.get(cursor)
return dataStore.data.first()[key]?.get(cursor)?.let { toDomainConverter.convertList(it) }
}
override suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
store(key, newValue)
val page = toDMConverter.convertList(value)
dataStore.updateData { stored ->
val walletPages = stored[key].orEmpty()
stored + (key to walletPages + (cursor to page))
}
}
override suspend fun remove(key: String) {
remove(keys = listOf(key))
}
override suspend fun remove(keys: List<String>) {
dataStore.updateData { stored -> stored - keys.toSet() }
}
}

View file

@ -8,5 +8,7 @@ interface TangemPayTxHistoryItemsStore {
suspend fun remove(key: String)
suspend fun remove(keys: List<String>)
suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>)
}

View file

@ -0,0 +1,102 @@
package com.tangem.datasource.local.visa.entity
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.serialization.SerializedCurrency
import com.tangem.domain.models.serialization.SerializedDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal sealed class TangemPayTxHistoryItemDM {
abstract val id: String
abstract val date: SerializedDateTime
abstract val amount: SerializedBigDecimal
abstract val currency: SerializedCurrency
abstract val jsonRepresentation: String
@Serializable
@SerialName("spend")
data class Spend(
@SerialName("id") override val id: String,
@SerialName("json_representation") override val jsonRepresentation: String,
@SerialName("date") override val date: SerializedDateTime,
@SerialName("amount") override val amount: SerializedBigDecimal,
@SerialName("currency") override val currency: SerializedCurrency,
@SerialName("authorized_amount") val authorizedAmount: SerializedBigDecimal,
@SerialName("local_amount") val localAmount: SerializedBigDecimal?,
@SerialName("local_currency") val localCurrency: SerializedCurrency?,
@SerialName("enriched_merchant_name") val enrichedMerchantName: String?,
@SerialName("merchant_name") val merchantName: String,
@SerialName("enriched_merchant_category") val enrichedMerchantCategory: String?,
@SerialName("merchant_category_code") val merchantCategoryCode: String?,
@SerialName("merchant_category") val merchantCategory: String?,
@SerialName("status") val status: Status,
@SerialName("enriched_merchant_icon_url") val enrichedMerchantIconUrl: String?,
@SerialName("declined_reason") val declinedReason: String?,
) : TangemPayTxHistoryItemDM()
@Serializable
@SerialName("payment")
data class Payment(
@SerialName("id") override val id: String,
@SerialName("json_representation") override val jsonRepresentation: String,
@SerialName("date") override val date: SerializedDateTime,
@SerialName("amount") override val amount: SerializedBigDecimal,
@SerialName("currency") override val currency: SerializedCurrency,
@SerialName("transaction_hash") val transactionHash: String?,
) : TangemPayTxHistoryItemDM()
@Serializable
@SerialName("fee")
data class Fee(
@SerialName("id") override val id: String,
@SerialName("json_representation") override val jsonRepresentation: String,
@SerialName("date") override val date: SerializedDateTime,
@SerialName("amount") override val amount: SerializedBigDecimal,
@SerialName("currency") override val currency: SerializedCurrency,
@SerialName("description") val description: String?,
) : TangemPayTxHistoryItemDM()
@Serializable
@SerialName("collateral")
data class Collateral(
@SerialName("id") override val id: String,
@SerialName("json_representation") override val jsonRepresentation: String,
@SerialName("date") override val date: SerializedDateTime,
@SerialName("amount") override val amount: SerializedBigDecimal,
@SerialName("currency") override val currency: SerializedCurrency,
@SerialName("transaction_hash") val transactionHash: String,
@SerialName("type") val type: Type,
) : TangemPayTxHistoryItemDM()
@Serializable
enum class Type {
@SerialName("deposit")
Deposit,
@SerialName("withdrawal")
Withdrawal,
}
@Serializable
enum class Status {
@SerialName("pending")
PENDING,
@SerialName("reserved")
RESERVED,
@SerialName("completed")
COMPLETED,
@SerialName("declined")
DECLINED,
@SerialName("reversed")
REVERSED,
@SerialName("unknown")
UNKNOWN,
}
}

View file

@ -0,0 +1,133 @@
package com.tangem.datasource.local.visa.entity
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
import javax.inject.Inject
internal class TangemPayTxHistoryItemToDMConverter @Inject constructor() :
Converter<TangemPayTxHistoryItem, TangemPayTxHistoryItemDM> {
override fun convert(value: TangemPayTxHistoryItem): TangemPayTxHistoryItemDM = when (value) {
is TangemPayTxHistoryItem.Spend -> TangemPayTxHistoryItemDM.Spend(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
authorizedAmount = value.authorizedAmount,
localAmount = value.localAmount,
localCurrency = value.localCurrency,
enrichedMerchantName = value.enrichedMerchantName,
merchantName = value.merchantName,
enrichedMerchantCategory = value.enrichedMerchantCategory,
merchantCategoryCode = value.merchantCategoryCode,
merchantCategory = value.merchantCategory,
status = value.status.toDM(),
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
declinedReason = value.declinedReason,
)
is TangemPayTxHistoryItem.Payment -> TangemPayTxHistoryItemDM.Payment(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
transactionHash = value.transactionHash,
)
is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryItemDM.Fee(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
description = value.description,
)
is TangemPayTxHistoryItem.Collateral -> TangemPayTxHistoryItemDM.Collateral(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
transactionHash = value.transactionHash,
type = value.type.toDM(),
)
}
}
internal class TangemPayTxHistoryItemToDomainConverter @Inject constructor() :
Converter<TangemPayTxHistoryItemDM, TangemPayTxHistoryItem> {
override fun convert(value: TangemPayTxHistoryItemDM): TangemPayTxHistoryItem = when (value) {
is TangemPayTxHistoryItemDM.Spend -> TangemPayTxHistoryItem.Spend(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
authorizedAmount = value.authorizedAmount,
localAmount = value.localAmount,
localCurrency = value.localCurrency,
enrichedMerchantName = value.enrichedMerchantName,
merchantName = value.merchantName,
enrichedMerchantCategory = value.enrichedMerchantCategory,
merchantCategoryCode = value.merchantCategoryCode,
merchantCategory = value.merchantCategory,
status = value.status.toDomain(),
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
declinedReason = value.declinedReason,
)
is TangemPayTxHistoryItemDM.Payment -> TangemPayTxHistoryItem.Payment(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
transactionHash = value.transactionHash,
)
is TangemPayTxHistoryItemDM.Fee -> TangemPayTxHistoryItem.Fee(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
description = value.description,
)
is TangemPayTxHistoryItemDM.Collateral -> TangemPayTxHistoryItem.Collateral(
id = value.id,
jsonRepresentation = value.jsonRepresentation,
date = value.date,
amount = value.amount,
currency = value.currency,
transactionHash = value.transactionHash,
type = value.type.toDomain(),
)
}
}
private fun TangemPayTxHistoryItem.Status.toDM(): TangemPayTxHistoryItemDM.Status = when (this) {
TangemPayTxHistoryItem.Status.PENDING -> TangemPayTxHistoryItemDM.Status.PENDING
TangemPayTxHistoryItem.Status.RESERVED -> TangemPayTxHistoryItemDM.Status.RESERVED
TangemPayTxHistoryItem.Status.COMPLETED -> TangemPayTxHistoryItemDM.Status.COMPLETED
TangemPayTxHistoryItem.Status.DECLINED -> TangemPayTxHistoryItemDM.Status.DECLINED
TangemPayTxHistoryItem.Status.REVERSED -> TangemPayTxHistoryItemDM.Status.REVERSED
TangemPayTxHistoryItem.Status.UNKNOWN -> TangemPayTxHistoryItemDM.Status.UNKNOWN
}
private fun TangemPayTxHistoryItemDM.Status.toDomain(): TangemPayTxHistoryItem.Status = when (this) {
TangemPayTxHistoryItemDM.Status.PENDING -> TangemPayTxHistoryItem.Status.PENDING
TangemPayTxHistoryItemDM.Status.RESERVED -> TangemPayTxHistoryItem.Status.RESERVED
TangemPayTxHistoryItemDM.Status.COMPLETED -> TangemPayTxHistoryItem.Status.COMPLETED
TangemPayTxHistoryItemDM.Status.DECLINED -> TangemPayTxHistoryItem.Status.DECLINED
TangemPayTxHistoryItemDM.Status.REVERSED -> TangemPayTxHistoryItem.Status.REVERSED
TangemPayTxHistoryItemDM.Status.UNKNOWN -> TangemPayTxHistoryItem.Status.UNKNOWN
}
private fun TangemPayTxHistoryItem.Type.toDM(): TangemPayTxHistoryItemDM.Type = when (this) {
TangemPayTxHistoryItem.Type.Deposit -> TangemPayTxHistoryItemDM.Type.Deposit
TangemPayTxHistoryItem.Type.Withdrawal -> TangemPayTxHistoryItemDM.Type.Withdrawal
}
private fun TangemPayTxHistoryItemDM.Type.toDomain(): TangemPayTxHistoryItem.Type = when (this) {
TangemPayTxHistoryItemDM.Type.Deposit -> TangemPayTxHistoryItem.Type.Deposit
TangemPayTxHistoryItemDM.Type.Withdrawal -> TangemPayTxHistoryItem.Type.Withdrawal
}

View file

@ -0,0 +1,119 @@
package com.tangem.datasource.local.visa
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.test.core.datastore.MockStateDataStore
import kotlinx.coroutines.test.runTest
import org.joda.time.DateTime
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
import java.util.Currency
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultTangemPayTxHistoryItemsStoreTest {
private lateinit var store: DefaultTangemPayTxHistoryItemsStore
@BeforeEach
fun setup() {
store = DefaultTangemPayTxHistoryItemsStore(
dataStore = MockStateDataStore(default = emptyMap()),
toDMConverter = TangemPayTxHistoryItemToDMConverter(),
toDomainConverter = TangemPayTxHistoryItemToDomainConverter(),
)
}
@Test
fun `GIVEN empty store WHEN getSyncOrNull THEN returns null`() = runTest {
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
assertThat(result).isNull()
}
@Test
fun `GIVEN stored page WHEN getSyncOrNull with same key and cursor THEN returns page`() = runTest {
// Arrange
val page = listOf(payment("1"), payment("2"))
store.store(key = WALLET_A, cursor = CURSOR_1, value = page)
// Act
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
// Assert
assertThat(result).isEqualTo(page)
}
@Test
fun `GIVEN page stored under one cursor WHEN getSyncOrNull with another cursor THEN returns null`() = runTest {
// Arrange
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("1")))
// Act
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_2)
// Assert
assertThat(result).isNull()
}
@Test
fun `GIVEN two cursors stored for one wallet WHEN getSyncOrNull THEN both pages are kept`() = runTest {
// Arrange
val page1 = listOf(payment("1"))
val page2 = listOf(payment("2"))
store.store(key = WALLET_A, cursor = CURSOR_1, value = page1)
store.store(key = WALLET_A, cursor = CURSOR_2, value = page2)
// Assert
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isEqualTo(page1)
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_2)).isEqualTo(page2)
}
@Test
fun `GIVEN entries for two wallets WHEN remove one wallet THEN only that wallet is cleared`() = runTest {
// Arrange
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
// Act
store.remove(WALLET_A)
// Assert
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isEqualTo(listOf(payment("b")))
}
@Test
fun `GIVEN entries for two wallets WHEN remove both in one call THEN both are cleared`() = runTest {
// Arrange
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
// Act
store.remove(listOf(WALLET_A, WALLET_B))
// Assert
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isNull()
}
private fun payment(id: String) = TangemPayTxHistoryItem.Payment(
id = id,
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("1.00"),
currency = Currency.getInstance("USD"),
transactionHash = null,
)
private companion object {
const val WALLET_A = "wallet-a"
const val WALLET_B = "wallet-b"
const val CURSOR_1 = "cursor-1"
const val CURSOR_2 = "cursor-2"
const val DATE_MILLIS = 1_700_000_000_000L
}
}

View file

@ -0,0 +1,122 @@
package com.tangem.datasource.local.visa
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import org.joda.time.DateTime
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
import java.util.Currency
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayTxHistoryStoreSerializationTest {
private val json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" }
private val serializer = MapSerializer(
keySerializer = String.serializer(),
valueSerializer = MapSerializer(
keySerializer = String.serializer(),
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
),
)
@Test
fun `GIVEN all tx history item types WHEN serialized and deserialized THEN value is preserved`() {
// Arrange
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
"wallet-1" to mapOf(
"initial_cursor_key" to listOf(spend(), payment(), fee(), collateral()),
),
)
// Act
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
// Assert
assertThat(restored).isEqualTo(original)
}
@Test
fun `GIVEN Collateral with type field WHEN serialized and deserialized THEN discriminator does not clash`() {
// Arrange
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
"wallet-1" to mapOf("cursor" to listOf(collateral())),
)
// Act
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
// Assert
assertThat(restored).isEqualTo(original)
}
@Test
fun `GIVEN tx history item WHEN serialized THEN uses stable SerialName under non-clashing discriminator`() {
// Arrange
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
"wallet-1" to mapOf("cursor" to listOf(collateral())),
)
// Act
val encoded = json.encodeToString(serializer, original)
// Assert
assertThat(encoded).contains("\"__type\":\"collateral\"")
}
private fun spend() = TangemPayTxHistoryItemDM.Spend(
id = "spend-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("12.34"),
currency = Currency.getInstance("USD"),
authorizedAmount = BigDecimal("12.34"),
localAmount = BigDecimal("11.00"),
localCurrency = Currency.getInstance("EUR"),
enrichedMerchantName = "Coffee Co",
merchantName = "COFFEE CO",
enrichedMerchantCategory = "Food",
merchantCategoryCode = "5814",
merchantCategory = "restaurants",
status = TangemPayTxHistoryItemDM.Status.COMPLETED,
enrichedMerchantIconUrl = "https://example.com/icon.png",
declinedReason = null,
)
private fun payment() = TangemPayTxHistoryItemDM.Payment(
id = "payment-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("100.00"),
currency = Currency.getInstance("USD"),
transactionHash = "0xabc",
)
private fun fee() = TangemPayTxHistoryItemDM.Fee(
id = "fee-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("0.50"),
currency = Currency.getInstance("USD"),
description = "network fee",
)
private fun collateral() = TangemPayTxHistoryItemDM.Collateral(
id = "collateral-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("250.00"),
currency = Currency.getInstance("USD"),
transactionHash = "0xdef",
type = TangemPayTxHistoryItemDM.Type.Deposit,
)
private companion object {
const val DATE_MILLIS = 1_700_000_000_000L
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.datasource.local.visa.entity
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.test.core.ProvideTestModels
import org.joda.time.DateTime
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import java.math.BigDecimal
import java.util.Currency
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayTxHistoryItemDMConverterTest {
private val toDMConverter = TangemPayTxHistoryItemToDMConverter()
private val toDomainConverter = TangemPayTxHistoryItemToDomainConverter()
@ParameterizedTest
@ProvideTestModels
fun `GIVEN every item subtype WHEN converted to DM and back THEN all fields preserved`(
item: TangemPayTxHistoryItem,
) {
assertRoundTrip(item)
}
@ParameterizedTest
@EnumSource(TangemPayTxHistoryItem.Status::class)
fun `GIVEN every Spend status WHEN converted to DM and back THEN status preserved`(
status: TangemPayTxHistoryItem.Status,
) {
assertRoundTrip(spend().copy(status = status))
}
@ParameterizedTest
@EnumSource(TangemPayTxHistoryItem.Type::class)
fun `GIVEN every Collateral type WHEN converted to DM and back THEN type preserved`(
type: TangemPayTxHistoryItem.Type,
) {
assertRoundTrip(collateral().copy(type = type))
}
private fun assertRoundTrip(item: TangemPayTxHistoryItem) {
// Act
val restored = toDomainConverter.convert(toDMConverter.convert(item))
// Assert
assertThat(restored).isEqualTo(item)
}
private fun provideTestModels() = listOf(spend(), payment(), fee(), collateral())
private fun spend() = TangemPayTxHistoryItem.Spend(
id = "spend-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("12.34"),
currency = Currency.getInstance("USD"),
authorizedAmount = BigDecimal("12.34"),
localAmount = BigDecimal("11.00"),
localCurrency = Currency.getInstance("EUR"),
enrichedMerchantName = "Coffee Co",
merchantName = "COFFEE CO",
enrichedMerchantCategory = "Food",
merchantCategoryCode = "5814",
merchantCategory = "restaurants",
status = TangemPayTxHistoryItem.Status.COMPLETED,
enrichedMerchantIconUrl = "https://example.com/icon.png",
declinedReason = null,
)
private fun payment() = TangemPayTxHistoryItem.Payment(
id = "payment-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("100.00"),
currency = Currency.getInstance("USD"),
transactionHash = "0xabc",
)
private fun fee() = TangemPayTxHistoryItem.Fee(
id = "fee-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("0.50"),
currency = Currency.getInstance("USD"),
description = "network fee",
)
private fun collateral() = TangemPayTxHistoryItem.Collateral(
id = "collateral-1",
jsonRepresentation = "{}",
date = DateTime(DATE_MILLIS),
amount = BigDecimal("250.00"),
currency = Currency.getInstance("USD"),
transactionHash = "0xdef",
type = TangemPayTxHistoryItem.Type.Deposit,
)
private companion object {
const val DATE_MILLIS = 1_700_000_000_000L
}
}