diff --git a/app/src/main/java/com/ivy/wallet/domain/action/category/CategoryIncomeWithAccountFiltersAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/category/CategoryIncomeWithAccountFiltersAct.kt index 23673cfdaa..6cb4af6afc 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/category/CategoryIncomeWithAccountFiltersAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/category/CategoryIncomeWithAccountFiltersAct.kt @@ -6,14 +6,14 @@ import com.ivy.wallet.domain.action.transaction.CalcTrnsIncomeExpenseAct import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.data.core.Transaction -import com.ivy.wallet.domain.pure.data.IncomeExpensePair +import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair import javax.inject.Inject class CategoryIncomeWithAccountFiltersAct @Inject constructor( - val calcTrnsIncomeExpenseAct: CalcTrnsIncomeExpenseAct -) : FPAction() { + private val calcTrnsIncomeExpenseAct: CalcTrnsIncomeExpenseAct +) : FPAction() { - override suspend fun Input.compose(): suspend () -> IncomeExpensePair = { + override suspend fun Input.compose(): suspend () -> IncomeExpenseTransferPair = { val accountFilterSet = accountFilterList.map { it.id }.toHashSet() transactions.filter { it.categoryId == category?.id diff --git a/app/src/main/java/com/ivy/wallet/domain/action/charts/PieChartAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/charts/PieChartAct.kt index 4a793da961..eef14e364c 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/charts/PieChartAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/charts/PieChartAct.kt @@ -1,16 +1,29 @@ package com.ivy.wallet.domain.action.charts +import androidx.compose.ui.graphics.toArgb +import com.ivy.fp.Pure +import com.ivy.fp.SideEffect import com.ivy.fp.action.FPAction import com.ivy.fp.action.then +import com.ivy.fp.action.thenFilter +import com.ivy.fp.action.thenMap +import com.ivy.wallet.R import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.category.CategoriesAct import com.ivy.wallet.domain.action.category.CategoryIncomeWithAccountFiltersAct import com.ivy.wallet.domain.action.transaction.CalcTrnsIncomeExpenseAct import com.ivy.wallet.domain.action.transaction.TrnsWithRangeAndAccFiltersAct import com.ivy.wallet.domain.data.TransactionType +import com.ivy.wallet.domain.data.core.Account +import com.ivy.wallet.domain.data.core.Category +import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.domain.pure.account.filterExcluded +import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair +import com.ivy.wallet.stringRes import com.ivy.wallet.ui.onboarding.model.FromToTimeRange import com.ivy.wallet.ui.statistic.level1.CategoryAmount +import com.ivy.wallet.ui.theme.RedLight +import java.math.BigDecimal import java.util.* import javax.inject.Inject @@ -21,17 +34,15 @@ class PieChartAct @Inject constructor( private val categoriesAct: CategoriesAct, private val categoryIncomeWithAccountFiltersAct: CategoryIncomeWithAccountFiltersAct ) : FPAction() { - override suspend fun Input.compose(): suspend () -> Output = suspend { - val allAccounts = accountsAct(Unit) - val accountsUsed = if (accountIdFilterList.isEmpty()) - allAccounts.let(::filterExcluded) - else - accountIdFilterList.mapNotNull { accID -> - allAccounts.find { it.id == accID } - } - val accountIdFilterSet = accountsUsed.map { it.id }.toHashSet() - Pair(accountsUsed, accountIdFilterSet) + private val accountTransfersCategory = + Category(stringRes(R.string.account_transfers), RedLight.toArgb(), "transfer") + + override suspend fun Input.compose(): suspend () -> Output = suspend { + getUsableAccounts( + accountIdFilterList = accountIdFilterList, + allAccounts = suspend { accountsAct(Unit) } + ) } then { val accountsUsed = it.first val accountIdFilterSet = it.second @@ -48,60 +59,182 @@ class PieChartAct @Inject constructor( val accountsUsed = it.first val transactions = it.second - val totalAmount = asyncIo { - val incomeExpensePair = calcTrnsIncomeExpenseAct( - CalcTrnsIncomeExpenseAct.Input( - transactions = transactions, - accounts = accountsUsed, + val incomeExpenseTransfer = calcTrnsIncomeExpenseAct( + CalcTrnsIncomeExpenseAct.Input( + transactions = transactions, + accounts = accountsUsed, + baseCurrency = baseCurrency + ) + ) + + val categoryAmounts = calculateCategoryAmounts( + type = type, + baseCurrency = baseCurrency, + allCategories = suspend { + categoriesAct(Unit).plus(null) //for unspecified + }, + transactions = suspend { transactions }, + accountsUsed = suspend { accountsUsed } + ) + + Pair(incomeExpenseTransfer, categoryAmounts) + } then { + + val totalAmount = calculateTotalAmount( + type = type, + treatTransferAsIncExp = treatTransferAsIncExp, + incomeExpenseTransfer = suspend { it.first } + ) + + val catAmountList = addAccountTransfersCategory( + treatTransferAsIncExp = treatTransferAsIncExp, + type = type, + incomeExpenseTransfer = suspend { it.first }, + accountTransfersCategory = accountTransfersCategory, + categoryAmounts = suspend { it.second } + ) + + Pair(totalAmount, catAmountList) + } then { + Output(it.first.toDouble(), it.second) + } + + @Pure + private suspend fun getUsableAccounts( + accountIdFilterList: List, + + @SideEffect + allAccounts: suspend () -> List + ): Pair, Set> { + + val accountsUsed = if (accountIdFilterList.isEmpty()) + allAccounts then ::filterExcluded + else + allAccounts thenFilter { + accountIdFilterList.contains(it.id) + } + + val accountsUsedIDSet = accountsUsed thenMap { it.id } then { it.toHashSet() } + + return Pair(accountsUsed(), accountsUsedIDSet()) + } + + @Pure + private suspend fun calculateCategoryAmounts( + type: TransactionType, + baseCurrency: String, + + @SideEffect + allCategories: suspend () -> List, + + @SideEffect + transactions: suspend () -> List, + + @SideEffect + accountsUsed: suspend () -> List, + ): List { + val trans = transactions() + val accUsed = accountsUsed() + + val catAmtList = allCategories thenMap { category -> + val catIncomeExpense = categoryIncomeWithAccountFiltersAct( + CategoryIncomeWithAccountFiltersAct.Input( + transactions = trans, + accountFilterList = accUsed, + category = category, baseCurrency = baseCurrency ) ) - when (type) { - TransactionType.INCOME -> incomeExpensePair.income.toDouble() - TransactionType.EXPENSE -> incomeExpensePair.expense.toDouble() - else -> error("not supported transactionType - $type") + CategoryAmount( + category = category, + amount = when (type) { + TransactionType.INCOME -> catIncomeExpense.income.toDouble() + TransactionType.EXPENSE -> catIncomeExpense.expense.toDouble() + else -> error("not supported transactionType - $type") + } + ) + } thenFilter { catAmt -> + catAmt.amount != 0.0 + } then { + it.sortedByDescending { ca -> ca.amount } + } + + return catAmtList() + } + + @Pure + private suspend fun calculateTotalAmount( + type: TransactionType, + treatTransferAsIncExp: Boolean, + + @SideEffect + incomeExpenseTransfer: suspend () -> IncomeExpenseTransferPair + ): BigDecimal { + val incExpQuad = incomeExpenseTransfer() + return when (type) { + TransactionType.INCOME -> { + incExpQuad.income + + if (treatTransferAsIncExp) + incExpQuad.transferIncome + else + BigDecimal.ZERO + } + TransactionType.EXPENSE -> { + incExpQuad.expense + + if (treatTransferAsIncExp) + incExpQuad.transferExpense + else + BigDecimal.ZERO } + else -> BigDecimal.ZERO } + } - val categoryAmounts = asyncIo { - val categories = categoriesAct(Unit) - categories - .plus(null) //for unspecified - .map { category -> - - val catIncomeExpense = categoryIncomeWithAccountFiltersAct( - CategoryIncomeWithAccountFiltersAct.Input( - transactions = transactions, - accountFilterList = accountsUsed, - category = category, - baseCurrency = baseCurrency - ) - ) + @Pure + private suspend fun addAccountTransfersCategory( + treatTransferAsIncExp: Boolean, + type: TransactionType, + accountTransfersCategory: Category, + + @SideEffect + incomeExpenseTransfer: suspend () -> IncomeExpenseTransferPair, + + @SideEffect + categoryAmounts: suspend () -> List + ): List { + + val incExpQuad = incomeExpenseTransfer() - CategoryAmount( - category = category, - amount = when (type) { - TransactionType.INCOME -> catIncomeExpense.income.toDouble() - TransactionType.EXPENSE -> catIncomeExpense.expense.toDouble() - else -> error("not supported transactionType - $type") - } + val catAmtList = + if (!treatTransferAsIncExp || incExpQuad.transferIncome == BigDecimal.ZERO && incExpQuad.transferExpense == BigDecimal.ZERO) + categoryAmounts then { it.sortedByDescending { ca -> ca.amount } } + else { + + val amt = if (type == TransactionType.INCOME) + incExpQuad.transferIncome.toDouble() + else + incExpQuad.transferExpense.toDouble() + + + categoryAmounts then { + it.plus( + CategoryAmount(accountTransfersCategory, amt) ) + } then { + it.sortedByDescending { ca -> ca.amount } } - .filter { catAmt -> - catAmt.amount != 0.0 - } - .sortedByDescending { it.amount } - } + } - Output(totalAmount = totalAmount.await(), categoryAmounts = categoryAmounts.await()) + return catAmtList() } data class Input( val baseCurrency: String, val range: FromToTimeRange, val type: TransactionType, - val accountIdFilterList: List + val accountIdFilterList: List, + val treatTransferAsIncExp: Boolean = false ) data class Output(val totalAmount: Double, val categoryAmounts: List) diff --git a/app/src/main/java/com/ivy/wallet/domain/action/transaction/CalcTrnsIncomeExpenseAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/transaction/CalcTrnsIncomeExpenseAct.kt index 002b9a1ad2..4b7d6d3979 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/transaction/CalcTrnsIncomeExpenseAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/transaction/CalcTrnsIncomeExpenseAct.kt @@ -8,20 +8,22 @@ import com.ivy.wallet.domain.action.exchange.ExchangeAct import com.ivy.wallet.domain.action.exchange.actInput import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Transaction -import com.ivy.wallet.domain.pure.data.IncomeExpensePair +import com.ivy.wallet.domain.pure.data.IncomeExpenseTransferPair import com.ivy.wallet.domain.pure.transaction.WalletValueFunctions import com.ivy.wallet.domain.pure.transaction.foldTransactionsSuspend import javax.inject.Inject class CalcTrnsIncomeExpenseAct @Inject constructor( private val exchangeAct: ExchangeAct -) : FPAction() { - override suspend fun Input.compose(): suspend () -> IncomeExpensePair = suspend { +) : FPAction() { + override suspend fun Input.compose(): suspend () -> IncomeExpenseTransferPair = suspend { foldTransactionsSuspend( transactions = transactions, valueFunctions = nonEmptyListOf( WalletValueFunctions::income, WalletValueFunctions::expense, + WalletValueFunctions::transferIncome, + WalletValueFunctions::transferExpenses ), arg = WalletValueFunctions.Argument( accounts = accounts, @@ -30,9 +32,11 @@ class CalcTrnsIncomeExpenseAct @Inject constructor( ) ) } then { values -> - IncomeExpensePair( + IncomeExpenseTransferPair( income = values[0], - expense = values[1] + expense = values[1], + transferIncome = values[2], + transferExpense = values[3] ) } diff --git a/app/src/main/java/com/ivy/wallet/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt index a39c5c306d..c67db5900c 100644 --- a/app/src/main/java/com/ivy/wallet/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt +++ b/app/src/main/java/com/ivy/wallet/domain/action/transaction/TrnsWithRangeAndAccFiltersAct.kt @@ -16,7 +16,7 @@ class TrnsWithRangeAndAccFiltersAct @Inject constructor( transactionDao.findAllBetween(range.from(), range.to()) .map { it.toDomain() } } thenFilter { - accountIdFilterSet.contains(it.accountId) + accountIdFilterSet.contains(it.accountId) || accountIdFilterSet.contains(it.toAccountId) } data class Input( diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/data/IncomeExpenseTransferPair.kt b/app/src/main/java/com/ivy/wallet/domain/pure/data/IncomeExpenseTransferPair.kt new file mode 100644 index 0000000000..2301ccfba6 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/domain/pure/data/IncomeExpenseTransferPair.kt @@ -0,0 +1,19 @@ +package com.ivy.wallet.domain.pure.data + +import java.math.BigDecimal + +data class IncomeExpenseTransferPair( + val income: BigDecimal, + val expense: BigDecimal, + val transferIncome: BigDecimal, + val transferExpense: BigDecimal +) { + companion object { + fun zero(): IncomeExpenseTransferPair = IncomeExpenseTransferPair( + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO + ) + } +} diff --git a/app/src/main/java/com/ivy/wallet/domain/pure/transaction/WalletValueFunctions.kt b/app/src/main/java/com/ivy/wallet/domain/pure/transaction/WalletValueFunctions.kt index 2af1d8b6e9..d046a0f04f 100644 --- a/app/src/main/java/com/ivy/wallet/domain/pure/transaction/WalletValueFunctions.kt +++ b/app/src/main/java/com/ivy/wallet/domain/pure/transaction/WalletValueFunctions.kt @@ -32,6 +32,22 @@ object WalletValueFunctions { } } + suspend fun transferIncome( + transaction: Transaction, + arg: Argument + ): BigDecimal = with(transaction) { + val condition = arg.accounts.any { it.id == this.toAccountId } + when { + type == TransactionType.TRANSFER && condition -> exchangeInBaseCurrency( + transaction = this.copy(amount = this.toAmount), + accounts = arg.accounts, + baseCurrency = arg.baseCurrency, + exchange = arg.exchange + ) + else -> BigDecimal.ZERO + } + } + suspend fun expense( transaction: Transaction, arg: Argument @@ -46,4 +62,20 @@ object WalletValueFunctions { else -> BigDecimal.ZERO } } + + suspend fun transferExpenses( + transaction: Transaction, + arg: Argument + ): BigDecimal = with(transaction) { + val condition = arg.accounts.any { it.id == this.accountId } + when { + type == TransactionType.TRANSFER && condition -> exchangeInBaseCurrency( + transaction = this, + accounts = arg.accounts, + baseCurrency = arg.baseCurrency, + exchange = arg.exchange + ) + else -> BigDecimal.ZERO + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt index 77491dbc6d..f7b4e05610 100644 --- a/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/statistic/level1/PieChartStatisticViewModel.kt @@ -11,6 +11,7 @@ import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.domain.deprecated.logic.currency.ExchangeRatesLogic import com.ivy.wallet.domain.deprecated.logic.currency.sumInBaseCurrency import com.ivy.wallet.domain.pure.data.WalletDAOs +import com.ivy.wallet.io.persistence.SharedPrefs import com.ivy.wallet.io.persistence.dao.CategoryDao import com.ivy.wallet.io.persistence.dao.SettingsDao import com.ivy.wallet.io.persistence.dao.TransactionDao @@ -41,7 +42,8 @@ class PieChartStatisticViewModel @Inject constructor( private val transactionDao: TransactionDao, private val exchangeRatesLogic: ExchangeRatesLogic, private val ivyContext: IvyWalletCtx, - private val pieChartAct: PieChartAct + private val pieChartAct: PieChartAct, + private val sharedPrefs: SharedPrefs ) : ViewModel() { private val _period = MutableStateFlow(ivyContext.selectedPeriod) val period = _period.readOnly() @@ -207,12 +209,19 @@ class PieChartStatisticViewModel @Inject constructor( val settings = ioThread { settingsDao.findFirst() } _baseCurrencyCode.value = settings.currency + val treatTransferAsIncExp = + sharedPrefs.getBoolean( + SharedPrefs.TRANSFERS_AS_INCOME_EXPENSE, + false + ) && accountIdFilterList.isNotEmpty() + val pieChartActOutput = pieChartAct( PieChartAct.Input( baseCurrency = _baseCurrencyCode.value, range = range, type = _type.value, - accountIdFilterList = accountIdFilterList + accountIdFilterList = accountIdFilterList, + treatTransferAsIncExp = treatTransferAsIncExp ) ) @@ -292,6 +301,6 @@ class PieChartStatisticViewModel @Inject constructor( } fun checkForUnspecifiedCategory(category: Category?): Boolean { - return category == null || category == transfersCategory + return category == null || category == transfersCategory || category.name == stringRes(R.string.account_transfers) } } \ No newline at end of file