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

Class Method, % Branch, % Line, % Instruction, %
WorkspaceInviteRepositoryImpl 57.1% (8/14) 50% (9/18) 76.1% (54/71) 71% (402/566)
WorkspaceInviteRepositoryImpl$Companion
WorkspaceInviteRepositoryImpl$enqueueUpsert$1
WorkspaceInviteRepositoryImpl$getAll$$inlined$map$1 0% (0/2)
WorkspaceInviteRepositoryImpl$getAll$$inlined$map$1$1
WorkspaceInviteRepositoryImpl$getAll$$inlined$map$1$2 0% (0/1)
WorkspaceInviteRepositoryImpl$getAll$$inlined$map$1$2$1
WorkspaceInviteRepositoryImpl$getByEmail$$inlined$map$1 0% (0/2)
WorkspaceInviteRepositoryImpl$getByEmail$$inlined$map$1$1
WorkspaceInviteRepositoryImpl$getByEmail$$inlined$map$1$2 0% (0/1)
WorkspaceInviteRepositoryImpl$getByEmail$$inlined$map$1$2$1
WorkspaceInviteRepositoryImpl$getById$1
WorkspaceInviteRepositoryImpl$getByTargetUserId-SyNjzC0$$inlined$map$1 0% (0/2)
WorkspaceInviteRepositoryImpl$getByTargetUserId-SyNjzC0$$inlined$map$1$1
WorkspaceInviteRepositoryImpl$getByTargetUserId-SyNjzC0$$inlined$map$1$2 0% (0/1)
WorkspaceInviteRepositoryImpl$getByTargetUserId-SyNjzC0$$inlined$map$1$2$1
WorkspaceInviteRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1 0% (0/2)
WorkspaceInviteRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$1
WorkspaceInviteRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2 0% (0/1)
WorkspaceInviteRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2$1
WorkspaceInviteRepositoryImpl$transition$1
WorkspaceInviteRepositoryImpl$upsert$1
Total 30.8% (8/26) 50% (9/18) 76.1% (54/71) 71% (402/566)


 package com.georgeci.moneysurfer.data.repository
 
 import co.touchlab.kermit.Logger
 import com.georgeci.moneysurfer.data.db.dao.WorkspaceInviteDao
 import com.georgeci.moneysurfer.data.db.entity.WorkspaceInviteEntity
 import com.georgeci.moneysurfer.domain.logging.redactEmail
 import com.georgeci.moneysurfer.domain.logging.redactUid
 import com.georgeci.moneysurfer.domain.model.InviteStatus
 import com.georgeci.moneysurfer.domain.model.WorkspaceInvite
 import com.georgeci.moneysurfer.domain.model.WorkspaceRole
 import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
 import com.georgeci.moneysurfer.domain.primitives.UserId
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceInviteId
 import com.georgeci.moneysurfer.domain.repositories.WorkspaceInviteRepository
 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 = [WorkspaceInviteRepository::class])
 class WorkspaceInviteRepositoryImpl(
     private val dao: WorkspaceInviteDao,
     private val outboxEnqueuer: OutboxEnqueuer,
     private val clock: ClockUseCase,
     private val timeFormatter: TimeFormatter,
 ) : WorkspaceInviteRepository {
 
     private val log = Logger.withTag(TAG)
 
     override fun getAll(): Flow<List<WorkspaceInvite>> =
         dao.getAll().map { list -> list.map { it.toDomain() } }
 
     override fun getByWorkspaceId(workspaceId: WorkspaceId): Flow<List<WorkspaceInvite>> =
         dao.getByWorkspaceId(workspaceId.value).map { list -> list.map { it.toDomain() } }
 
     override fun getByEmail(email: String): Flow<List<WorkspaceInvite>> =
         dao.getByEmail(email).map { list -> list.map { it.toDomain() } }
 
     override fun getByTargetUserId(userId: UserId): Flow<List<WorkspaceInvite>> =
         dao.getByTargetUserId(userId.value).map { list -> list.map { it.toDomain() } }
 
     override suspend fun getById(id: WorkspaceInviteId): WorkspaceInvite? =
         dao.getById(id.value)?.toDomain()
 
     override suspend fun upsert(invite: WorkspaceInvite) {
         val now = clock.now().toEpochMilliseconds()
         val existing = dao.getById(invite.id.value)
         val isNew = existing == null
         val entity = invite.toEntity().copy(
             createdAt = existing?.createdAt ?: timeFormatter.formatInstant(invite.createdAt),
             updatedAt = now,
         )
         dao.upsert(entity)
         log.i {
             "[upsert] id=${entity.id} wid=${entity.workspaceId} email=${entity.email.redactEmail()} " +
                 "status=${entity.status} role=${entity.role} target=${entity.targetUserId.redactUid()} " +
                 "isNew=$isNew updatedAt=${entity.updatedAt} expiresAt=${entity.expiresAt}"
         }
         enqueueUpsert(
             entity = entity,
             operation = if (isNew) MutationOperation.INSERT else MutationOperation.UPDATE,
         )
     }
 
     override suspend fun cancel(id: WorkspaceInviteId) {
         transition(id, InviteStatus.CANCELLED)
     }
 
     override suspend fun markAccepted(id: WorkspaceInviteId) {
         transition(id, InviteStatus.ACCEPTED)
     }
 
     override suspend fun markDeclined(id: WorkspaceInviteId) {
         transition(id, InviteStatus.DECLINED)
     }
 
     private suspend fun transition(id: WorkspaceInviteId, target: InviteStatus) {
         val current = dao.getById(id.value)
         if (current == null) {
             log.w { "[transition] id=${id.value} not found locally — skipping $target" }
             return
         }
         val now = clock.now().toEpochMilliseconds()
         val updated = current.copy(
             status = target.name,
             respondedAt = now,
             updatedAt = now,
         )
         dao.upsert(updated)
         log.i {
             "[transition] id=${updated.id} wid=${updated.workspaceId} ${current.status}→$target " +
                 "email=${updated.email.redactEmail()} respondedAt=${updated.respondedAt}"
         }
         enqueueUpsert(updated, MutationOperation.UPDATE)
     }
 
     private suspend fun enqueueUpsert(entity: WorkspaceInviteEntity, operation: MutationOperation) {
         outboxEnqueuer.enqueueUpsert(
             entityType = SyncEntityTypes.WORKSPACE_INVITE,
             entityId = entity.id,
             scopeKey = entity.workspaceId,
             operation = operation,
         )
         log.i { "[enqueue] id=${entity.id} wid=${entity.workspaceId} op=$operation" }
     }
 
     private fun WorkspaceInviteEntity.toDomain() = WorkspaceInvite(
         id = WorkspaceInviteId(id),
         workspaceId = WorkspaceId(workspaceId),
         email = email,
         targetUserId = targetUserId?.let(::UserId),
         role = WorkspaceRole.entries.firstOrNull { it.name == role } ?: WorkspaceRole.VIEWER,
         status = InviteStatus.entries.firstOrNull { it.name == status } ?: InviteStatus.PENDING,
         invitedByUserId = UserId(invitedByUserId),
         createdAt = timeFormatter.parseInstant(createdAt),
         updatedAt = timeFormatter.parseInstant(updatedAt),
         expiresAt = timeFormatter.parseInstant(expiresAt),
         respondedAt = timeFormatter.parseInstantOrNull(respondedAt),
     )
 
     private fun WorkspaceInvite.toEntity() = WorkspaceInviteEntity(
         id = id.value,
         workspaceId = workspaceId.value,
         email = email,
         targetUserId = targetUserId?.value,
         role = role.name,
         status = status.name,
         invitedByUserId = invitedByUserId.value,
         createdAt = timeFormatter.formatInstant(createdAt),
         updatedAt = timeFormatter.formatInstant(updatedAt),
         expiresAt = timeFormatter.formatInstant(expiresAt),
         respondedAt = timeFormatter.formatInstantOrNull(respondedAt),
     )
 
     private companion object {
         const val TAG = "InviteRepo"
     }
 }