Updated on 2026-08-14
This commit is contained in:
parent
4341673767
commit
03b3a7d0a2
12 changed files with 168 additions and 181 deletions
|
|
@ -49,8 +49,12 @@ internal object AccountDomainModule {
|
|||
@Singleton
|
||||
fun provideRecoverCryptoPortfolioUseCase(
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
mainAccountTokensMigration: MainAccountTokensMigration,
|
||||
): RecoverCryptoPortfolioUseCase {
|
||||
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
|
||||
return RecoverCryptoPortfolioUseCase(
|
||||
crudRepository = accountsCRUDRepository,
|
||||
mainAccountTokensMigration = mainAccountTokensMigration,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ interface TangemTechApi {
|
|||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") eTag: String,
|
||||
@Body body: SaveWalletAccountsResponse,
|
||||
): ApiResponse<Unit>
|
||||
): ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
@GET("/v1/wallets/{walletId}/accounts/archived")
|
||||
suspend fun getWalletArchivedAccounts(
|
||||
|
|
|
|||
|
|
@ -63,23 +63,24 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
push(userWalletId = userWalletId, accounts = response.accounts)
|
||||
store(userWalletId = userWalletId, response = response)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
val store = getAccountsResponseStore(userWalletId = userWalletId)
|
||||
|
||||
store.updateData { response }
|
||||
}
|
||||
|
||||
override suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) {
|
||||
push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts))
|
||||
override suspend fun push(
|
||||
userWalletId: UserWalletId,
|
||||
accounts: List<WalletAccountDTO>,
|
||||
): GetWalletAccountsResponse? {
|
||||
return push(userWalletId = userWalletId, body = SaveWalletAccountsResponse(accounts = accounts))
|
||||
}
|
||||
|
||||
override suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse) {
|
||||
safeApiCall(
|
||||
override suspend fun push(
|
||||
userWalletId: UserWalletId,
|
||||
body: SaveWalletAccountsResponse,
|
||||
): GetWalletAccountsResponse? {
|
||||
return safeApiCall(
|
||||
call = {
|
||||
var eTag = getETag(userWalletId)
|
||||
|
||||
|
|
@ -105,6 +106,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -152,7 +155,12 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
pushAndStore(userWalletId, response)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
|
||||
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts)
|
||||
|
||||
if (syncedResponse != null) {
|
||||
store(userWalletId = userWalletId, response = syncedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
error: ApiResponseError,
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) -> Unit,
|
||||
storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit,
|
||||
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
|
||||
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
||||
): GetWalletAccountsResponse? {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
if (isResponseUpToDate) {
|
||||
|
|
@ -56,17 +56,17 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
return savedAccountsResponse
|
||||
}
|
||||
|
||||
val response = savedAccountsResponse ?: defaultWalletAccountsResponseFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
userTokensResponse = getFromLegacyStore(userWalletId),
|
||||
)
|
||||
|
||||
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
|
||||
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
|
||||
|
||||
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
||||
if (isNotFoundError) {
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
|
||||
val updatedResponse = pushWalletAccounts(userWalletId, accountDTOs)
|
||||
|
||||
if (updatedResponse != null) {
|
||||
response = updatedResponse
|
||||
}
|
||||
}
|
||||
|
||||
storeWalletAccounts(userWalletId, response)
|
||||
|
|
@ -74,6 +74,13 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
|||
return response
|
||||
}
|
||||
|
||||
private suspend fun createDefaultResponse(userWalletId: UserWalletId): GetWalletAccountsResponse {
|
||||
return defaultWalletAccountsResponseFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
userTokensResponse = getFromLegacyStore(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getFromLegacyStore(userWalletId: UserWalletId): UserTokensResponse? {
|
||||
return userTokensResponseStore.getSyncOrNull(userWalletId)
|
||||
?.let {
|
||||
|
|
|
|||
|
|
@ -102,12 +102,16 @@ internal class DefaultAccountsCRUDRepository(
|
|||
}
|
||||
|
||||
override suspend fun saveAccounts(accountList: AccountList) {
|
||||
val userWallet = userWalletsStore.getSyncStrict(accountList.userWalletId)
|
||||
val converter = convertersContainer.createCryptoPortfolioConverter(userWalletId = accountList.userWalletId)
|
||||
|
||||
val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet)
|
||||
val accountsResponse = converter.convert(value = accountList)
|
||||
val accountDTOs = converter.convertListBack(
|
||||
input = accountList.accounts.filterIsInstance<Account.CryptoPortfolio>(),
|
||||
)
|
||||
|
||||
walletAccountsSaver.pushAndStore(userWalletId = userWallet.walletId, response = accountsResponse)
|
||||
val syncedResponse = walletAccountsSaver.push(userWalletId = accountList.userWalletId, accounts = accountDTOs)
|
||||
if (syncedResponse != null) {
|
||||
walletAccountsSaver.store(userWalletId = accountList.userWalletId, response = syncedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveAccount(account: Account.CryptoPortfolio) {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(updatedAccountsResponse.accounts),
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
} returns ApiResponse.Success(data = updatedAccountsResponse)
|
||||
|
||||
// Act
|
||||
fetcher.fetch(userWalletId)
|
||||
|
|
@ -273,26 +273,26 @@ class DefaultWalletAccountsFetcherTest {
|
|||
@Test
|
||||
fun `push should call saveWalletAccounts with correct params`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null))
|
||||
val response = SaveWalletAccountsResponse(accounts)
|
||||
val getResponse = createGetWalletAccountsResponse(userWalletId, tokens = null)
|
||||
val saveResponse = SaveWalletAccountsResponse(getResponse.accounts)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = response,
|
||||
body = saveResponse,
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
} returns ApiResponse.Success(data = getResponse)
|
||||
|
||||
// Act
|
||||
fetcher.push(userWalletId, response)
|
||||
fetcher.push(userWalletId, saveResponse)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = response,
|
||||
body = saveResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ class DefaultWalletAccountsFetcherTest {
|
|||
eTag = eTag,
|
||||
body = response,
|
||||
)
|
||||
} returns saveApiResponse as ApiResponse<Unit>
|
||||
} returns saveApiResponse as ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
// Act
|
||||
val actual = runCatching { fetcher.push(userWalletId, response) }.exceptionOrNull()!!
|
||||
|
|
@ -326,42 +326,6 @@ class DefaultWalletAccountsFetcherTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class PushAndStore {
|
||||
|
||||
@Test
|
||||
fun `pushAndStore should call push and store with correct params`() = runTest {
|
||||
// Arrange
|
||||
val accounts = listOf(createWalletAccountDTO(userWalletId = userWalletId, tokens = null))
|
||||
val response = createGetWalletAccountsResponse(userWalletId).copy(accounts = accounts)
|
||||
|
||||
coEvery {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(accounts = response.accounts),
|
||||
)
|
||||
} returns ApiResponse.Success(data = Unit)
|
||||
|
||||
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
|
||||
|
||||
// Act
|
||||
fetcher.pushAndStore(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
coVerifyOrder {
|
||||
tangemTechApi.saveWalletAccounts(
|
||||
walletId = userWalletId.stringValue,
|
||||
eTag = eTag,
|
||||
body = SaveWalletAccountsResponse(accounts = response.accounts),
|
||||
)
|
||||
accountsResponseStoreFactory.create(userWalletId)
|
||||
accountsResponseStore.updateData(any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createToken(
|
||||
networkId: String = "ethereum",
|
||||
derivationPath: String = "m/44'/60'/0'/0/0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.converter.createGetWalletAccountsResponse
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
|
|
@ -35,6 +37,10 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory,
|
||||
)
|
||||
|
||||
private val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse =
|
||||
mockk(relaxed = true)
|
||||
private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(
|
||||
|
|
@ -53,9 +59,6 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
errorBody = null,
|
||||
)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk()
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk()
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
|
|
@ -84,29 +87,11 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
errorBody = null,
|
||||
)
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
name = "Michael Dotson",
|
||||
derivationIndex = 7135,
|
||||
icon = "consectetuer",
|
||||
iconColor = "ferri",
|
||||
tokens = listOf(),
|
||||
totalTokens = 7738,
|
||||
totalNetworks = 3348,
|
||||
)
|
||||
val accountDTO = createWalletAccountDTO(userWalletId)
|
||||
|
||||
val savedAccountsResponse = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
|
|
@ -119,8 +104,8 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
|
||||
// Assert
|
||||
coVerify {
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
||||
|
|
@ -163,9 +148,6 @@ class FetchWalletAccountsErrorHandlerTest {
|
|||
defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse)
|
||||
} returns savedAccountsResponse
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
|
|
@ -37,6 +36,7 @@ import kotlin.time.Duration.Companion.minutes
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("UnusedFlow")
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultAccountsCRUDRepositoryTest {
|
||||
|
||||
|
|
@ -580,24 +580,18 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
@Test
|
||||
fun `saveAccounts should call API and update store`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accounts = accountList.accounts.filterIsInstance<CryptoPortfolio>()
|
||||
|
||||
val accountsResponse = mockk<GetWalletAccountsResponse>()
|
||||
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
accountsResponseStoreFlow.value = accountsResponse
|
||||
|
||||
val converter = mockk<GetWalletAccountsResponseConverter> {
|
||||
every { this@mockk.convert(accountList) } returns accountsResponse
|
||||
val converter = mockk<CryptoPortfolioConverter> {
|
||||
every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts
|
||||
}
|
||||
|
||||
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
|
||||
|
||||
every {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet)
|
||||
} returns converter
|
||||
every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter
|
||||
coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } returns accountsResponse
|
||||
|
||||
// Act
|
||||
repository.saveAccounts(accountList)
|
||||
|
|
@ -606,37 +600,30 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
Truth.assertThat(accountsResponseStoreFlow.value).isEqualTo(accountsResponse)
|
||||
|
||||
coVerifyOrder {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet)
|
||||
converter.convert(accountList)
|
||||
walletAccountsSaver.pushAndStore(userWalletId, accountsResponse)
|
||||
convertersContainer.createCryptoPortfolioConverter(userWalletId)
|
||||
converter.convertListBack(accounts)
|
||||
walletAccountsSaver.push(userWalletId, accountsResponse.accounts)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAccounts if API request is failed`() = runTest {
|
||||
// Arrange
|
||||
val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
val accountList = AccountList.empty(userWalletId = userWalletId)
|
||||
val accounts = accountList.accounts.filterIsInstance<CryptoPortfolio>()
|
||||
|
||||
val accountsResponse = mockk<GetWalletAccountsResponse>()
|
||||
val accountsResponse = createGetWalletAccountsResponse(userWalletId)
|
||||
accountsResponseStoreFlow.value = accountsResponse
|
||||
|
||||
val converter = mockk<GetWalletAccountsResponseConverter> {
|
||||
every { this@mockk.convert(accountList) } returns accountsResponse
|
||||
val converter = mockk<CryptoPortfolioConverter> {
|
||||
every { this@mockk.convertListBack(accounts) } returns accountsResponse.accounts
|
||||
}
|
||||
|
||||
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
|
||||
|
||||
every {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet)
|
||||
} returns converter
|
||||
every { convertersContainer.createCryptoPortfolioConverter(userWalletId) } returns converter
|
||||
|
||||
val exception = Exception("Test error")
|
||||
|
||||
coEvery { walletAccountsSaver.pushAndStore(userWalletId, accountsResponse) } throws exception
|
||||
coEvery { walletAccountsSaver.push(userWalletId, accountsResponse.accounts) } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { repository.saveAccounts(accountList) }.exceptionOrNull()!!
|
||||
|
|
@ -646,9 +633,8 @@ class DefaultAccountsCRUDRepositoryTest {
|
|||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
convertersContainer.getWalletAccountsResponseCF.create(userWallet)
|
||||
converter.convert(accountList)
|
||||
walletAccountsSaver.pushAndStore(userWalletId, accountsResponse)
|
||||
convertersContainer.createCryptoPortfolioConverter(userWalletId)
|
||||
converter.convertListBack(accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,14 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
*/
|
||||
interface WalletAccountsSaver {
|
||||
|
||||
/** Push and store wallet accounts [response] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun pushAndStore(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
|
||||
|
||||
/** Store wallet accounts [response] by [userWalletId] */
|
||||
suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse)
|
||||
|
||||
/** Push wallet accounts [body] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse)
|
||||
suspend fun push(userWalletId: UserWalletId, body: SaveWalletAccountsResponse): GetWalletAccountsResponse?
|
||||
|
||||
/** Push wallet accounts [accounts] by [userWalletId] */
|
||||
@Throws
|
||||
suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>)
|
||||
suspend fun push(userWalletId: UserWalletId, accounts: List<WalletAccountDTO>): GetWalletAccountsResponse?
|
||||
}
|
||||
|
|
@ -5,9 +5,11 @@ import arrow.core.getOrElse
|
|||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
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.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -16,11 +18,13 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
* Use case for recovering a crypto portfolio account from archived accounts
|
||||
*
|
||||
* @property crudRepository repository for performing CRUD operations on accounts
|
||||
* @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RecoverCryptoPortfolioUseCase(
|
||||
private val crudRepository: AccountsCRUDRepository,
|
||||
private val mainAccountTokensMigration: MainAccountTokensMigration,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -30,15 +34,25 @@ class RecoverCryptoPortfolioUseCase(
|
|||
*/
|
||||
suspend operator fun invoke(accountId: AccountId): Either<Error, Account.CryptoPortfolio> = either {
|
||||
val accountList = getAccountList(userWalletId = accountId.userWalletId)
|
||||
|
||||
ensure(accountList.canAddMoreAccounts) {
|
||||
raise(Error.AccountListRequirementsNotMet(cause = AccountList.Error.ExceedsMaxAccountsCount))
|
||||
}
|
||||
|
||||
val archivedAccount = getArchivedAccount(accountId = accountId)
|
||||
|
||||
val recoveredAccount = archivedAccount.recover()
|
||||
|
||||
val updatedAccountList = (accountList + recoveredAccount)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) }
|
||||
.getOrElse { raise(Error.AccountListRequirementsNotMet(cause = it)) }
|
||||
|
||||
saveAccounts(updatedAccountList)
|
||||
|
||||
mainAccountTokensMigration.migrate(
|
||||
userWalletId = accountId.userWalletId,
|
||||
derivationIndex = recoveredAccount.derivationIndex,
|
||||
)
|
||||
|
||||
recoveredAccount
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +61,9 @@ class RecoverCryptoPortfolioUseCase(
|
|||
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },
|
||||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) }
|
||||
.getOrElse {
|
||||
raise(Error.DataOperationFailed(message = "Account list not found for wallet $userWalletId"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getArchivedAccount(accountId: AccountId): ArchivedAccount {
|
||||
|
|
@ -56,7 +72,7 @@ class RecoverCryptoPortfolioUseCase(
|
|||
catch = { raise(Error.DataOperationFailed(cause = it)) },
|
||||
)
|
||||
.getOrElse {
|
||||
raise(Error.CriticalTechError.AccountNotFound(accountId = accountId))
|
||||
raise(Error.DataOperationFailed(message = "Account not found: $accountId"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +82,6 @@ class RecoverCryptoPortfolioUseCase(
|
|||
accountName = this.name,
|
||||
icon = this.icon,
|
||||
derivationIndex = this.derivationIndex,
|
||||
// TODO: [REDACTED_JIRA]
|
||||
cryptoCurrencies = emptySet(),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,37 +102,18 @@ class RecoverCryptoPortfolioUseCase(
|
|||
get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error"
|
||||
|
||||
/**
|
||||
* Critical technical errors that can occur during the recovery operation
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
sealed interface CriticalTechError : Error {
|
||||
|
||||
/**
|
||||
|
||||
*
|
||||
* @property userWalletId the unique identifier of the user wallet
|
||||
*/
|
||||
data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError {
|
||||
override fun toString(): String = "$tag: Accounts for $userWalletId are not created"
|
||||
}
|
||||
|
||||
/** Error indicating that the account with [accountId] was not found */
|
||||
data class AccountNotFound(val accountId: AccountId) : CriticalTechError {
|
||||
override fun toString(): String = "$tag: Account with ID $accountId not found"
|
||||
}
|
||||
|
||||
/**
|
||||
* Error indicating that the account list requirements were not met.
|
||||
*
|
||||
* @property cause the underlying cause of the error
|
||||
*/
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error {
|
||||
override fun toString(): String = "$tag: Account list requirements not met: $cause"
|
||||
}
|
||||
data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error {
|
||||
override fun toString(): String = "$tag: Account list requirements not met: $cause"
|
||||
}
|
||||
|
||||
/** Error indicating that a data operation failed */
|
||||
data class DataOperationFailed(val cause: Throwable) : Error {
|
||||
override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}"
|
||||
|
||||
constructor(message: String) : this(cause = IllegalStateException(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.google.common.truth.Truth
|
|||
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.tokens.MainAccountTokensMigration
|
||||
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error
|
||||
import com.tangem.domain.account.utils.createAccount
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
|
|
@ -26,7 +27,11 @@ import org.junit.jupiter.api.TestInstance
|
|||
class RecoverCryptoPortfolioUseCaseTest {
|
||||
|
||||
private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true)
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(crudRepository)
|
||||
private val mainAccountTokensMigration: MainAccountTokensMigration = mockk()
|
||||
private val useCase = RecoverCryptoPortfolioUseCase(
|
||||
crudRepository = crudRepository,
|
||||
mainAccountTokensMigration = mainAccountTokensMigration,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
|
|
@ -51,6 +56,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
|
||||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption()
|
||||
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption()
|
||||
coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right()
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
|
|
@ -59,7 +65,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = account.right()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
|
|
@ -77,13 +83,14 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
coEvery { crudRepository.getAccountListSync(userWalletId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(accountId)
|
||||
val actual = useCase(accountId).leftOrNull() as Error.DataOperationFailed
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
val expected = IllegalStateException("Account list not found for wallet $userWalletId")
|
||||
Truth.assertThat(actual.cause).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccountSync(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
|
|
@ -108,7 +115,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerifySequence { crudRepository.getAccountListSync(userWalletId) }
|
||||
coVerify(inverse = true) {
|
||||
crudRepository.getArchivedAccountSync(any())
|
||||
crudRepository.saveAccounts(any())
|
||||
|
|
@ -132,7 +139,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
}
|
||||
|
|
@ -149,13 +156,14 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None
|
||||
|
||||
// Act
|
||||
val actual = useCase(account.accountId)
|
||||
val actual = useCase(account.accountId).leftOrNull() as Error.DataOperationFailed
|
||||
|
||||
// Assert
|
||||
val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
val expected = IllegalStateException("Account not found: ${account.accountId}")
|
||||
Truth.assertThat(actual.cause).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
}
|
||||
|
|
@ -190,7 +198,7 @@ class RecoverCryptoPortfolioUseCaseTest {
|
|||
val expected = Error.DataOperationFailed(exception).left()
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
coVerifySequence {
|
||||
crudRepository.getAccountListSync(userWalletId)
|
||||
crudRepository.getArchivedAccountSync(account.accountId)
|
||||
crudRepository.saveAccounts(updatedAccountList)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.account.archived
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -11,6 +13,8 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.usecase.ArchivedAccountList
|
||||
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
|
||||
|
|
@ -20,6 +24,7 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder
|
||||
import com.tangem.features.account.createedit.error.AccountFeatureError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -37,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
|
||||
private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase,
|
||||
private val umBuilder: AccountArchivedUMBuilder,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ArchivedAccountListComponent.Params>()
|
||||
|
|
@ -103,11 +109,37 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch(dispatchers.default) {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
.onLeft { Timber.e(it.toString()) }
|
||||
.onRight { showSuccessRecoverMessage() }
|
||||
router.pop()
|
||||
.onLeft(::handleRecoverError)
|
||||
.onRight {
|
||||
showSuccessRecoverMessage()
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRecoverError(error: RecoverCryptoPortfolioUseCase.Error) {
|
||||
if (error is RecoverCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet &&
|
||||
error.cause is AccountList.Error.ExceedsMaxAccountsCount
|
||||
) {
|
||||
// TODO("account") show alert that max accounts count reached
|
||||
// https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=24765-180563&t=vk6TCy4MkYol1cPb-4
|
||||
return
|
||||
}
|
||||
|
||||
val featureError = AccountFeatureError.ArchivedAccountList.FailedToRecoverAccount(cause = error)
|
||||
logError(error = featureError)
|
||||
messageSender.showErrorDialog(universalError = featureError, onDismiss = router::pop)
|
||||
}
|
||||
|
||||
private fun logError(error: AccountFeatureError, params: Map<String, String> = mapOf()) {
|
||||
val exception = IllegalStateException(error.toString())
|
||||
|
||||
Timber.e(exception)
|
||||
|
||||
analyticsExceptionHandler.sendException(
|
||||
event = ExceptionAnalyticsEvent(exception = exception, params = params),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showSuccessRecoverMessage() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue