Coverage Summary for Class: RemoveMemberUseCase (com.georgeci.moneysurfer.domain.usecase)

Class Method, % Branch, % Line, % Instruction, %
RemoveMemberUseCase 100% (2/2) 75% (6/8) 100% (27/27) 99.3% (272/274)
RemoveMemberUseCase$Companion
RemoveMemberUseCase$invoke$1
RemoveMemberUseCase$Params 100% (1/1) 100% (3/3) 100% (12/12)
Total 100% (3/3) 75% (6/8) 100% (30/30) 99.3% (284/286)


 package com.georgeci.moneysurfer.domain.usecase
 
 import arrow.core.Either
 import arrow.core.raise.either
 import arrow.core.raise.ensure
 import co.touchlab.kermit.Logger
 import com.georgeci.moneysurfer.domain.auth.SessionPointers
 import com.georgeci.moneysurfer.domain.logging.redactUid
 import com.georgeci.moneysurfer.domain.model.WorkspaceMemberStatus
 import com.georgeci.moneysurfer.domain.model.WorkspaceRole
 import com.georgeci.moneysurfer.domain.primitives.UserId
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
 import com.georgeci.moneysurfer.domain.repositories.WorkspaceMemberRepository
 import org.koin.core.annotation.Single
 
 /**
  * Owner removes another member: status → REMOVED.
  *
  * Refuses to remove the last OWNER ([InviteError.OwnerCannotLeave]) — workspace must always
  * have at least one owner. Transferring ownership is a separate (post-v1) flow.
  */
 @Single
 class RemoveMemberUseCase(
     private val memberRepository: WorkspaceMemberRepository,
     private val session: SessionPointers,
 ) {
     private val log = Logger.withTag(TAG)
 
     data class Params(
         val workspaceId: WorkspaceId,
         val targetUserId: UserId,
     )
 
     suspend operator fun invoke(params: Params): Either<InviteError, Unit> = either {
         val (callerId, members, target) = resolveOwnerActionTarget(
             session = session,
             memberRepository = memberRepository,
             workspaceId = params.workspaceId,
             targetUserId = params.targetUserId,
         )
 
         if (target.role == WorkspaceRole.OWNER) {
             val remainingOwners = members.count {
                 it.role == WorkspaceRole.OWNER &&
                     it.status == WorkspaceMemberStatus.ACTIVE &&
                     it.userId != params.targetUserId
             }
             ensure(remainingOwners >= 1) { InviteError.OwnerCannotLeave }
         }
 
         Either.catch {
             memberRepository.markRemoved(
                 userId = params.targetUserId,
                 workspaceId = params.workspaceId,
                 removedByUserId = callerId,
             )
         }
             .onLeft { log.e(it) { "[local] markRemoved failed uid=${params.targetUserId.value.redactUid()}" } }
             .mapLeft { InviteError.LocalWriteFailed(it) }
             .bind()
 
         log.i { "[done] removed uid=${params.targetUserId.value.redactUid()} by=${callerId.value.redactUid()}" }
     }
 
     private companion object {
         const val TAG = "RemoveMember"
     }
 }