fix: multiple filters break contact search
This commit is contained in:
@@ -160,9 +160,33 @@ class PaymentCacheRepository(context: Context) {
|
||||
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()
|
||||
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")
|
||||
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")
|
||||
suspend fun deleteAll()
|
||||
|
||||
|
||||
@@ -119,22 +119,6 @@ class HistoryViewModel(
|
||||
?.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 ──────────────────────────────────────────────
|
||||
private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null)
|
||||
@@ -183,6 +167,22 @@ class HistoryViewModel(
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
@@ -269,7 +269,7 @@ class HistoryViewModel(
|
||||
.findCheckingIdsByContactName(f.searchQuery)
|
||||
if (contactCheckingIds.isNotEmpty()) {
|
||||
val contactPayments = paymentCache
|
||||
.queryByCheckingIds(contactCheckingIds)
|
||||
.queryByCheckingIds(contactCheckingIds, f)
|
||||
// Merge without duplicates
|
||||
val existingIds = baseResults.map { it.checkingId }.toSet()
|
||||
baseResults += contactPayments
|
||||
@@ -282,17 +282,12 @@ class HistoryViewModel(
|
||||
val contactCheckingIds = contactRepo
|
||||
.findCheckingIdsByContactIds(f.contactIds.toList())
|
||||
if (contactCheckingIds.isNotEmpty()) {
|
||||
// Intersect: only keep base results that belong
|
||||
// to the selected contacts, OR fetch them directly
|
||||
val existingIds = baseResults.map { it.checkingId }.toSet()
|
||||
val missing = contactCheckingIds.filter { it !in existingIds }
|
||||
if (missing.isNotEmpty()) {
|
||||
baseResults += paymentCache.queryByCheckingIds(missing)
|
||||
}
|
||||
// Now narrow: remove payments NOT linked to selected contacts
|
||||
val contactCheckingIdSet = contactCheckingIds.toSet()
|
||||
_roomResults.value = baseResults
|
||||
.filter { it.checkingId in contactCheckingIdSet }
|
||||
// Fetch ALL payments for these contacts, WITH the full filter applied
|
||||
val contactFiltered = paymentCache.queryByCheckingIds(
|
||||
checkingIds = contactCheckingIds,
|
||||
filter = f
|
||||
)
|
||||
_roomResults.value = contactFiltered
|
||||
.also {
|
||||
Timber.d("FILTER [ROOM+CONTACT] contacts=${f.contactIds} → ${it.size} results")
|
||||
}
|
||||
@@ -305,7 +300,7 @@ class HistoryViewModel(
|
||||
}
|
||||
|
||||
_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
|
||||
|| minCreatedAt != null
|
||||
|| maxCreatedAt != null
|
||||
|| contactIds.isNotEmpty()
|
||||
|
||||
fun PaymentFilter.needsRoomQuery(): Boolean =
|
||||
searchQuery.isNotBlank() ||
|
||||
|
||||
Reference in New Issue
Block a user