Coverage Summary for Class: CreateTransferUseCase (com.georgeci.moneysurfer.domain.usecase)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| CreateTransferUseCase |
100%
(3/3)
|
100%
(6/6)
|
100%
(54/54)
|
100%
(284/284)
|
| CreateTransferUseCase$Companion |
|
| CreateTransferUseCase$ensureTransferCategory$1 |
|
| CreateTransferUseCase$invoke$1 |
|
| CreateTransferUseCase$Params |
100%
(1/1)
|
|
100%
(8/8)
|
100%
(37/37)
|
| Total |
100%
(4/4)
|
100%
(6/6)
|
100%
(62/62)
|
100%
(321/321)
|
package com.georgeci.moneysurfer.domain.usecase
import com.georgeci.moneysurfer.domain.model.Account
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.domain.model.Transaction
import com.georgeci.moneysurfer.domain.primitives.CategoryId
import com.georgeci.moneysurfer.domain.primitives.CategorySystemKind
import com.georgeci.moneysurfer.domain.primitives.CategoryType
import com.georgeci.moneysurfer.domain.primitives.Money
import com.georgeci.moneysurfer.domain.primitives.TransactionId
import com.georgeci.moneysurfer.domain.primitives.TransactionType
import com.georgeci.moneysurfer.domain.primitives.TransferId
import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
import com.georgeci.moneysurfer.domain.repositories.CategoryRepository
import kotlinx.coroutines.flow.first
import kotlinx.datetime.LocalDate
import org.koin.core.annotation.Single
import kotlin.time.Instant
@Single
class CreateTransferUseCase(
private val categoryRepository: CategoryRepository,
private val applyTransactionChange: ApplyTransactionChangeUseCase,
private val getCurrentTime: GetCurrentTimeUseCase,
) {
data class Params(
val from: Account,
val to: Account,
val fromMoney: Money,
val toMoney: Money,
val note: String,
val operationAt: Instant,
val operationDate: LocalDate,
)
suspend operator fun invoke(params: Params) {
require(params.from.id != params.to.id) { "Transfer source and destination must differ" }
val now = getCurrentTime()
val transferId = TransferId.uuid()
val transferCategoryId = ensureTransferCategory(params.from.workspaceId, now)
val outLeg = Transaction(
id = TransactionId.uuid(),
workspaceId = params.from.workspaceId,
accountId = params.from.id,
money = params.fromMoney.abs(),
currencyCode = params.from.currencyCode,
categoryId = transferCategoryId,
note = params.note,
operationAt = params.operationAt,
operationDate = params.operationDate,
type = TransactionType.EXPENSE,
createdAt = now,
updatedAt = now,
transferId = transferId,
)
val inLeg = Transaction(
id = TransactionId.uuid(),
workspaceId = params.to.workspaceId,
accountId = params.to.id,
money = params.toMoney.abs(),
currencyCode = params.to.currencyCode,
categoryId = transferCategoryId,
note = params.note,
operationAt = params.operationAt,
operationDate = params.operationDate,
type = TransactionType.INCOME,
createdAt = now,
updatedAt = now,
transferId = transferId,
)
applyTransactionChange(old = null, new = outLeg)
applyTransactionChange(old = null, new = inLeg)
}
/**
* Resolve the workspace's seeded "Transfer" system category, creating it lazily for
* workspaces that pre-date the seed. Without this, both legs would persist with a
* `null` categoryId and break the "auto-tag transfers" contract.
*/
private suspend fun ensureTransferCategory(
workspaceId: WorkspaceId,
now: Instant,
): CategoryId {
val existing = categoryRepository
.getByWorkspaceId(workspaceId)
.first()
.firstOrNull { it.systemKind == CategorySystemKind.TRANSFER }
if (existing != null) return existing.id
val seeded = Category(
id = CategoryId.uuid(),
workspaceId = workspaceId,
name = TRANSFER_CATEGORY_NAME,
type = CategoryType.TRANSFER,
parentId = null,
createdAt = now,
updatedAt = now,
systemKind = CategorySystemKind.TRANSFER,
)
categoryRepository.insert(seeded)
return seeded.id
}
private companion object {
const val TRANSFER_CATEGORY_NAME = "Transfer"
}
}