v1.3.6: auto-dismiss stale Fuliza insight + refresh after reparse

The Fuliza outstanding insight is stateful — it should disappear the
moment the underlying outstanding hits 0. Today it sticks until
manually dismissed, even after data updates.

InsightEngine now tracks a STATEFUL_KEYS set (just "fuliza:outstanding"
for now). If a rule that owns one of those keys returns no candidates
in this run, the row is deleted outright rather than being left around
as a tombstone.

SettingsViewModel.reparseAll() now also calls insightsRepository
.generateAll() once the row update completes. So the moment the user
finishes "Re-parse ALL transactions", any stale Fuliza alert reading
the limit-as-outstanding goes away, and any newly correct alerts show
up immediately.
This commit is contained in:
ngenohkevin
2026-05-16 22:30:17 +03:00
parent f83f644cd3
commit f40a93e62a
4 changed files with 34 additions and 3 deletions
+2 -2
View File
@@ -27,8 +27,8 @@ android {
applicationId = "com.ledga.app"
minSdk = 26
targetSdk = 34
versionCode = 8
versionName = "1.3.5"
versionCode = 9
versionName = "1.3.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -88,6 +88,9 @@ interface InsightDao {
@Query("DELETE FROM insights WHERE id = :id")
suspend fun deleteById(id: Long)
@Query("DELETE FROM insights WHERE naturalKey = :key")
suspend fun deleteByKey(key: String)
@Query("DELETE FROM insights WHERE generatedAt < :before AND dismissedAt IS NOT NULL")
suspend fun pruneOldDismissed(before: Long)
}
@@ -49,6 +49,13 @@ class InsightEngine @Inject constructor(
}
candidates.forEach { upsert(it, now) }
// Stateful keys: if a rule that "owns" a singleton key didn't emit
// this run, the underlying condition is gone — delete the stale row
// outright (vs leaving it to expire 30 days post-dismiss).
val emittedKeys = candidates.map { it.naturalKey }.toSet()
STATEFUL_KEYS.forEach { key ->
if (key !in emittedKeys) insightDao.deleteByKey(key)
}
insightDao.pruneOldDismissed(before = now - 30L * DAY_MS)
return candidates.size
}
@@ -85,5 +92,19 @@ class InsightEngine @Inject constructor(
companion object {
private const val DAY_MS = 86_400_000L
/**
* Natural keys whose underlying condition is stateful — only true when
* a current event matches. If a rule using one of these keys returns
* no candidates this run, the existing insight is stale and should
* be deleted, not just re-emitted.
*
* Per-period keys (anomaly:1:2026-W12, nudge:2:2026-04, fees:2026-04)
* are NOT stateful in this sense — they refer to a fixed past period
* and should persist in history.
*/
private val STATEFUL_KEYS = setOf(
"fuliza:outstanding",
)
}
}
@@ -9,6 +9,7 @@ import com.ledga.app.data.repository.ExportResult
import com.ledga.app.data.repository.FontScale
import com.ledga.app.data.repository.ImportFromFileResult
import com.ledga.app.data.repository.ImportResult
import com.ledga.app.data.repository.InsightsRepository
import com.ledga.app.data.repository.ReparseResult
import com.ledga.app.data.repository.SettingsRepository
import com.ledga.app.data.repository.SmsImporter
@@ -31,7 +32,8 @@ class SettingsViewModel @Inject constructor(
private val transactionRepository: TransactionRepository,
private val smsImporter: SmsImporter,
private val exportImportRepository: ExportImportRepository,
private val backupRepository: BackupRepository
private val backupRepository: BackupRepository,
private val insightsRepository: InsightsRepository,
) : ViewModel() {
val themeMode: StateFlow<ThemeMode> = settingsRepository.getThemeMode()
@@ -123,6 +125,11 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch {
try {
_reparseAllResult.value = transactionRepository.reparseAllTransactions()
// Refresh insights immediately so the Home teaser + Insights
// screen reflect the corrected data. Stateful insights (like
// Fuliza outstanding) get auto-dismissed by the engine when
// their condition is no longer true.
insightsRepository.generateAll()
} finally {
_reparseAllRunning.value = false
}