Coverage Summary for Class: DemoDataWriter (com.georgeci.moneysurfer.domain.debug)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| DemoDataWriter |
100%
(4/4)
|
100%
(2/2)
|
100%
(41/41)
|
100%
(538/538)
|
| DemoDataWriter$seedCategoriesIfMissing$1 |
|
| DemoDataWriter$snapshot$1 |
|
| DemoDataWriter$write$1 |
|
| Total |
100%
(4/4)
|
100%
(2/2)
|
100%
(41/41)
|
100%
(538/538)
|
package com.georgeci.moneysurfer.domain.debug
import com.georgeci.moneysurfer.domain.constants.DEFAULT_CATEGORY_SEEDS
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.domain.primitives.CategoryId
import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
import com.georgeci.moneysurfer.domain.repositories.AccountRepository
import com.georgeci.moneysurfer.domain.repositories.BudgetRepository
import com.georgeci.moneysurfer.domain.repositories.CategoryRepository
import com.georgeci.moneysurfer.domain.repositories.GoalContributionRepository
import com.georgeci.moneysurfer.domain.repositories.SavingsGoalRepository
import com.georgeci.moneysurfer.domain.usecase.ApplyTransactionChangeUseCase
import kotlinx.coroutines.flow.first
import org.koin.core.annotation.Single
import kotlin.time.Instant
/**
* What the workspace looked like before the run, plus the categories this run had to create to
* make it plannable at all — the one count the generator cannot report on itself.
*/
internal data class DemoDataStart(
val snapshot: DemoDataSnapshot,
val categoriesSeeded: Int,
)
/**
* The IO half of [DebugDataPrefiller]: reads what the workspace already holds, then writes the
* plan the generator produced.
*
* Every write goes through the ordinary repositories rather than a bulk path of its own — that is
* what puts the rows on the outbox, and it is the only reason prefilled data reaches Firestore for
* a signed-in tester. Transactions specifically go through [ApplyTransactionChangeUseCase], the
* single writer that keeps `Account.balance` a projection of the ledger; inserting them straight
* would leave every account showing its opening balance forever.
*/
@Single
internal class DemoDataWriter(
private val accountRepository: AccountRepository,
private val categoryRepository: CategoryRepository,
private val budgetRepository: BudgetRepository,
private val savingsGoalRepository: SavingsGoalRepository,
private val goalContributionRepository: GoalContributionRepository,
private val applyTransactionChange: ApplyTransactionChangeUseCase,
) {
/**
* State the generator plans against. Categories are seeded first when the workspace has none:
* a transaction with no category is legal but tells the tester nothing, and every screen that
* groups by category would come up empty.
*/
suspend fun snapshot(
workspaceId: WorkspaceId,
currency: CurrencyCode,
now: Instant,
): DemoDataStart {
val seeded = seedCategoriesIfMissing(workspaceId, now)
return DemoDataStart(
snapshot = DemoDataSnapshot(
workspaceId = workspaceId,
currency = currency,
accounts = accountRepository.getByWorkspaceId(workspaceId).first(),
categories = categoryRepository.getByWorkspaceId(workspaceId).first(),
budgetNames = budgetRepository.getByWorkspaceId(workspaceId).first().map { it.name }.toSet(),
goalTitles = savingsGoalRepository.getByWorkspaceId(workspaceId).first().map { it.title }.toSet(),
),
categoriesSeeded = seeded,
)
}
suspend fun write(plan: DemoDataPlan, categoriesSeeded: Int): DebugPrefillReport {
plan.accounts.forEach { accountRepository.insert(it) }
plan.transactions.forEach { applyTransactionChange(old = null, new = it) }
plan.budgets.forEach { budgetRepository.insert(it) }
plan.goals.forEach { savingsGoalRepository.insert(it) }
plan.contributions.forEach { goalContributionRepository.insert(it) }
return DebugPrefillReport(
accounts = plan.accounts.size,
categories = categoriesSeeded,
transactions = plan.transactions.size,
budgets = plan.budgets.size,
goals = plan.goals.size,
)
}
/**
* Normally a no-op: `CreateWorkspaceUseCase` seeds the same list when the workspace is created.
* It matters for a workspace pulled from a device that deleted its categories, and for the
* hand-built ones in tests.
*/
private suspend fun seedCategoriesIfMissing(workspaceId: WorkspaceId, now: Instant): Int {
if (categoryRepository.getByWorkspaceId(workspaceId).first().isNotEmpty()) return 0
DEFAULT_CATEGORY_SEEDS.forEach { seed ->
categoryRepository.insert(
Category(
id = CategoryId.uuid(),
workspaceId = workspaceId,
name = seed.name,
type = seed.type,
parentId = null,
createdAt = now,
systemKind = seed.systemKind,
),
)
}
return DEFAULT_CATEGORY_SEEDS.size
}
}