Coverage Summary for Class: CategoryRepositoryImpl (com.georgeci.moneysurfer.data.repository)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| CategoryRepositoryImpl |
100%
(10/10)
|
67.9%
(19/28)
|
98.2%
(54/55)
|
96.1%
(490/510)
|
| CategoryRepositoryImpl$delete$1 |
|
| CategoryRepositoryImpl$getAll$$inlined$map$1 |
0%
(0/2)
|
|
| CategoryRepositoryImpl$getAll$$inlined$map$1$1 |
|
| CategoryRepositoryImpl$getAll$$inlined$map$1$2 |
0%
(0/1)
|
|
| CategoryRepositoryImpl$getAll$$inlined$map$1$2$1 |
|
| CategoryRepositoryImpl$getById$1 |
|
| CategoryRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1 |
0%
(0/2)
|
|
| CategoryRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$1 |
|
| CategoryRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2 |
0%
(0/1)
|
|
| CategoryRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2$1 |
|
| CategoryRepositoryImpl$insert$1 |
|
| CategoryRepositoryImpl$update$1 |
|
| Total |
62.5%
(10/16)
|
67.9%
(19/28)
|
98.2%
(54/55)
|
96.1%
(490/510)
|
package com.georgeci.moneysurfer.data.repository
import com.georgeci.moneysurfer.data.db.dao.CategoryDao
import com.georgeci.moneysurfer.data.db.entity.CategoryEntity
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.domain.model.CategoryAppearance
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.ClockUseCase
import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
import com.georgeci.moneysurfer.domain.repositories.CategoryRepository
import com.georgeci.moneysurfer.domain.sync.SyncEntityTypes
import com.georgeci.moneysurfer.sync.repository.MutationOperation
import com.georgeci.moneysurfer.sync.repository.OutboxEnqueuer
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Single
@Single(binds = [CategoryRepository::class])
class CategoryRepositoryImpl(
private val dao: CategoryDao,
private val outboxEnqueuer: OutboxEnqueuer,
private val clock: ClockUseCase,
private val timeFormatter: TimeFormatter,
) : CategoryRepository {
override fun getAll(): Flow<List<Category>> =
dao.getAll().map { list -> list.map { it.toDomain() } }
override fun getByWorkspaceId(workspaceId: WorkspaceId): Flow<List<Category>> =
dao.getByWorkspaceId(workspaceId.value).map { list -> list.map { it.toDomain() } }
override suspend fun getById(id: CategoryId): Category? =
dao.getById(id.value)?.toDomain()
override suspend fun insert(category: Category) {
val entity = category.toEntity()
dao.insert(entity)
enqueueUpsert(entity, MutationOperation.INSERT)
}
override suspend fun update(category: Category) {
val existingCreatedAt = dao.getById(category.id.value)?.createdAt
val entity = category.toEntity().copy(
createdAt = existingCreatedAt ?: category.toEntity().createdAt,
updatedAt = clock.now().toEpochMilliseconds(),
)
dao.update(entity)
enqueueUpsert(entity, MutationOperation.UPDATE)
}
override suspend fun delete(id: CategoryId) {
val existing = dao.getById(id.value)
dao.delete(id.value)
if (existing != null) {
outboxEnqueuer.enqueueDelete(
entityType = SyncEntityTypes.CATEGORY,
entityId = existing.id,
scopeKey = existing.workspaceId,
)
}
}
private suspend fun enqueueUpsert(entity: CategoryEntity, operation: MutationOperation) {
outboxEnqueuer.enqueueUpsert(
entityType = SyncEntityTypes.CATEGORY,
entityId = entity.id,
scopeKey = entity.workspaceId,
operation = operation,
)
}
private fun CategoryEntity.toDomain(): Category {
val kind = systemKind?.let { value ->
runCatching { CategorySystemKind.valueOf(value) }.getOrNull()
}
return Category(
id = CategoryId(id),
workspaceId = WorkspaceId(workspaceId),
name = name,
type = runCatching { CategoryType.valueOf(type) }.getOrDefault(CategoryType.EXPENSE),
parentId = parentId?.let { CategoryId(it) },
createdAt = timeFormatter.parseInstant(createdAt),
updatedAt = timeFormatter.parseInstant(updatedAt),
systemKind = kind,
// The v26 backfill fills every local row, but a row pulled from a client that
// predates the fields still arrives on the sentinels — resolve those here so the
// rest of the app never has to know an unset appearance exists.
iconKey = iconKey.takeIf(CategoryAppearance::isStoredIconKey)
?: CategoryAppearance.defaultIconKey(id, kind),
hue = hue.takeIf(CategoryAppearance::isStoredHue)
?: CategoryAppearance.defaultHue(id, kind),
)
}
private fun Category.toEntity() = CategoryEntity(
id = id.value,
workspaceId = workspaceId.value,
name = name,
type = type.name,
parentId = parentId?.value,
createdAt = timeFormatter.formatInstant(createdAt),
updatedAt = timeFormatter.formatInstant(updatedAt),
systemKind = systemKind?.name,
iconKey = iconKey,
hue = hue,
)
}