Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-04 13:58:50 +04:00
parent be160483d2
commit 158d625e05
10 changed files with 288 additions and 0 deletions

1
domain/account/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
api(projects.domain.models)
api(projects.domain.wallets.models)
implementation(deps.arrow.core)
implementation(deps.kotlin.serialization)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,92 @@
package com.tangem.domain.account.models
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.serialization.Serializable
/**
* Represents a list of accounts associated with a user wallet
*
* @property userWallet the user wallet associated with the account list
* @property accounts a set of accounts belonging to the user wallet
* @property totalAccounts the total number of accounts
*
[REDACTED_AUTHOR]
*/
@Serializable
data class AccountList private constructor(
val userWallet: UserWallet,
val accounts: Set<Account>,
val totalAccounts: Int,
) {
/** Retrieves the main crypto portfolio account from the list of accounts */
val mainAccount: Account.CryptoPortfolio
get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio
/**
* Represents possible errors that can occur when creating an `AccountList`
*/
@Serializable
sealed interface Error {
val tag: String
get() = this::class.simpleName ?: "AccountListError"
@Serializable
data object EmptyAccountsList : Error {
override fun toString(): String = "$tag: The accounts list cannot be empty"
}
@Serializable
data object MainAccountNotFound : Error {
override fun toString(): String {
return "$tag: Account list does not contain a main crypto portfolio account"
}
}
@Serializable
data object ExceedsMaxMainAccountsCount : Error {
override fun toString(): String {
return "$tag: There should be at most one main crypto portfolio in the account list"
}
}
}
companion object {
/**
* Factory method to create an `AccountList` instance.
* Validates the input to ensure the accounts list is not empty and contains exactly one main account.
*
* @param userWallet the user wallet associated with the account list
* @param accounts a set of accounts belonging to the user wallet
* @param totalAccounts the total number of accounts
*/
operator fun invoke(
userWallet: UserWallet,
accounts: Set<Account>,
totalAccounts: Int,
): Either<Error, AccountList> = either {
ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList }
val mainAccountsCount = accounts.mainAccountsCount()
ensure(mainAccountsCount == 1) {
if (mainAccountsCount == 0) {
Error.MainAccountNotFound
} else {
Error.ExceedsMaxMainAccountsCount
}
}
AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts)
}
private fun Set<Account>.mainAccountsCount(): Int {
return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true }
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.domain.account.models
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.serialization.Serializable
/**
* Represents a list of account statuses associated with a user wallet
*
* @property userWallet the user wallet to which the account statuses belong
* @property accountStatuses a set of account statuses associated with the user wallet
* @property totalAccounts the total number of accounts
*
[REDACTED_AUTHOR]
*/
@Serializable
data class AccountStatusList(
val userWallet: UserWallet,
val accountStatuses: Set<AccountStatus>,
val totalAccounts: Int,
)

View file

@ -0,0 +1,108 @@
package com.tangem.domain.account.models
import arrow.core.Either
import arrow.core.left
import com.google.common.truth.Truth
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountListTest {
@Test
fun mainAccount() {
// Arrange
val mainAccount = createAccount(isMain = true)
val accountList = AccountList(
userWallet = mockk(),
accounts = setOf(mainAccount),
totalAccounts = 1,
)
.getOrNull()!!
// Act
val actual = accountList.mainAccount
// Assert
val expected = mainAccount
Truth.assertThat(actual).isEqualTo(expected)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Create {
private val userWallet = mockk<UserWallet>()
@BeforeEach
fun resetMocks() {
clearMocks(userWallet)
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun invoke(model: CreateTestModel) {
// Act
val actual = AccountList(
userWallet = userWallet,
accounts = model.accounts,
totalAccounts = model.accounts.size,
)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
CreateTestModel(
accounts = emptySet(),
expected = AccountList.Error.EmptyAccountsList.left(),
),
CreateTestModel(
accounts = setOf(createAccount(isMain = false)),
expected = AccountList.Error.MainAccountNotFound.left(),
),
CreateTestModel(
accounts = setOf(
createAccount(isMain = true),
createAccount(isMain = true),
),
expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(),
),
createAccount(isMain = true).let {
CreateTestModel(
accounts = setOf(it),
expected = AccountList(
userWallet = userWallet,
accounts = setOf(it),
totalAccounts = 1,
),
)
},
)
}
data class CreateTestModel(
val accounts: Set<Account>,
val expected: Either<AccountList.Error, AccountList>,
)
private fun createAccount(isMain: Boolean = false): Account.CryptoPortfolio {
return mockk<Account.CryptoPortfolio> {
every { isMainAccount } returns isMain
}
}
}

View file

@ -8,12 +8,14 @@ import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
/**
* Represents an account
*
[REDACTED_AUTHOR]
*/
@Serializable
sealed interface Account {
/** Unique identifier of the account */
@ -36,6 +38,7 @@ sealed interface Account {
* @property isArchived indicates whether the account is archived
* @property cryptoCurrencyList list of tokens associated with the account
*/
@Serializable
data class CryptoPortfolio private constructor(
override val accountId: AccountId,
override val name: AccountName,
@ -64,6 +67,7 @@ sealed interface Account {
* @property sortType sorting type for the tokens
* @property groupType grouping type for the tokens
*/
@Serializable
data class CryptoCurrencyList(
val currencies: Set<CryptoCurrency>,
val sortType: TokensSortType,
@ -73,14 +77,17 @@ sealed interface Account {
/**
* Represents possible errors when creating a crypto portfolio account
*/
@Serializable
sealed interface Error {
/** Error indicating that the account name is blank */
@Serializable
data class AccountNameError(val cause: AccountName.Error) : Error {
override fun toString(): String = cause.toString()
}
/** Error indicating that the derivation index is negative */
@Serializable
data object NegativeDerivationIndex : Error {
override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative"
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.models.account
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
/**
* Represents a unique identifier for an account
@ -8,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId
* @property value a unique string value that distinguishes this account
* @property userWalletId the identifier of the user wallet associated with the account
*/
@Serializable
data class AccountId(
val value: String,
val userWalletId: UserWalletId,

View file

@ -3,6 +3,7 @@ package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.serialization.Serializable
/**
* Represents an account name
@ -11,6 +12,7 @@ import arrow.core.raise.ensure
*
[REDACTED_AUTHOR]
*/
@Serializable
data class AccountName private constructor(
val value: String,
) {
@ -18,11 +20,13 @@ data class AccountName private constructor(
/**
* Represents possible validation errors
*/
@Serializable
sealed interface Error {
/**
* Error indicating that the account name is blank
*/
@Serializable
data object Empty : Error {
override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank"
}
@ -30,6 +34,7 @@ data class AccountName private constructor(
/**
* Error indicating that the account name exceeds the maximum allowed length
*/
@Serializable
data object ExceedsMaxLength : Error {
override fun toString(): String {
return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters"

View file

@ -0,0 +1,28 @@
package com.tangem.domain.models.account
import com.tangem.domain.models.tokenlist.TokenList
import kotlinx.serialization.Serializable
/**
* Represents the status of an account
*
[REDACTED_AUTHOR]
*/
@Serializable
sealed interface AccountStatus {
/** The account associated with this status */
val account: Account
/**
* Represents the status of a crypto portfolio account
*
* @property account the crypto portfolio account
* @property tokenList the list of tokens associated with the account
*/
@Serializable
data class CryptoPortfolio(
override val account: Account.CryptoPortfolio,
val tokenList: TokenList,
) : AccountStatus
}

View file

@ -276,6 +276,7 @@ include(":features:welcome:impl")
include(":domain:models")
include(":domain:legacy")
include(":domain:account")
include(":domain:card")
include(":domain:core")
include(":domain:demo")