Add re-parse unparsed messages button

Settings > Data now has "Re-parse Unparsed Messages" button that
re-runs the fixed parser on all UNKNOWN transactions, properly
categorizing previously missed Fuliza auto-deductions, withdrawals,
airtime, and reversals.
This commit is contained in:
ngenohkevin
2026-03-21 22:26:45 +03:00
parent 729ef15cd3
commit d29498edbb
4 changed files with 63 additions and 1 deletions
@@ -102,4 +102,10 @@ interface TransactionDao {
@Query("SELECT COUNT(*) FROM transactions WHERE type = 'UNKNOWN'")
fun getUnparsedCount(): Flow<Int>
@Query("SELECT * FROM transactions WHERE type = 'UNKNOWN'")
suspend fun getUnparsedSync(): List<TransactionEntity>
@Query("DELETE FROM transactions WHERE id = :id")
suspend fun deleteById(id: Long)
}
@@ -85,6 +85,35 @@ class TransactionRepository @Inject constructor(
transactionDao.updateCategory(transactionId, categoryId)
}
suspend fun reparseUnknownTransactions(): ReparseResult {
val unknowns = transactionDao.getUnparsedSync()
var fixed = 0
var stillUnknown = 0
for (entity in unknowns) {
val result = com.ledga.app.data.parser.MpesaSmsParser.parse(entity.rawSms, entity.timestamp)
when (result) {
is com.ledga.app.data.parser.ParseResult.Success -> {
if (result.transaction.type != com.ledga.app.data.parser.TransactionType.UNKNOWN) {
// Delete old UNKNOWN entry, insert re-parsed one
transactionDao.deleteById(entity.id)
insertTransaction(result.transaction)
fixed++
} else {
stillUnknown++
}
}
is com.ledga.app.data.parser.ParseResult.Failure -> {
// Balance checks etc get filtered — delete the UNKNOWN entry
transactionDao.deleteById(entity.id)
fixed++
}
}
}
return ReparseResult(total = unknowns.size, fixed = fixed, stillUnknown = stillUnknown)
}
private suspend fun autoCategorize(parsed: ParsedTransaction): Long? {
// 1. Type-based defaults
val typeDefault = when (parsed.type) {
@@ -130,3 +159,5 @@ class TransactionRepository @Inject constructor(
return typeDefault ?: 13L // Default to "Other"
}
}
data class ReparseResult(val total: Int, val fixed: Int, val stillUnknown: Int)
@@ -45,6 +45,7 @@ fun SettingsScreen(
val largeTxnAlert by viewModel.largeTxnAlertEnabled.collectAsState()
val unparsedCount by viewModel.unparsedCount.collectAsState()
val importStatus by viewModel.importStatus.collectAsState()
val reparseResult by viewModel.reparseResult.collectAsState()
val exportResult by viewModel.exportResult.collectAsState()
val fileImportResult by viewModel.fileImportResult.collectAsState()
@@ -160,8 +161,22 @@ fun SettingsScreen(
}
if (unparsedCount > 0) {
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = { viewModel.reparseUnknown() },
modifier = Modifier.fillMaxWidth()
) {
Text("Re-parse Unparsed Messages ($unparsedCount)")
}
if (reparseResult != null) {
Text(
text = "Unparsed Messages ($unparsedCount)",
text = "Fixed ${reparseResult!!.fixed} of ${reparseResult!!.total} (${reparseResult!!.stillUnknown} still unknown)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = "View Unparsed Messages",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
@@ -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.ReparseResult
import com.ledga.app.data.repository.SettingsRepository
import com.ledga.app.data.repository.SmsImporter
import com.ledga.app.data.repository.TransactionRepository
@@ -99,6 +100,15 @@ class SettingsViewModel @Inject constructor(
}
}
private val _reparseResult = MutableStateFlow<ReparseResult?>(null)
val reparseResult: StateFlow<ReparseResult?> = _reparseResult
fun reparseUnknown() {
viewModelScope.launch {
_reparseResult.value = transactionRepository.reparseUnknownTransactions()
}
}
private val _exportResult = MutableStateFlow<ExportResult?>(null)
val exportResult: StateFlow<ExportResult?> = _exportResult