Coverage Summary for Class: CreateSplitTransactionUseCase (com.georgeci.moneysurfer.domain.usecase)

Class Method, % Branch, % Line, % Instruction, %
CreateSplitTransactionUseCase 100% (2/2) 100% (6/6) 100% (29/29) 100% (155/155)
CreateSplitTransactionUseCase$invoke$1
CreateSplitTransactionUseCase$Leg 100% (1/1) 100% (1/1) 100% (12/12)
CreateSplitTransactionUseCase$Params 100% (1/1) 100% (9/9) 100% (51/51)
Total 100% (4/4) 100% (6/6) 100% (39/39) 100% (218/218)


 package com.georgeci.moneysurfer.domain.usecase
 
 import com.georgeci.moneysurfer.domain.model.Account
 import com.georgeci.moneysurfer.domain.model.Transaction
 import com.georgeci.moneysurfer.domain.primitives.CategoryId
 import com.georgeci.moneysurfer.domain.primitives.Money
 import com.georgeci.moneysurfer.domain.primitives.SplitId
 import com.georgeci.moneysurfer.domain.primitives.TransactionId
 import com.georgeci.moneysurfer.domain.primitives.TransactionStatus
 import com.georgeci.moneysurfer.domain.primitives.TransactionType
 import kotlinx.datetime.LocalDate
 import org.koin.core.annotation.Single
 import kotlin.time.Instant
 
 /** Fewer legs than this is not a split — it is an ordinary transaction, created the ordinary way. */
 private const val MIN_SPLIT_LEGS = 2
 
 /**
  * Writes one receipt as N sibling transactions sharing a [SplitId] — groceries and household
  * chemicals from the same supermarket run, each landing in its own category.
  *
  * Every leg is a complete transaction, so budgets, monthly totals, spend history and the account
  * balance need no split-awareness at all: a leg filed under "Household chemicals" is indistinguishable
  * from an ordinary transaction to all of them. That is the whole reason a split is sibling rows
  * rather than a child allocation table (the other half of the reason is sync: per-entity LWW cannot
  * keep a parent and its children consistent when they are pulled independently). The trade-offs are
  * written up in `docs/plans/split-transaction-across-categories.md`.
  *
  * The invariant this establishes is that the legs of a group differ **only** in category and
  * amount: account, currency, business date, moment and type are the receipt's, shared by all of
  * them. [UpdateTransactionUseCase] re-establishes it whenever a single leg is later edited through
  * the ordinary edit path, so a group cannot drift into two payments.
  */
 @Single
 class CreateSplitTransactionUseCase(
     private val applyTransactionChange: ApplyTransactionChangeUseCase,
     private val getCurrentTime: GetCurrentTimeUseCase,
 ) {
 
     /** One allocation of the receipt: where it is filed and how much of the payment it took. */
     data class Leg(val categoryId: CategoryId?, val money: Money)
 
     data class Params(
         val account: Account,
         val legs: List<Leg>,
         val note: String,
         val merchant: String = "",
         val tags: List<String> = emptyList(),
         val operationAt: Instant,
         val operationDate: LocalDate,
         val type: TransactionType,
         val status: TransactionStatus = TransactionStatus.ACTUAL,
     )
 
     /**
      * Returns the legs as written, oldest first — the caller can hand them straight to
      * [DeleteTransactionUseCase]'s Undo counterpart or assert on them in a test.
      *
      * Throws [IllegalArgumentException] on a group that could not be one receipt: fewer than two
      * legs, or a leg carrying no money. Both are programmer errors — the creation screen keeps its
      * Save button disabled until neither holds.
      */
     suspend operator fun invoke(params: Params): List<Transaction> {
         require(params.legs.size >= MIN_SPLIT_LEGS) {
             "A split needs at least $MIN_SPLIT_LEGS legs, got ${params.legs.size}"
         }
         require(params.legs.none { it.money.abs().isZero() }) { "A split leg must carry an amount" }
 
         val now = getCurrentTime()
         val splitId = SplitId.uuid()
         val rows = params.legs.map { leg ->
             Transaction(
                 id = TransactionId.uuid(),
                 workspaceId = params.account.workspaceId,
                 accountId = params.account.id,
                 money = leg.money.abs(),
                 currencyCode = params.account.currencyCode,
                 categoryId = leg.categoryId,
                 note = params.note,
                 merchant = params.merchant,
                 tags = params.tags,
                 operationAt = params.operationAt,
                 operationDate = params.operationDate,
                 type = params.type,
                 status = params.status,
                 createdAt = now,
                 updatedAt = now,
                 splitId = splitId,
             )
         }
         rows.forEach { applyTransactionChange(old = null, new = it) }
         return rows
     }
 }