fix: multiple filters break contact search

This commit is contained in:
2026-06-13 14:53:31 +02:00
parent 2bec9f3908
commit ef03a986a4
4 changed files with 73 additions and 32 deletions
@@ -160,9 +160,33 @@ class PaymentCacheRepository(context: Context) {
Timber.d("PAYMENTS [CLEARED ] cache wiped") Timber.d("PAYMENTS [CLEARED ] cache wiped")
} }
// Fetches specific payments by checkingId (used after a contact lookup) /**
suspend fun queryByCheckingIds(checkingIds: List<String>): List<PaymentRecord> { * Fetch payments by checkingId with the full PaymentFilter applied.
* Used by HistoryViewModel for contact name search supplement and
* contact ID filter. Amount/date/status are applied in SQL;
* direction and type are handled in-memory afterwards (same as queryAll).
*/
suspend fun queryByCheckingIds(
checkingIds : List<String>,
filter : PaymentFilter
): List<PaymentRecord> {
if (checkingIds.isEmpty()) return emptyList() if (checkingIds.isEmpty()) return emptyList()
return dao.getByCheckingIds(checkingIds).map { it.toDomain() } val statusPattern: String? = if (filter.statuses.isEmpty()) null else {
filter.statuses.flatMap { s ->
when (s) {
StatusFilter.COMPLETED -> listOf("success", "complete", "paid")
StatusFilter.PENDING -> listOf("pending", "in_flight", "inflight")
StatusFilter.FAILED -> listOf("failed", "error", "expired")
}
}.joinToString(",")
}
return dao.getByCheckingIdsFiltered(
checkingIds = checkingIds,
minAmountMsat = filter.minAmountSat?.let { it * 1000L },
maxAmountMsat = filter.maxAmountSat?.let { it * 1000L },
minCreatedAt = filter.minCreatedAt,
maxCreatedAt = filter.maxCreatedAt,
statusPattern = statusPattern
).map { it.toDomain() }
} }
} }
@@ -35,6 +35,27 @@ interface PaymentDao {
@Query("SELECT * FROM payment_records WHERE checkingId IN (:checkingIds) ORDER BY createdAt DESC") @Query("SELECT * FROM payment_records WHERE checkingId IN (:checkingIds) ORDER BY createdAt DESC")
suspend fun getByCheckingIds(checkingIds: List<String>): List<PaymentRecordEntity> suspend fun getByCheckingIds(checkingIds: List<String>): List<PaymentRecordEntity>
@Query("""
SELECT * FROM payment_records
WHERE checkingId IN (:checkingIds)
AND (:minAmountMsat IS NULL OR ABS(amountMsat) >= :minAmountMsat)
AND (:maxAmountMsat IS NULL OR ABS(amountMsat) <= :maxAmountMsat)
AND (:minCreatedAt IS NULL OR createdAt >= :minCreatedAt)
AND (:maxCreatedAt IS NULL OR createdAt <= :maxCreatedAt)
AND (:statusPattern IS NULL OR
',' || :statusPattern || ',' LIKE '%,' || lower(status) || ',%')
ORDER BY createdAt DESC, time DESC
""")
suspend fun getByCheckingIdsFiltered(
checkingIds : List<String>,
minAmountMsat : Long? = null,
maxAmountMsat : Long? = null,
minCreatedAt : Long? = null,
maxCreatedAt : Long? = null,
statusPattern : String? = null
): List<PaymentRecordEntity>
@Query("DELETE FROM payment_records") @Query("DELETE FROM payment_records")
suspend fun deleteAll() suspend fun deleteAll()
@@ -119,22 +119,6 @@ class HistoryViewModel(
?.payments?.firstOrNull { it.checkingId == checkingId } ?.payments?.firstOrNull { it.checkingId == checkingId }
} }
// Maps contactId → display name (for filter chips)
val contactDisplayNames: StateFlow<Map<String, String>> =
contactRepo.observeAllContacts()
.map { contacts ->
contacts.associate { c ->
c.contact.id to (c.contact.localAlias
?: c.contact.displayName
?: c.contact.name
?: "Unknown")
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyMap()
)
// ── Room-backed query results ────────────────────────────────────────────── // ── Room-backed query results ──────────────────────────────────────────────
private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null) private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null)
@@ -183,6 +167,22 @@ class HistoryViewModel(
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyMap() initialValue = emptyMap()
) )
// Maps contactId → display name (for filter chips)
val contactDisplayNames: StateFlow<Map<String, String>> =
contactRepo.observeAllContacts()
.map { contacts ->
contacts.associate { c ->
c.contact.id to (c.contact.localAlias
?: c.contact.displayName
?: c.contact.name
?: "Unknown")
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyMap()
)
// ── Contact assignment ─────────────────────────────────────────────────── // ── Contact assignment ───────────────────────────────────────────────────
@@ -269,7 +269,7 @@ class HistoryViewModel(
.findCheckingIdsByContactName(f.searchQuery) .findCheckingIdsByContactName(f.searchQuery)
if (contactCheckingIds.isNotEmpty()) { if (contactCheckingIds.isNotEmpty()) {
val contactPayments = paymentCache val contactPayments = paymentCache
.queryByCheckingIds(contactCheckingIds) .queryByCheckingIds(contactCheckingIds, f)
// Merge without duplicates // Merge without duplicates
val existingIds = baseResults.map { it.checkingId }.toSet() val existingIds = baseResults.map { it.checkingId }.toSet()
baseResults += contactPayments baseResults += contactPayments
@@ -282,17 +282,12 @@ class HistoryViewModel(
val contactCheckingIds = contactRepo val contactCheckingIds = contactRepo
.findCheckingIdsByContactIds(f.contactIds.toList()) .findCheckingIdsByContactIds(f.contactIds.toList())
if (contactCheckingIds.isNotEmpty()) { if (contactCheckingIds.isNotEmpty()) {
// Intersect: only keep base results that belong // Fetch ALL payments for these contacts, WITH the full filter applied
// to the selected contacts, OR fetch them directly val contactFiltered = paymentCache.queryByCheckingIds(
val existingIds = baseResults.map { it.checkingId }.toSet() checkingIds = contactCheckingIds,
val missing = contactCheckingIds.filter { it !in existingIds } filter = f
if (missing.isNotEmpty()) { )
baseResults += paymentCache.queryByCheckingIds(missing) _roomResults.value = contactFiltered
}
// Now narrow: remove payments NOT linked to selected contacts
val contactCheckingIdSet = contactCheckingIds.toSet()
_roomResults.value = baseResults
.filter { it.checkingId in contactCheckingIdSet }
.also { .also {
Timber.d("FILTER [ROOM+CONTACT] contacts=${f.contactIds}${it.size} results") Timber.d("FILTER [ROOM+CONTACT] contacts=${f.contactIds}${it.size} results")
} }
@@ -305,7 +300,7 @@ class HistoryViewModel(
} }
_roomResults.value = baseResults.also { _roomResults.value = baseResults.also {
Timber.d("FILTER [ROOM] q=\"${f.searchQuery}\"${it.size} results (${it.size - (paymentCache.queryAll(f.searchQuery, f).size)} from contacts)") Timber.d("FILTER [ROOM] q=\"${f.searchQuery}\"${it.size} results")
} }
} }
} }
@@ -30,6 +30,7 @@ val PaymentFilter.isActive: Boolean
|| maxAmountSat != null || maxAmountSat != null
|| minCreatedAt != null || minCreatedAt != null
|| maxCreatedAt != null || maxCreatedAt != null
|| contactIds.isNotEmpty()
fun PaymentFilter.needsRoomQuery(): Boolean = fun PaymentFilter.needsRoomQuery(): Boolean =
searchQuery.isNotBlank() || searchQuery.isNotBlank() ||