Coverage Summary for Class: WorkspaceSyncPlugin (com.georgeci.moneysurfer.data.sync.plugin)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| WorkspaceSyncPlugin |
75%
(3/4)
|
66.7%
(6/9)
|
84.6%
(22/26)
|
88.3%
(211/239)
|
| WorkspaceSyncPlugin$applyDoc$1 |
|
| WorkspaceSyncPlugin$push$1 |
|
| WorkspaceSyncPlugin$WhenMappings |
|
| Total |
75%
(3/4)
|
66.7%
(6/9)
|
84.6%
(22/26)
|
88.3%
(211/239)
|
package com.georgeci.moneysurfer.data.sync.plugin
import com.georgeci.moneysurfer.data.db.dao.UserDao
import com.georgeci.moneysurfer.data.db.dao.WorkspaceDao
import com.georgeci.moneysurfer.data.db.entity.UserEntity
import com.georgeci.moneysurfer.data.remote.WorkspaceDoc
import com.georgeci.moneysurfer.data.sync.WorkspaceDocRef
import com.georgeci.moneysurfer.data.sync.WorkspaceDocumentWriter
import com.georgeci.moneysurfer.data.sync.WorkspaceRefRegistrar
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.plugin.EntityApplyResult
import com.georgeci.moneysurfer.sync.plugin.RemoteDocument
import com.georgeci.moneysurfer.sync.plugin.SyncEntityPlugin
import com.georgeci.moneysurfer.sync.repository.MutationOperation
import com.georgeci.moneysurfer.sync.repository.PendingMutation
import org.koin.core.annotation.Single
/**
* Handles workspace root-doc push and pull.
*
* [isWorkspaceRootPlugin] = true signals [PullRemoteChangesUseCaseImpl] to call
* [applyDoc] with the workspace root document before any subcollection plugin runs.
* This guarantees the local [WorkspaceEntity] exists when accounts / categories /
* transactions are upserted (all carry a Room FK workspaceId → WorkspaceEntity).
*
* [firestoreCollectionName] remains null — root docs are not a subcollection and
* are not fetched via [WorkspaceCollectionReader.fetchUpdatedSince].
*
* [applyDoc] also stubs the owner's [UserEntity] via insertIgnore so the
* WorkspaceEntity FK ownerId → UserEntity is satisfied even on a fresh device
* before the members collection is pulled.
*/
@Single(binds = [SyncEntityPlugin::class])
class WorkspaceSyncPlugin(
private val writer: WorkspaceDocumentWriter,
private val appInfo: AppInfo,
private val workspaceDao: WorkspaceDao,
private val userDao: UserDao,
private val workspaceRefRegistrar: WorkspaceRefRegistrar,
) : SyncEntityPlugin {
override val entityType: String = SyncEntityTypes.WORKSPACE
override val firestoreCollectionName: String? = null
override val pullPriority: Int = SyncPullPriorities.WORKSPACE
override suspend fun push(mutation: PendingMutation) {
val ref = WorkspaceDocRef.root(mutation.entityId)
when (mutation.operation) {
MutationOperation.INSERT,
MutationOperation.UPDATE,
-> {
val entity = workspaceDao.getById(mutation.entityId) ?: return
writer.set(
ref = ref,
strategy = WorkspaceDoc.serializer(),
value = entity.toDoc().copy(clientVersionCode = appInfo.versionCode),
)
// The ref travels with the document. A workspace created while sync was
// disabled has no `users/{uid}.workspaceIds` entry and nothing else would ever
// add one, leaving it invisible to every other device (issue #342).
workspaceRefRegistrar.register(mutation.entityId)
}
MutationOperation.DELETE -> writer.tombstone(
ref,
tombstonePatchFor(mutation, clientVersionCode = appInfo.versionCode),
)
}
}
override suspend fun applyDoc(doc: RemoteDocument, scopeKey: String): EntityApplyResult {
val dto = doc.decodeOrNull(WorkspaceDoc.serializer()) ?: return SKIPPED_APPLY_RESULT
if (dto.deletedAt != null) {
// Soft-delete: workspace is tombstoned. Do not remove from Room —
// the sync layer handles cleanup; just skip the upsert.
return EntityApplyResult(applied = false, wasConflict = false)
}
// Stub the owner's UserEntity so the FK ownerId → UserEntity is satisfied
// before workspaceDao.upsertAll(). insertIgnore is a no-op if the row exists.
userDao.insertIgnore(UserEntity(id = dto.ownerId, displayName = null, isAnon = false))
workspaceDao.upsertAll(listOf(dto.toEntity(id = doc.id)))
return EntityApplyResult(applied = true, wasConflict = false)
}
}