Coverage Summary for Class: BalanceImpactKt (com.georgeci.moneysurfer.domain.model)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| BalanceImpactKt |
100%
(2/2)
|
100%
(23/23)
|
100%
(18/18)
|
100%
(130/130)
|
| BalanceImpactKt$WhenMappings |
|
| Total |
100%
(2/2)
|
100%
(23/23)
|
100%
(18/18)
|
100%
(130/130)
|
package com.georgeci.moneysurfer.domain.model
import com.georgeci.moneysurfer.domain.primitives.AccountId
import com.georgeci.moneysurfer.domain.primitives.Money
import com.georgeci.moneysurfer.domain.primitives.TransactionStatus
import com.georgeci.moneysurfer.domain.primitives.TransactionType
/**
* Effect of a transaction on account balances. Pure, no IO.
*
* Only ACTUAL transactions contribute. PLANNED rows return [emptyMap]; deleted rows
* are absent from the source data and never reach this function.
*
* Sign comes from [TransactionType]; we use [Money.abs] so callers passing a signed
* `money` (legacy convention where expense was a negative Money) still get the right
* impact. New code should write positive [Money] and rely on [type] for the sign.
*/
fun Transaction.balanceImpact(): Map<AccountId, Money> {
if (status != TransactionStatus.ACTUAL) return emptyMap()
val magnitude = money.abs()
val signed = when (type) {
TransactionType.INCOME -> magnitude
TransactionType.OPENING_BALANCE -> magnitude
TransactionType.EXPENSE -> -magnitude
}
return mapOf(accountId to signed)
}
/**
* Net delta to apply to account balances when a transaction transitions from [old] to [new].
*
* Both arguments are nullable:
* - `old == null && new != null` → create
* - `old != null && new != null` → update
* - `old != null && new == null` → delete (the row was hard-deleted locally)
* - `old == null && new == null` → no-op (idempotent at the boundary)
*
* The result drops zero entries so callers never apply a no-op increment.
*/
fun calculateBalanceDelta(
old: Transaction?,
new: Transaction?,
): Map<AccountId, Money> {
val a = old?.balanceImpact().orEmpty()
val b = new?.balanceImpact().orEmpty()
if (a.isEmpty() && b.isEmpty()) return emptyMap()
val keys = a.keys + b.keys
val result = mutableMapOf<AccountId, Money>()
for (key in keys) {
val before = a[key] ?: Money.zero()
val after = b[key] ?: Money.zero()
val diff = after - before
if (!diff.isZero()) result[key] = diff
}
return result
}