Skip to content

Commit

Permalink
[Domain] Build AccountBalanceUseCase (#3418)
Browse files Browse the repository at this point in the history
* Calculates the all balance in all assets that it have

* Create None Balance

* impl calculate method to return ExchangedAccountBalance

* create a BalanceBuilder

* improve the logic

* add NoneBalance

* drop unnecessary code

* return NoneBalance if the balance is empty

* refactor

* get new value from merge

* refactor

* rename methods params

* refactor

* renaming

* Add KDocs

* solve detekt issues

* refactor

* Create BalanceBuilderTest

* solve detekt issues

* unit test - when stats are empty

* unit test - process one deposit come from incomes

* unit test - process two deposits in different currencies come from incomes

* unit test - process two deposits in same currency come from incomes and transfersIn

* unit test - process two deposits in different currencies come from incomes and transfersIn

* unit test - process two deposits in different currencies come from incomes and one transfersIn

* improve the logic when the balance should be empty

* unit tests - refactor and add new cases

* improve the BalanceBuilder

* unit test - add two more cases
  • Loading branch information
rodrigoliveirac committed Aug 26, 2024
1 parent a116fff commit 2db1bdd
Show file tree
Hide file tree
Showing 3 changed files with 451 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.ivy.domain.usecase

import com.ivy.data.model.primitive.AssetCode
import com.ivy.data.model.primitive.NonZeroDouble
import com.ivy.data.model.primitive.PositiveDouble

class BalanceBuilder {

private val balance = mutableMapOf<AssetCode, NonZeroDouble>()

/**
* Updates `balance` with deposits from income and transfers in.
*
* @param incomes Deposit amounts from income sources.
* @param transfersIn Deposit amounts from transfer in sources.
*/
fun processDeposits(
incomes: Map<AssetCode, PositiveDouble>,
transfersIn: Map<AssetCode, PositiveDouble>,
) {
combine(incomes, transfersIn).forEach { (asset, amount) ->
NonZeroDouble
.from((balance[asset]?.value ?: 0.0) + amount.value)
.onRight { newValue ->
balance[asset] = newValue
}
}
}

/**
* Updates `balance` with withdrawals from expenses and transfers out.
*
* @param expenses Deposit amounts from expense sources.
* @param transfersOut Deposit amounts from transfer out sources.
*/
fun processWithdrawals(
expenses: Map<AssetCode, PositiveDouble>,
transfersOut: Map<AssetCode, PositiveDouble>,
) {
combine(expenses, transfersOut).forEach { (asset, amount) ->
val sub = (balance[asset]?.value ?: 0.0) - amount.value
NonZeroDouble
.from(sub)
.onLeft {
if (sub == 0.0) {
balance.remove(asset)
}
}
.onRight { newValue ->
balance[asset] = newValue
}
}
}

/**
* Combines two maps by summing their values. If the sum results in an error,
* the value from the `a` map is used.
*
* @return A map with combined values from the two input maps.
*/
private fun combine(
a: Map<AssetCode, PositiveDouble>,
b: Map<AssetCode, PositiveDouble>,
): Map<AssetCode, PositiveDouble> {
val c = a.toMutableMap()
b.forEach { (asset, amount) ->
c.merge(asset, amount) { b, c ->
PositiveDouble
.from(b.value + c.value)
.fold(
ifLeft = { c },
ifRight = { it }
)
}
}
return c
}

fun build(): Map<AssetCode, NonZeroDouble> = balance
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.ivy.domain.usecase.account

import arrow.core.None
import arrow.core.Option
import com.ivy.data.model.AccountId
import com.ivy.data.model.Value
import com.ivy.data.model.primitive.AssetCode
import com.ivy.data.model.primitive.NonZeroDouble
import com.ivy.data.repository.TransactionRepository
import com.ivy.domain.usecase.BalanceBuilder
import com.ivy.domain.usecase.exchange.ExchangeUseCase
import javax.inject.Inject

Expand All @@ -21,9 +23,18 @@ class AccountBalanceUseCase @Inject constructor(
*/
suspend fun calculate(
account: AccountId,
outCurrency: AssetCode
outCurrency: AssetCode,
): ExchangedAccountBalance {
TODO("Not implemented")
val balance = calculate(account)
return if (balance.isEmpty()) {
ExchangedAccountBalance.NoneBalance
} else {
val exchangeResult = exchangeUseCase.convert(values = balance, to = outCurrency)
ExchangedAccountBalance(
balance = exchangeResult.exchanged,
exchangeErrors = exchangeResult.exchangeErrors
)
}
}

/**
Expand All @@ -33,11 +44,33 @@ class AccountBalanceUseCase @Inject constructor(
suspend fun calculate(
account: AccountId,
): Map<AssetCode, NonZeroDouble> {
TODO("Not implemented")
val accountStats = accountStatsUseCase.calculate(
account = account,
transactions = transactionRepository.findAll()
)

return BalanceBuilder().run {
processDeposits(
incomes = accountStats.income.values,
transfersIn = accountStats.transfersIn.values
)
processWithdrawals(
expenses = accountStats.expense.values,
transfersOut = accountStats.transfersOut.values
)
build()
}
}
}

data class ExchangedAccountBalance(
val balance: Option<Value>,
val exchangeErrors: Set<AssetCode>
)
val exchangeErrors: Set<AssetCode>,
) {
companion object {
val NoneBalance = ExchangedAccountBalance(
balance = None,
exchangeErrors = emptySet()
)
}
}
Loading

0 comments on commit 2db1bdd

Please sign in to comment.