Coverage Summary for Class: RecordGoalContributionKt (com.georgeci.moneysurfer.domain.usecase)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| RecordGoalContributionKt |
100%
(2/2)
|
92.9%
(13/14)
|
100%
(24/24)
|
100%
(356/356)
|
| RecordGoalContributionKt$recordContribution$1 |
|
| Total |
100%
(2/2)
|
92.9%
(13/14)
|
100%
(24/24)
|
100%
(356/356)
|
package com.georgeci.moneysurfer.domain.usecase
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import com.georgeci.moneysurfer.domain.model.GoalContribution
import com.georgeci.moneysurfer.domain.model.GoalStatus
import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
import com.georgeci.moneysurfer.domain.primitives.GoalId
import com.georgeci.moneysurfer.domain.primitives.Money
import com.georgeci.moneysurfer.domain.repositories.GoalContributionRepository
import com.georgeci.moneysurfer.domain.repositories.SavingsGoalRepository
import kotlinx.datetime.LocalDate
/**
* The one write path for both [AddContributionUseCase] and [WithdrawFromGoalUseCase] —
* a withdrawal is a contribution with a negative amount (decision 3 in md/goals.md), so
* the invariants, the insert and the ACTIVE ↔ COMPLETED flip are checked in one place.
*/
@Suppress("LongParameterList")
internal suspend fun Raise<GoalActionError>.recordContribution(
goalRepository: SavingsGoalRepository,
contributionRepository: GoalContributionRepository,
clock: ClockUseCase,
goalId: GoalId,
signedAmount: Money,
occurredOn: LocalDate,
note: String,
): GoalContribution {
ensure(!signedAmount.isZero()) { GoalActionError.InvalidAmount }
val goal = goalRepository.getById(goalId) ?: raise(GoalActionError.GoalNotFound)
val saved = contributionRepository.savedAmount(goalId)
if (signedAmount.isPositive()) {
ensure(goal.status.acceptsContributions) { GoalActionError.GoalNotAcceptingContributions }
} else {
ensure(saved + signedAmount >= Money.zero()) { GoalActionError.WithdrawalBelowZero }
}
val now = clock.now()
val contribution = GoalContribution(
workspaceId = goal.workspaceId,
goalId = goalId,
amount = signedAmount,
occurredOn = occurredOn,
note = note,
createdAt = now,
)
Either.catch { contributionRepository.insert(contribution) }
.mapLeft { GoalActionError.LocalWriteFailed(it) }
.bind()
val resolved = goal.status.afterProgressChange((saved + signedAmount) >= goal.target)
if (resolved != goal.status) {
Either.catch { goalRepository.update(goal.copy(status = resolved)) }
.mapLeft { GoalActionError.LocalWriteFailed(it) }
.bind()
}
return contribution
}
/** PAUSED and ARCHIVED goals take no new money; a withdrawal is still allowed. */
internal val GoalStatus.acceptsContributions: Boolean
get() = this == GoalStatus.ACTIVE || this == GoalStatus.COMPLETED