Coverage Summary for Class: WorkspaceMemberSyncPlugin (com.georgeci.moneysurfer.data.sync.plugin)

Class Method, % Branch, % Line, % Instruction, %
WorkspaceMemberSyncPlugin 75% (3/4) 62.5% (5/8) 97.1% (33/34) 96.7% (295/305)
WorkspaceMemberSyncPlugin$applyDoc$1
WorkspaceMemberSyncPlugin$push$2 100% (1/1) 50% (2/4) 100% (10/10) 98% (100/102)
Total 80% (4/5) 58.3% (7/12) 97.7% (43/44) 97.1% (395/407)


 package com.georgeci.moneysurfer.data.sync.plugin
 import com.georgeci.moneysurfer.data.db.dao.UserDao
 import com.georgeci.moneysurfer.data.db.dao.WorkspaceInviteDao
 import com.georgeci.moneysurfer.data.db.dao.WorkspaceMemberDao
 import com.georgeci.moneysurfer.data.db.entity.UserEntity
 import com.georgeci.moneysurfer.data.remote.WorkspaceMemberDoc
 import com.georgeci.moneysurfer.data.sync.WorkspaceDocumentWriter
 import com.georgeci.moneysurfer.data.sync.toDoc
 import com.georgeci.moneysurfer.data.sync.toEntity
 import com.georgeci.moneysurfer.domain.AppInfo
 import com.georgeci.moneysurfer.domain.sync.SyncEntityTypes
 import com.georgeci.moneysurfer.sync.api.SyncCollection
 import com.georgeci.moneysurfer.sync.plugin.EntityApplyResult
 import com.georgeci.moneysurfer.sync.plugin.RemoteDocument
 import com.georgeci.moneysurfer.sync.plugin.SyncEntityPlugin
 import com.georgeci.moneysurfer.sync.repository.ConflictMetadata
 import com.georgeci.moneysurfer.sync.repository.ConflictResolver
 import com.georgeci.moneysurfer.sync.repository.PendingMutation
 import org.koin.core.annotation.Single
 import kotlin.time.Instant
 
 @Single(binds = [SyncEntityPlugin::class])
 class WorkspaceMemberSyncPlugin(
     private val writer: WorkspaceDocumentWriter,
     private val appInfo: AppInfo,
     private val conflictResolver: ConflictResolver,
     private val workspaceMemberDao: WorkspaceMemberDao,
     private val workspaceInviteDao: WorkspaceInviteDao,
     private val userDao: UserDao,
 ) : SyncEntityPlugin {
 
     override val entityType: String = SyncEntityTypes.WORKSPACE_MEMBER
     override val firestoreCollectionName: String = SyncCollection.WORKSPACE_MEMBERS
     override val pullPriority: Int = SyncPullPriorities.MEMBERS
 
     override suspend fun push(mutation: PendingMutation) = writer.pushToCollection(
         mutation = mutation,
         collectionName = SyncCollection.WORKSPACE_MEMBERS,
         clientVersionCode = appInfo.versionCode,
         strategy = WorkspaceMemberDoc.serializer(),
     ) {
         val scopeKey = mutation.scopeKey!!
         val entity = workspaceMemberDao.getById(
             userId = mutation.entityId,
             workspaceId = scopeKey,
         )
         entity?.toDoc()?.copy(
             clientVersionCode = appInfo.versionCode,
             // Stamp the admitting invite so the rules can authorize the accept-invite
             // self-create (issue #152). Usually null for owner-created rows and not
             // required for owner writes (the rules' owner branch never consults it);
             // it may still be non-null if a joinable invite happens to target the
             // owner, which is harmless.
             inviteId = workspaceInviteDao.findJoinableInviteId(
                 workspaceId = scopeKey,
                 userId = entity.userId,
                 email = entity.email,
             ),
         )
     }
 
     override suspend fun applyDoc(doc: RemoteDocument, scopeKey: String): EntityApplyResult {
         val dto = doc.decodeOrNull(WorkspaceMemberDoc.serializer()) ?: return SKIPPED_APPLY_RESULT
         if (dto.deletedAt != null) {
             workspaceMemberDao.delete(userId = doc.id, workspaceId = scopeKey)
             return EntityApplyResult(applied = true, wasConflict = false)
         }
         userDao.insertIgnore(
             UserEntity(
                 id = doc.id,
                 displayName = dto.displayName.ifEmpty { null },
                 isAnon = false,
             ),
         )
         val local = workspaceMemberDao.getById(userId = doc.id, workspaceId = scopeKey)
         val resolution = conflictResolver.resolve(
             local = local,
             remote = dto.toEntity(workspaceId = scopeKey),
             metadata = ConflictMetadata(
                 entityType = SyncEntityTypes.WORKSPACE_MEMBER,
                 entityId = doc.id,
                 localUpdatedAt = local?.updatedAt?.let(Instant::fromEpochMilliseconds),
                 remoteUpdatedAt = Instant.fromEpochMilliseconds(dto.updatedAt),
             ),
         )
         return applyResolution(resolution) { workspaceMemberDao.upsert(it) }
     }
 }