TransactionManager.kt (10534B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2024 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.transactions 18 19 import android.util.Log 20 import androidx.annotation.UiThread 21 import kotlinx.coroutines.CoroutineScope 22 import kotlinx.coroutines.flow.MutableStateFlow 23 import kotlinx.coroutines.flow.StateFlow 24 import kotlinx.coroutines.flow.asStateFlow 25 import kotlinx.coroutines.launch 26 import kotlinx.serialization.SerialName 27 import kotlinx.serialization.Serializable 28 import kotlinx.serialization.json.encodeToJsonElement 29 import kotlinx.serialization.json.jsonPrimitive 30 import net.taler.wallet.PrefsStateFilter 31 import net.taler.wallet.main.TAG 32 import net.taler.wallet.backend.BackendManager 33 import net.taler.wallet.backend.TalerErrorInfo 34 import net.taler.wallet.backend.WalletBackendApi 35 import net.taler.wallet.balances.ScopeInfo 36 import net.taler.wallet.transactions.TransactionAction.Delete 37 import org.json.JSONObject 38 39 sealed class TransactionsResult { 40 data object None : TransactionsResult() 41 data class Error(val error: TalerErrorInfo) : TransactionsResult() 42 data class Success(val transactions: List<Transaction>) : TransactionsResult() 43 } 44 45 @Serializable 46 enum class TransactionStateFilter { 47 @SerialName("final") 48 Final, 49 50 @SerialName("nonfinal") 51 Nonfinal, 52 53 @SerialName("done") 54 Done; 55 56 fun toPrefs(): PrefsStateFilter = when(this) { 57 Done -> PrefsStateFilter.DONE 58 Final -> PrefsStateFilter.FINAL 59 Nonfinal -> PrefsStateFilter.NONFINAL 60 } 61 62 companion object { 63 fun fromPrefs(prefs: PrefsStateFilter): TransactionStateFilter? = when(prefs) { 64 PrefsStateFilter.DONE -> Done 65 PrefsStateFilter.FINAL -> Final 66 PrefsStateFilter.NONFINAL -> Nonfinal 67 PrefsStateFilter.NONE, 68 PrefsStateFilter.UNRECOGNIZED -> null 69 } 70 } 71 } 72 73 class TransactionManager( 74 private val api: WalletBackendApi, 75 private val scope: CoroutineScope, 76 ) { 77 private val allTransactions = HashMap<ScopeInfo, List<Transaction>>() 78 private val mTransactions = HashMap<ScopeInfo, MutableStateFlow<TransactionsResult>>() 79 private val mSelectedTransaction = MutableStateFlow<Transaction?>(null) 80 81 val selectedTransaction = mSelectedTransaction.asStateFlow() 82 83 // This function must be called ONLY when scopeInfo / searchQuery change! 84 // Use remember() {} in Compose to prevent multiple calls during recomposition 85 fun transactionsFlow( 86 scopeInfo: ScopeInfo? = null, 87 searchQuery: String? = null, 88 stateFilter: TransactionStateFilter? = null, 89 ): StateFlow<TransactionsResult> { 90 return if (scopeInfo != null) { 91 loadTransactions(scopeInfo, searchQuery, stateFilter) 92 mTransactions[scopeInfo]?.asStateFlow() 93 ?: MutableStateFlow(TransactionsResult.None) 94 } else { 95 MutableStateFlow(TransactionsResult.None) 96 } 97 } 98 99 fun loadTransactions( 100 scopeInfo: ScopeInfo? = null, 101 searchQuery: String? = null, 102 stateFilter: TransactionStateFilter? = null, 103 ) { 104 Log.d(TAG, "loadTransactions($scopeInfo, $searchQuery, $stateFilter)") 105 val scopes = scopeInfo?.let { listOf(it) } ?: mTransactions.keys.toList() 106 if (scopes.isEmpty()) return 107 108 scopes.forEach { s -> 109 // initialize key with empty state flow 110 if (mTransactions[s] == null) { 111 mTransactions[s] = MutableStateFlow(TransactionsResult.None) 112 } 113 114 scope.launch { 115 // return cached transactions if available 116 if (searchQuery == null) allTransactions[s]?.let { txs -> 117 mTransactions[s]?.value = TransactionsResult.Success(txs) 118 } 119 120 // ...then fetch new ones 121 val res = getTransactions(s, searchQuery, filterByState = stateFilter) 122 if (res is TransactionsResult.Success) { 123 allTransactions[s] = res.transactions 124 } 125 126 // ...and then emit them when available 127 mTransactions[s]?.value = res 128 } 129 } 130 } 131 132 private suspend fun getTransactions( 133 scope: ScopeInfo, 134 searchQuery: String?, 135 filterByState: TransactionStateFilter? = null, 136 offsetTransactionId: String? = null, 137 limit: Int? = null, 138 ): TransactionsResult { 139 var result: TransactionsResult = TransactionsResult.None 140 api.request("getTransactionsV2", Transactions.serializer()) { 141 if (searchQuery != null) put("search", searchQuery) 142 if (filterByState != null) put( 143 "filterByState", 144 BackendManager.json 145 .encodeToJsonElement(filterByState) 146 .jsonPrimitive.content, 147 ) 148 if (offsetTransactionId != null) put("offsetTransactionId", offsetTransactionId) 149 if (limit != null) put("limit", limit) 150 put("scopeInfo", JSONObject(BackendManager.json.encodeToString(scope))) 151 }.onError { error -> 152 Log.e(TAG, "Error: getTransactions error result: $error") 153 result = TransactionsResult.Error(error) 154 }.onSuccess { res -> 155 result = TransactionsResult.Success(res 156 .transactions 157 .reversed()) 158 } 159 160 return result 161 } 162 163 suspend fun getTransactionById(id: String): Transaction? { 164 var transaction: Transaction? = null 165 api.request("getTransactionById", Transaction.serializer()) { 166 put("transactionId", id) 167 }.onError { 168 Log.e(TAG, "Error getting transaction $it") 169 }.onSuccess { result -> 170 transaction = result 171 } 172 173 return transaction 174 } 175 176 /** 177 * Returns true if given [transactionId] was found and selected, false otherwise. 178 */ 179 suspend fun selectTransaction(transactionId: String): Boolean { 180 val transaction = getTransactionById(transactionId) 181 if (transaction != null) { 182 mSelectedTransaction.emit(transaction) 183 return true 184 } else { 185 return false 186 } 187 } 188 189 fun selectTransactionSync(transactionId: String, onSuccess: () -> Unit) = scope.launch { 190 if (selectTransaction(transactionId)) { 191 onSuccess() 192 } 193 } 194 195 @UiThread 196 fun updateTransactionIfSelected(id: String) = scope.launch { 197 val selectedTransaction = selectedTransaction.value 198 if (selectedTransaction?.transactionId != id) return@launch 199 getTransactionById(id)?.let { tx -> 200 if (tx.transactionId == selectedTransaction.transactionId) { 201 Log.d(TAG, "updating selected transaction: ${tx.transactionId}") 202 mSelectedTransaction.value = tx 203 } 204 } ?: Log.d(TAG, "Error updating selected transaction $id") 205 } 206 207 fun selectTransaction(tx: Transaction?) = scope.launch { 208 mSelectedTransaction.value = tx 209 } 210 211 fun deleteTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) = 212 scope.launch { 213 api.request<Unit>("deleteTransaction") { 214 put("transactionId", transactionId) 215 }.onError { 216 onError(it) 217 }.onSuccess { 218 // re-load transactions as our list is stale otherwise 219 loadTransactions() 220 } 221 } 222 223 fun retryTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) = 224 scope.launch { 225 api.request<Unit>("retryTransaction") { 226 put("transactionId", transactionId) 227 }.onError { 228 onError(it) 229 }.onSuccess { 230 loadTransactions() 231 } 232 } 233 234 fun abortTransaction( 235 transactionId: String, 236 onSuccess: () -> Unit, 237 onError: (it: TalerErrorInfo) -> Unit, 238 ) = 239 scope.launch { 240 api.request<Unit>("abortTransaction") { 241 put("transactionId", transactionId) 242 }.onError { 243 onError(it) 244 }.onSuccess { 245 onSuccess() 246 loadTransactions() 247 } 248 } 249 250 fun failTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) = 251 scope.launch { 252 api.request<Unit>("failTransaction") { 253 put("transactionId", transactionId) 254 }.onError { 255 onError(it) 256 }.onSuccess { 257 loadTransactions() 258 } 259 } 260 261 fun suspendTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) = 262 scope.launch { 263 api.request<Unit>("suspendTransaction") { 264 put("transactionId", transactionId) 265 }.onError { 266 onError(it) 267 }.onSuccess { 268 loadTransactions() 269 } 270 } 271 272 fun resumeTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) = 273 scope.launch { 274 api.request<Unit>("resumeTransaction") { 275 put("transactionId", transactionId) 276 }.onError { 277 onError(it) 278 }.onSuccess { 279 loadTransactions() 280 } 281 } 282 283 fun deleteTransactions(transactionIds: List<String>, onError: (it: TalerErrorInfo) -> Unit) = 284 scope.launch { 285 allTransactions.values.flatten().filter { transaction -> 286 transaction.transactionId in transactionIds && Delete in transaction.txActions 287 }.forEach { toBeDeletedTx -> 288 api.request<Unit>("deleteTransaction") { 289 put("transactionId", toBeDeletedTx.transactionId) 290 }.onError { 291 onError(it) 292 } 293 } 294 loadTransactions() 295 } 296 }