Coverage Summary for Class: SavingsGoalRepositoryImpl (com.georgeci.moneysurfer.data.repository)

Class Method, % Branch, % Line, % Instruction, %
SavingsGoalRepositoryImpl 60% (6/10) 31.2% (5/16) 73% (46/63) 66.9% (417/623)
SavingsGoalRepositoryImpl$delete$1
SavingsGoalRepositoryImpl$getById$1
SavingsGoalRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1 0% (0/2)
SavingsGoalRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$1
SavingsGoalRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2 0% (0/1)
SavingsGoalRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2$1
SavingsGoalRepositoryImpl$insert$1
SavingsGoalRepositoryImpl$observeById-wYBBLD8$$inlined$map$1 0% (0/2)
SavingsGoalRepositoryImpl$observeById-wYBBLD8$$inlined$map$1$1
SavingsGoalRepositoryImpl$observeById-wYBBLD8$$inlined$map$1$2 0% (0/1)
SavingsGoalRepositoryImpl$observeById-wYBBLD8$$inlined$map$1$2$1
SavingsGoalRepositoryImpl$update$1
Total 37.5% (6/16) 31.2% (5/16) 73% (46/63) 66.9% (417/623)


 package com.georgeci.moneysurfer.data.repository
 
 import com.georgeci.moneysurfer.data.db.dao.GoalContributionDao
 import com.georgeci.moneysurfer.data.db.dao.GoalDao
 import com.georgeci.moneysurfer.data.db.entity.GoalEntity
 import com.georgeci.moneysurfer.domain.model.GoalStatus
 import com.georgeci.moneysurfer.domain.model.SavingsGoal
 import com.georgeci.moneysurfer.domain.primitives.AccountId
 import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
 import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
 import com.georgeci.moneysurfer.domain.primitives.GoalId
 import com.georgeci.moneysurfer.domain.primitives.Money
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
 import com.georgeci.moneysurfer.domain.repositories.SavingsGoalRepository
 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 = [SavingsGoalRepository::class])
 class SavingsGoalRepositoryImpl(
     private val dao: GoalDao,
     private val contributionDao: GoalContributionDao,
     private val outboxEnqueuer: OutboxEnqueuer,
     private val clock: ClockUseCase,
     private val timeFormatter: TimeFormatter,
 ) : SavingsGoalRepository {
 
     override fun getByWorkspaceId(workspaceId: WorkspaceId): Flow<List<SavingsGoal>> =
         dao.getByWorkspaceId(workspaceId.value).map { list -> list.map { it.toDomain() } }
 
     override suspend fun getById(id: GoalId): SavingsGoal? =
         dao.getById(id.value)?.toDomain()
 
     override fun observeById(id: GoalId): Flow<SavingsGoal?> =
         dao.observeById(id.value).map { it?.toDomain() }
 
     override suspend fun insert(goal: SavingsGoal) {
         val entity = goal.toEntity()
         dao.insert(entity)
         enqueueUpsert(entity, MutationOperation.INSERT)
     }
 
     override suspend fun update(goal: SavingsGoal) {
         val entity = goal.toEntity().copy(
             createdAt = dao.getById(goal.id.value)?.createdAt ?: goal.toEntity().createdAt,
             updatedAt = clock.now().toEpochMilliseconds(),
         )
         dao.update(entity)
         enqueueUpsert(entity, MutationOperation.UPDATE)
     }
 
     /**
      * Reads the contributions BEFORE the delete: Room's `ON DELETE CASCADE` removes
      * them without a DAO call, so this is the last moment their ids are knowable.
      * Without these enqueues the remote rows would survive as orphans under a goal
      * that no longer exists.
      */
     override suspend fun delete(id: GoalId) {
         val existing = dao.getById(id.value) ?: return
         val contributions = contributionDao.listByGoalId(id.value)
         dao.delete(id.value)
         contributions.forEach { contribution ->
             outboxEnqueuer.enqueueDelete(
                 entityType = SyncEntityTypes.GOAL_CONTRIBUTION,
                 entityId = contribution.id,
                 scopeKey = contribution.workspaceId,
             )
         }
         outboxEnqueuer.enqueueDelete(
             entityType = SyncEntityTypes.GOAL,
             entityId = existing.id,
             scopeKey = existing.workspaceId,
         )
     }
 
     private suspend fun enqueueUpsert(entity: GoalEntity, operation: MutationOperation) {
         outboxEnqueuer.enqueueUpsert(
             entityType = SyncEntityTypes.GOAL,
             entityId = entity.id,
             scopeKey = entity.workspaceId,
             operation = operation,
         )
     }
 
     private fun GoalEntity.toDomain() = SavingsGoal(
         id = GoalId(id),
         workspaceId = WorkspaceId(workspaceId),
         title = title,
         emoji = emoji,
         hue = hue,
         target = Money.fromMinor(target),
         currencyCode = CurrencyCode(currencyCode),
         startDate = timeFormatter.parseLocalDate(startDate),
         targetDate = targetDate?.let { timeFormatter.parseLocalDateOrNull(it) },
         accountId = accountId?.let { AccountId(it) },
         status = runCatching { GoalStatus.valueOf(status) }.getOrDefault(GoalStatus.ACTIVE),
         createdAt = timeFormatter.parseInstant(createdAt),
         updatedAt = timeFormatter.parseInstant(updatedAt),
     )
 
     private fun SavingsGoal.toEntity() = GoalEntity(
         id = id.value,
         workspaceId = workspaceId.value,
         title = title,
         emoji = emoji,
         hue = hue,
         target = target.minor,
         currencyCode = currencyCode.value,
         startDate = timeFormatter.formatLocalDate(startDate),
         targetDate = targetDate?.let { timeFormatter.formatLocalDate(it) },
         accountId = accountId?.value,
         status = status.name,
         createdAt = timeFormatter.formatInstant(createdAt),
         updatedAt = timeFormatter.formatInstant(updatedAt),
     )
 }