Coverage Summary for Class: UpdateTransactionUseCase (com.georgeci.moneysurfer.domain.usecase)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| UpdateTransactionUseCase |
100%
(3/3)
|
100%
(6/6)
|
100%
(20/20)
|
100%
(189/189)
|
| UpdateTransactionUseCase$invoke$1 |
|
| UpdateTransactionUseCase$realignSplitSiblings$1 |
|
| Total |
100%
(3/3)
|
100%
(6/6)
|
100%
(20/20)
|
100%
(189/189)
|
package com.georgeci.moneysurfer.domain.usecase
import com.georgeci.moneysurfer.domain.model.Transaction
import com.georgeci.moneysurfer.domain.repositories.TransactionRepository
import org.koin.core.annotation.Single
@Single
class UpdateTransactionUseCase(
private val transactionRepository: TransactionRepository,
private val applyTransactionChange: ApplyTransactionChangeUseCase,
) {
/**
* Saves [transaction] over the stored row of the same id.
*
* The tombstone lookup is not belt-and-braces: a row can be deleted between the edit screen
* opening and the user pressing Save — by a pulled delete from another device, or by a swipe on
* a list still sitting in the back stack. Since issue #346 the deleted row keeps its id, so
* treating "no live row" as "create" would drive [ApplyTransactionChangeUseCase] into an insert
* that collides with the surviving primary key, failing the save *after* the balance had
* already moved.
*
* Restoring first is also the right answer for the user: they pressed Save on a row they were
* editing, and the edit is newer than the delete — the same reasoning by which a peer's newer
* edit lifts a tombstone on pull. When the id names nothing at all (already purged), `old` stays
* null and the insert branch is correct.
*/
suspend operator fun invoke(transaction: Transaction) {
val old = transactionRepository.getById(transaction.id)
?: applyTransactionChange.restore(transaction.id)
applyTransactionChange(old = old, new = transaction)
realignSplitSiblings(transaction)
}
/**
* Carries the fields a split's legs share onto the siblings of an edited leg.
*
* The legs of a split are one receipt, so they have to agree on account, currency, business
* date, moment and type — differ on any of those and the group stops describing a single
* payment, which is exactly what the collapsed list row and the details breakdown assume. The
* ordinary edit screen edits one leg and knows nothing about splits, so the invariant is
* re-established here rather than defended by rejecting the edit: refusing to move a receipt to
* the right date because it happens to be split would be a worse answer than moving every leg.
*
* Category and amount are deliberately not propagated — they are the whole point of the legs. A
* sibling already in step is left untouched, so the ordinary single-category edit still enqueues
* exactly one row for sync.
*/
private suspend fun realignSplitSiblings(edited: Transaction) {
val splitId = edited.splitId ?: return
transactionRepository.getBySplitId(splitId)
.filter { it.id != edited.id }
.forEach { sibling ->
val realigned = sibling.copy(
accountId = edited.accountId,
currencyCode = edited.currencyCode,
operationAt = edited.operationAt,
operationDate = edited.operationDate,
type = edited.type,
)
if (realigned != sibling) {
applyTransactionChange(old = sibling, new = realigned)
}
}
}
}