taler-android

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

HistoryManager.kt (7179B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 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.merchantpos.history
     18 
     19 import androidx.annotation.StringRes
     20 import androidx.annotation.UiThread
     21 import androidx.lifecycle.LiveData
     22 import androidx.lifecycle.MutableLiveData
     23 import kotlinx.coroutines.CoroutineScope
     24 import kotlinx.coroutines.launch
     25 import net.taler.lib.android.assertUiThread
     26 import net.taler.merchantlib.CheckPaymentResponse
     27 import net.taler.merchantlib.MerchantApi
     28 import net.taler.merchantlib.OrderHistoryEntry
     29 import net.taler.merchantpos.R
     30 import net.taler.merchantpos.config.ConfigManager
     31 
     32 sealed class HistoryResult {
     33     class Success(val items: List<OrderHistoryEntry>) : HistoryResult()
     34 }
     35 
     36 class HistoryError(
     37     @StringRes val mainResId: Int,
     38     val msg: String,
     39 )
     40 
     41 class HistoryManager(
     42     private val configManager: ConfigManager,
     43     private val scope: CoroutineScope,
     44     private val api: MerchantApi
     45 ) {
     46     companion object {
     47         private const val PAGE_SIZE = 20
     48         private const val LOAD_MORE_THRESHOLD = 5
     49     }
     50 
     51     private val mIsLoading = MutableLiveData(false)
     52     val isLoading: LiveData<Boolean> = mIsLoading
     53     private val mIsLoadingMore = MutableLiveData(false)
     54     val isLoadingMore: LiveData<Boolean> = mIsLoadingMore
     55 
     56     private val mItems = MutableLiveData<HistoryResult>()
     57     val items: LiveData<HistoryResult> = mItems
     58     private val mError = MutableLiveData<HistoryError?>(null)
     59     val error: LiveData<HistoryError?> = mError
     60 
     61     private val loadedItems = mutableListOf<OrderHistoryEntry>()
     62     private var nextOffset: Long? = null
     63     private var reachedEnd = false
     64 
     65     @UiThread
     66     internal fun fetchHistory() = fetchHistoryPage(reset = true)
     67 
     68     @UiThread
     69     internal fun loadMoreHistoryIfNeeded(lastVisibleIndex: Int) {
     70         val currentItems = (mItems.value as? HistoryResult.Success)?.items ?: return
     71         if (mIsLoading.value == true || mIsLoadingMore.value == true || reachedEnd) return
     72         if (currentItems.isEmpty()) return
     73         if (lastVisibleIndex < currentItems.lastIndex - LOAD_MORE_THRESHOLD) return
     74         fetchHistoryPage(reset = false)
     75     }
     76 
     77     @UiThread
     78     private fun fetchHistoryPage(reset: Boolean) = scope.launch {
     79         if (reset) {
     80             mIsLoading.value = true
     81             mIsLoadingMore.value = false
     82         } else {
     83             mIsLoadingMore.value = true
     84         }
     85         val merchantConfig = configManager.merchantConfig!!
     86         val offset = if (reset) null else nextOffset
     87         api.getOrderHistory(
     88             merchantConfig = merchantConfig,
     89             limit = -PAGE_SIZE,
     90             offset = offset,
     91         ).handle({ onError(R.string.error_history, it) }) { response ->
     92             assertUiThread()
     93             mIsLoading.value = false
     94             mIsLoadingMore.value = false
     95             if (reset) {
     96                 loadedItems.clear()
     97                 reachedEnd = false
     98             }
     99             val page = response.orders
    100             val existingOrderIds = loadedItems.mapTo(mutableSetOf()) { it.orderId }
    101             val newItems = page.filterNot { it.orderId in existingOrderIds }
    102             if (newItems.isNotEmpty()) {
    103                 loadedItems += newItems
    104                 nextOffset = newItems.lastOrNull()?.rowId ?: loadedItems.lastOrNull()?.rowId
    105             }
    106             if (page.size < PAGE_SIZE || newItems.isEmpty()) {
    107                 reachedEnd = true
    108             }
    109             publishItems()
    110             enrichOrders(newItems)
    111         }
    112     }
    113 
    114     private val mForceDeleteOrderId = MutableLiveData<String?>(null)
    115     val forceDeleteOrderId: LiveData<String?> = mForceDeleteOrderId
    116 
    117     @UiThread
    118     internal fun deleteOrder(orderId: String) = scope.launch {
    119         mIsLoading.value = true
    120         mIsLoadingMore.value = false
    121         val merchantConfig = configManager.merchantConfig!!
    122         api.deleteOrder(merchantConfig, orderId).handle({ errorMsg ->
    123             assertUiThread()
    124             mIsLoading.value = false
    125             mForceDeleteOrderId.value = orderId
    126         }) {
    127             assertUiThread()
    128             configManager.refreshInventory()
    129             fetchHistory()
    130         }
    131     }
    132 
    133     @UiThread
    134     internal fun forceDeleteOrder(orderId: String) = scope.launch {
    135         mForceDeleteOrderId.postValue(null)
    136         mIsLoading.value = true
    137         val merchantConfig = configManager.merchantConfig!!
    138         api.deleteOrder(merchantConfig, orderId, force = true).handle({ errorMsg ->
    139             onError(R.string.error_delete_order, errorMsg)
    140         }) {
    141             assertUiThread()
    142             configManager.refreshInventory()
    143             fetchHistory()
    144         }
    145     }
    146 
    147     @UiThread
    148     internal fun clearForceDeletePrompt() {
    149         mForceDeleteOrderId.value = null
    150     }
    151 
    152     @UiThread
    153     internal fun clearError() {
    154         mError.value = null
    155     }
    156 
    157     private fun publishItems() {
    158         mItems.value = HistoryResult.Success(loadedItems.toList())
    159     }
    160 
    161     @UiThread
    162     internal fun debugSetHistory(items: List<OrderHistoryEntry>) {
    163         loadedItems.clear()
    164         loadedItems.addAll(items)
    165         publishItems()
    166     }
    167 
    168     private fun enrichOrders(items: List<OrderHistoryEntry>) = scope.launch {
    169         val merchantConfig = configManager.merchantConfig ?: return@launch
    170         items.filter { it.paid }.forEach { item ->
    171             api.checkOrder(merchantConfig, item.orderId).handle(null) { response ->
    172                 assertUiThread()
    173                 val paidResponse = response as? CheckPaymentResponse.Paid ?: return@handle
    174                 updateItem(
    175                     orderId = item.orderId,
    176                     transform = { current ->
    177                         current.copy(
    178                             refunded = paidResponse.refunded,
    179                             refundAmount = paidResponse.refundAmount ?: current.refundAmount,
    180                             refundPending = paidResponse.refundPending,
    181                         )
    182                     }
    183                 )
    184             }
    185         }
    186     }
    187 
    188     private fun updateItem(
    189         orderId: String,
    190         transform: (OrderHistoryEntry) -> OrderHistoryEntry,
    191     ) {
    192         val index = loadedItems.indexOfFirst { it.orderId == orderId }
    193         if (index == -1) return
    194         loadedItems[index] = transform(loadedItems[index])
    195         publishItems()
    196     }
    197 
    198     private fun onError(@StringRes mainResId: Int, msg: String) {
    199         assertUiThread()
    200         mIsLoading.value = false
    201         mIsLoadingMore.value = false
    202         mError.value = HistoryError(mainResId, msg)
    203     }
    204 }