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

Class Method, % Branch, % Line, % Instruction, %
AccountRepositoryImpl 92.9% (13/14) 61.1% (11/18) 90.5% (67/74) 91% (669/735)
AccountRepositoryImpl$delete$1
AccountRepositoryImpl$getAll$$inlined$map$1 0% (0/2)
AccountRepositoryImpl$getAll$$inlined$map$1$1
AccountRepositoryImpl$getAll$$inlined$map$1$2 0% (0/1)
AccountRepositoryImpl$getAll$$inlined$map$1$2$1
AccountRepositoryImpl$getById$1
AccountRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1 0% (0/2)
AccountRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$1
AccountRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2 0% (0/1)
AccountRepositoryImpl$getByWorkspaceId-DBbEM4U$$inlined$map$1$2$1
AccountRepositoryImpl$insert$1
AccountRepositoryImpl$reorder$1
AccountRepositoryImpl$setArchived$1
AccountRepositoryImpl$update$1
Total 65% (13/20) 61.1% (11/18) 90.5% (67/74) 91% (669/735)


 package com.georgeci.moneysurfer.data.repository
 
 import com.georgeci.moneysurfer.data.db.dao.AccountDao
 import com.georgeci.moneysurfer.data.db.entity.AccountEntity
 import com.georgeci.moneysurfer.data.db.entity.AccountExtraDetailsColumn
 import com.georgeci.moneysurfer.domain.model.Account
 import com.georgeci.moneysurfer.domain.primitives.AccountId
 import com.georgeci.moneysurfer.domain.primitives.AccountType
 import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
 import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
 import com.georgeci.moneysurfer.domain.primitives.Money
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
 import com.georgeci.moneysurfer.domain.repositories.AccountRepository
 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 = [AccountRepository::class])
 class AccountRepositoryImpl(
     private val dao: AccountDao,
     private val outboxEnqueuer: OutboxEnqueuer,
     private val clock: ClockUseCase,
     private val timeFormatter: TimeFormatter,
 ) : AccountRepository {
 
     override fun getAll(): Flow<List<Account>> =
         dao.getAll().map { list -> list.map { it.toDomain() } }
 
     override fun getByWorkspaceId(workspaceId: WorkspaceId): Flow<List<Account>> =
         dao.getByWorkspaceId(workspaceId.value).map { list -> list.map { it.toDomain() } }
 
     override suspend fun getById(id: AccountId): Account? =
         dao.getById(id.value)?.toDomain()
 
     override suspend fun insert(account: Account) {
         val entity = account.toEntity().copy(
             updatedAt = clock.now().toEpochMilliseconds(),
             // A new account goes to the end of the list the user has arranged. Positions belong
             // to [reorder]; nothing upstream of here knows what the rest of the workspace holds.
             sortOrder = dao.nextSortOrder(account.workspaceId.value),
         )
         dao.insert(entity)
         enqueueUpsert(entity, MutationOperation.INSERT)
     }
 
     override suspend fun update(account: Account) {
         val entity = account.toEntity().copy(updatedAt = clock.now().toEpochMilliseconds())
         dao.update(entity)
         enqueueUpsert(entity, MutationOperation.UPDATE)
     }
 
     override suspend fun delete(id: AccountId) {
         val existing = dao.getById(id.value)
         dao.delete(id.value)
         if (existing != null) {
             outboxEnqueuer.enqueueDelete(
                 entityType = SyncEntityTypes.ACCOUNT,
                 entityId = existing.id,
                 scopeKey = existing.workspaceId,
             )
         }
     }
 
     override suspend fun applyDelta(accountId: AccountId, delta: Money) {
         if (delta.isZero()) return
         dao.applyDelta(accountId.value, delta.minor, clock.now().toEpochMilliseconds())
     }
 
     override suspend fun setBalance(accountId: AccountId, balance: Money) {
         dao.setBalance(accountId.value, balance.minor, clock.now().toEpochMilliseconds())
     }
 
     override suspend fun setArchived(accountId: AccountId, archived: Boolean) {
         val existing = dao.getById(accountId.value) ?: return
         if (existing.archived == archived) return
         val now = clock.now().toEpochMilliseconds()
         dao.setArchived(
             id = accountId.value,
             archived = archived,
             // Restoring clears the stamp — an account that comes back has no archive date.
             archivedAt = now.takeIf { archived },
             updatedAt = now,
         )
         outboxEnqueuer.enqueueUpsert(
             entityType = SyncEntityTypes.ACCOUNT,
             entityId = existing.id,
             scopeKey = existing.workspaceId,
             operation = MutationOperation.UPDATE,
         )
     }
 
     override suspend fun reorder(orderedIds: List<AccountId>) {
         // Unknown ids drop out rather than leaving a hole in the numbering — a stale id from a
         // list the user dragged just as a sync deleted a row must not push its neighbours apart.
         val ordered = orderedIds.mapNotNull { dao.getById(it.value) }
         val moved = ordered.withIndex().filter { (position, entity) -> entity.sortOrder != position }
         if (moved.isEmpty()) return
         val now = clock.now().toEpochMilliseconds()
         dao.setSortOrders(moved.associate { (position, entity) -> entity.id to position }, now)
         moved.forEach { (_, entity) -> enqueueUpsert(entity, MutationOperation.UPDATE) }
     }
 
     private suspend fun enqueueUpsert(entity: AccountEntity, operation: MutationOperation) {
         outboxEnqueuer.enqueueUpsert(
             entityType = SyncEntityTypes.ACCOUNT,
             entityId = entity.id,
             scopeKey = entity.workspaceId,
             operation = operation,
         )
     }
 
     private fun AccountEntity.toDomain() = Account(
         id = AccountId(id),
         workspaceId = WorkspaceId(workspaceId),
         name = name,
         type = runCatching { AccountType.valueOf(type) }.getOrDefault(AccountType.SAVINGS),
         currencyCode = CurrencyCode(currency),
         balance = Money.fromMinor(balance),
         archived = archived,
         updatedAt = timeFormatter.parseInstant(updatedAt),
         archivedAt = timeFormatter.parseInstantOrNull(archivedAt),
         extraDetails = AccountExtraDetailsColumn.decode(extraDetails),
         sortOrder = sortOrder,
     )
 
     private fun Account.toEntity() = AccountEntity(
         id = id.value,
         workspaceId = workspaceId.value,
         name = name,
         type = type.name,
         currency = currencyCode.value,
         balance = balance.minor,
         archived = archived,
         updatedAt = timeFormatter.formatInstant(updatedAt),
         archivedAt = timeFormatter.formatInstantOrNull(archivedAt),
         extraDetails = AccountExtraDetailsColumn.encode(extraDetails),
         sortOrder = sortOrder,
     )
 }