diff --git a/buildSrc/src/main/kotlin/ivy.kotlin-android.gradle.kts b/buildSrc/src/main/kotlin/ivy.kotlin-android.gradle.kts index 8f7cb80692..85894c1adb 100644 --- a/buildSrc/src/main/kotlin/ivy.kotlin-android.gradle.kts +++ b/buildSrc/src/main/kotlin/ivy.kotlin-android.gradle.kts @@ -23,6 +23,13 @@ android { } } +gradle.projectsEvaluated { + // Increase tests Heap Size because of Kotest property-based tests + tasks.withType { + maxHeapSize = "2048m" + } +} + dependencies { implementation(libs.bundles.arrow) implementation(libs.bundles.kotlin) diff --git a/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt b/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt index eac044a372..87c279499e 100644 --- a/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt +++ b/screen/home/src/main/java/com/ivy/home/HomeViewModel.kt @@ -26,7 +26,7 @@ import com.ivy.legacy.data.model.TimePeriod import com.ivy.legacy.data.model.toCloseTimeRange import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Settings -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.action.settings.UpdateSettingsAct import com.ivy.legacy.domain.action.viewmodel.home.ShouldHideIncomeAct import com.ivy.legacy.utils.dateNowUTC @@ -358,7 +358,7 @@ class HomeViewModel @Inject constructor( upcoming.value = LegacyDueSection( trns = with(transactionMapper) { result.upcomingTrns.map { - it.toEntity().toDomain() + it.toEntity().toLegacyDomain() }.toImmutableList() }, stats = result.upcoming, @@ -370,7 +370,7 @@ class HomeViewModel @Inject constructor( overdue.value = LegacyDueSection( trns = with(transactionMapper) { result.overdueTrns.map { - it.toEntity().toDomain() + it.toEntity().toLegacyDomain() }.toImmutableList() }, stats = result.overdue, diff --git a/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt b/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt index d1c89885d5..b71056e86b 100644 --- a/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt +++ b/screen/import-data/src/main/java/com/ivy/importdata/csv/domain/CSVImporterV2.kt @@ -22,7 +22,7 @@ import com.ivy.importdata.csv.ImportantFields import com.ivy.importdata.csv.OptionalFields import com.ivy.importdata.csv.TransferFields import com.ivy.legacy.datamodel.Account -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.datamodel.toEntity import com.ivy.legacy.utils.toLowerCaseLocal import com.ivy.wallet.domain.data.IvyCurrency @@ -64,7 +64,7 @@ class CSVImporterV2 @Inject constructor( newCategoryColorIndex = 0 newAccountColorIndex = 0 - accounts = accountDao.findAll().map { it.toDomain() } + accounts = accountDao.findAll().map { it.toLegacyDomain() } val initialAccountsCount = accounts.size categories = categoryRepository.findAll() @@ -280,7 +280,7 @@ class CSVImporterV2 @Inject constructor( val domainAccount = newAccount.toDomainAccount(currencyRepository).getOrNull() ?: return null accountRepository.save(domainAccount) - accounts = accountDao.findAll().map { it.toDomain() } + accounts = accountDao.findAll().map { it.toLegacyDomain() } return newAccount } diff --git a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt index 933728d3c2..d45dd81a2d 100644 --- a/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt +++ b/screen/loans/src/main/java/com/ivy/loans/loandetails/LoanDetailsViewModel.kt @@ -15,7 +15,7 @@ import com.ivy.frp.test.TestIdlingResource import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Loan import com.ivy.legacy.datamodel.LoanRecord -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.computationThread import com.ivy.legacy.utils.ioThread @@ -258,7 +258,7 @@ class LoanDetailsViewModel @Inject constructor( ) DisplayLoanRecord( - it.toDomain(), + it.toLegacyDomain(), account = account, loanRecordTransaction = trans != null, loanRecordCurrencyCode = account?.currency ?: defaultCurrencyCode, @@ -307,7 +307,7 @@ class LoanDetailsViewModel @Inject constructor( } associatedTransaction = ioThread { - transactionDao.findLoanTransaction(loanId = loan.value!!.id)?.toDomain() + transactionDao.findLoanTransaction(loanId = loan.value!!.id)?.toLegacyDomain() } associatedTransaction?.let { diff --git a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt index 7add3b5197..eb7ea015d0 100644 --- a/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt +++ b/screen/planned-payments/src/main/java/com/ivy/planned/edit/EditPlannedViewModel.kt @@ -17,7 +17,7 @@ import com.ivy.data.model.IntervalType import com.ivy.data.repository.CategoryRepository import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.PlannedPaymentRule -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.ioThread import com.ivy.navigation.EditPlannedScreen @@ -275,7 +275,7 @@ class EditPlannedViewModel @Inject constructor( reset() loadedRule = screen.plannedPaymentRuleId?.let { - ioThread { plannedPaymentRuleDao.findById(it)!!.toDomain() } + ioThread { plannedPaymentRuleDao.findById(it)!!.toLegacyDomain() } } ?: PlannedPaymentRule( startDate = null, intervalN = null, @@ -303,7 +303,7 @@ class EditPlannedViewModel @Inject constructor( intervalType.value = rule.intervalType initialTitle.value = rule.title description.value = rule.description - val selectedAccount = ioThread { accountDao.findById(rule.accountId)!!.toDomain() } + val selectedAccount = ioThread { accountDao.findById(rule.accountId)!!.toLegacyDomain() } account.value = selectedAccount category.value = rule.categoryId?.let { ioThread { categoryRepository.findById(CategoryId(it)) } diff --git a/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt b/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt index 4bcd9f86bb..316a69e032 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportScreen.kt @@ -28,20 +28,17 @@ import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.compose.viewModel import com.ivy.base.legacy.stringRes import com.ivy.base.model.TransactionType -import com.ivy.ui.rememberScrollPositionListState import com.ivy.data.model.Category import com.ivy.data.model.CategoryId import com.ivy.data.model.primitive.ColorInt import com.ivy.data.model.primitive.IconAsset import com.ivy.data.model.primitive.NotBlankTrimmedString -import com.ivy.data.repository.mapper.TransactionMapper import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.legacy.IvyWalletPreview import com.ivy.legacy.data.AppBaseData import com.ivy.legacy.data.LegacyDueSection import com.ivy.legacy.datamodel.Account -import com.ivy.legacy.datamodel.temp.toDomain import com.ivy.legacy.ui.component.IncomeExpensesCards import com.ivy.legacy.ui.component.transaction.TransactionsDividerLine import com.ivy.legacy.ui.component.transaction.transactions @@ -51,6 +48,7 @@ import com.ivy.navigation.PieChartStatisticScreen import com.ivy.navigation.ReportScreen import com.ivy.navigation.navigation import com.ivy.ui.R +import com.ivy.ui.rememberScrollPositionListState import com.ivy.wallet.domain.pure.data.IncomeExpensePair import com.ivy.wallet.ui.theme.Gray import com.ivy.wallet.ui.theme.Green @@ -94,9 +92,7 @@ private fun BoxWithConstraintsScope.UI( state: ReportScreenState = ReportScreenState(), onEventHandler: (ReportScreenEvent) -> Unit = {} ) { - val transactionMapper = TransactionMapper() - val legacyTransactions = - with(transactionMapper) { state.transactions.map { it.toEntity().toDomain() } } + val legacyTransactions = state.transactions val nav = navigation() val context = LocalContext.current @@ -237,11 +233,7 @@ private fun BoxWithConstraintsScope.UI( ), upcoming = LegacyDueSection( - trns = with(transactionMapper) { - state.upcomingTransactions.map { - it.toEntity().toDomain() - }.toImmutableList() - }, + trns = state.upcomingTransactions, stats = IncomeExpensePair( income = state.upcomingIncome.toBigDecimal(), expense = state.upcomingExpenses.toBigDecimal() @@ -254,11 +246,7 @@ private fun BoxWithConstraintsScope.UI( }, overdue = LegacyDueSection( - trns = with(transactionMapper) { - state.overdueTransactions.map { - it.toEntity().toDomain() - }.toImmutableList() - }, + trns = state.overdueTransactions, stats = IncomeExpensePair( income = state.overdueIncome.toBigDecimal(), expense = state.overdueExpenses.toBigDecimal() diff --git a/screen/reports/src/main/java/com/ivy/reports/ReportScreenState.kt b/screen/reports/src/main/java/com/ivy/reports/ReportScreenState.kt index ac1d97fe38..2f7a4af3d3 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportScreenState.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportScreenState.kt @@ -20,8 +20,8 @@ data class ReportScreenState( val overdueIncome: Double = 0.0, val overdueExpenses: Double = 0.0, val history: ImmutableList = persistentListOf(), - val upcomingTransactions: ImmutableList = persistentListOf(), - val overdueTransactions: ImmutableList = persistentListOf(), + val upcomingTransactions: ImmutableList = persistentListOf(), + val overdueTransactions: ImmutableList = persistentListOf(), val categories: ImmutableList = persistentListOf(), val accounts: ImmutableList = persistentListOf(), val upcomingExpanded: Boolean = false, @@ -29,7 +29,7 @@ data class ReportScreenState( val filter: ReportFilter? = null, val loading: Boolean = false, val accountIdFilters: ImmutableList = persistentListOf(), - val transactions: ImmutableList = persistentListOf(), + val transactions: ImmutableList = persistentListOf(), val filterOverlayVisible: Boolean = false, val showTransfersAsIncExpCheckbox: Boolean = false, val treatTransfersAsIncExp: Boolean = false, diff --git a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt index f93715e6e5..fed56e0168 100644 --- a/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt +++ b/screen/reports/src/main/java/com/ivy/reports/ReportViewModel.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.toArgb import androidx.lifecycle.viewModelScope -import com.ivy.ui.ComposeViewModel +import com.ivy.base.legacy.LegacyTransaction import com.ivy.base.legacy.TransactionHistoryItem import com.ivy.base.legacy.stringRes import com.ivy.base.model.TransactionType @@ -33,11 +33,13 @@ import com.ivy.domain.usecase.csv.ExportCsvUseCase import com.ivy.frp.filterSuspend import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.datamodel.Account +import com.ivy.legacy.datamodel.temp.toLegacy import com.ivy.legacy.utils.getISOFormattedDateTime import com.ivy.legacy.utils.scopedIOThread import com.ivy.legacy.utils.timeNowUTC import com.ivy.legacy.utils.toLowerCaseLocal import com.ivy.legacy.utils.uiThread +import com.ivy.ui.ComposeViewModel import com.ivy.ui.R import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.exchange.ExchangeAct @@ -106,14 +108,15 @@ class ReportViewModel @Inject constructor( private val overdueExpenses = mutableDoubleStateOf(0.0) private val history = mutableStateOf>(persistentListOf()) private val upcomingTransactions = - mutableStateOf>(persistentListOf()) - private val overdueTransactions = mutableStateOf>(persistentListOf()) + mutableStateOf>(persistentListOf()) + private val overdueTransactions = + mutableStateOf>(persistentListOf()) private val accounts = mutableStateOf>(persistentListOf()) private val upcomingExpanded = mutableStateOf(false) private val overdueExpanded = mutableStateOf(false) private val loading = mutableStateOf(false) private val accountIdFilters = mutableStateOf>(persistentListOf()) - private val transactions = mutableStateOf>(persistentListOf()) + private val transactions = mutableStateOf>(persistentListOf()) private val filterOverlayVisible = mutableStateOf(false) private val showTransfersAsIncExpCheckbox = mutableStateOf(false) private val treatTransfersAsIncExp = mutableStateOf(false) @@ -297,13 +300,19 @@ class ReportViewModel @Inject constructor( overdueIncome.doubleValue = overdueIncomeExpense.income.toDouble() overdueExpenses.doubleValue = overdueIncomeExpense.expense.toDouble() history.value = historyWithDateDividers.await().toImmutableList() - upcomingTransactions.value = upcomingTransactionsList - overdueTransactions.value = overdue + upcomingTransactions.value = upcomingTransactionsList.map { + it.toLegacy(transactionMapper) + }.toImmutableList() + overdueTransactions.value = overdue.map { + it.toLegacy(transactionMapper) + }.toImmutableList() accounts.value = tempAccounts.toImmutableList() filter.value = reportFilter loading.value = false accountIdFilters.value = accountFilterIdList.await().toImmutableList() - transactions.value = transactionsList + transactions.value = transactionsList.map { + it.toLegacy(transactionMapper) + }.toImmutableList() balance.doubleValue = tempBalance filterOverlayVisible.value = false showTransfersAsIncExpCheckbox.value = diff --git a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt index f5942ed38d..418f1feb08 100644 --- a/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt +++ b/screen/transactions/src/main/java/com/ivy/transactions/TransactionsViewModel.kt @@ -26,8 +26,8 @@ import com.ivy.frp.then import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.data.model.TimePeriod import com.ivy.legacy.data.model.toCloseTimeRange -import com.ivy.legacy.datamodel.temp.toDomain import com.ivy.legacy.datamodel.temp.toImmutableLegacyTags +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.computationThread import com.ivy.legacy.utils.dateNowUTC @@ -327,7 +327,7 @@ class TransactionsViewModel @Inject constructor( private suspend fun initForAccount(accountId: UUID) { val initialAccount = ioThread { - accountDao.findById(accountId)?.toDomain() ?: error("account not found") + accountDao.findById(accountId)?.toLegacyDomain() ?: error("account not found") } account.value = initialAccount val range = period.value.toRange(ivyContext.startDayOfMonth) @@ -378,7 +378,7 @@ class TransactionsViewModel @Inject constructor( it.map { val tags = tagsRepository.findByIds(it.tags).toImmutableLegacyTags() - it.toEntity().toDomain(tags = tags) + it.toEntity().toLegacyDomain(tags = tags) } } ) @@ -402,7 +402,7 @@ class TransactionsViewModel @Inject constructor( upcoming.value = ioThread { with(transactionMapper) { - accountLogic.upcoming(initialAccount, range).map { it.toEntity().toDomain() } + accountLogic.upcoming(initialAccount, range).map { it.toEntity().toLegacyDomain() } }.toImmutableList() } @@ -418,9 +418,8 @@ class TransactionsViewModel @Inject constructor( overdue.value = ioThread { with(transactionMapper) { accountLogic.overdue(initialAccount, range).map { - it.toEntity().toDomain() - } - .toImmutableList() + it.toEntity().toLegacyDomain() + }.toImmutableList() } } } diff --git a/shared/base/src/main/java/com/ivy/base/TimeProvider.kt b/shared/base/src/main/java/com/ivy/base/TimeProvider.kt new file mode 100644 index 0000000000..41039f12ae --- /dev/null +++ b/shared/base/src/main/java/com/ivy/base/TimeProvider.kt @@ -0,0 +1,9 @@ +package com.ivy.base + +import java.time.ZoneId +import javax.inject.Inject + +@Suppress("UnnecessaryPassThroughClass") +class TimeProvider @Inject constructor() { + fun getZoneId(): ZoneId = ZoneId.systemDefault() +} \ No newline at end of file diff --git a/shared/base/src/main/java/com/ivy/base/legacy/Transaction.kt b/shared/base/src/main/java/com/ivy/base/legacy/Transaction.kt index c0b6af8c61..7f74d86890 100644 --- a/shared/base/src/main/java/com/ivy/base/legacy/Transaction.kt +++ b/shared/base/src/main/java/com/ivy/base/legacy/Transaction.kt @@ -10,6 +10,8 @@ import java.time.LocalDateTime import java.time.LocalTime import java.util.UUID +typealias LegacyTransaction = Transaction + @Deprecated("Legacy data model. Will be deleted") @Immutable data class Transaction( diff --git a/shared/data/core/build.gradle.kts b/shared/data/core/build.gradle.kts index 00826d5bee..4692fb8c0e 100644 --- a/shared/data/core/build.gradle.kts +++ b/shared/data/core/build.gradle.kts @@ -15,5 +15,6 @@ dependencies { implementation(libs.datastore) implementation(libs.bundles.ktor) + testImplementation(projects.shared.data.modelTesting) androidTestImplementation(libs.bundles.integration.testing) } diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt index 9c53b35b5c..e6cfce7510 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/TransactionRepository.kt @@ -8,220 +8,55 @@ import com.ivy.data.model.Transaction import com.ivy.data.model.TransactionId import com.ivy.data.model.Transfer import java.time.LocalDateTime -import java.util.UUID interface TransactionRepository { + suspend fun findById(id: TransactionId): Transaction? + suspend fun findByIds(ids: List): List suspend fun findAll(): List - - @Suppress("FunctionNaming") - suspend fun findAll_LIMIT_1(): List - - suspend fun findAllIncome(): List - - suspend fun findAllExpense(): List - - suspend fun findAllTransfer(): List - suspend fun findAllIncomeByAccount(accountId: AccountId): List - suspend fun findAllExpenseByAccount(accountId: AccountId): List - suspend fun findAllTransferByAccount(accountId: AccountId): List - - suspend fun findAllIncomeByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllExpenseByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllTransferByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllTransfersToAccount( - toAccountId: AccountId, - ): List - - suspend fun findAllTransfersToAccountBetween( - toAccountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List + suspend fun findAllTransfersToAccount(toAccountId: AccountId): List suspend fun findAllBetween( startDate: LocalDateTime, endDate: LocalDateTime ): List - suspend fun findAllByAccountAndBetween( accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime ): List - - suspend fun findAllByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllUnspecifiedAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllIncomeByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllExpenseByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllTransferByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllUnspecifiedIncomeAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllUnspecifiedExpenseAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - - suspend fun findAllUnspecifiedTransferAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List - suspend fun findAllToAccountAndBetween( toAccountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime ): List - suspend fun findAllDueToBetween( startDate: LocalDateTime, endDate: LocalDateTime ): List - suspend fun findAllDueToBetweenByCategory( startDate: LocalDateTime, endDate: LocalDateTime, categoryId: CategoryId ): List - suspend fun findAllDueToBetweenByCategoryUnspecified( startDate: LocalDateTime, endDate: LocalDateTime, ): List - suspend fun findAllDueToBetweenByAccount( startDate: LocalDateTime, endDate: LocalDateTime, accountId: AccountId ): List - suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List - - suspend fun findAllIncomeBetween( - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List - - suspend fun findAllExpenseBetween( - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List - - suspend fun findAllTransferBetween( - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List - - suspend fun findAllBetweenAndRecurringRuleId( - startDate: LocalDateTime, - endDate: LocalDateTime, - recurringRuleId: UUID - ): List - - suspend fun findById(id: TransactionId): Transaction? - suspend fun findByIds(ids: List): List - - suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean = false - ): List - - suspend fun countHappenedTransactions(): Long - - suspend fun findAllByTitleMatchingPattern(pattern: String): List - - suspend fun countByTitleMatchingPattern( - pattern: String, - ): Long - - suspend fun findAllByCategory( - categoryId: CategoryId, - ): List - - suspend fun countByTitleMatchingPatternAndCategoryId( - pattern: String, - categoryId: CategoryId - ): Long - - suspend fun findAllByAccount( - accountId: AccountId - ): List - - suspend fun countByTitleMatchingPatternAndAccountId( - pattern: String, - accountId: AccountId - ): Long - - suspend fun findLoanTransaction( - loanId: UUID - ): Transaction? - - suspend fun findLoanRecordTransaction( - loanRecordId: UUID - ): Transaction? - - suspend fun findAllByLoanId( - loanId: UUID - ): List - - suspend fun save(accountId: AccountId, value: Transaction) - - suspend fun saveMany(accountId: AccountId, value: List) + suspend fun save(value: Transaction) + suspend fun saveMany(value: List) suspend fun flagDeleted(id: TransactionId) - - suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) - - suspend fun flagDeletedByAccountId(accountId: AccountId) - suspend fun deleteById(id: TransactionId) - suspend fun deleteAllByAccountId(accountId: AccountId) - suspend fun deleteAll() } \ No newline at end of file diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt index db67e35322..30a2d9cdc3 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/impl/TransactionRepositoryImpl.kt @@ -12,10 +12,8 @@ import com.ivy.data.model.Income import com.ivy.data.model.Transaction import com.ivy.data.model.TransactionId import com.ivy.data.model.Transfer -import com.ivy.data.model.primitive.AssetCode import com.ivy.data.model.primitive.AssociationId import com.ivy.data.model.primitive.TagId -import com.ivy.data.repository.AccountRepository import com.ivy.data.repository.TagsRepository import com.ivy.data.repository.TransactionRepository import com.ivy.data.repository.mapper.TransactionMapper @@ -25,299 +23,73 @@ import java.time.LocalDateTime import java.util.UUID import javax.inject.Inject -@Suppress("LargeClass") class TransactionRepositoryImpl @Inject constructor( - private val accountRepository: AccountRepository, private val mapper: TransactionMapper, private val transactionDao: TransactionDao, private val writeTransactionDao: WriteTransactionDao, private val dispatchersProvider: DispatchersProvider, private val tagRepository: TagsRepository ) : TransactionRepository { - override suspend fun findAll(): List { - return withContext(dispatchersProvider.io) { - val tagMap = async { findAllTagAssociations() } - val transactions = transactionDao.findAll() - transactions.mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - val tags = tagMap.await()[it.id] ?: emptyList() - with(mapper) { - it.toDomain( - accountAssetCode = accountAssetCode, - toAccountAssetCode = toAccountAssetCode, - tags = tags - ) - }.getOrNull() - } - } - } - - override suspend fun findAll_LIMIT_1(): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAll_LIMIT_1().mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllIncome(): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByType(TransactionType.INCOME).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } - } - } - - override suspend fun findAllExpense(): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByType(TransactionType.EXPENSE).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } - } - } - - override suspend fun findAllTransfer(): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByType(TransactionType.TRANSFER).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer + override suspend fun findAll(): List = withContext(dispatchersProvider.io) { + val tagMap = async { findAllTagAssociations() } + retrieveTrns( + dbCall = transactionDao::findAll, + retrieveTags = { + tagMap.await()[it.id] ?: emptyList() } - } - } - - override suspend fun findAllIncomeByAccount(accountId: AccountId): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccount(TransactionType.INCOME, accountId.value) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } - } + ) } - override suspend fun findAllExpenseByAccount(accountId: AccountId): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccount(TransactionType.EXPENSE, accountId.value) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } - } - } - - override suspend fun findAllTransferByAccount(accountId: AccountId): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccount(TransactionType.TRANSFER, accountId.value) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } - } - } - - override suspend fun findAllIncomeByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccountBetween( + override suspend fun findAllIncomeByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( type = TransactionType.INCOME, - accountId = accountId.value, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } + accountId = accountId.value + ) } - } + ).filterIsInstance() - override suspend fun findAllExpenseByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccountBetween( + override suspend fun findAllExpenseByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( type = TransactionType.EXPENSE, - accountId = accountId.value, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } + accountId = accountId.value + ) } - } + ).filterIsInstance() - override suspend fun findAllTransferByAccountBetween( - accountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTypeAndAccountBetween( + override suspend fun findAllTransferByAccount( + accountId: AccountId + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByTypeAndAccount( type = TransactionType.TRANSFER, - accountId = accountId.value, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } + accountId = accountId.value + ) } - } + ).filterIsInstance() override suspend fun findAllTransfersToAccount( toAccountId: AccountId - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllTransfersToAccount(toAccountId.value).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllTransfersToAccount(toAccountId = toAccountId.value) } - } - - override suspend fun findAllTransfersToAccountBetween( - toAccountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime, - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllTransfersToAccountBetween( - toAccountId = toAccountId.value, - startDate = startDate, - endDate = endDate, - type = TransactionType.TRANSFER - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } - } - } + ).filterIsInstance() override suspend fun findAllBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - val transactions = transactionDao.findAllBetween(startDate, endDate) - val tagAssociationMap = getTagsForTransactionIds(transactions) - - transactions.mapNotNull { - val tags = tagAssociationMap[it.id] ?: emptyList() - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - - with(mapper) { - it.toDomain( - accountAssetCode = accountAssetCode, - toAccountAssetCode = toAccountAssetCode, - tags = tags - ) - }.getOrNull() - } + ): List = withContext(dispatchersProvider.io) { + val transactions = transactionDao.findAllBetween(startDate, endDate) + val tagAssociationMap = getTagsForTransactionIds(transactions) + transactions.mapNotNull { + val tags = tagAssociationMap[it.id] ?: emptyList() + with(mapper) { it.toDomain(tags = tags) }.getOrNull() } } @@ -325,524 +97,105 @@ class TransactionRepositoryImpl @Inject constructor( accountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByAccountAndBetween(accountId.value, startDate, endDate) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByCategoryAndBetween(categoryId.value, startDate, endDate) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllUnspecifiedAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllUnspecifiedAndBetween(startDate, endDate).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllIncomeByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByCategoryAndTypeAndBetween( - categoryId = categoryId.value, - type = TransactionType.INCOME, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } - } - } - - override suspend fun findAllExpenseByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByCategoryAndTypeAndBetween( - categoryId = categoryId.value, - type = TransactionType.EXPENSE, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } - } - } - - override suspend fun findAllTransferByCategoryAndBetween( - categoryId: CategoryId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByCategoryAndTypeAndBetween( - categoryId = categoryId.value, - type = TransactionType.TRANSFER, - startDate = startDate, - endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } - } - } - - override suspend fun findAllUnspecifiedIncomeAndBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllUnspecifiedAndTypeAndBetween( - type = TransactionType.INCOME, + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllByAccountAndBetween( + accountId = accountId.value, startDate = startDate, endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } + ) } - } + ) - override suspend fun findAllUnspecifiedExpenseAndBetween( + override suspend fun findAllToAccountAndBetween( + toAccountId: AccountId, startDate: LocalDateTime, endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllUnspecifiedAndTypeAndBetween( - type = TransactionType.EXPENSE, + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllToAccountAndBetween( + toAccountId = toAccountId.value, startDate = startDate, endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } + ) } - } + ) - override suspend fun findAllUnspecifiedTransferAndBetween( + override suspend fun findAllDueToBetween( startDate: LocalDateTime, endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllUnspecifiedAndTypeAndBetween( - type = TransactionType.TRANSFER, + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetween( startDate = startDate, endDate = endDate - ).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } - } - } - - override suspend fun findAllToAccountAndBetween( - toAccountId: AccountId, - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllToAccountAndBetween(toAccountId.value, startDate, endDate) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllDueToBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllDueToBetween(startDate, endDate).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + ) } - } + ) override suspend fun findAllDueToBetweenByCategory( startDate: LocalDateTime, endDate: LocalDateTime, categoryId: CategoryId - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllDueToBetweenByCategory(startDate, endDate, categoryId.value) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByCategory( + startDate = startDate, + endDate = endDate, + categoryId = categoryId.value + ) } - } + ) override suspend fun findAllDueToBetweenByCategoryUnspecified( startDate: LocalDateTime, endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllDueToBetweenByCategoryUnspecified(startDate, endDate).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByCategoryUnspecified( + startDate = startDate, + endDate = endDate + ) } - } + ) override suspend fun findAllDueToBetweenByAccount( startDate: LocalDateTime, endDate: LocalDateTime, accountId: AccountId - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllDueToBetweenByAccount(startDate, endDate, accountId.value) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllByRecurringRuleId(recurringRuleId: UUID): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByRecurringRuleId(recurringRuleId).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllIncomeBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllBetweenAndType(startDate, endDate, TransactionType.INCOME) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Income - } - } - } - - override suspend fun findAllExpenseBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllBetweenAndType(startDate, endDate, TransactionType.EXPENSE) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Expense - } - } - } - - override suspend fun findAllTransferBetween( - startDate: LocalDateTime, - endDate: LocalDateTime - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllBetweenAndType(startDate, endDate, TransactionType.TRANSFER) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { - it.toDomain( - accountAssetCode, - toAccountAssetCode - ) - }.getOrNull() as? Transfer - } - } - } - - override suspend fun findAllBetweenAndRecurringRuleId( - startDate: LocalDateTime, - endDate: LocalDateTime, - recurringRuleId: UUID - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllBetweenAndRecurringRuleId(startDate, endDate, recurringRuleId) - .mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + ): List = retrieveTrns( + dbCall = { + transactionDao.findAllDueToBetweenByAccount( + startDate = startDate, + endDate = endDate, + accountId = accountId.value + ) } - } + ) - override suspend fun findById(id: TransactionId): Transaction? { - return withContext(dispatchersProvider.io) { - transactionDao.findById(id.value)?.let { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + override suspend fun findById( + id: TransactionId + ): Transaction? = withContext(dispatchersProvider.io) { + transactionDao.findById(id.value)?.let { + with(mapper) { it.toDomain() }.getOrNull() } } override suspend fun findByIds(ids: List): List { return withContext(dispatchersProvider.io) { val tagMap = async { findTagsForTransactionIds(ids) } - transactionDao.findByIds(ids.map { it.value }).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - val tags = tagMap.await()[it.id] ?: emptyList() - with(mapper) { - it.toDomain( - accountAssetCode = accountAssetCode, - toAccountAssetCode = toAccountAssetCode, - tags = tags - ) - }.getOrNull() - } - } - } - - override suspend fun findByIsSyncedAndIsDeleted( - synced: Boolean, - deleted: Boolean - ): List { - return withContext(dispatchersProvider.io) { - transactionDao.findByIsSyncedAndIsDeleted(synced, deleted).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun countHappenedTransactions(): Long { - return withContext(dispatchersProvider.io) { - transactionDao.countHappenedTransactions() - } - } - - override suspend fun findAllByTitleMatchingPattern(pattern: String): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByTitleMatchingPattern(pattern).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun countByTitleMatchingPattern(pattern: String): Long { - return withContext(dispatchersProvider.io) { - transactionDao.countByTitleMatchingPattern(pattern) - } - } - - override suspend fun findAllByCategory(categoryId: CategoryId): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByCategory(categoryId.value).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun countByTitleMatchingPatternAndCategoryId( - pattern: String, - categoryId: CategoryId - ): Long { - return withContext(dispatchersProvider.io) { - transactionDao.countByTitleMatchingPatternAndCategoryId(pattern, categoryId.value) - } - } - - override suspend fun findAllByAccount(accountId: AccountId): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByAccount(accountId.value).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun countByTitleMatchingPatternAndAccountId( - pattern: String, - accountId: AccountId - ): Long { - return withContext(dispatchersProvider.io) { - transactionDao.countByTitleMatchingPatternAndAccountId(pattern, accountId.value) - } - } - - override suspend fun findLoanTransaction(loanId: UUID): Transaction? { - return withContext(dispatchersProvider.io) { - transactionDao.findLoanTransaction(loanId)?.let { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findLoanRecordTransaction(loanRecordId: UUID): Transaction? { - return withContext(dispatchersProvider.io) { - transactionDao.findLoanRecordTransaction(loanRecordId)?.let { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } - } - } - - override suspend fun findAllByLoanId(loanId: UUID): List { - return withContext(dispatchersProvider.io) { - transactionDao.findAllByLoanId(loanId).mapNotNull { - val (accountAssetCode, toAccountAssetCode) = getAssetCodes( - it.accountId, - it.toAccountId - ) - with(mapper) { it.toDomain(accountAssetCode, toAccountAssetCode) }.getOrNull() - } + retrieveTrns( + dbCall = { + transactionDao.findByIds(ids.map { it.value }) + }, + retrieveTags = { + tagMap.await()[it.id] ?: emptyList() + } + ) } } - override suspend fun save(accountId: AccountId, value: Transaction) { + override suspend fun save(value: Transaction) { withContext(dispatchersProvider.io) { writeTransactionDao.save( with(mapper) { value.toEntity() } @@ -850,10 +203,7 @@ class TransactionRepositoryImpl @Inject constructor( } } - override suspend fun saveMany( - accountId: AccountId, - value: List - ) { + override suspend fun saveMany(value: List) { withContext(dispatchersProvider.io) { writeTransactionDao.saveMany( value.map { with(mapper) { it.toEntity() } } @@ -867,18 +217,6 @@ class TransactionRepositoryImpl @Inject constructor( } } - override suspend fun flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId: UUID) { - withContext(dispatchersProvider.io) { - writeTransactionDao.flagDeletedByRecurringRuleIdAndNoDateTime(recurringRuleId) - } - } - - override suspend fun flagDeletedByAccountId(accountId: AccountId) { - withContext(dispatchersProvider.io) { - writeTransactionDao.flagDeletedByAccountId(accountId.value) - } - } - override suspend fun deleteById(id: TransactionId) { withContext(dispatchersProvider.io) { writeTransactionDao.deleteById(id.value) @@ -897,18 +235,13 @@ class TransactionRepositoryImpl @Inject constructor( } } - private suspend fun getAssetCodes( - accountId: UUID, - toAccountId: UUID? - ): Pair { - val assetCode = getAssetCodeForAccount(accountId) - val toAssetCode = getAssetCodeForAccount(toAccountId) - return Pair(assetCode, toAssetCode) - } - - private suspend fun getAssetCodeForAccount(accountId: UUID?): AssetCode? { - accountId ?: return null - return accountRepository.findById(AccountId(accountId))?.asset + private suspend fun retrieveTrns( + dbCall: suspend () -> List, + retrieveTags: suspend (TransactionEntity) -> List = { emptyList() }, + ): List = withContext(dispatchersProvider.io) { + dbCall().mapNotNull { + with(mapper) { it.toDomain(tags = retrieveTags(it)) }.getOrNull() + } } private suspend fun getTagsForTransactionIds( diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt index b1148d114a..1502db09c8 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/AccountMapper.kt @@ -16,11 +16,11 @@ import javax.inject.Inject class AccountMapper @Inject constructor( private val currencyRepository: CurrencyRepository ) { - suspend fun AccountEntity.toDomain(): Either = either { - com.ivy.data.model.Account( - id = com.ivy.data.model.AccountId(id), + suspend fun AccountEntity.toDomain(): Either = either { + Account( + id = AccountId(id), name = NotBlankTrimmedString.from(name).bind(), - asset = currency?.let(AssetCode::from)?.bind() + asset = currency?.let(AssetCode::from)?.getOrNull() ?: currencyRepository.getBaseCurrency(), color = ColorInt(color), icon = icon?.let(IconAsset::from)?.getOrNull(), @@ -31,7 +31,7 @@ class AccountMapper @Inject constructor( ) } - fun com.ivy.data.model.Account.toEntity(): AccountEntity { + fun Account.toEntity(): AccountEntity { return AccountEntity( name = name.value, currency = asset.code, diff --git a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt index d16cf753e9..3c96cd0b27 100644 --- a/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt +++ b/shared/data/core/src/main/java/com/ivy/data/repository/mapper/TransactionMapper.kt @@ -2,6 +2,9 @@ package com.ivy.data.repository.mapper import arrow.core.Either import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.raise.ensureNotNull +import com.ivy.base.TimeProvider import com.ivy.base.model.TransactionType import com.ivy.data.db.entity.TransactionEntity import com.ivy.data.model.AccountId @@ -13,113 +16,113 @@ import com.ivy.data.model.TransactionId import com.ivy.data.model.TransactionMetadata import com.ivy.data.model.Transfer import com.ivy.data.model.common.Value -import com.ivy.data.model.primitive.AssetCode +import com.ivy.data.model.getFromAccount +import com.ivy.data.model.getToAccount import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.model.primitive.PositiveDouble import com.ivy.data.model.primitive.TagId +import com.ivy.data.repository.AccountRepository import java.time.Instant -import java.time.ZoneId import javax.inject.Inject -class TransactionMapper @Inject constructor() { +class TransactionMapper @Inject constructor( + private val accountRepository: AccountRepository, + private val timeProvider: TimeProvider, +) { - @Suppress("CyclomaticComplexMethod") - fun TransactionEntity.toDomain( - accountAssetCode: AssetCode?, - toAccountAssetCode: AssetCode? = null, + suspend fun TransactionEntity.toDomain( tags: List = emptyList() - ): Either = either { - val metadata = com.ivy.data.model.TransactionMetadata( + ): Either = either { + val metadata = TransactionMetadata( recurringRuleId = recurringRuleId, loanId = loanId, loanRecordId = loanRecordId ) - val zoneId = ZoneId.systemDefault() val settled = dateTime != null - val time = dateTime?.atZone(zoneId)?.toInstant() - ?: dueDate?.atZone(zoneId)?.toInstant() - ?: raise("Missing transaction time for entity: ${this@toDomain}") + val time = mapTime().bind() + val accountId = AccountId(accountId) + val sourceAccount = accountRepository.findById(accountId) + ensureNotNull(sourceAccount) { "No source account for transaction: ${this@toDomain}" } val fromValue = Value( amount = PositiveDouble.from(amount).bind(), - asset = accountAssetCode - ?: raise("No asset code associated with the account for this transaction '${this@toDomain}'") + asset = sourceAccount.asset ) - val notBlankTrimmedTitle = title?.let(NotBlankTrimmedString::from)?.getOrNull() - val notBlankTrimmedDescription = description?.let(NotBlankTrimmedString::from)?.getOrNull() + val notBlankTitle = title?.let(NotBlankTrimmedString::from)?.getOrNull() + val notBlankDescription = description?.let(NotBlankTrimmedString::from)?.getOrNull() + val category = categoryId?.let(::CategoryId) + val transactionId = TransactionId(id) when (type) { TransactionType.INCOME -> { - com.ivy.data.model.Income( - id = com.ivy.data.model.TransactionId(id), - title = notBlankTrimmedTitle, - description = notBlankTrimmedDescription, - category = categoryId?.let { com.ivy.data.model.CategoryId(it) }, + Income( + id = transactionId, + value = fromValue, + account = accountId, + title = notBlankTitle, + description = notBlankDescription, + category = category, time = time, settled = settled, metadata = metadata, lastUpdated = Instant.EPOCH, removed = isDeleted, - value = fromValue, - account = com.ivy.data.model.AccountId(accountId), tags = tags ) } TransactionType.EXPENSE -> { - com.ivy.data.model.Expense( - id = com.ivy.data.model.TransactionId(id), - title = notBlankTrimmedTitle, - description = notBlankTrimmedDescription, - category = categoryId?.let { com.ivy.data.model.CategoryId(it) }, + Expense( + id = transactionId, + account = accountId, + value = fromValue, + title = notBlankTitle, + description = notBlankDescription, + category = category, time = time, settled = settled, metadata = metadata, lastUpdated = Instant.EPOCH, removed = isDeleted, - value = fromValue, - account = com.ivy.data.model.AccountId(accountId), tags = tags ) } TransactionType.TRANSFER -> { - val toValue = Value( - amount = toAmount?.let { PositiveDouble.from(it).bind() } - ?: raise("Missing transfer amount for transaction '${this@toDomain}'"), - asset = toAccountAssetCode - ?: raise( - "No asset code associated with the destination account for this " + - "transaction '${this@toDomain}'" - ) - ) - - val toAccount = toAccountId?.let(::AccountId) ?: raise( - "No destination account id associated" + - " with this transaction '${this@toDomain}'" - ) + val toAccountId = toAccountId?.let(::AccountId) + ensureNotNull(toAccountId) { + "No destination account id associated with transaction '${this@toDomain}'" + } + ensure(accountId != toAccountId) { + "Self transfers aren't allowed. Source and destination accounts " + + "must be different for transaction: ${this@toDomain}" + } - if (accountId == toAccount.value) { - raise( - "Source account id and destination accounts " + - "are same with this transaction '${this@toDomain}'" - ) + val toAccount = accountRepository.findById(toAccountId) + ensureNotNull(toAccount) { + "No destination account associated with transaction '${this@toDomain}'" } - com.ivy.data.model.Transfer( - id = com.ivy.data.model.TransactionId(id), - title = notBlankTrimmedTitle, - description = notBlankTrimmedDescription, - category = categoryId?.let { com.ivy.data.model.CategoryId(it) }, + val toValue = Value( + amount = toAmount?.let(PositiveDouble::from)?.getOrNull() + ?: fromValue.amount, + asset = toAccount.asset + ) + + Transfer( + id = transactionId, + title = notBlankTitle, + description = notBlankDescription, + category = category, time = time, settled = settled, metadata = metadata, lastUpdated = Instant.EPOCH, removed = isDeleted, - fromAccount = com.ivy.data.model.AccountId(accountId), + fromAccount = accountId, fromValue = fromValue, - toAccount = toAccount, + toAccount = toAccountId, toValue = toValue, tags = tags ) @@ -127,37 +130,35 @@ class TransactionMapper @Inject constructor() { } } - fun com.ivy.data.model.Transaction.toEntity(): TransactionEntity { - val dateTime = time.atZone(ZoneId.systemDefault()).toLocalDateTime() + private fun TransactionEntity.mapTime(): Either = either { + val time = (dateTime ?: dueDate)?.atZone(timeProvider.getZoneId())?.toInstant() + ensureNotNull(time) { "Missing transaction time for entity: $this" } + time + } + + fun Transaction.toEntity(): TransactionEntity { + val dateTime = time.atZone(timeProvider.getZoneId()).toLocalDateTime() return TransactionEntity( - accountId = when (this) { - is com.ivy.data.model.Expense -> account.value - is com.ivy.data.model.Income -> account.value - is com.ivy.data.model.Transfer -> fromAccount.value - }, + accountId = getFromAccount().value, type = when (this) { - is com.ivy.data.model.Expense -> TransactionType.EXPENSE - is com.ivy.data.model.Income -> TransactionType.INCOME - is com.ivy.data.model.Transfer -> TransactionType.TRANSFER + is Expense -> TransactionType.EXPENSE + is Income -> TransactionType.INCOME + is Transfer -> TransactionType.TRANSFER }, amount = when (this) { - is com.ivy.data.model.Expense -> value.amount.value - is com.ivy.data.model.Income -> value.amount.value - is com.ivy.data.model.Transfer -> fromValue.amount.value - }, - toAccountId = if (this is com.ivy.data.model.Transfer) { - toAccount.value - } else { - null + is Expense -> value.amount.value + is Income -> value.amount.value + is Transfer -> fromValue.amount.value }, - toAmount = if (this is com.ivy.data.model.Transfer) { + toAccountId = getToAccount()?.value, + toAmount = if (this is Transfer) { toValue.amount.value } else { null }, title = title?.value, description = description?.value, - dateTime = dateTime, + dateTime = dateTime.takeIf { settled }, categoryId = category?.value, dueDate = dateTime.takeIf { !settled }, recurringRuleId = metadata.recurringRuleId, diff --git a/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt b/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt new file mode 100644 index 0000000000..b5afb1d165 --- /dev/null +++ b/shared/data/core/src/test/java/com/ivy/data/ArbTransactionEntity.kt @@ -0,0 +1,151 @@ +package com.ivy.data + +import com.ivy.base.model.TransactionType +import com.ivy.data.db.entity.TransactionEntity +import com.ivy.data.model.testing.accountId +import com.ivy.data.model.testing.maybe +import com.ivy.data.model.testing.or +import com.ivy.data.model.testing.positiveDoubleExact +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.localDateTime +import io.kotest.property.arbitrary.negativeDouble +import io.kotest.property.arbitrary.of +import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.uuid +import java.util.UUID + +fun Arb.Companion.invalidTransactionEntity(): Arb = Arb.or( + a = Arb.invalidIncomeOrExpense(), + b = Arb.invalidTransfer() +) + +fun Arb.Companion.validTransactionEntity(): Arb = Arb.or( + a = Arb.validIncomeOrExpense(), + b = Arb.validTransfer() +) + +fun Arb.Companion.invalidTransfer(): Arb = arbitrary { + var entity = validTransfer().bind() + val invalidReasons = InvalidTransferReason.entries.shuffled().take( + Arb.int(1 until InvalidTransferReason.entries.size).bind() + ).toSet() + + if (InvalidTransferReason.SameAccountAndToAccount in invalidReasons) { + val accountId = UUID.randomUUID() + entity = entity.copy( + accountId = accountId, + toAccountId = accountId + ) + } + + if (InvalidTransferReason.MissingToAccount in invalidReasons) { + entity = entity.copy( + toAccountId = null + ) + } + + entity +} + +fun Arb.Companion.validTransfer(): Arb = arbitrary { + val isPlannedPayment = Arb.boolean().bind() + + val account = Arb.accountId().bind().value + val toAccount = Arb.accountId() + .filter { it.value != account } + .bind().value + + TransactionEntity( + accountId = account, + type = TransactionType.TRANSFER, + amount = Arb.positiveDoubleExact().bind().value, + toAccountId = toAccount, + toAmount = Arb.maybe(Arb.positiveDoubleExact()).bind()?.value, + title = Arb.maybe(Arb.string()).bind(), + description = Arb.maybe(Arb.string()).bind(), + dateTime = Arb.localDateTime().bind().takeIf { + !isPlannedPayment || Arb.boolean().bind() + }, + dueDate = Arb.localDateTime().bind().takeIf { + isPlannedPayment || Arb.boolean().bind() + }, + categoryId = Arb.maybe(Arb.uuid()).bind(), + recurringRuleId = Arb.maybe(Arb.uuid()).bind(), + attachmentUrl = Arb.maybe(Arb.string()).bind(), + loanId = Arb.maybe(Arb.uuid()).bind(), + loanRecordId = Arb.maybe(Arb.uuid()).bind(), + isSynced = Arb.boolean().bind(), + isDeleted = Arb.boolean().bind(), + id = Arb.uuid().bind() + ) +} + +fun Arb.Companion.invalidIncomeOrExpense(): Arb = arbitrary { + var entity = validIncomeOrExpense().bind() + val invalidReasons = InvalidIncomeOrExpenseReason.entries.shuffled().take( + Arb.int(1 until InvalidIncomeOrExpenseReason.entries.size).bind() + ).toSet() + + if (InvalidIncomeOrExpenseReason.MissingTime in invalidReasons) { + entity = entity.copy( + dateTime = null, + dueDate = null, + ) + } + if (InvalidIncomeOrExpenseReason.InfiniteAmount in invalidReasons) { + entity = entity.copy( + amount = Double.POSITIVE_INFINITY + ) + } + if (InvalidIncomeOrExpenseReason.NonPositiveAmount in invalidReasons) { + entity = entity.copy( + amount = Arb.or(Arb.negativeDouble(), Arb.of(0.0)).bind() + ) + } + + entity +} + +fun Arb.Companion.validIncomeOrExpense(): Arb = arbitrary { + val isPlannedPayment = Arb.boolean().bind() + + TransactionEntity( + accountId = Arb.uuid().bind(), + type = Arb.of(TransactionType.INCOME, TransactionType.EXPENSE).bind(), + amount = Arb.positiveDoubleExact().bind().value, + toAccountId = Arb.maybe(Arb.accountId()).bind()?.value, + toAmount = Arb.maybe(Arb.double()).bind(), + title = Arb.maybe(Arb.string()).bind(), + description = Arb.maybe(Arb.string()).bind(), + dateTime = Arb.localDateTime().bind().takeIf { + !isPlannedPayment || Arb.boolean().bind() + }, + dueDate = Arb.localDateTime().bind().takeIf { + isPlannedPayment || Arb.boolean().bind() + }, + categoryId = Arb.maybe(Arb.uuid()).bind(), + recurringRuleId = Arb.maybe(Arb.uuid()).bind(), + attachmentUrl = Arb.maybe(Arb.string()).bind(), + loanId = Arb.maybe(Arb.uuid()).bind(), + loanRecordId = Arb.maybe(Arb.uuid()).bind(), + isSynced = Arb.boolean().bind(), + isDeleted = Arb.boolean().bind(), + id = Arb.uuid().bind() + ) +} + +enum class InvalidIncomeOrExpenseReason { + MissingTime, + NonPositiveAmount, + InfiniteAmount, +} + +enum class InvalidTransferReason { + MissingToAccount, + SameAccountAndToAccount, +} \ No newline at end of file diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt new file mode 100644 index 0000000000..31a15bf6ee --- /dev/null +++ b/shared/data/core/src/test/java/com/ivy/data/repository/impl/TransactionRepositoryImplTest.kt @@ -0,0 +1,494 @@ +package com.ivy.data.repository.impl + +import arrow.core.Either +import arrow.core.Some +import arrow.core.identity +import com.ivy.base.TestDispatchersProvider +import com.ivy.base.model.TransactionType +import com.ivy.data.db.dao.fake.FakeTransactionDao +import com.ivy.data.db.dao.read.TransactionDao +import com.ivy.data.db.dao.write.WriteTransactionDao +import com.ivy.data.db.entity.TransactionEntity +import com.ivy.data.invalidTransactionEntity +import com.ivy.data.model.AccountId +import com.ivy.data.model.Expense +import com.ivy.data.model.Income +import com.ivy.data.model.Transaction +import com.ivy.data.model.Transfer +import com.ivy.data.model.testing.ModelFixtures +import com.ivy.data.model.testing.accountId +import com.ivy.data.model.testing.transaction +import com.ivy.data.model.testing.transactionId +import com.ivy.data.repository.TagsRepository +import com.ivy.data.repository.TransactionRepository +import com.ivy.data.repository.mapper.TransactionMapper +import com.ivy.data.validTransactionEntity +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.localDateTime +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.kotest.property.checkAll +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +typealias TrnMappingRow = Pair> + +class TransactionRepositoryImplTest { + + private val mapper = mockk() + private val transactionDao = mockk() + private val writeTransactionDao = mockk() + private val tagRepository = mockk(relaxed = true) + + private lateinit var repository: TransactionRepository + + @Before + fun setup() { + repository = newRepository(fakeDao = null) + } + + private fun newRepository( + fakeDao: FakeTransactionDao?, + ): TransactionRepository = TransactionRepositoryImpl( + mapper = mapper, + transactionDao = fakeDao ?: transactionDao, + writeTransactionDao = fakeDao ?: writeTransactionDao, + dispatchersProvider = TestDispatchersProvider, + tagRepository = tagRepository + ) + + @Test + fun `find by id - not existing`() = runTest { + // given + val transactionId = ModelFixtures.TransactionId + coEvery { + transactionDao.findById(transactionId.value) + } returns null + + // when + val trn = repository.findById(transactionId) + + // then + trn shouldBe null + } + + @Test + fun `find by id - existing, successful mapping`() = runTest { + // given + val transactionId = ModelFixtures.TransactionId + val entity = mockk() + val transaction = mockk() + coEvery { + transactionDao.findById(transactionId.value) + } returns entity + with(mapper) { + coEvery { entity.toDomain(any()) } returns Either.Right(transaction) + } + + // when + val trn = repository.findById(transactionId) + + // then + trn shouldBe transaction + } + + @Test + fun `find by id - existing, failed mapping`() = runTest { + // given + val transactionId = ModelFixtures.TransactionId + val entity = mockk() + coEvery { + transactionDao.findById(transactionId.value) + } returns entity + with(mapper) { + coEvery { entity.toDomain(any()) } returns Either.Left("err") + } + + // when + val trn = repository.findById(transactionId) + + // then + trn shouldBe null + } + + @Test + fun `find all`() = transactionsTestCase( + daoMethod = transactionDao::findAll, + repoMethod = repository::findAll + ) + + @Test + fun findAllIncomeByAccount() { + val account = ModelFixtures.AccountId + + transactionsTestCase( + daoMethod = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.INCOME, + accountId = account.value + ) + }, + repoMethod = { + repository.findAllIncomeByAccount(account) + }, + mapExpectedResult = { it.filterIsInstance() } + ) + } + + @Test + fun findAllExpenseByAccount() { + val account = ModelFixtures.AccountId + + transactionsTestCase( + daoMethod = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.EXPENSE, + accountId = account.value + ) + }, + repoMethod = { + repository.findAllExpenseByAccount(account) + }, + mapExpectedResult = { it.filterIsInstance() } + ) + } + + @Test + fun findAllTransferByAccount() { + val account = ModelFixtures.AccountId + + transactionsTestCase( + daoMethod = { + transactionDao.findAllByTypeAndAccount( + type = TransactionType.TRANSFER, + accountId = account.value + ) + }, + repoMethod = { + repository.findAllTransferByAccount(account) + }, + mapExpectedResult = { it.filterIsInstance() } + ) + } + + @Test + fun findAllTransfersToAccount() { + val account = ModelFixtures.AccountId + + transactionsTestCase( + daoMethod = { + transactionDao.findAllTransfersToAccount( + toAccountId = account.value + ) + }, + repoMethod = { + repository.findAllTransfersToAccount(account) + }, + mapExpectedResult = { it.filterIsInstance() } + ) + } + + @Test + fun `find all by ids`() { + val ids = Arb.list(Arb.transactionId()).next() + transactionsTestCase( + daoMethod = { + transactionDao.findByIds(ids.map { it.value }) + }, + repoMethod = { + repository.findByIds(ids) + } + ) + } + + @Test + fun `find all between`() { + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllBetween( + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllBetween( + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun findAllByAccountAndBetween() { + val account = ModelFixtures.AccountId + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllByAccountAndBetween( + accountId = account.value, + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllByAccountAndBetween( + accountId = account, + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun findAllToAccountAndBetween() { + val account = ModelFixtures.AccountId + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllToAccountAndBetween( + toAccountId = account.value, + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllToAccountAndBetween( + toAccountId = account, + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun findAllDueToBetweenByCategory() { + val category = ModelFixtures.CategoryId + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllDueToBetweenByCategory( + categoryId = category.value, + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllDueToBetweenByCategory( + categoryId = category, + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun findAllDueToBetweenByCategoryUnspecified() { + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllDueToBetweenByCategoryUnspecified( + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllDueToBetweenByCategoryUnspecified( + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun findAllDueToBetweenByAccount() { + val account = ModelFixtures.AccountId + val startDate = Arb.localDateTime().next() + val endDate = Arb.localDateTime().next() + + transactionsTestCase( + daoMethod = { + transactionDao.findAllDueToBetweenByAccount( + accountId = account.value, + startDate = startDate, + endDate = endDate, + ) + }, + repoMethod = { + repository.findAllDueToBetweenByAccount( + accountId = account, + startDate = startDate, + endDate = endDate, + ) + } + ) + } + + @Test + fun save() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val trn = mockkFakeTrnMapping() + + // when + repository.save(trn) + + // then + val savedTrn = repository.findById(trn.id) + savedTrn shouldBe trn + } + + @Test + fun saveMany() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val trn1 = mockkFakeTrnMapping() + val trn2 = mockkFakeTrnMapping() + + // when + repository.saveMany(listOf(trn1, trn2)) + + // then + val savedTrns = repository.findAll() + savedTrns.toSet() shouldBe setOf(trn1, trn2) + } + + @Test + fun flagDeleted() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val trn = mockkFakeTrnMapping() + repository.save(trn) + + // when + repository.flagDeleted(trn.id) + + // then + repository.findById(trn.id) shouldBe null + } + + @Test + fun deleteById() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val trn = mockkFakeTrnMapping() + repository.save(trn) + + // when + repository.deleteById(trn.id) + + // then + repository.findById(trn.id) shouldBe null + } + + @Test + fun deleteAllByAccountId() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val acc1 = Arb.accountId().next() + val acc2 = Arb.accountId().next() + val trnOneAcc1 = mockkFakeTrnMapping(account = acc1) + val trnTwoAcc1 = mockkFakeTrnMapping(account = acc1) + val trnAcc2 = mockkFakeTrnMapping(account = acc2) + repository.saveMany(listOf(trnOneAcc1, trnTwoAcc1, trnAcc2)) + + // when + repository.deleteAllByAccountId(accountId = acc1) + + // then + repository.findAll() shouldBe listOf(trnAcc2) + } + + @Test + fun deleteAll() = runTest { + // given + repository = newRepository(fakeDao = FakeTransactionDao()) + val trn1 = mockkFakeTrnMapping() + val trn2 = mockkFakeTrnMapping() + val trn3 = mockkFakeTrnMapping() + repository.saveMany(listOf(trn1, trn2, trn3)) + + // when + repository.deleteAll() + + // then + repository.findAll() shouldBe emptyList() + } + + private fun mockkFakeTrnMapping( + account: AccountId = Arb.accountId().next() + ): Transaction { + val trn = Arb.transaction(account = Some(account)).next() + val entity = mockk(relaxed = true) { + every { id } returns trn.id.value + every { accountId } returns account.value + } + with(mapper) { + every { trn.toEntity() } returns entity + coEvery { entity.toDomain(any()) } returns Either.Right(trn) + } + return trn + } + + private fun transactionsTestCase( + daoMethod: suspend () -> List, + repoMethod: suspend () -> List, + mapExpectedResult: (List) -> List = ::identity + ) = runTest { + checkAll( + Arb.map( + arb = Arb.trnMappingRow(), + minSize = 0, + maxSize = 10, + ) + ) { trnMapping -> + // given + coEvery { daoMethod() } returns trnMapping.keys.toList() + trnMapping.forEach { (entity, mappingRes) -> + with(mapper) { + coEvery { entity.toDomain(any()) } returns mappingRes + } + } + + // when + val trns = repoMethod() + + // then + val expectedTrns = trnMapping.values.mapNotNull { it.getOrNull() } + trns.toSet() shouldBe mapExpectedResult(expectedTrns).toSet() + } + } + + private fun Arb.Companion.trnMappingRow(): Arb = arbitrary { + val isValid = Arb.boolean().bind() + if (isValid) { + Arb.validTransactionEntity().bind() to Either.Right(Arb.transaction().bind()) + } else { + Arb.invalidTransactionEntity().bind() to Either.Left(Arb.string().bind()) + } + } +} \ No newline at end of file diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt index 0e69fe392c..b3a91847cf 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/AccountMapperTest.kt @@ -34,14 +34,14 @@ class AccountMapperTest { fun `maps domain to entity`() { // given val id = UUID.randomUUID() - val account = com.ivy.data.model.Account( - id = com.ivy.data.model.AccountId(id), + val account = Account( + id = AccountId(id), name = NotBlankTrimmedString.unsafe("Test"), asset = AssetCode.unsafe("USD"), color = ColorInt(value = 42), icon = IconAsset.unsafe("icon"), includeInBalance = true, - orderNum = 0.0, + orderNum = 3.14, lastUpdated = Instant.EPOCH, removed = false ) @@ -56,29 +56,33 @@ class AccountMapperTest { color = 42, icon = "icon", includeInBalance = true, + orderNum = 3.14, isSynced = true, isDeleted = false, id = id, ) } + // region entity to domain @Test fun `maps entity to domain - valid entity`() = runTest { // given - val entity = ValidEntity + val entity = ValidEntity.copy( + orderNum = 42.0 + ) // when val result = with(mapper) { entity.toDomain() } // then - result.shouldBeRight() shouldBe com.ivy.data.model.Account( - id = com.ivy.data.model.AccountId(entity.id), + result.shouldBeRight() shouldBe Account( + id = AccountId(entity.id), name = NotBlankTrimmedString.unsafe("Test"), asset = AssetCode.unsafe("USD"), color = ColorInt(value = 42), icon = IconAsset.unsafe("icon"), includeInBalance = true, - orderNum = 0.0, + orderNum = 42.0, lastUpdated = Instant.EPOCH, removed = false ) @@ -118,7 +122,7 @@ class AccountMapperTest { val result = with(mapper) { missingIconEntity.toDomain() } // then - result.shouldBeRight() + result.shouldBeRight().icon shouldBe null } @Test @@ -130,8 +134,9 @@ class AccountMapperTest { val result = with(mapper) { invalidIconEntity.toDomain() } // then - result.shouldBeRight() + result.shouldBeRight().icon shouldBe null } + // endregion companion object { val ValidEntity = AccountEntity( diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperPropertyTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperPropertyTest.kt new file mode 100644 index 0000000000..cd9314d9ba --- /dev/null +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperPropertyTest.kt @@ -0,0 +1,147 @@ +package com.ivy.data.repository.mapper + +import arrow.core.Some +import com.ivy.data.db.entity.TransactionEntity +import com.ivy.data.invalidIncomeOrExpense +import com.ivy.data.invalidTransfer +import com.ivy.data.model.AccountId +import com.ivy.data.model.Transfer +import com.ivy.data.model.getFromAccount +import com.ivy.data.model.getFromValue +import com.ivy.data.model.testing.account +import com.ivy.data.model.testing.transaction +import com.ivy.data.repository.AccountRepository +import com.ivy.data.validIncomeOrExpense +import com.ivy.data.validTransfer +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.next +import io.kotest.property.checkAll +import io.kotest.property.forAll +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.time.ZoneId + +class TransactionMapperPropertyTest { + + private val accountRepo = mockk() + + private lateinit var mapper: TransactionMapper + + @Before + fun setup() { + mapper = TransactionMapper( + accountRepository = accountRepo, + timeProvider = mockk { + every { getZoneId() } returns ZoneId.of("UTC") + } + ) + } + + @Test + fun `property - domain-entity isomorphism`() = runTest { + checkAll(Arb.transaction()) { trnOrig -> + // given + val account = Arb.account( + accountId = Some(trnOrig.getFromAccount()), + asset = Some(trnOrig.getFromValue().asset) + ).next() + coEvery { accountRepo.findById(account.id) } returns account + + if (trnOrig is Transfer) { + val toAccount = Arb.account( + accountId = Some(trnOrig.toAccount), + asset = Some(trnOrig.toValue.asset) + ).next() + coEvery { accountRepo.findById(toAccount.id) } returns toAccount + } + + with(mapper) { + // when: domain -> entity -> domain + val entityOne = trnOrig.toEntity() + val trnTwo = entityOne.toDomain(tags = emptyList()).getOrNull() + + // then: the recovered domain trn must be the same + trnTwo.shouldNotBeNull() shouldBe trnOrig + + // and when again: domain -> entity + val entityTwo = trnTwo.toEntity() + + // then: the recovered entity must be the same + entityTwo shouldBe entityOne + } + } + } + + @Test + fun `maps valid transfer to domain - success`() = runTest { + forAll(Arb.validTransfer()) { entity -> + // given + mockkValidAccounts(entity) + + // when + val res = with(mapper) { entity.toDomain(tags = emptyList()) } + + // then + res.isRight() + } + } + + @Test + fun `maps invalid transfer to domain - fails`() = runTest { + forAll(Arb.invalidTransfer()) { entity -> + // given + mockkValidAccounts(entity) + + // when + val res = with(mapper) { entity.toDomain(tags = emptyList()) } + + // then + res.isLeft() + } + } + + @Test + fun `maps invalid incomes or expense to domain - fails`() = runTest { + forAll(Arb.invalidIncomeOrExpense()) { entity -> + // given + mockkValidAccounts(entity) + + // when + val res = with(mapper) { entity.toDomain(tags = emptyList()) } + + // then + res.isLeft() + } + } + + @Test + fun `maps valid incomes or expense to domain - success`() = runTest { + forAll(Arb.validIncomeOrExpense()) { entity -> + // given + mockkValidAccounts(entity) + + // when + val res = with(mapper) { entity.toDomain(tags = emptyList()) } + + // then + res.isRight() + } + } + + private fun mockkValidAccounts(entity: TransactionEntity) { + coEvery { + accountRepo.findById(AccountId(entity.accountId)) + } returns Arb.account().next() + entity.toAccountId?.let { toAccountId -> + coEvery { + accountRepo.findById(AccountId(toAccountId)) + } returns Arb.account().next() + } + } +} \ No newline at end of file diff --git a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt index d6a6f22d66..4fdfd1477f 100644 --- a/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt +++ b/shared/data/core/src/test/java/com/ivy/data/repository/mapper/TransactionMapperTest.kt @@ -1,5 +1,9 @@ package com.ivy.data.repository.mapper +import arrow.core.Some +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.ivy.base.TimeProvider import com.ivy.base.model.TransactionType import com.ivy.data.db.entity.TransactionEntity import com.ivy.data.model.AccountId @@ -11,44 +15,69 @@ import com.ivy.data.model.TransactionMetadata import com.ivy.data.model.Transfer import com.ivy.data.model.common.Value import com.ivy.data.model.primitive.AssetCode +import com.ivy.data.model.primitive.AssetCode.Companion.EUR +import com.ivy.data.model.primitive.AssetCode.Companion.USD import com.ivy.data.model.primitive.NotBlankTrimmedString import com.ivy.data.model.primitive.PositiveDouble +import com.ivy.data.model.testing.account +import com.ivy.data.repository.AccountRepository +import io.kotest.assertions.arrow.core.shouldBeLeft import io.kotest.assertions.arrow.core.shouldBeRight import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.next +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.util.UUID +@RunWith(TestParameterInjector::class) class TransactionMapperTest { + private val accountRepo = mockk() + private val timeProvider = mockk { + every { getZoneId() } returns ZoneId.of("UTC") + } + private lateinit var mapper: TransactionMapper @Before fun setup() { - mapper = TransactionMapper() + mapper = TransactionMapper( + accountRepository = accountRepo, + timeProvider = timeProvider, + ) } + // region entity -> domain @Test - fun `maps domain income to entity`() { + fun `maps domain income to entity`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) { // given - val income = com.ivy.data.model.Income( + val income = Income( id = TransactionId, title = NotBlankTrimmedString.unsafe("Income"), description = NotBlankTrimmedString.unsafe("Income desc"), category = CategoryId, time = InstantNow, - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = InstantNow, - removed = false, + removed = removed, value = Value( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -61,6 +90,7 @@ class TransactionMapperTest { val entity = with(mapper) { income.toEntity() } // then + val dateTime = InstantNow.atZone(timeProvider.getZoneId()).toLocalDateTime() entity shouldBe TransactionEntity( accountId = AccountId.value, type = TransactionType.INCOME, @@ -69,36 +99,39 @@ class TransactionMapperTest { toAmount = null, title = "Income", description = "Income desc", - dateTime = InstantNow.atZone(ZoneId.systemDefault()).toLocalDateTime(), + dateTime = dateTime.takeIf { settled }, categoryId = CategoryId.value, - dueDate = null, + dueDate = dateTime.takeIf { !settled }, recurringRuleId = RecurringRuleId, attachmentUrl = null, loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = false, + isDeleted = removed, id = TransactionId.value ) } @Test - fun `maps domain expense to entity`() { + fun `maps domain expense to entity`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) { // given - val expense = com.ivy.data.model.Expense( + val expense = Expense( id = TransactionId, title = NotBlankTrimmedString.unsafe("Expense"), description = NotBlankTrimmedString.unsafe("Expense desc"), category = CategoryId, time = InstantNow, - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = Instant.EPOCH, - removed = false, + removed = removed, value = Value( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -111,6 +144,7 @@ class TransactionMapperTest { val entity = with(mapper) { expense.toEntity() } // then + val dateTime = InstantNow.atZone(timeProvider.getZoneId()).toLocalDateTime() entity shouldBe TransactionEntity( accountId = AccountId.value, type = TransactionType.EXPENSE, @@ -119,36 +153,39 @@ class TransactionMapperTest { toAmount = null, title = "Expense", description = "Expense desc", - dateTime = InstantNow.atZone(ZoneId.systemDefault()).toLocalDateTime(), + dateTime = dateTime.takeIf { settled }, categoryId = CategoryId.value, - dueDate = null, + dueDate = dateTime.takeIf { !settled }, recurringRuleId = RecurringRuleId, attachmentUrl = null, loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = false, + isDeleted = removed, id = TransactionId.value ) } @Test - fun `maps domain transfer to entity`() { + fun `maps domain transfer to entity`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) { // given - val transfer = com.ivy.data.model.Transfer( + val transfer = Transfer( id = TransactionId, title = NotBlankTrimmedString.unsafe("Transfer"), description = NotBlankTrimmedString.unsafe("Transfer desc"), category = CategoryId, time = InstantNow, - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = Instant.EPOCH, - removed = false, + removed = removed, fromValue = Value( amount = PositiveDouble.unsafe(100.0), asset = AssetCode.unsafe("NGN") @@ -166,6 +203,7 @@ class TransactionMapperTest { val entity = with(mapper) { transfer.toEntity() } // then + val dateTime = InstantNow.atZone(timeProvider.getZoneId()).toLocalDateTime() entity shouldBe TransactionEntity( accountId = AccountId.value, type = TransactionType.TRANSFER, @@ -174,42 +212,52 @@ class TransactionMapperTest { toAmount = 100.0, title = "Transfer", description = "Transfer desc", - dateTime = InstantNow.atZone(ZoneId.systemDefault()).toLocalDateTime(), + dateTime = dateTime.takeIf { settled }, categoryId = CategoryId.value, - dueDate = null, + dueDate = dateTime.takeIf { !settled }, recurringRuleId = RecurringRuleId, attachmentUrl = null, loanId = LoanId, loanRecordId = LoanRecordId, isSynced = true, - isDeleted = false, + isDeleted = removed, id = TransactionId.value ) } + // endregion + // domain -> entity @Test - fun `maps income entity to domain - valid income`() { + fun `maps income entity to domain - valid income`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { // given - val entity = ValidIncome + val entity = ValidIncome.copy( + dateTime = DateTime.takeIf { settled }, + dueDate = DateTime.takeIf { !settled }, + isDeleted = removed, + ) + mockkAccounts(account = EUR) // when - val income = with(mapper) { entity.toDomain(EUR) } + val income = with(mapper) { entity.toDomain() } // then - income.shouldBeRight() shouldBe com.ivy.data.model.Income( + income.shouldBeRight() shouldBe Income( id = TransactionId, title = NotBlankTrimmedString.unsafe("Income"), description = NotBlankTrimmedString.unsafe("Income desc"), category = CategoryId, - time = DateTime.atZone(ZoneId.systemDefault()).toInstant(), - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = Instant.EPOCH, - removed = false, + removed = removed, value = Value( amount = PositiveDouble.unsafe(100.0), asset = EUR @@ -220,194 +268,261 @@ class TransactionMapperTest { } @Test - fun `maps income entity to domain - blank title is okay`() { + fun `maps income entity to domain - blank title is okay`() = runTest { val blankTitleEntity = ValidIncome.copy(title = "") + mockkAccounts(account = EUR) // when - val income = with(mapper) { blankTitleEntity.toDomain(USD) } + val income = with(mapper) { blankTitleEntity.toDomain() } // then income.shouldBeRight() } @Test - fun `maps income entity to domain - blank description is okay`() { + fun `maps income entity to domain - blank description is okay`() = runTest { val blankDescriptionEntity = ValidIncome.copy(description = "") + mockkAccounts(account = EUR) // when - val income = with(mapper) { blankDescriptionEntity.toDomain(EUR) } + val income = with(mapper) { blankDescriptionEntity.toDomain() } // then income.shouldBeRight() } @Test - fun `maps income entity to domain - no category is okay`() { + fun `maps income entity to domain - no category is okay`() = runTest { val noCategoryEntity = ValidIncome.copy(categoryId = null) + mockkAccounts(account = EUR) // when - val income = with(mapper) { noCategoryEntity.toDomain(USD) } + val income = with(mapper) { noCategoryEntity.toDomain() } // then income.shouldBeRight() } @Test - fun `maps income entity to domain - no recurringId is okay`() { + fun `maps income entity to domain - no recurringId is okay`() = runTest { val noRecurringId = ValidIncome.copy(recurringRuleId = null) + mockkAccounts(account = EUR) // when - val income = with(mapper) { noRecurringId.toDomain(EUR) } + val income = with(mapper) { noRecurringId.toDomain() } // then income.shouldBeRight() } @Test - fun `maps income entity to domain - no loanId is okay`() { + fun `maps income entity to domain - no loanId is okay`() = runTest { + // given val noLoanId = ValidIncome.copy(loanId = null) + mockkAccounts(account = USD) // when - val income = with(mapper) { noLoanId.toDomain(USD) } + val income = with(mapper) { noLoanId.toDomain() } // then income.shouldBeRight() } @Test - fun `maps income entity to domain - no loanRecordId is okay`() { + fun `maps income entity to domain - no loanRecordId is okay`() = runTest { + // given val noLoanRecordId = ValidIncome.copy(loanRecordId = null) + mockkAccounts(account = EUR) // when - val income = with(mapper) { noLoanRecordId.toDomain(EUR) } + val income = with(mapper) { noLoanRecordId.toDomain() } // then income.shouldBeRight() } @Test - fun `expense entity to domain - valid expense`() { + fun `income entity to domain - missing source account is failure`() = runTest { + // given + coEvery { accountRepo.findById(AccountId) } returns null + + // when + val transfer = with(mapper) { ValidIncome.toDomain() } + + // then + transfer.shouldBeLeft() + } + + @Test + fun `expense entity to domain - valid expense`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { // given - val entity = ValidExpense + val entity = ValidExpense.copy( + dateTime = DateTime.takeIf { settled }, + dueDate = DateTime.takeIf { !settled }, + isDeleted = removed + ) + mockkAccounts(account = EUR) // when - val expense = with(mapper) { entity.toDomain(EUR) } + val expense = with(mapper) { entity.toDomain() } // then - expense.shouldBeRight() shouldBe com.ivy.data.model.Expense( + expense.shouldBeRight() shouldBe Expense( id = TransactionId, title = NotBlankTrimmedString.unsafe("Expense"), description = NotBlankTrimmedString.unsafe("Expense desc"), category = CategoryId, - time = DateTime.atZone(ZoneId.systemDefault()).toInstant(), - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = Instant.EPOCH, - removed = false, - value = Value(amount = PositiveDouble.unsafe(100.0), asset = EUR), + removed = removed, + value = Value( + amount = PositiveDouble.unsafe(100.0), + asset = EUR + ), account = AccountId, tags = persistentListOf() ) } @Test - fun `expense entity to domain - blank title is okay`() { + fun `expense entity to domain - blank title is okay`() = runTest { val blankTitleEntity = ValidExpense.copy(title = "") + mockkAccounts(account = USD) // when - val expense = with(mapper) { blankTitleEntity.toDomain(USD) } + val expense = with(mapper) { blankTitleEntity.toDomain() } // then expense.shouldBeRight() } @Test - fun `expense entity to domain - blank description is okay`() { + fun `expense entity to domain - blank description is okay`() = runTest { val blankDescriptionEntity = ValidExpense.copy(description = "") + mockkAccounts(account = EUR) // when - val expense = with(mapper) { blankDescriptionEntity.toDomain(USD) } + val expense = with(mapper) { blankDescriptionEntity.toDomain() } // then expense.shouldBeRight() } @Test - fun `expense entity to domain - no category is okay`() { + fun `expense entity to domain - no category is okay`() = runTest { + // given val noCategoryEntity = ValidExpense.copy(categoryId = null) + mockkAccounts(account = EUR) // when - val expense = with(mapper) { noCategoryEntity.toDomain(EUR) } + val expense = with(mapper) { noCategoryEntity.toDomain() } // then expense.shouldBeRight() } @Test - fun `expense entity to domain - no recurringId is okay`() { + fun `expense entity to domain - no recurringId is okay`() = runTest { val noRecurringId = ValidExpense.copy(recurringRuleId = null) + mockkAccounts(account = EUR) // when - val expense = with(mapper) { noRecurringId.toDomain(EUR) } + val expense = with(mapper) { noRecurringId.toDomain() } // then expense.shouldBeRight() } @Test - fun `expense entity to domain - no loanId is okay`() { + fun `expense entity to domain - no loanId is okay`() = runTest { val noLoanId = ValidExpense.copy(loanId = null) + mockkAccounts(account = USD) // when - val expense = with(mapper) { noLoanId.toDomain(USD) } + val expense = with(mapper) { noLoanId.toDomain() } // then expense.shouldBeRight() } @Test - fun `expense entity to domain - no loanRecordId is okay`() { + fun `expense entity to domain - no loanRecordId is okay`() = runTest { + // given val noLoanRecordId = ValidExpense.copy(loanRecordId = null) + mockkAccounts(account = EUR) // when - val expense = with(mapper) { noLoanRecordId.toDomain(EUR) } + val expense = with(mapper) { noLoanRecordId.toDomain() } // then expense.shouldBeRight() } @Test - fun `transfer entity to domain - valid transfer`() { + fun `expense entity to domain - missing source account is failure`() = runTest { + // given + coEvery { accountRepo.findById(AccountId) } returns null + // when - val transfer = with(mapper) { ValidTransfer.toDomain(USD, EUR) } + val transfer = with(mapper) { ValidExpense.toDomain() } // then - transfer.shouldBeRight() shouldBe com.ivy.data.model.Transfer( + transfer.shouldBeLeft() + } + + @Test + fun `transfer entity to domain - valid transfer`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { + // given + val entity = ValidTransfer.copy( + dateTime = DateTime.takeIf { settled }, + dueDate = DateTime.takeIf { !settled }, + isDeleted = removed, + amount = 50.0, + toAmount = 55.0, + ) + mockkAccounts( + account = EUR, + toAccount = USD + ) + + // when + val transfer = with(mapper) { entity.toDomain() } + + // then + transfer.shouldBeRight() shouldBe Transfer( id = TransactionId, title = NotBlankTrimmedString.unsafe("Transfer"), description = NotBlankTrimmedString.unsafe("Transfer desc"), category = CategoryId, - time = DateTime.atZone(ZoneId.systemDefault()).toInstant(), - settled = true, - metadata = com.ivy.data.model.TransactionMetadata( + time = DateTime.atZone(timeProvider.getZoneId()).toInstant(), + settled = settled, + metadata = TransactionMetadata( recurringRuleId = RecurringRuleId, loanId = LoanId, loanRecordId = LoanRecordId ), lastUpdated = Instant.EPOCH, - removed = false, + removed = removed, fromValue = Value( - amount = PositiveDouble.unsafe(100.0), - asset = USD + amount = PositiveDouble.unsafe(50.0), + asset = EUR ), fromAccount = AccountId, toValue = Value( - amount = PositiveDouble.unsafe(100.0), - asset = EUR + amount = PositiveDouble.unsafe(55.0), + asset = USD ), toAccount = ToAccountId, tags = persistentListOf() @@ -415,83 +530,171 @@ class TransactionMapperTest { } @Test - fun `transfer entity to domain - blank title is okay`() { + fun `transfer entity to domain - blank title is okay`() = runTest { + // given val blankTitleEntity = ValidTransfer.copy(title = "") + mockkAccounts( + account = EUR, + toAccount = EUR + ) // when - val transfer = with(mapper) { blankTitleEntity.toDomain(USD, USD) } + val transfer = with(mapper) { blankTitleEntity.toDomain() } // then transfer.shouldBeRight() } @Test - fun `transfer entity to domain - blank description is okay`() { + fun `transfer entity to domain - blank description is okay`() = runTest { val blankDescriptionEntity = ValidTransfer.copy(description = "") + mockkAccounts( + account = USD, + toAccount = USD + ) // when - val transfer = with(mapper) { blankDescriptionEntity.toDomain(EUR, USD) } + val transfer = with(mapper) { blankDescriptionEntity.toDomain() } // then transfer.shouldBeRight() } @Test - fun `transfer entity to domain - no category is okay`() { + fun `transfer entity to domain - no category is okay`() = runTest { + // given val noCategoryEntity = ValidTransfer.copy(categoryId = null) + mockkAccounts( + account = USD, + toAccount = EUR + ) // when - val transfer = with(mapper) { noCategoryEntity.toDomain(EUR, EUR) } + val transfer = with(mapper) { noCategoryEntity.toDomain() } // then transfer.shouldBeRight() } @Test - fun `transfer entity to domain - no recurringId is okay`() { + fun `transfer entity to domain - no recurringId is okay`() = runTest { + // given val noRecurringId = ValidTransfer.copy(recurringRuleId = null) + mockkAccounts( + account = EUR, + toAccount = USD + ) // when - val transfer = with(mapper) { noRecurringId.toDomain(EUR, EUR) } + val transfer = with(mapper) { noRecurringId.toDomain() } // then transfer.shouldBeRight() } @Test - fun `transfer entity to domain - no loanId is okay`() { + fun `transfer entity to domain - no loanId is okay`() = runTest { + // given val noLoanId = ValidTransfer.copy(loanId = null) + mockkAccounts( + account = USD, + toAccount = USD + ) // when - val transfer = with(mapper) { noLoanId.toDomain(USD, USD) } + val transfer = with(mapper) { noLoanId.toDomain() } // then transfer.shouldBeRight() } @Test - fun `transfer entity to domain - no loanRecordId is okay`() { + fun `transfer entity to domain - no loanRecordId is okay`() = runTest { + // given val noLoanRecordId = ValidTransfer.copy(loanRecordId = null) + mockkAccounts( + account = EUR, + toAccount = USD + ) // when - val transfer = with(mapper) { noLoanRecordId.toDomain(USD, EUR) } + val transfer = with(mapper) { noLoanRecordId.toDomain() } // then transfer.shouldBeRight() } + @Test + fun `transfer entity to domain - no toAmount is okay`() = runTest { + // given + val noLoanRecordId = ValidTransfer.copy(toAmount = null) + mockkAccounts( + account = EUR, + toAccount = USD + ) + + // when + val transfer = with(mapper) { noLoanRecordId.toDomain() } + + // then + transfer.shouldBeRight() + } + + @Test + fun `transfer entity to domain - missing source account is failure`() = runTest { + // given + coEvery { accountRepo.findById(AccountId) } returns null + coEvery { + accountRepo.findById(ToAccountId) + } returns Arb.account(asset = Some(EUR)).next() + + // when + val transfer = with(mapper) { ValidTransfer.toDomain() } + + // then + transfer.shouldBeLeft() + } + + @Test + fun `transfer entity to domain - missing destination account is failure`() = runTest { + // given + coEvery { + accountRepo.findById(AccountId) + } returns Arb.account(asset = Some(EUR)).next() + coEvery { accountRepo.findById(ToAccountId) } returns null + + // when + val transfer = with(mapper) { ValidTransfer.toDomain() } + + // then + transfer.shouldBeLeft() + } + // endregion + + private fun mockkAccounts( + account: AssetCode, + toAccount: AssetCode? = null, + ) { + coEvery { + accountRepo.findById(AccountId) + } returns Arb.account(asset = Some(account)).next() + if (toAccount != null) { + coEvery { + accountRepo.findById(ToAccountId) + } returns Arb.account(asset = Some(toAccount)).next() + } + } + companion object { val DateTime = LocalDateTime.now() - val AccountId = com.ivy.data.model.AccountId(UUID.randomUUID()) - val ToAccountId = com.ivy.data.model.AccountId(UUID.randomUUID()) - val CategoryId = com.ivy.data.model.CategoryId(UUID.randomUUID()) + val AccountId = AccountId(UUID.randomUUID()) + val ToAccountId = AccountId(UUID.randomUUID()) + val CategoryId = CategoryId(UUID.randomUUID()) val RecurringRuleId = UUID.randomUUID() val LoanId = UUID.randomUUID() val LoanRecordId = UUID.randomUUID() - val TransactionId = com.ivy.data.model.TransactionId(UUID.randomUUID()) + val TransactionId = TransactionId(UUID.randomUUID()) val InstantNow = Instant.now() - val USD = AssetCode.unsafe("USD") - val EUR = AssetCode.unsafe("EUR") val ValidIncome = TransactionEntity( accountId = AccountId.value, diff --git a/shared/data/model-testing/build.gradle.kts b/shared/data/model-testing/build.gradle.kts index 86919cecc9..ff013c5df7 100644 --- a/shared/data/model-testing/build.gradle.kts +++ b/shared/data/model-testing/build.gradle.kts @@ -4,4 +4,10 @@ plugins { android { namespace = "com.ivy.data.model.testing" +} + +dependencies { + implementation(projects.shared.data.model) + + implementation(libs.bundles.testing) } \ No newline at end of file diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt new file mode 100644 index 0000000000..fda1c0f987 --- /dev/null +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbAccount.kt @@ -0,0 +1,37 @@ +package com.ivy.data.model.testing + +import arrow.core.None +import arrow.core.Option +import arrow.core.getOrElse +import com.ivy.data.model.Account +import com.ivy.data.model.AccountId +import com.ivy.data.model.primitive.AssetCode +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.uuid +import java.time.Instant + +fun Arb.Companion.account( + accountId: Option = None, + removed: Option = None, + asset: Option = None, + includeInBalance: Option = None, + orderNum: Option = None, +): Arb = arbitrary { + Account( + id = accountId.getOrElse { Arb.accountId().bind() }, + name = Arb.notBlankTrimmedString().bind(), + asset = asset.getOrElse { Arb.assetCode().bind() }, + color = Arb.colorInt().bind(), + icon = Arb.maybe(Arb.iconAsset()).bind(), + includeInBalance = includeInBalance.getOrElse { Arb.boolean().bind() }, + orderNum = orderNum.getOrElse { Arb.double().bind() }, + lastUpdated = Instant.EPOCH, + removed = removed.getOrElse { Arb.boolean().bind() } + ) +} + +fun Arb.Companion.accountId(): Arb = Arb.uuid().map(::AccountId) diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt new file mode 100644 index 0000000000..a2f76e1a6a --- /dev/null +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbCategory.kt @@ -0,0 +1,8 @@ +package com.ivy.data.model.testing + +import com.ivy.data.model.CategoryId +import io.kotest.property.Arb +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.uuid + +fun Arb.Companion.categoryId(): Arb = Arb.uuid().map(::CategoryId) \ No newline at end of file diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt new file mode 100644 index 0000000000..87b60eb20a --- /dev/null +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbTransaction.kt @@ -0,0 +1,208 @@ +package com.ivy.data.model.testing + +import arrow.core.None +import arrow.core.Option +import arrow.core.Some +import arrow.core.getOrElse +import com.ivy.data.model.AccountId +import com.ivy.data.model.CategoryId +import com.ivy.data.model.Expense +import com.ivy.data.model.Income +import com.ivy.data.model.Transaction +import com.ivy.data.model.TransactionId +import com.ivy.data.model.TransactionMetadata +import com.ivy.data.model.Transfer +import com.ivy.data.model.common.Value +import com.ivy.data.model.primitive.AssetCode +import com.ivy.data.model.primitive.PositiveDouble +import io.kotest.property.Arb +import io.kotest.property.arbitrary.ArbitraryBuilderContext +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.instant +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.removeEdgecases +import io.kotest.property.arbitrary.uuid +import java.time.Instant +import java.util.concurrent.TimeUnit + +fun Arb.Companion.income( + accountId: Option = None, + categoryId: Option = None, + settled: Option = None, + time: Option = None, + removed: Option = Some(false), + amount: Option = None, + asset: Option = None, + id: Option = None +): Arb = arbitrary { + Income( + id = id.getOrElse { Arb.transactionId().bind() }, + title = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + description = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + category = categoryId.getOrElse { Arb.maybe(Arb.categoryId()).bind() }, + time = arbInstant(time), + settled = settled.getOrElse { Arb.boolean().bind() }, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = removed.getOrElse { Arb.boolean().bind() }, + tags = listOf(), + value = Value( + amount = amount.getOrElse { Arb.positiveDoubleExact().bind() }, + asset = asset.getOrElse { Arb.assetCode().bind() } + ), + account = accountId.getOrElse { Arb.accountId().bind() } + ) +} + +fun Arb.Companion.expense( + accountId: Option = None, + categoryId: Option = None, + settled: Option = None, + time: Option = None, + removed: Option = Some(false), + amount: Option = None, + asset: Option = None, + id: Option = None +): Arb = arbitrary { + Expense( + id = id.getOrElse { Arb.transactionId().bind() }, + title = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + description = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + category = categoryId.getOrElse { Arb.maybe(Arb.categoryId()).bind() }, + time = arbInstant(time), + settled = settled.getOrElse { Arb.boolean().bind() }, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = removed.getOrElse { Arb.boolean().bind() }, + tags = listOf(), + value = Value( + amount = amount.getOrElse { Arb.positiveDoubleExact().bind() }, + asset = asset.getOrElse { Arb.assetCode().bind() } + ), + account = accountId.getOrElse { Arb.accountId().bind() } + ) +} + +fun Arb.Companion.transfer( + categoryId: Option = None, + settled: Option = None, + time: Option = None, + removed: Option = Some(false), + fromAccount: Option = None, + fromAmount: Option = None, + fromAsset: Option = None, + toAccount: Option = None, + toAmount: Option = None, + toAsset: Option = None, + id: Option = None +): Arb = arbitrary { + val fromAccountVal = fromAccount.getOrElse { Arb.accountId().bind() } + Transfer( + id = id.getOrElse { Arb.transactionId().bind() }, + title = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + description = Arb.maybe(Arb.notBlankTrimmedString()).bind(), + category = categoryId.getOrElse { Arb.maybe(Arb.categoryId()).bind() }, + time = arbInstant(time), + settled = settled.getOrElse { Arb.boolean().bind() }, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = removed.getOrElse { Arb.boolean().bind() }, + tags = listOf(), + fromValue = Value( + amount = fromAmount.getOrElse { Arb.positiveDoubleExact().bind() }, + asset = fromAsset.getOrElse { Arb.assetCode().bind() } + ), + fromAccount = fromAccountVal, + toAccount = toAccount.getOrElse { + Arb.accountId().filter { it != fromAccountVal }.bind() + }, + toValue = Value( + amount = toAmount.getOrElse { Arb.positiveDoubleExact().bind() }, + asset = toAsset.getOrElse { Arb.assetCode().bind() } + ) + ) +} + +@Suppress("MagicNumber") +fun Arb.Companion.transaction( + account: Option = None, + fromAsset: Option = None, + toAsset: Option = None, +): Arb = arbitrary { + when (Arb.int(1..3).bind()) { + 1 -> income( + accountId = account, + asset = fromAsset, + ).bind() + + 2 -> expense( + accountId = account, + asset = fromAsset, + ).bind() + + 3 -> transfer( + fromAccount = account, + fromAsset = fromAsset, + toAsset = toAsset, + ).bind() + + else -> error("Arb.Companion.Transaction error - it's not your test but the test utils.") + } +} + +fun Arb.Companion.transactionId(): Arb = Arb.uuid().map(::TransactionId) + +fun Arb.Companion.assetCode(): Arb = Arb.notBlankTrimmedString().map { + AssetCode.unsafe(it.value) +} + +@Suppress("MagicNumber") +private suspend fun ArbitraryBuilderContext.arbInstant( + time: Option +): Instant { + // Because of legacy timezone conversions + val safeOffset = TimeUnit.SECONDS.toDays(365 * 4) + val safeMaxValue = Instant.MAX.minusSeconds(safeOffset) + val safeMinValue = Instant.MIN.plusSeconds(safeOffset) + return when (time) { + None -> Arb.instant( + minValue = safeMinValue, + maxValue = safeMaxValue + ).removeEdgecases().bind() + + is Some -> when (val arbTime = time.value) { + is ArbTime.Before -> Arb.instant( + minValue = safeMinValue, + maxValue = minOf(arbTime.before, safeMaxValue) + ).bind() + + is ArbTime.After -> Arb.instant( + minValue = maxOf(arbTime.after, safeMinValue), + maxValue = safeMaxValue + ).bind() + + is ArbTime.Exactly -> arbTime.time + } + } +} + +sealed interface ArbTime { + data class Before(val before: Instant) : ArbTime + data class After(val after: Instant) : ArbTime + data class Exactly(val time: Instant) : ArbTime +} \ No newline at end of file diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbUtils.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbUtils.kt new file mode 100644 index 0000000000..8553c78866 --- /dev/null +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ArbUtils.kt @@ -0,0 +1,47 @@ +package com.ivy.data.model.testing + +import com.ivy.data.model.primitive.ColorInt +import com.ivy.data.model.primitive.IconAsset +import com.ivy.data.model.primitive.NotBlankTrimmedString +import com.ivy.data.model.primitive.PositiveDouble +import io.kotest.property.Arb +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.filter +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.positiveDouble +import io.kotest.property.arbitrary.string + +fun Arb.Companion.notBlankTrimmedString(): Arb = Arb.string() + .filter { it.trim().isNotBlank() } + .map(NotBlankTrimmedString::unsafe) + +fun Arb.Companion.maybe(arb: Arb): Arb = arbitrary { + val shouldTake = Arb.boolean().bind() + if (shouldTake) { + arb.bind() + } else { + null + } +} + +fun Arb.Companion.or(a: Arb, b: Arb): Arb = arbitrary { + if (Arb.boolean().bind()) { + a.bind() + } else { + b.bind() + } +} + +fun Arb.Companion.ofValue(a: A): Arb = arbitrary { a } + +fun Arb.Companion.positiveDoubleExact(): Arb = Arb.positiveDouble() + .filter { it.isFinite() } + .map { PositiveDouble.unsafe(it) } + +fun Arb.Companion.colorInt(): Arb = Arb.int().map(::ColorInt) + +fun Arb.Companion.iconAsset(): Arb = Arb.notBlankTrimmedString().map { + IconAsset.unsafe(it.value.filter { c -> !c.isWhitespace() }) +} \ No newline at end of file diff --git a/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ModelFixtures.kt b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ModelFixtures.kt new file mode 100644 index 0000000000..592a89f812 --- /dev/null +++ b/shared/data/model-testing/src/main/java/com/ivy/data/model/testing/ModelFixtures.kt @@ -0,0 +1,12 @@ +package com.ivy.data.model.testing + +import com.ivy.data.model.AccountId +import com.ivy.data.model.CategoryId +import com.ivy.data.model.TransactionId +import java.util.UUID + +object ModelFixtures { + val AccountId = AccountId(UUID.randomUUID()) + val CategoryId = CategoryId(UUID.randomUUID()) + val TransactionId = TransactionId(UUID.randomUUID()) +} diff --git a/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt new file mode 100644 index 0000000000..dc5ff36d33 --- /dev/null +++ b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbTransactionTest.kt @@ -0,0 +1,168 @@ +package com.ivy.data.model.testing + +import arrow.core.Some +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.ivy.data.model.AccountId +import com.ivy.data.model.common.Value +import com.ivy.data.model.primitive.AssetCode +import com.ivy.data.model.primitive.PositiveDouble +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.checkAll +import io.kotest.property.forAll +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import java.time.Instant +import java.util.UUID + +@RunWith(TestParameterInjector::class) +class ArbTransactionTest { + @Test + fun `generates arb income`() = runTest { + forAll(Arb.income()) { + true + } + } + + @Test + fun `arb income respects passed params`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { + // given + val transactionId = ModelFixtures.TransactionId + val accountId = ModelFixtures.AccountId + val categoryId = ModelFixtures.CategoryId + val now = Instant.now() + val amount = PositiveDouble.unsafe(42.37) + val asset = AssetCode.USD + + // when + checkAll( + Arb.income( + accountId = Some(accountId), + categoryId = Some(categoryId), + settled = Some(settled), + time = Some(ArbTime.Exactly(now)), + removed = Some(removed), + amount = Some(amount), + asset = Some(asset), + id = Some(transactionId), + ) + ) { income -> + // then + income.id shouldBe transactionId + income.account shouldBe accountId + income.category shouldBe categoryId + income.settled shouldBe settled + income.removed shouldBe removed + income.time shouldBe now + income.value shouldBe Value(amount, asset) + } + } + + @Test + fun `generates arb expense`() = runTest { + forAll(Arb.expense()) { + true + } + } + + @Test + fun `arb expense respects passed params`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { + // given + val transactionId = ModelFixtures.TransactionId + val accountId = ModelFixtures.AccountId + val categoryId = ModelFixtures.CategoryId + val now = Instant.now() + val amount = PositiveDouble.unsafe(42.37) + val asset = AssetCode.USD + + // when + checkAll( + Arb.expense( + accountId = Some(accountId), + categoryId = Some(categoryId), + settled = Some(settled), + time = Some(ArbTime.Exactly(now)), + removed = Some(removed), + amount = Some(amount), + asset = Some(asset), + id = Some(transactionId), + ) + ) { expense -> + // then + expense.id shouldBe transactionId + expense.account shouldBe accountId + expense.category shouldBe categoryId + expense.settled shouldBe settled + expense.removed shouldBe removed + expense.time shouldBe now + expense.value shouldBe Value(amount, asset) + } + } + + @Test + fun `generates arb transfer`() = runTest { + forAll(Arb.transfer()) { + true + } + } + + @Test + fun `arb transfer respects passed params`( + @TestParameter settled: Boolean, + @TestParameter removed: Boolean, + ) = runTest { + // given + val transactionId = ModelFixtures.TransactionId + val categoryId = ModelFixtures.CategoryId + val now = Instant.now() + val fromAccount = AccountId(UUID.randomUUID()) + val fromAmount = PositiveDouble.unsafe(42.37) + val fromAsset = AssetCode.USD + val toAccount = AccountId(UUID.randomUUID()) + val toAmount = PositiveDouble.unsafe(3.14) + val toAsset = AssetCode.EUR + + // when + checkAll( + Arb.transfer( + categoryId = Some(categoryId), + settled = Some(settled), + time = Some(ArbTime.Exactly(now)), + removed = Some(removed), + id = Some(transactionId), + fromAccount = Some(fromAccount), + fromAmount = Some(fromAmount), + fromAsset = Some(fromAsset), + toAccount = Some(toAccount), + toAmount = Some(toAmount), + toAsset = Some(toAsset) + ) + ) { transfer -> + // then + transfer.id shouldBe transactionId + transfer.category shouldBe categoryId + transfer.settled shouldBe settled + transfer.removed shouldBe removed + transfer.time shouldBe now + transfer.fromAccount shouldBe fromAccount + transfer.fromValue shouldBe Value(fromAmount, fromAsset) + transfer.toAccount shouldBe toAccount + transfer.toValue shouldBe Value(toAmount, toAsset) + } + } + + @Test + fun `generates arb transaction`() = runTest { + forAll(Arb.transfer()) { + true + } + } +} \ No newline at end of file diff --git a/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbUtilTest.kt b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbUtilTest.kt new file mode 100644 index 0000000000..0adf038606 --- /dev/null +++ b/shared/data/model-testing/src/test/java/com/ivy/data/model/testing/ArbUtilTest.kt @@ -0,0 +1,54 @@ +package com.ivy.data.model.testing + +import com.ivy.data.model.primitive.IconAsset +import com.ivy.data.model.primitive.NotBlankTrimmedString +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.int +import io.kotest.property.forAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ArbUtilTest { + @Test + fun `generates arb NotBlankTrimmedStrings`() = runTest { + forAll(Arb.notBlankTrimmedString()) { + NotBlankTrimmedString.from(it.value).isRight() + } + } + + @Test + fun `generates arb PositiveDoubles`() = runTest { + forAll(Arb.positiveDoubleExact()) { + it.value >= 0.0 && it.value.isFinite() + } + } + + @Test + fun `arb maybe handles both cases`() = runTest { + // given + var nonNullCaseGenerated = false + var nullCaseGenerated = false + + // when + forAll(Arb.maybe(Arb.int())) { + if (it != null) { + nonNullCaseGenerated = true + } else { + nullCaseGenerated = true + } + true + } + + // then + nonNullCaseGenerated shouldBe true + nullCaseGenerated shouldBe true + } + + @Test + fun `generates arb IconAsset`() = runTest { + forAll(Arb.iconAsset()) { + IconAsset.from(it.id).isRight() + } + } +} \ No newline at end of file diff --git a/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt b/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt index f7fe2b9c4c..e471b36e18 100644 --- a/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt +++ b/shared/data/model/src/main/kotlin/com/ivy/data/model/Transaction.kt @@ -19,6 +19,8 @@ sealed interface Transaction : Syncable { val time: Instant val settled: Boolean val metadata: TransactionMetadata + + // TODO: Get rid of Tags from the core model because of perf. and complexity val tags: List } @@ -76,4 +78,22 @@ data class TransactionMetadata( val loanId: UUID? = null, // This refers to the loan record id that is linked with a transaction val loanRecordId: UUID?, -) \ No newline at end of file +) + +fun Transaction.getFromValue(): Value = when (this) { + is Expense -> value + is Income -> value + is Transfer -> fromValue +} + +fun Transaction.getFromAccount(): AccountId = when (this) { + is Expense -> account + is Income -> account + is Transfer -> fromAccount +} + +fun Transaction.getToAccount(): AccountId? = when (this) { + is Expense -> null + is Income -> null + is Transfer -> toAccount +} diff --git a/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt b/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt new file mode 100644 index 0000000000..827852180b --- /dev/null +++ b/shared/data/model/src/test/java/com/ivy/data/model/primitive/TransactionTest.kt @@ -0,0 +1,203 @@ +package com.ivy.data.model.primitive + +import com.ivy.data.model.AccountId +import com.ivy.data.model.Expense +import com.ivy.data.model.Income +import com.ivy.data.model.TransactionId +import com.ivy.data.model.TransactionMetadata +import com.ivy.data.model.Transfer +import com.ivy.data.model.common.Value +import com.ivy.data.model.getFromAccount +import com.ivy.data.model.getFromValue +import com.ivy.data.model.getToAccount +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.junit.Test +import java.time.Instant +import java.util.UUID + +class TransactionTest { + @Test + fun `getFromAccount - income`() { + // given + val trn = Income + + // when + val accountId = trn.getFromAccount() + + // then + accountId shouldBe AccountId + } + + @Test + fun `getFromAccount - expense`() { + // given + val trn = Expense + + // when + val accountId = trn.getFromAccount() + + // then + accountId.shouldNotBeNull() shouldBe AccountId + } + + @Test + fun `getFromAccount - transfer`() { + // given + val trn = Transfer + + // when + val accountId = trn.getFromAccount() + + // then + accountId shouldBe AccountId + } + + @Test + fun `getToAccount - income`() { + // given + val trn = Income + + // when + val accountId = trn.getToAccount() + + // then + accountId shouldBe null + } + + @Test + fun `getToAccount - expense`() { + // given + val trn = Expense + + // when + val accountId = trn.getToAccount() + + // then + accountId shouldBe null + } + + @Test + fun `getToAccount - transfer`() { + // given + val trn = Transfer + + // when + val accountId = trn.getToAccount() + + // then + accountId shouldBe ToAccountId + } + + @Test + fun `getFromValue - income`() { + // given + val trn = Income + + // when + val value = trn.getFromValue() + + // then + value shouldBe Income.value + } + + @Test + fun `getFromValue - expense`() { + // given + val trn = Expense + + // when + val value = trn.getFromValue() + + // then + value shouldBe Expense.value + } + + @Test + fun `getFromValue - transfer`() { + // given + val trn = Transfer + + // when + val value = trn.getFromValue() + + // then + value shouldBe Transfer.fromValue + } + + companion object { + val AccountId = AccountId(UUID.randomUUID()) + val ToAccountId = AccountId(UUID.randomUUID()) + + val Expense = Expense( + id = TransactionId(UUID.randomUUID()), + title = null, + description = null, + category = null, + time = Instant.EPOCH, + settled = false, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = false, + tags = listOf(), + value = Value( + amount = PositiveDouble.unsafe(1.0), + asset = AssetCode.EUR + ), + account = AccountId, + ) + + val Income = Income( + id = TransactionId(UUID.randomUUID()), + title = null, + description = null, + category = null, + time = Instant.EPOCH, + settled = false, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = false, + tags = listOf(), + value = Value( + amount = PositiveDouble.unsafe(1.0), + asset = AssetCode.EUR + ), + account = AccountId, + ) + + val Transfer = Transfer( + id = TransactionId(UUID.randomUUID()), + title = null, + description = null, + category = null, + time = Instant.EPOCH, + settled = false, + metadata = TransactionMetadata( + recurringRuleId = null, + loanId = null, + loanRecordId = null + ), + lastUpdated = Instant.EPOCH, + removed = false, + tags = listOf(), + fromAccount = AccountId, + fromValue = Value( + amount = PositiveDouble.unsafe(1.0), + asset = AssetCode.EUR + ), + toValue = Value( + amount = PositiveDouble.unsafe(1.0), + asset = AssetCode.EUR + ), + toAccount = ToAccountId, + ) + } +} \ No newline at end of file diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/AccountExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/AccountExt.kt index 5450151869..2332086294 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/AccountExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/AccountExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.AccountEntity import com.ivy.legacy.datamodel.Account -fun AccountEntity.toDomain(): Account = Account( +fun AccountEntity.toLegacyDomain(): Account = Account( name = name, currency = currency, color = color, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/BudgetExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/BudgetExt.kt index cb2fae5453..90aa4f169a 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/BudgetExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/BudgetExt.kt @@ -4,7 +4,7 @@ import com.ivy.data.db.entity.BudgetEntity import com.ivy.legacy.datamodel.Budget import java.util.UUID -fun BudgetEntity.toDomain(): Budget = Budget( +fun BudgetEntity.toLegacyDomain(): Budget = Budget( name = name, amount = amount, categoryIdsSerialized = categoryIdsSerialized, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/CategoryExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/CategoryExt.kt index 02d4dfc253..7f81e8694b 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/CategoryExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/CategoryExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.CategoryEntity import com.ivy.legacy.datamodel.Category -fun CategoryEntity.toDomain(): Category = Category( +fun CategoryEntity.toLegacyDomain(): Category = Category( name = name, color = color, icon = icon, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/ExchangeRateExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/ExchangeRateExt.kt index f013581cb1..93d4e74e81 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/ExchangeRateExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/ExchangeRateExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.ExchangeRateEntity import com.ivy.legacy.datamodel.ExchangeRate -fun ExchangeRateEntity.toDomain(): ExchangeRate = ExchangeRate( +fun ExchangeRateEntity.toLegacyDomain(): ExchangeRate = ExchangeRate( baseCurrency = baseCurrency, currency = currency, rate = rate diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanExt.kt index 57f32bba43..2757139601 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanExt.kt @@ -4,7 +4,7 @@ import com.ivy.data.db.entity.LoanEntity import com.ivy.data.model.LoanType import com.ivy.legacy.datamodel.Loan -fun LoanEntity.toDomain(): Loan = Loan( +fun LoanEntity.toLegacyDomain(): Loan = Loan( name = name, amount = amount, type = type, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt index a4925e64b7..4cb3045bf0 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/LoanRecordExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.LoanRecordEntity import com.ivy.legacy.datamodel.LoanRecord -fun LoanRecordEntity.toDomain(): LoanRecord = LoanRecord( +fun LoanRecordEntity.toLegacyDomain(): LoanRecord = LoanRecord( loanId = loanId, amount = amount, note = note, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/PlannedPaymentRuleExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/PlannedPaymentRuleExt.kt index fa44182c01..3fa410cf2a 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/PlannedPaymentRuleExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/PlannedPaymentRuleExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.PlannedPaymentRuleEntity import com.ivy.legacy.datamodel.PlannedPaymentRule -fun PlannedPaymentRuleEntity.toDomain(): PlannedPaymentRule = PlannedPaymentRule( +fun PlannedPaymentRuleEntity.toLegacyDomain(): PlannedPaymentRule = PlannedPaymentRule( startDate = startDate, intervalN = intervalN, intervalType = intervalType, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/SettingsExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/SettingsExt.kt index 4312864780..c3973194ee 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/SettingsExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/SettingsExt.kt @@ -3,7 +3,7 @@ package com.ivy.legacy.datamodel.temp import com.ivy.data.db.entity.SettingsEntity import com.ivy.legacy.datamodel.Settings -fun SettingsEntity.toDomain(): Settings = Settings( +fun SettingsEntity.toLegacyDomain(): Settings = Settings( theme = theme, baseCurrency = currency, bufferAmount = bufferAmount.toBigDecimal(), diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/TransactionExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/TransactionExt.kt index c7a5439020..7c0a544e02 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/TransactionExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/datamodel/temp/TransactionExt.kt @@ -1,14 +1,22 @@ package com.ivy.legacy.datamodel.temp import com.ivy.base.legacy.LegacyTag -import com.ivy.base.legacy.Transaction +import com.ivy.base.legacy.LegacyTransaction import com.ivy.data.db.entity.TransactionEntity import com.ivy.data.model.Tag +import com.ivy.data.model.Transaction +import com.ivy.data.repository.mapper.TransactionMapper import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -fun TransactionEntity.toDomain(tags: ImmutableList = persistentListOf()): Transaction = Transaction( +fun Transaction.toLegacy(mapper: TransactionMapper): LegacyTransaction { + return with(mapper) { toEntity().toLegacyDomain() } +} + +fun TransactionEntity.toLegacyDomain( + tags: ImmutableList = persistentListOf() +): LegacyTransaction = LegacyTransaction( accountId = accountId, type = type, amount = amount.toBigDecimal(), @@ -26,6 +34,7 @@ fun TransactionEntity.toDomain(tags: ImmutableList = persistentListOf id = id, tags = tags ) + fun Tag.toLegacyTag(): LegacyTag = LegacyTag(this.id.value, this.name.value) fun List.toImmutableLegacyTags(): ImmutableList = this.map { it.toLegacyTag() }.toImmutableList() diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountByIdAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountByIdAct.kt index 00f0f659b0..a67defc656 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountByIdAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountByIdAct.kt @@ -4,7 +4,7 @@ import com.ivy.data.db.dao.read.AccountDao import com.ivy.frp.action.FPAction import com.ivy.frp.then import com.ivy.legacy.datamodel.Account -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -15,6 +15,6 @@ class AccountByIdAct @Inject constructor( override suspend fun UUID.compose(): suspend () -> Account? = suspend { this // accountId } then accountDao::findById then { - it?.toDomain() + it?.toLegacyDomain() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountsAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountsAct.kt index c68ac6356a..6163b451cf 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountsAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/account/AccountsAct.kt @@ -3,7 +3,7 @@ package com.ivy.wallet.domain.action.account import com.ivy.data.db.dao.read.AccountDao import com.ivy.frp.action.FPAction import com.ivy.legacy.datamodel.Account -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -13,6 +13,6 @@ class AccountsAct @Inject constructor( ) : FPAction>() { override suspend fun Unit.compose(): suspend () -> ImmutableList = suspend { - io { accountDao.findAll().map { it.toDomain() }.toImmutableList() } + io { accountDao.findAll().map { it.toLegacyDomain() }.toImmutableList() } } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/budget/BudgetsAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/budget/BudgetsAct.kt index 14c5a978fb..e34f53bec8 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/budget/BudgetsAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/budget/BudgetsAct.kt @@ -5,7 +5,7 @@ import com.ivy.frp.action.FPAction import com.ivy.frp.action.thenMap import com.ivy.frp.then import com.ivy.legacy.datamodel.Budget -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -15,5 +15,5 @@ class BudgetsAct @Inject constructor( ) : FPAction>() { override suspend fun Unit.compose(): suspend () -> ImmutableList = suspend { budgetDao.findAll() - } thenMap { it.toDomain() } then { it.toImmutableList() } + } thenMap { it.toLegacyDomain() } then { it.toImmutableList() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoriesAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoriesAct.kt index 7d64b7b527..eb2e0dd852 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoriesAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoriesAct.kt @@ -5,7 +5,7 @@ import com.ivy.frp.action.FPAction import com.ivy.frp.action.thenMap import com.ivy.frp.then import com.ivy.legacy.datamodel.Category -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -17,5 +17,5 @@ class CategoriesAct @Inject constructor( io { categoryDao.findAll() } - } thenMap { it.toDomain() } then { it.toImmutableList() } + } thenMap { it.toLegacyDomain() } then { it.toImmutableList() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryByIdAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryByIdAct.kt index e461c11a64..32f1067b1f 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryByIdAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryByIdAct.kt @@ -3,7 +3,7 @@ package com.ivy.wallet.domain.action.category import com.ivy.data.db.dao.read.CategoryDao import com.ivy.frp.action.FPAction import com.ivy.legacy.datamodel.Category -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -11,6 +11,6 @@ class CategoryByIdAct @Inject constructor( private val categoryDao: CategoryDao ) : FPAction() { override suspend fun UUID.compose(): suspend () -> Category? = suspend { - categoryDao.findById(this)?.toDomain() + categoryDao.findById(this)?.toLegacyDomain() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryTrnsBetweenAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryTrnsBetweenAct.kt index bde9f2371f..7294f65a88 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryTrnsBetweenAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/category/CategoryTrnsBetweenAct.kt @@ -4,7 +4,7 @@ import com.ivy.base.legacy.Transaction import com.ivy.data.db.dao.read.TransactionDao import com.ivy.frp.action.FPAction import com.ivy.frp.action.thenMap -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.wallet.domain.pure.data.ClosedTimeRange import java.util.UUID import javax.inject.Inject @@ -21,7 +21,7 @@ class CategoryTrnsBetweenAct @Inject constructor( categoryId = categoryId ) } - } thenMap { it.toDomain() } + } thenMap { it.toLegacyDomain() } data class Input( val categoryId: UUID, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/exchange/ExchangeAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/exchange/ExchangeAct.kt index 4dc9541003..353ff02a8d 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/exchange/ExchangeAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/exchange/ExchangeAct.kt @@ -4,7 +4,7 @@ import arrow.core.Option import com.ivy.data.db.dao.read.ExchangeRatesDao import com.ivy.frp.action.FPAction import com.ivy.frp.then -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.wallet.domain.pure.exchange.ExchangeData import com.ivy.wallet.domain.pure.exchange.exchange import java.math.BigDecimal @@ -18,7 +18,7 @@ class ExchangeAct @Inject constructor( data = data, amount = amount, getExchangeRate = exchangeRatesDao::findByBaseCurrencyAndCurrency then { - it?.toDomain() + it?.toLegacyDomain() } ) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoanByIdAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoanByIdAct.kt index 2a5e956f96..be4205fedc 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoanByIdAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoanByIdAct.kt @@ -3,7 +3,7 @@ package com.ivy.wallet.domain.action.loan import com.ivy.data.db.dao.read.LoanDao import com.ivy.frp.action.FPAction import com.ivy.legacy.datamodel.Loan -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -11,6 +11,6 @@ class LoanByIdAct @Inject constructor( private val loanDao: LoanDao ) : FPAction() { override suspend fun UUID.compose(): suspend () -> Loan? = suspend { - loanDao.findById(this)?.toDomain() + loanDao.findById(this)?.toLegacyDomain() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoansAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoansAct.kt index fba3a84007..2053446f11 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoansAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/loan/LoansAct.kt @@ -5,7 +5,7 @@ import com.ivy.frp.action.FPAction import com.ivy.frp.action.thenMap import com.ivy.frp.then import com.ivy.legacy.datamodel.Loan -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -15,5 +15,5 @@ class LoansAct @Inject constructor( ) : FPAction>() { override suspend fun Unit.compose(): suspend () -> ImmutableList = suspend { loanDao.findAll() - } thenMap { it.toDomain() } then { it.toImmutableList() } + } thenMap { it.toLegacyDomain() } then { it.toImmutableList() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/settings/SettingsAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/settings/SettingsAct.kt index c8045cba00..34770928ef 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/settings/SettingsAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/settings/SettingsAct.kt @@ -5,7 +5,7 @@ import com.ivy.data.db.dao.read.SettingsDao import com.ivy.frp.action.FPAction import com.ivy.frp.then import com.ivy.legacy.datamodel.Settings -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import javax.inject.Inject class SettingsAct @Inject constructor( @@ -13,7 +13,7 @@ class SettingsAct @Inject constructor( ) : FPAction() { override suspend fun Unit.compose(): suspend () -> Settings = suspend { io { settingsDao.findFirst() } - } then { it.toDomain() } + } then { it.toLegacyDomain() } suspend fun getSettingsWithNextTheme(): Settings { val currentSettings = this(Unit) diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnByIdAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnByIdAct.kt index 1dd485ebae..7e7e40f5ba 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnByIdAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnByIdAct.kt @@ -4,7 +4,7 @@ import com.ivy.base.legacy.Transaction import com.ivy.data.db.dao.read.TransactionDao import com.ivy.frp.action.FPAction import com.ivy.frp.then -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -14,6 +14,6 @@ class TrnByIdAct @Inject constructor( override suspend fun UUID.compose(): suspend () -> Transaction? = suspend { this // transactionId } then transactionDao::findById then { - it?.toDomain() + it?.toLegacyDomain() } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithDateDivsAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithDateDivsAct.kt index 09600617e0..50ad8e838b 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithDateDivsAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithDateDivsAct.kt @@ -1,22 +1,26 @@ package com.ivy.wallet.domain.action.transaction +import com.ivy.base.TimeProvider import com.ivy.base.legacy.Transaction import com.ivy.base.legacy.TransactionHistoryItem import com.ivy.data.db.dao.read.AccountDao +import com.ivy.data.repository.AccountRepository import com.ivy.data.repository.TagsRepository import com.ivy.frp.action.FPAction import com.ivy.frp.then -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.pure.transaction.LegacyTrnDateDividers +import com.ivy.legacy.domain.pure.transaction.transactionsWithDateDividers import com.ivy.wallet.domain.action.exchange.ExchangeAct import com.ivy.wallet.domain.action.exchange.actInput -import com.ivy.legacy.domain.pure.transaction.transactionsWithDateDividers import javax.inject.Inject class TrnsWithDateDivsAct @Inject constructor( private val accountDao: AccountDao, private val exchangeAct: ExchangeAct, private val tagsRepository: TagsRepository, + private val accountRepository: AccountRepository, + private val timeProvider: TimeProvider, ) : FPAction>() { override suspend fun Input.compose(): suspend () -> List = suspend { @@ -24,8 +28,9 @@ class TrnsWithDateDivsAct @Inject constructor( transactions = transactions, baseCurrencyCode = baseCurrency, getTags = { tagIds -> tagsRepository.findByIds(tagIds) }, - - getAccount = accountDao::findById then { it?.toDomain() }, + getAccount = accountDao::findById then { it?.toLegacyDomain() }, + accountRepository = accountRepository, + timeProvider = timeProvider, exchange = ::actInput then exchangeAct ) } @@ -47,7 +52,7 @@ class LegacyTrnsWithDateDivsAct @Inject constructor( transactions = transactions, baseCurrencyCode = baseCurrency, - getAccount = accountDao::findById then { it?.toDomain() }, + getAccount = accountDao::findById then { it?.toLegacyDomain() }, exchange = ::actInput then exchangeAct ) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt index 699089d96b..b317f77c2c 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt @@ -4,7 +4,7 @@ import com.ivy.base.legacy.Transaction import com.ivy.data.db.dao.read.TransactionDao import com.ivy.frp.action.FPAction import com.ivy.frp.action.thenFilter -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -14,7 +14,7 @@ class TrnsWithRangeAndAccFiltersAct @Inject constructor( override suspend fun Input.compose(): suspend () -> List = suspend { transactionDao.findAllBetween(range.from(), range.to()) - .map { it.toDomain() } + .map { it.toLegacyDomain() } } thenFilter { accountIdFilterSet.contains(it.accountId) || accountIdFilterSet.contains(it.toAccountId) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt index f65634b5dc..e52aefaa83 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/PlannedPaymentsLogic.kt @@ -9,12 +9,11 @@ import com.ivy.data.db.dao.read.TransactionDao import com.ivy.data.db.dao.write.WritePlannedPaymentRuleDao import com.ivy.data.db.dao.write.WriteTransactionDao import com.ivy.data.model.IntervalType -import com.ivy.data.temp.migration.getAccount import com.ivy.data.temp.migration.settleNow import com.ivy.data.repository.TransactionRepository import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.PlannedPaymentRule -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.datamodel.toEntity import com.ivy.legacy.utils.ioThread import com.ivy.legacy.utils.timeNowUTC @@ -46,9 +45,9 @@ class PlannedPaymentsLogic @Inject constructor( endDate = range.to() ).sumOf { val amount = exchangeRatesLogic.amountBaseCurrency( - transaction = it.toDomain(), + transaction = it.toLegacyDomain(), baseCurrency = baseCurrency, - accounts = accounts.map { it.toDomain() } + accounts = accounts.map { it.toLegacyDomain() } ) when (it.type) { @@ -60,7 +59,7 @@ class PlannedPaymentsLogic @Inject constructor( } suspend fun oneTime(): List { - return plannedPaymentRuleDao.findAllByOneTime(oneTime = true).map { it.toDomain() } + return plannedPaymentRuleDao.findAllByOneTime(oneTime = true).map { it.toLegacyDomain() } } suspend fun oneTimeIncome(): Double { @@ -84,7 +83,7 @@ class PlannedPaymentsLogic @Inject constructor( } suspend fun recurring(): List = - plannedPaymentRuleDao.findAllByOneTime(oneTime = false).map { it.toDomain() } + plannedPaymentRuleDao.findAllByOneTime(oneTime = false).map { it.toLegacyDomain() } suspend fun recurringIncome(): Double { return recurring() @@ -106,7 +105,7 @@ class PlannedPaymentsLogic @Inject constructor( amountForMonthInBaseCurrency( plannedPayment = it, baseCurrency = baseCurrency, - accounts = accounts.map { it.toDomain() } + accounts = accounts.map { it.toLegacyDomain() } ) } } @@ -213,7 +212,7 @@ class PlannedPaymentsLogic @Inject constructor( if (skipTransaction) { transactionRepository.flagDeleted(paidTransaction.id) } else { - transactionRepository.save(paidTransaction.getAccount(), paidTransaction) + transactionRepository.save(paidTransaction) } if (plannedPaymentRule != null && plannedPaymentRule.oneTime) { @@ -255,7 +254,7 @@ class PlannedPaymentsLogic @Inject constructor( } } else { paidTransactions.forEach { paidTransaction -> - transactionRepository.save(paidTransaction.getAccount(), paidTransaction) + transactionRepository.save(paidTransaction) } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/SmartTitleSuggestionsLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/SmartTitleSuggestionsLogic.kt index 1b96f21023..35a7bd50ea 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/SmartTitleSuggestionsLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/SmartTitleSuggestionsLogic.kt @@ -2,7 +2,7 @@ package com.ivy.wallet.domain.deprecated.logic import com.ivy.base.legacy.Transaction import com.ivy.data.db.dao.read.TransactionDao -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.utils.capitalizeWords import com.ivy.legacy.utils.isNotNullOrBlank import java.util.* @@ -31,7 +31,7 @@ class SmartTitleSuggestionsLogic @Inject constructor( if (title != null && title.isNotEmpty()) { // suggest by title val suggestionsByTitle = transactionDao.findAllByTitleMatchingPattern("$title%") - .map { it.toDomain() } + .map { it.toLegacyDomain() } .extractUniqueTitles() .sortedByMostUsedFirst { transactionDao.countByTitleMatchingPattern("$it%") @@ -49,7 +49,7 @@ class SmartTitleSuggestionsLogic @Inject constructor( .findAllByCategory( categoryId = categoryId ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } // exclude already suggested suggestions so they're ordered by priority at the end .extractUniqueTitles(excludeSuggestions = suggestions) .sortedByMostUsedFirst { @@ -71,7 +71,7 @@ class SmartTitleSuggestionsLogic @Inject constructor( .findAllByAccount( accountId = accountId ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } // exclude already suggested suggestions so they're ordered by priority at the end .extractUniqueTitles(excludeSuggestions = suggestions) .sortedByMostUsedFirst { diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/WalletCategoryLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/WalletCategoryLogic.kt index 8566040497..6758669c26 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/WalletCategoryLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/WalletCategoryLogic.kt @@ -13,7 +13,7 @@ import com.ivy.legacy.data.model.filterOverdue import com.ivy.legacy.data.model.filterOverdueLegacy import com.ivy.legacy.data.model.filterUpcoming import com.ivy.legacy.data.model.filterUpcomingLegacy -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.pure.transaction.LegacyTrnDateDividers import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic import com.ivy.wallet.domain.deprecated.logic.currency.sumInBaseCurrency @@ -36,7 +36,7 @@ class WalletCategoryLogic @Inject constructor( transactions: List = emptyList() ): Double { val baseCurrency = settingsDao.findFirst().currency - val accounts = accountDao.findAll().map { it.toDomain() } + val accounts = accountDao.findAll().map { it.toLegacyDomain() } return historyByCategory( category, @@ -70,7 +70,7 @@ class WalletCategoryLogic @Inject constructor( type = TransactionType.INCOME, startDate = range.from(), endDate = range.to() - ).map { it.toDomain() } + ).map { it.toLegacyDomain() } .filter { accountFilterSet.isEmpty() || accountFilterSet.contains(it.accountId) } @@ -110,7 +110,7 @@ class WalletCategoryLogic @Inject constructor( ) .filter { accountFilterSet.isEmpty() || accountFilterSet.contains(it.accountId) - }.map { it.toDomain() } + }.map { it.toLegacyDomain() } .sumInBaseCurrency( exchangeRatesLogic = exchangeRatesLogic, settingsDao = settingsDao, @@ -143,7 +143,7 @@ class WalletCategoryLogic @Inject constructor( type = TransactionType.INCOME, startDate = range.from(), endDate = range.to() - ).map { it.toDomain() } + ).map { it.toLegacyDomain() } .sumInBaseCurrency( exchangeRatesLogic = exchangeRatesLogic, settingsDao = settingsDao, @@ -157,7 +157,7 @@ class WalletCategoryLogic @Inject constructor( type = TransactionType.EXPENSE, startDate = range.from(), endDate = range.to() - ).map { it.toDomain() } + ).map { it.toLegacyDomain() } .sumInBaseCurrency( exchangeRatesLogic = exchangeRatesLogic, settingsDao = settingsDao, @@ -196,7 +196,7 @@ class WalletCategoryLogic @Inject constructor( categoryId = category.id.value, startDate = range.from(), endDate = range.to() - ).map { it.toDomain() } + ).map { it.toLegacyDomain() } } return trans.filter { @@ -210,7 +210,7 @@ class WalletCategoryLogic @Inject constructor( .findAllUnspecifiedAndBetween( startDate = range.from(), endDate = range.to() - ).map { it.toDomain() } + ).map { it.toLegacyDomain() } .withDateDividers( exchangeRatesLogic = exchangeRatesLogic, settingsDao = settingsDao, @@ -275,7 +275,7 @@ class WalletCategoryLogic @Inject constructor( startDate = range.upcomingFrom(), endDate = range.to() ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } .filterUpcomingLegacy() } @@ -296,7 +296,7 @@ class WalletCategoryLogic @Inject constructor( startDate = range.upcomingFrom(), endDate = range.to() ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } .filterUpcomingLegacy() } @@ -365,7 +365,7 @@ class WalletCategoryLogic @Inject constructor( startDate = range.from(), endDate = range.overdueTo() ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } .filterOverdueLegacy() } @@ -387,7 +387,7 @@ class WalletCategoryLogic @Inject constructor( startDate = range.from(), endDate = range.overdueTo() ) - .map { it.toDomain() } + .map { it.toLegacyDomain() } .filterOverdueLegacy() } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt index 40478fd74a..2f89473006 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/csv/CSVImporter.kt @@ -20,7 +20,7 @@ import com.ivy.data.repository.CurrencyRepository import com.ivy.design.IVY_COLOR_PICKER_COLORS_FREE import com.ivy.design.l0_system.Green import com.ivy.design.l0_system.IvyDark -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.datamodel.toEntity import com.ivy.legacy.utils.convertLocalToUTC import com.ivy.legacy.utils.timeNowUTC @@ -92,7 +92,7 @@ class CSVImporter @Inject constructor( newCategoryColorIndex = 0 newAccountColorIndex = 0 - accounts = accountDao.findAll().map { it.toDomain() } + accounts = accountDao.findAll().map { it.toLegacyDomain() } val initialAccountsCount = accounts.size categories = categoryRepository.findAll() @@ -472,7 +472,7 @@ class CSVImporter @Inject constructor( val domainAccount = newAccount.toDomainAccount(currencyRepository).getOrNull() ?: return null accountRepository.save(domainAccount) - accounts = accountDao.findAll().map { it.toDomain() } + accounts = accountDao.findAll().map { it.toLegacyDomain() } return newAccount } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/currency/ExchangeRatesLogic.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/currency/ExchangeRatesLogic.kt index 20621a00d1..c0f5688d69 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/currency/ExchangeRatesLogic.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/currency/ExchangeRatesLogic.kt @@ -6,7 +6,7 @@ import com.ivy.data.db.dao.read.ExchangeRatesDao import com.ivy.data.db.dao.read.SettingsDao import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.PlannedPaymentRule -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import java.util.UUID import javax.inject.Inject @@ -130,7 +130,7 @@ suspend fun Iterable.sumInBaseCurrency( exchangeRatesLogic.amountBaseCurrency( transaction = it, baseCurrency = baseCurrency, - accounts = accounts.map { it.toDomain() } + accounts = accounts.map { it.toLegacyDomain() } ) } } @@ -148,7 +148,7 @@ suspend fun Iterable.sumByDoublePlannedInBaseCurrency( exchangeRatesLogic.amountBaseCurrency( plannedPayment = it, baseCurrency = baseCurrency, - accounts = accounts.map { it.toDomain() } + accounts = accounts.map { it.toLegacyDomain() } ) } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanMapper.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanMapper.kt index 5dd5baee1e..1e219a4a88 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanMapper.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanMapper.kt @@ -7,7 +7,7 @@ import com.ivy.data.model.LoanType import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Loan import com.ivy.legacy.datamodel.LoanRecord -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.deprecated.logic.loantrasactions.LoanTransactionsCore import com.ivy.legacy.utils.computationThread import com.ivy.legacy.utils.scopedIOThread @@ -66,7 +66,7 @@ class LTLoanMapper @Inject constructor( newLoanAccountId: UUID?, loanId: UUID ) { - val accounts = ltCore.fetchAccounts().map { it.toDomain() } + val accounts = ltCore.fetchAccounts().map { it.toLegacyDomain() } computationThread { if (oldLoanAccountId == newLoanAccountId || oldLoanAccountId.fetchAssociatedCurrencyCode( accounts @@ -112,7 +112,7 @@ class LTLoanMapper @Inject constructor( accountId = transaction.accountId ) - ltCore.saveLoan(modifiedLoan.toDomain()) + ltCore.saveLoan(modifiedLoan.toLegacyDomain()) } onBackgroundProcessingEnd() } @@ -124,7 +124,7 @@ class LTLoanMapper @Inject constructor( return scopedIOThread { scope -> val loanRecords = ltCore.fetchAllLoanRecords(loanId = loanId) - .map { it.toDomain() } + .map { it.toLegacyDomain() } .map { loanRecord -> scope.async { val convertedAmount: Double? = @@ -135,7 +135,7 @@ class LTLoanMapper @Inject constructor( newLoanRecordAccountID = loanRecord.accountId, newLoanRecordAmount = loanRecord.amount, loanAccountId = newAccountId, - accounts = ltCore.fetchAccounts().map { it.toDomain() }, + accounts = ltCore.fetchAccounts().map { it.toLegacyDomain() }, ) loanRecord.copy(convertedAmount = convertedAmount) } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanRecordMapper.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanRecordMapper.kt index 5164d79a06..f024ab3243 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanRecordMapper.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LTLoanRecordMapper.kt @@ -3,7 +3,7 @@ package com.ivy.wallet.domain.deprecated.logic.loantrasactions import com.ivy.base.legacy.Transaction import com.ivy.legacy.datamodel.Loan import com.ivy.legacy.datamodel.LoanRecord -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.domain.deprecated.logic.loantrasactions.LoanTransactionsCore import com.ivy.legacy.utils.computationThread import com.ivy.wallet.domain.deprecated.logic.model.CreateLoanRecordData @@ -82,7 +82,7 @@ class LTLoanRecordMapper @Inject constructor( newLoanRecordAccountID = transaction.accountId, newLoanRecordAmount = transaction.amount.toDouble(), loanAccountId = loan.accountId, - accounts = ltCore.fetchAccounts().map { it.toDomain() } + accounts = ltCore.fetchAccounts().map { it.toLegacyDomain() } ) val modifiedLoanRecord = loanRecord.copy( @@ -92,7 +92,7 @@ class LTLoanRecordMapper @Inject constructor( accountId = transaction.accountId, convertedAmount = convertedAmount ) - ltCore.saveLoanRecords(modifiedLoanRecord.toDomain()) + ltCore.saveLoanRecords(modifiedLoanRecord.toLegacyDomain()) } onBackgroundProcessingEnd() } @@ -110,7 +110,7 @@ class LTLoanRecordMapper @Inject constructor( newLoanRecordAccountID = newLoanRecord.accountId, newLoanRecordAmount = newLoanRecord.amount, loanAccountId = loanAccountId, - accounts = ltCore.fetchAccounts().map { it.toDomain() }, + accounts = ltCore.fetchAccounts().map { it.toLegacyDomain() }, reCalculateLoanAmount = reCalculateLoanAmount ) } @@ -126,7 +126,7 @@ class LTLoanRecordMapper @Inject constructor( newLoanRecordAccountID = data.account?.id, newLoanRecordAmount = data.amount, loanAccountId = loanAccountId, - accounts = ltCore.fetchAccounts().map { it.toDomain() }, + accounts = ltCore.fetchAccounts().map { it.toLegacyDomain() }, ) } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt index c915eedad7..e58795e118 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/deprecated/logic/loantrasactions/LoanTransactionsCore.kt @@ -24,7 +24,7 @@ import com.ivy.legacy.IvyWalletCtx import com.ivy.legacy.datamodel.Account import com.ivy.legacy.datamodel.Loan import com.ivy.legacy.datamodel.LoanRecord -import com.ivy.legacy.datamodel.temp.toDomain +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.datamodel.toEntity import com.ivy.legacy.utils.computationThread import com.ivy.legacy.utils.ioThread @@ -76,9 +76,9 @@ class LoanTransactionsCore @Inject constructor( val transactions: List = if (loanId != null) { transactionDao.findAllByLoanId(loanId = loanId) - .map { it.toDomain() } + .map { it.toLegacyDomain() } } else { - listOf(transactionDao.findLoanRecordTransaction(loanRecordId!!)).map { it?.toDomain() } + listOf(transactionDao.findLoanRecordTransaction(loanRecordId!!)).map { it?.toLegacyDomain() } } transactions.forEach { trans -> @@ -334,7 +334,7 @@ class LoanTransactionsCore @Inject constructor( suspend fun fetchLoanRecordTransaction(loanRecordId: UUID?): Transaction? { return loanRecordId?.let { ioThread { - transactionDao.findLoanRecordTransaction(it)?.toDomain() + transactionDao.findLoanRecordTransaction(it)?.toLegacyDomain() } } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/pure/transaction/TrnDateDividers.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/pure/transaction/TrnDateDividers.kt index d46e269edf..9f04d5ff1d 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/domain/pure/transaction/TrnDateDividers.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/domain/pure/transaction/TrnDateDividers.kt @@ -2,6 +2,7 @@ package com.ivy.legacy.domain.pure.transaction import arrow.core.Option import arrow.core.toOption +import com.ivy.base.TimeProvider import com.ivy.base.legacy.TransactionHistoryItem import com.ivy.base.time.convertToLocal import com.ivy.data.db.dao.read.AccountDao @@ -9,14 +10,15 @@ import com.ivy.data.db.dao.read.SettingsDao import com.ivy.data.model.Tag import com.ivy.data.model.Transaction import com.ivy.data.model.primitive.TagId +import com.ivy.data.repository.AccountRepository import com.ivy.data.repository.TagsRepository import com.ivy.data.repository.mapper.TransactionMapper import com.ivy.frp.Pure import com.ivy.frp.SideEffect import com.ivy.frp.then import com.ivy.legacy.datamodel.Account -import com.ivy.legacy.datamodel.temp.toDomain import com.ivy.legacy.datamodel.temp.toImmutableLegacyTags +import com.ivy.legacy.datamodel.temp.toLegacyDomain import com.ivy.legacy.utils.convertUTCtoLocal import com.ivy.legacy.utils.toEpochSeconds import com.ivy.wallet.domain.data.TransactionHistoryDateDivider @@ -37,13 +39,17 @@ suspend fun List.withDateDividers( exchangeRatesLogic: ExchangeRatesLogic, settingsDao: SettingsDao, accountDao: AccountDao, - tagsRepository: TagsRepository + tagsRepository: TagsRepository, + accountRepository: AccountRepository, + timeProvider: TimeProvider, ): List { return transactionsWithDateDividers( transactions = this, baseCurrencyCode = settingsDao.findFirst().currency, - getAccount = accountDao::findById then { it?.toDomain() }, + getAccount = accountDao::findById then { it?.toLegacyDomain() }, getTags = { tagsIds -> tagsRepository.findByIds(tagsIds) }, + accountRepository = accountRepository, + timeProvider = timeProvider, exchange = { data, amount -> exchangeRatesLogic.convertAmount( baseCurrency = data.baseCurrency, @@ -59,6 +65,8 @@ suspend fun List.withDateDividers( suspend fun transactionsWithDateDividers( transactions: List, baseCurrencyCode: String, + accountRepository: AccountRepository, + timeProvider: TimeProvider, @SideEffect getAccount: suspend (accountId: UUID) -> Account?, @@ -68,7 +76,7 @@ suspend fun transactionsWithDateDividers( getTags: suspend (tagIds: List) -> List = { emptyList() }, ): List { if (transactions.isEmpty()) return emptyList() - val transactionsMapper = TransactionMapper() + val transactionsMapper = TransactionMapper(accountRepository, timeProvider) return transactions .groupBy { it.time.convertToLocal().toLocalDate() } .filterKeys { it != null } @@ -87,7 +95,7 @@ suspend fun transactionsWithDateDividers( val legacyTransactionsForDate = with(transactionsMapper) { transactionsForDate.map { it.toEntity() - .toDomain(tags = getTags(it.tags).toImmutableLegacyTags()) + .toLegacyDomain(tags = getTags(it.tags).toImmutableLegacyTags()) } } listOf( @@ -119,7 +127,7 @@ object LegacyTrnDateDividers { return transactionsWithDateDividers( transactions = this, baseCurrencyCode = settingsDao.findFirst().currency, - getAccount = accountDao::findById then { it?.toDomain() }, + getAccount = accountDao::findById then { it?.toLegacyDomain() }, exchange = { data, amount -> exchangeRatesLogic.convertAmount( baseCurrency = data.baseCurrency,