taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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 }