diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index feebdbedd1..d2eb7a9b1c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.account.status.usecase import arrow.core.Either import arrow.core.getOrElse +import arrow.core.merge import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either @@ -9,10 +10,12 @@ import arrow.core.raise.ensure import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.status.utils.AccountNameIndexer import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.wallet.UserWalletId /** @@ -45,8 +48,7 @@ class RecoverCryptoPortfolioUseCase( val recoveredAccount = archivedAccount.recover() - val updatedAccountList = (accountList + recoveredAccount) - .getOrElse { raise(Error.AccountListRequirementsNotMet(cause = it)) } + val updatedAccountList = add(accountList, recoveredAccount) saveAccounts(updatedAccountList) @@ -97,6 +99,25 @@ class RecoverCryptoPortfolioUseCase( ) } + private fun Raise.add(accountList: AccountList, recoveredAccount: Account.CryptoPortfolio): AccountList { + val maybeAccountList = accountList + recoveredAccount + + return maybeAccountList.mapLeft { + val recoveredAccountName = recoveredAccount.accountName as? AccountName.Custom + ?: raise(Error.DataOperationFailed(message = "Recovered account should have a custom name")) + + val indexedName = AccountNameIndexer.transform(from = recoveredAccountName.value) + .let(AccountName::invoke).getOrNull() + ?: raise(Error.DataOperationFailed(message = "Failed to generate indexed account name")) + + val renamedAccount = recoveredAccount.copy(accountName = indexedName) + + // recursively try to add the account with the new name + add(accountList = accountList, recoveredAccount = renamedAccount) + } + .merge() + } + private suspend fun refreshBalances(accountId: AccountId): Either = either { val currencies = catch( block = { crudRepository.getAccountSync(accountId = accountId) }, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountNameIndexer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountNameIndexer.kt new file mode 100644 index 0000000000..5062d2c5b8 --- /dev/null +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountNameIndexer.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.account.status.utils + +import com.tangem.domain.models.account.AccountName + +/** + * Utility object for indexing account names to ensure uniqueness. + * + * @see [iOS](https://github.com/tangem-developments/tangem-app-ios/blob/32c56f1a566ecec00364ffeaf6b3c9929419ad15/Tangem/Domain/Accounts/Archiving/Utils/UnarchivedCryptoAccountNameIndexer.swift#L11) + * +[REDACTED_AUTHOR] + */ +internal object AccountNameIndexer { + + private const val INITIAL_INDEX = 1 + private const val INDEX_PREFIX = "(" + private const val INDEX_SUFFIX = ")" + + /** + * Transforms the given account name by appending or incrementing an index suffix to ensure uniqueness. + * + * @param from the original account name + * + * @return the transformed account name with an updated index suffix + */ + fun transform(from: String): String { + val (newIndex, currentIndex) = extractIndices(from) + val accountNameSuffix = makeStringFromIndex(newIndex) + val accountNamePrefixLength = (AccountName.MAX_LENGTH - accountNameSuffix.length).coerceAtLeast(0) + + var accountNamePrefix = from + if (currentIndex != null) { + val accountNameCurrentSuffix = makeStringFromIndex(currentIndex) + val accountNameCurrentSuffixLength = accountNameCurrentSuffix.length + accountNamePrefix = accountNamePrefix.dropLast(accountNameCurrentSuffixLength) + } + + accountNamePrefix = accountNamePrefix.take(accountNamePrefixLength) + + return accountNamePrefix + accountNameSuffix + } + + private fun extractIndices(string: String): Pair { + val pattern = """\${INDEX_PREFIX}(\d+)\${INDEX_SUFFIX}$""" + val regex = Regex(pattern) + val match = regex.find(string) + + return if (match != null && match.groupValues.size > 1) { + val currentIndex = match.groupValues[1].toIntOrNull() + + if (currentIndex != null) { + Pair(currentIndex + 1, currentIndex) + } else { + Pair(INITIAL_INDEX, null) + } + } else { + Pair(INITIAL_INDEX, null) + } + } + + private fun makeStringFromIndex(index: Int): String { + return "${INDEX_PREFIX}$index${INDEX_SUFFIX}" + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/AccountNameIndexerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/AccountNameIndexerTest.kt new file mode 100644 index 0000000000..13a9b27103 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/AccountNameIndexerTest.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.account.status.utils + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountNameIndexerTest { + + @ParameterizedTest + @ProvideTestModels + fun transform(model: TestModel) { + // Act + val actual = AccountNameIndexer.transform(model.input) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + TestModel(input = "Bitcoin", expected = "Bitcoin(1)"), + TestModel(input = "Bitcoin(5)", expected = "Bitcoin(6)"), + TestModel(input = "Ethereum(123)", expected = "Ethereum(124)"), + TestModel(input = "Account(0)", expected = "Account(1)"), + TestModel(input = "Bitcoin(old)new", expected = "Bitcoin(old)new(1)"), + TestModel(input = "Bitcoin(10)new", expected = "Bitcoin(10)new(1)"), + TestModel(input = "", expected = "(1)"), + TestModel(input = "(42)", expected = "(43)"), + TestModel(input = "Account(999999)", expected = "Account(1000000)"), + TestModel(input = "Wallet(9)", expected = "Wallet(10)"), + TestModel(input = "My Account (5)", expected = "My Account (6)"), + TestModel(input = "Bitcoin💰(2)", expected = "Bitcoin💰(3)"), + TestModel(input = "Bitcoin(abc)", expected = "Bitcoin(abc)(1)"), + TestModel(input = "Bitcoin()", expected = "Bitcoin()(1)"), + TestModel(input = "Bitcoin(", expected = "Bitcoin((1)"), + TestModel(input = "Bitcoin)", expected = "Bitcoin)(1)"), + TestModel(input = "EthereumEthereumEthereum(999)", expected = "EthereumEthere(1000)"), + TestModel(input = "a".repeat(100), expected = "a".repeat(17) + "(1)"), + ) + + data class TestModel(val input: String, val expected: String) +} \ No newline at end of file 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 ead33f51a3..0a5433424d 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 @@ -74,7 +74,8 @@ sealed interface AccountName { companion object { - private const val MAX_LENGTH = 20 + /** Maximum allowed length for an account name */ + const val MAX_LENGTH = 20 /** * Factory method to create an [AccountName] instance.