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

Class Method, % Branch, % Line, % Instruction, %
PurgeDeletedTransactionsUseCase 100% (3/3) 100% (5/5) 100% (23/23)
PurgeDeletedTransactionsUseCase$Companion 100% (1/1) 100% (1/1) 100% (1/1)
Total 100% (4/4) 100% (6/6) 100% (24/24)


 package com.georgeci.moneysurfer.domain.usecase
 
 import com.georgeci.moneysurfer.domain.primitives.ClockUseCase
 import com.georgeci.moneysurfer.domain.repositories.TransactionRetentionRepository
 import org.koin.core.annotation.Single
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.days
 
 /**
  * The retention half of soft delete (issue #346): tombstoned transactions are kept for
  * [RETENTION] and then dropped for good.
  *
  * **Who runs it.** The app itself, once per launch, from `AppLaunchViewModel` — the same place
  * the first-run seed repairs a half-finished install. Deliberately not a background worker: the
  * work is a single indexed DELETE over rows nobody is reading, there is nothing to schedule
  * around, and a worker would need scheduling on two platforms to do what one call at startup
  * already does. An app that is never launched keeps its tombstones, which costs a few rows and
  * loses nothing.
  *
  * **Why thirty days.** Everything that reads a tombstone is a local, short-lived interaction: the
  * Undo Snackbar (seconds), an edit that was already open when the row was deleted
  * ([UpdateTransactionUseCase]), and a CSV import, which must find the tombstone rather than insert
  * over a surviving id. Thirty days is far past all three and short enough that deleted rows do not
  * accumulate. Nothing here is waiting on another device — see below.
  *
  * **The window is measured from the delete's own timestamp**, which for a tombstone that arrived by
  * sync is the clock of the device that made the delete, not the moment this one heard about it. A
  * delete pushed by a peer that had been offline for longer than [RETENTION] therefore lands here
  * already expired and is collected on the next launch. That is deliberate rather than an oversight:
  * none of the three readers above outlives the trip, there is no UI for undoing a delete made on
  * another device, and a CSV import that finds no tombstone simply inserts the row instead — the
  * same end state.
  *
  * **What it does not do.** The remote doc keeps its `deletedAt`. Purging here says nothing about
  * any peer's copy — the tombstone stays on the server for them to pull, and collecting *that* is a
  * separate open gap (docs/architecture/sync-gaps.md).
  */
 @Single
 class PurgeDeletedTransactionsUseCase(
     private val retention: TransactionRetentionRepository,
     private val clock: ClockUseCase,
 ) {
 
     /** Returns how many tombstones were dropped. */
     suspend operator fun invoke(): Int = retention.purgeDeletedBefore(clock.now() - RETENTION)
 
     companion object {
         /** How long a deleted transaction stays recoverable before it is gone for good. */
         val RETENTION: Duration = 30.days
     }
 }