From 158d625e050aa87c22d9a0d1c696afd3962d6ab3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 13:58:50 +0400 Subject: [PATCH] Updated on 2026-08-14 --- domain/account/.gitignore | 1 + domain/account/build.gradle.kts | 23 ++++ .../domain/account/models/AccountList.kt | 92 +++++++++++++++ .../account/models/AccountStatusList.kt | 21 ++++ .../domain/account/models/AccountListTest.kt | 108 ++++++++++++++++++ .../tangem/domain/models/account/Account.kt | 7 ++ .../tangem/domain/models/account/AccountId.kt | 2 + .../domain/models/account/AccountName.kt | 5 + .../domain/models/account/AccountStatus.kt | 28 +++++ settings.gradle.kts | 1 + 10 files changed, 288 insertions(+) create mode 100644 domain/account/.gitignore create mode 100644 domain/account/build.gradle.kts create mode 100644 domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt diff --git a/domain/account/.gitignore b/domain/account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts new file mode 100644 index 0000000000..d21a2a628e --- /dev/null +++ b/domain/account/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +tasks.withType().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) +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt new file mode 100644 index 0000000000..fdc9f393b0 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -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, + 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, + totalAccounts: Int, + ): Either = 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.mainAccountsCount(): Int { + return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true } + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt new file mode 100644 index 0000000000..acd8f76d35 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -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, + val totalAccounts: Int, +) \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt new file mode 100644 index 0000000000..2abafd8934 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -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() + + @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, + val expected: Either, + ) + + private fun createAccount(isMain: Boolean = false): Account.CryptoPortfolio { + return mockk { + every { isMainAccount } returns isMain + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index 398930b72e..bbea116526 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -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, 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" } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index 2f5728579c..725f7143e9 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -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, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt index e51a8e5a30..532687ef6c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -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" diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt new file mode 100644 index 0000000000..73bc34e70e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -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 +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9eb66f4052..de1aaf92ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")