Coverage Summary for Class: TransactionCreationSplitEditingKt (com.georgeci.moneysurfer.feature.transaction.creation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| TransactionCreationSplitEditingKt |
100%
(1/1)
|
100%
(8/8)
|
95.8%
(23/24)
|
100%
(37/37)
|
99.5%
(440/442)
|
package com.georgeci.moneysurfer.feature.transaction.creation
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.domain.usecase.CreateSplitTransactionUseCase
/**
* Pure state transitions of the split editor and of the category chooser that feeds it.
*
* Kept beside the ViewModel rather than inside it: every one of these is a `Content → Content`
* function of its arguments alone, so they read (and test) as the small pieces of arithmetic they
* are, and the ViewModel keeps only the work that needs a repository, a side effect or the screen's
* key counter.
*/
/**
* Opens the split editor seeded with two lines — the minimum a split is — the first carrying
* whatever category the form already had, or closes it again, discarding the lines.
*
* Nothing about the receipt itself changes: the amount, account, note and date stay put, and the
* type segment keeps deciding whether the legs are expenses or income.
*/
internal fun TransactionCreationState.Content.withSplitToggled(
nextKey: () -> Int,
): TransactionCreationState.Content = if (isSplit) {
copy(splitLines = emptyList())
} else {
copy(
splitLines = listOf(
TransactionSplitLineUi(key = nextKey(), category = selectedCategory),
TransactionSplitLineUi(key = nextKey()),
),
)
}
/**
* Appends a line, keeping the figure the previous last line was showing.
*
* The trailing line renders the remainder and is read-only, so it never stores what it displays.
* Without this the line demoted by the new one would come back blank — the amount the user could
* see a moment ago would silently move to the new line and leave a zero leg behind that only
* disables Save, with nothing on screen pointing at it. An over-assigned remainder is not pinned:
* it is negative, the editor is already reporting that, and storing it would flip its sign.
*/
internal fun TransactionCreationState.Content.withSplitLineAdded(
key: Int,
): TransactionCreationState.Content {
val remainder = splitRemainder
val pinned = splitLines.mapIndexed { index, line ->
if (index == splitLines.lastIndex && remainder.isPositive()) {
line.copy(amount = remainder.toAmountInput())
} else {
line
}
}
return copy(splitLines = pinned + TransactionSplitLineUi(key = key))
}
/**
* Removes one line, and closes the editor when fewer than [MIN_SPLIT_LINES] are left: a single line
* is not a split, and leaving one behind would show an editor whose Save button silently refuses.
*/
internal fun TransactionCreationState.Content.withSplitLineRemoved(
key: Int,
): TransactionCreationState.Content {
val remaining = splitLines.filterNot { it.key == key }
return copy(splitLines = if (remaining.size < MIN_SPLIT_LINES) emptyList() else remaining)
}
internal fun TransactionCreationState.Content.withSplitLineAmount(
key: Int,
amount: String,
): TransactionCreationState.Content = copy(
splitLines = splitLines.map { line ->
if (line.key == key) line.copy(amount = amount) else line
},
)
/** The legs a save would write: each line's category with its share of the amount. */
internal fun TransactionCreationState.Content.splitLegs(): List<CreateSplitTransactionUseCase.Leg> =
splitLines.zip(splitAmounts) { line, money ->
CreateSplitTransactionUseCase.Leg(categoryId = line.category?.id, money = money)
}
/** What [slot] currently holds — the fallback when a picked category cannot be resolved. */
internal fun TransactionCreationState.Content.categoryInSlot(slot: CategorySlot): Category? =
when (slot) {
CategorySlot.Single -> selectedCategory
is CategorySlot.SplitLine -> splitLines.firstOrNull { it.key == slot.key }?.category
}
internal fun TransactionCreationState.Content.withPickedCategory(
categories: List<Category>,
selected: Category?,
slot: CategorySlot,
): TransactionCreationState.Content = when (slot) {
CategorySlot.Single -> withSelectedCategory(categories, selected)
// A split line's category is the line's alone: the grid's selection and the "most used"
// ordering both describe the single-category form, and moving them would leave the form
// pointing at whichever line the user edited last.
is CategorySlot.SplitLine -> copy(
categories = categories,
splitLines = splitLines.map { line ->
if (line.key == slot.key) line.copy(category = selected) else line
},
)
}
internal fun TransactionCreationState.Content.withSelectedCategory(
categories: List<Category>,
selected: Category?,
): TransactionCreationState.Content = copy(
categories = categories,
selectedCategory = selected,
displayCategories = buildDisplayCategories(
categories = categories,
counts = categoryUsageCounts,
type = type.categoryType(),
selected = selected,
),
)